diff --git a/lib/app/router.dart b/lib/app/router.dart index 352c11e..80a1996 100644 --- a/lib/app/router.dart +++ b/lib/app/router.dart @@ -10,6 +10,7 @@ import '../features/clients/presentation/client_form_screen.dart'; import '../features/emails/presentation/emails_screen.dart'; import '../features/emails/presentation/email_compose_screen.dart'; import '../features/events/presentation/events_screen.dart'; +import '../features/profile/presentation/profile_screen.dart'; import '../shared/providers/auth_provider.dart'; final routerProvider = Provider((ref) { @@ -81,6 +82,10 @@ final routerProvider = Provider((ref) { path: '/events', builder: (context, state) => const EventsScreen(), ), + GoRoute( + path: '/profile', + builder: (context, state) => const ProfileScreen(), + ), ], ), ], @@ -115,6 +120,11 @@ class MainShell extends StatelessWidget { selectedIcon: Icon(Icons.event), label: 'Events', ), + NavigationDestination( + icon: Icon(Icons.person_outline), + selectedIcon: Icon(Icons.person), + label: 'Profile', + ), ], ), ); @@ -124,6 +134,7 @@ class MainShell extends StatelessWidget { final location = GoRouterState.of(context).matchedLocation; if (location.startsWith('/emails')) return 1; if (location.startsWith('/events')) return 2; + if (location.startsWith('/profile')) return 3; return 0; } @@ -138,6 +149,9 @@ class MainShell extends StatelessWidget { case 2: context.go('/events'); break; + case 3: + context.go('/profile'); + break; } } } diff --git a/lib/features/clients/presentation/clients_screen.dart b/lib/features/clients/presentation/clients_screen.dart index 984897a..89945ca 100644 --- a/lib/features/clients/presentation/clients_screen.dart +++ b/lib/features/clients/presentation/clients_screen.dart @@ -34,14 +34,6 @@ class _ClientsScreenState extends ConsumerState { return Scaffold( appBar: AppBar( title: const Text('Clients'), - actions: [ - IconButton( - icon: const Icon(Icons.logout), - onPressed: () async { - await ref.read(authStateProvider.notifier).signOut(); - }, - ), - ], ), body: Column( children: [ diff --git a/lib/features/profile/presentation/profile_screen.dart b/lib/features/profile/presentation/profile_screen.dart new file mode 100644 index 0000000..40539f5 --- /dev/null +++ b/lib/features/profile/presentation/profile_screen.dart @@ -0,0 +1,255 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../shared/services/api_client.dart'; +import '../../../shared/providers/auth_provider.dart'; + +final profileProvider = FutureProvider.autoDispose>((ref) async { + final apiClient = ref.watch(apiClientProvider); + return apiClient.getProfile(); +}); + +class ProfileScreen extends ConsumerStatefulWidget { + const ProfileScreen({super.key}); + + @override + ConsumerState createState() => _ProfileScreenState(); +} + +class _ProfileScreenState extends ConsumerState { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _titleController = TextEditingController(); + final _companyController = TextEditingController(); + final _phoneController = TextEditingController(); + final _signatureController = TextEditingController(); + + bool _isLoading = false; + bool _initialized = false; + + @override + void dispose() { + _nameController.dispose(); + _titleController.dispose(); + _companyController.dispose(); + _phoneController.dispose(); + _signatureController.dispose(); + super.dispose(); + } + + void _initializeForm(Map profile) { + if (!_initialized) { + _nameController.text = profile['name'] ?? ''; + _titleController.text = profile['title'] ?? ''; + _companyController.text = profile['company'] ?? ''; + _phoneController.text = profile['phone'] ?? ''; + _signatureController.text = profile['emailSignature'] ?? ''; + _initialized = true; + } + } + + Future _saveProfile() async { + if (!_formKey.currentState!.validate()) return; + + setState(() => _isLoading = true); + + try { + await ref.read(apiClientProvider).updateProfile({ + 'name': _nameController.text.trim(), + 'title': _titleController.text.trim(), + 'company': _companyController.text.trim(), + 'phone': _phoneController.text.trim(), + 'emailSignature': _signatureController.text.trim(), + }); + + ref.invalidate(profileProvider); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Profile saved')), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to save: $e')), + ); + } + } finally { + if (mounted) { + setState(() => _isLoading = false); + } + } + } + + @override + Widget build(BuildContext context) { + final profileAsync = ref.watch(profileProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('Profile'), + actions: [ + IconButton( + icon: const Icon(Icons.logout), + tooltip: 'Sign Out', + onPressed: () async { + await ref.read(authStateProvider.notifier).signOut(); + }, + ), + ], + ), + body: profileAsync.when( + data: (profile) { + _initializeForm(profile); + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Avatar + Center( + child: CircleAvatar( + radius: 48, + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + child: Text( + _getInitials(profile['name'] ?? ''), + style: TextStyle( + fontSize: 32, + color: Theme.of(context).colorScheme.onPrimaryContainer, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(height: 8), + Center( + child: Text( + profile['email'] ?? '', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey, + ), + ), + ), + const SizedBox(height: 32), + + // Form fields + Text( + 'Basic Info', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Full Name', + hintText: 'John Smith', + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Name is required'; + } + return null; + }, + ), + const SizedBox(height: 16), + + TextFormField( + controller: _titleController, + decoration: const InputDecoration( + labelText: 'Title', + hintText: 'Senior Wealth Advisor', + ), + ), + const SizedBox(height: 16), + + TextFormField( + controller: _companyController, + decoration: const InputDecoration( + labelText: 'Company', + hintText: 'ABC Financial Group', + ), + ), + const SizedBox(height: 16), + + TextFormField( + controller: _phoneController, + decoration: const InputDecoration( + labelText: 'Phone', + hintText: '(555) 123-4567', + ), + keyboardType: TextInputType.phone, + ), + const SizedBox(height: 32), + + Text( + 'Email Signature', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'Optional custom signature for generated emails', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.grey, + ), + ), + const SizedBox(height: 16), + + TextFormField( + controller: _signatureController, + decoration: const InputDecoration( + hintText: 'Best regards,\nJohn Smith\nSenior Wealth Advisor\n(555) 123-4567', + alignLabelWithHint: true, + ), + maxLines: 5, + ), + const SizedBox(height: 32), + + FilledButton( + onPressed: _isLoading ? null : _saveProfile, + child: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Save Profile'), + ), + ], + ), + ), + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 64, color: Colors.red.shade300), + const SizedBox(height: 16), + const Text('Failed to load profile'), + TextButton( + onPressed: () => ref.invalidate(profileProvider), + child: const Text('Retry'), + ), + ], + ), + ), + ), + ); + } + + String _getInitials(String name) { + final parts = name.trim().split(' '); + if (parts.isEmpty) return '?'; + if (parts.length == 1) return parts[0][0].toUpperCase(); + return '${parts[0][0]}${parts[parts.length - 1][0]}'.toUpperCase(); + } +} diff --git a/lib/shared/services/api_client.dart b/lib/shared/services/api_client.dart index e60608d..741af59 100644 --- a/lib/shared/services/api_client.dart +++ b/lib/shared/services/api_client.dart @@ -187,4 +187,15 @@ class ApiClient { Future syncClientEvents(String clientId) async { await _dio.post('/api/events/sync/$clientId'); } + + // Profile + Future> getProfile() async { + final response = await _dio.get('/api/profile'); + return response.data; + } + + Future> updateProfile(Map data) async { + final response = await _dio.put('/api/profile', data: data); + return response.data; + } }