import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/announcement.dart'; import '../../providers/announcements_provider.dart'; import '../../theme/m3_motion.dart'; import '../../utils/snackbar.dart'; import '../../widgets/app_breakpoints.dart'; /// All user roles available for announcement visibility. const _allRoles = ['admin', 'dispatcher', 'programmer', 'it_staff', 'standard']; /// Default visible roles (standard is excluded by default). const _defaultVisibleRoles = ['admin', 'dispatcher', 'programmer', 'it_staff']; /// Human-readable labels for each role. const _roleLabels = { 'admin': 'Admin', 'dispatcher': 'Dispatcher', 'programmer': 'Programmer', 'it_staff': 'IT Staff', 'standard': 'Standard', }; /// Shows the create/edit announcement dialog. /// /// On mobile, uses a full-screen bottom sheet; on desktop, a centered dialog. Future showCreateAnnouncementDialog( BuildContext context, { Announcement? editing, }) async { final width = MediaQuery.sizeOf(context).width; if (width < AppBreakpoints.tablet) { await m3ShowBottomSheet( context: context, isScrollControlled: true, builder: (ctx) => _CreateAnnouncementContent(editing: editing), ); } else { await m3ShowDialog( context: context, builder: (ctx) => Dialog( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 560), child: _CreateAnnouncementContent(editing: editing), ), ), ); } } class _CreateAnnouncementContent extends ConsumerStatefulWidget { const _CreateAnnouncementContent({this.editing}); final Announcement? editing; @override ConsumerState<_CreateAnnouncementContent> createState() => _CreateAnnouncementContentState(); } class _CreateAnnouncementContentState extends ConsumerState<_CreateAnnouncementContent> { late final TextEditingController _titleCtrl; late final TextEditingController _bodyCtrl; late Set _selectedRoles; bool _isTemplate = false; bool _submitting = false; // Template selection String? _selectedTemplateId; @override void initState() { super.initState(); final source = widget.editing; _titleCtrl = TextEditingController(text: source?.title ?? ''); _bodyCtrl = TextEditingController(text: source?.body ?? ''); _selectedRoles = source != null ? Set.from(source.visibleRoles) : Set.from(_defaultVisibleRoles); _isTemplate = widget.editing?.isTemplate ?? false; } @override void dispose() { _titleCtrl.dispose(); _bodyCtrl.dispose(); super.dispose(); } bool get _canSubmit { final hasContent = _titleCtrl.text.trim().isNotEmpty && _bodyCtrl.text.trim().isNotEmpty; final hasRoles = _selectedRoles.isNotEmpty; return hasContent && hasRoles && !_submitting; } void _applyTemplate(Announcement template) { setState(() { _selectedTemplateId = template.id; _titleCtrl.text = template.title; _bodyCtrl.text = template.body; _selectedRoles = Set.from(template.visibleRoles); }); } void _clearTemplate() { setState(() { _selectedTemplateId = null; }); } Future _submit() async { if (!_canSubmit) return; setState(() => _submitting = true); try { final ctrl = ref.read(announcementsControllerProvider); if (widget.editing != null) { await ctrl.updateAnnouncement( id: widget.editing!.id, title: _titleCtrl.text.trim(), body: _bodyCtrl.text.trim(), visibleRoles: _selectedRoles.toList(), isTemplate: _isTemplate, ); } else { await ctrl.createAnnouncement( title: _titleCtrl.text.trim(), body: _bodyCtrl.text.trim(), visibleRoles: _selectedRoles.toList(), isTemplate: _isTemplate, templateId: _selectedTemplateId, ); } if (mounted) Navigator.of(context).pop(); } on AnnouncementNotificationException { // Saved successfully; only push notification delivery failed. if (mounted) { final messenger = ScaffoldMessenger.of(context); Navigator.of(context).pop(); messenger.showSnackBar( const SnackBar( content: Text('Posted, but some notifications failed to send.'), ), ); } } catch (e) { if (mounted) showErrorSnackBar(context, 'Failed to save: $e'); } finally { if (mounted) setState(() => _submitting = false); } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final tt = Theme.of(context).textTheme; final isEditing = widget.editing != null; // Get available templates from the stream (filter client-side) final templates = ref .watch(announcementsProvider) .valueOrNull ?.where((a) => a.isTemplate) .toList() ?? []; return SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Dialog title Text( isEditing ? 'Edit Announcement' : 'New Announcement', style: tt.titleLarge?.copyWith(fontWeight: FontWeight.w600), ), const SizedBox(height: 20), // Template selector (only for new announcements) if (!isEditing && templates.isNotEmpty) ...[ DropdownButtonFormField( decoration: InputDecoration( labelText: 'Post from template (optional)', border: const OutlineInputBorder(), suffixIcon: _selectedTemplateId != null ? IconButton( icon: const Icon(Icons.clear), tooltip: 'Clear template', onPressed: _clearTemplate, ) : null, ), key: ValueKey(_selectedTemplateId), initialValue: _selectedTemplateId, items: [ const DropdownMenuItem( value: null, child: Text('No template'), ), ...templates.map( (t) => DropdownMenuItem( value: t.id, child: Text( t.title.isEmpty ? '(Untitled)' : t.title, overflow: TextOverflow.ellipsis, ), ), ), ], onChanged: (id) { if (id == null) { _clearTemplate(); } else { final tmpl = templates.firstWhere((t) => t.id == id); _applyTemplate(tmpl); } }, ), const SizedBox(height: 16), ], // Title field TextField( controller: _titleCtrl, decoration: const InputDecoration( labelText: 'Title', border: OutlineInputBorder(), ), textInputAction: TextInputAction.next, onChanged: (_) => setState(() {}), ), const SizedBox(height: 16), // Body field TextField( controller: _bodyCtrl, decoration: const InputDecoration( labelText: 'Body', border: OutlineInputBorder(), alignLabelWithHint: true, ), maxLines: 6, minLines: 3, onChanged: (_) => setState(() {}), ), const SizedBox(height: 16), // Role visibility Text('Visible to:', style: tt.labelLarge), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 6, children: _allRoles.map((role) { final selected = _selectedRoles.contains(role); return FilterChip( label: Text(_roleLabels[role] ?? role), selected: selected, onSelected: (val) { setState(() { if (val) { _selectedRoles.add(role); } else { _selectedRoles.remove(role); } }); }, ); }).toList(), ), const SizedBox(height: 16), // Template toggle SwitchListTile( contentPadding: EdgeInsets.zero, title: Text('Save as template', style: tt.bodyMedium), subtitle: Text( 'Templates can be selected when creating new announcements.', style: tt.bodySmall?.copyWith(color: cs.onSurfaceVariant), ), value: _isTemplate, onChanged: (val) => setState(() => _isTemplate = val), ), const SizedBox(height: 20), // Action buttons Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), const SizedBox(width: 12), FilledButton( onPressed: _canSubmit ? _submit : null, child: _submitting ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white), ) : Text(isEditing ? 'Save' : 'Post'), ), ], ), ], ), ); } }