Initial Flutter scaffold: Riverpod + GoRouter + Dio
This commit is contained in:
219
lib/features/emails/presentation/email_compose_screen.dart
Normal file
219
lib/features/emails/presentation/email_compose_screen.dart
Normal 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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user