Add Profile screen with name, title, company, phone, signature settings

This commit is contained in:
2026-01-27 22:59:25 +00:00
parent 99f0983446
commit 25733fe90b
4 changed files with 280 additions and 8 deletions

View File

@@ -10,6 +10,7 @@ import '../features/clients/presentation/client_form_screen.dart';
import '../features/emails/presentation/emails_screen.dart'; import '../features/emails/presentation/emails_screen.dart';
import '../features/emails/presentation/email_compose_screen.dart'; import '../features/emails/presentation/email_compose_screen.dart';
import '../features/events/presentation/events_screen.dart'; import '../features/events/presentation/events_screen.dart';
import '../features/profile/presentation/profile_screen.dart';
import '../shared/providers/auth_provider.dart'; import '../shared/providers/auth_provider.dart';
final routerProvider = Provider<GoRouter>((ref) { final routerProvider = Provider<GoRouter>((ref) {
@@ -81,6 +82,10 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/events', path: '/events',
builder: (context, state) => const EventsScreen(), 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), selectedIcon: Icon(Icons.event),
label: 'Events', 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; final location = GoRouterState.of(context).matchedLocation;
if (location.startsWith('/emails')) return 1; if (location.startsWith('/emails')) return 1;
if (location.startsWith('/events')) return 2; if (location.startsWith('/events')) return 2;
if (location.startsWith('/profile')) return 3;
return 0; return 0;
} }
@@ -138,6 +149,9 @@ class MainShell extends StatelessWidget {
case 2: case 2:
context.go('/events'); context.go('/events');
break; break;
case 3:
context.go('/profile');
break;
} }
} }
} }

View File

@@ -34,14 +34,6 @@ class _ClientsScreenState extends ConsumerState<ClientsScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Clients'), title: const Text('Clients'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () async {
await ref.read(authStateProvider.notifier).signOut();
},
),
],
), ),
body: Column( body: Column(
children: [ children: [

View File

@@ -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<Map<String, dynamic>>((ref) async {
final apiClient = ref.watch(apiClientProvider);
return apiClient.getProfile();
});
class ProfileScreen extends ConsumerStatefulWidget {
const ProfileScreen({super.key});
@override
ConsumerState<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends ConsumerState<ProfileScreen> {
final _formKey = GlobalKey<FormState>();
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<String, dynamic> 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<void> _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();
}
}

View File

@@ -187,4 +187,15 @@ class ApiClient {
Future<void> syncClientEvents(String clientId) async { Future<void> syncClientEvents(String clientId) async {
await _dio.post('/api/events/sync/$clientId'); await _dio.post('/api/events/sync/$clientId');
} }
// Profile
Future<Map<String, dynamic>> getProfile() async {
final response = await _dio.get('/api/profile');
return response.data;
}
Future<Map<String, dynamic>> updateProfile(Map<String, dynamic> data) async {
final response = await _dio.put('/api/profile', data: data);
return response.data;
}
} }