Initial Flutter scaffold: Riverpod + GoRouter + Dio

This commit is contained in:
2026-01-27 13:20:01 +00:00
commit c8ac4ec8bc
18 changed files with 2728 additions and 0 deletions

View File

@@ -0,0 +1,219 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../shared/services/api_client.dart';
import '../../clients/presentation/clients_screen.dart';
class EmailComposeScreen extends ConsumerStatefulWidget {
final String? clientId;
const EmailComposeScreen({super.key, this.clientId});
@override
ConsumerState<EmailComposeScreen> createState() => _EmailComposeScreenState();
}
class _EmailComposeScreenState extends ConsumerState<EmailComposeScreen> {
final _purposeController = TextEditingController();
final _subjectController = TextEditingController();
final _contentController = TextEditingController();
String? _selectedClientId;
bool _isGenerating = false;
bool _hasGenerated = false;
@override
void initState() {
super.initState();
_selectedClientId = widget.clientId;
}
@override
void dispose() {
_purposeController.dispose();
_subjectController.dispose();
_contentController.dispose();
super.dispose();
}
Future<void> _generateEmail() async {
if (_selectedClientId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please select a client')),
);
return;
}
if (_purposeController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please enter a purpose')),
);
return;
}
setState(() => _isGenerating = true);
try {
final result = await ref.read(apiClientProvider).generateEmail(
clientId: _selectedClientId!,
purpose: _purposeController.text.trim(),
);
setState(() {
_subjectController.text = result['subject'] ?? '';
_contentController.text = result['content'] ?? '';
_hasGenerated = true;
});
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to generate: $e')),
);
}
} finally {
if (mounted) {
setState(() => _isGenerating = false);
}
}
}
@override
Widget build(BuildContext context) {
final clientsAsync = ref.watch(clientsProvider(null));
return Scaffold(
appBar: AppBar(
title: const Text('Compose Email'),
actions: [
if (_hasGenerated)
TextButton(
onPressed: () {
// Email was already saved as draft during generation
context.go('/emails');
},
child: const Text('Done'),
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Client selector
Text(
'Select Client',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
clientsAsync.when(
data: (clients) => DropdownButtonFormField<String>(
value: _selectedClientId,
decoration: const InputDecoration(
hintText: 'Choose a client',
),
items: clients.map((client) => DropdownMenuItem(
value: client['id'] as String,
child: Text('${client['firstName']} ${client['lastName']}'),
)).toList(),
onChanged: (value) {
setState(() {
_selectedClientId = value;
_hasGenerated = false;
});
},
),
loading: () => const LinearProgressIndicator(),
error: (e, s) => Text('Error loading clients: $e'),
),
const SizedBox(height: 24),
// Purpose input
Text(
'Email Purpose',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
TextFormField(
controller: _purposeController,
decoration: const InputDecoration(
hintText: 'e.g., Follow up after meeting, Birthday wishes, Market update',
),
maxLines: 2,
),
const SizedBox(height: 16),
// Generate button
FilledButton.icon(
onPressed: _isGenerating ? null : _generateEmail,
icon: _isGenerating
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.auto_awesome),
label: Text(_isGenerating ? 'Generating...' : 'Generate with AI'),
),
if (_hasGenerated) ...[
const SizedBox(height: 32),
const Divider(),
const SizedBox(height: 16),
// Subject
Text(
'Subject',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
TextFormField(
controller: _subjectController,
decoration: const InputDecoration(
hintText: 'Email subject',
),
),
const SizedBox(height: 16),
// Content
Text(
'Content',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
TextFormField(
controller: _contentController,
decoration: const InputDecoration(
hintText: 'Email content',
),
maxLines: 12,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _generateEmail,
icon: const Icon(Icons.refresh),
label: const Text('Regenerate'),
),
),
],
),
],
],
),
);
}
}