import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../models/office.dart'; import '../../models/notification_item.dart'; import '../../providers/notifications_provider.dart'; import '../../providers/tickets_provider.dart'; import '../../providers/typing_provider.dart'; import '../../widgets/responsive_body.dart'; import '../../widgets/typing_dots.dart'; class TicketsListScreen extends ConsumerWidget { const TicketsListScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final ticketsAsync = ref.watch(ticketsProvider); final officesAsync = ref.watch(officesProvider); final notificationsAsync = ref.watch(notificationsProvider); return Scaffold( body: ResponsiveBody( child: ticketsAsync.when( data: (tickets) { if (tickets.isEmpty) { return const Center(child: Text('No tickets yet.')); } final officeById = { for (final office in officesAsync.valueOrNull ?? []) office.id: office, }; final unreadByTicketId = _unreadByTicketId(notificationsAsync); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.only(top: 16, bottom: 8), child: Align( alignment: Alignment.center, child: Text( 'Tickets', textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w700, ), ), ), ), Expanded( child: ListView.separated( padding: const EdgeInsets.only(bottom: 24), itemCount: tickets.length, separatorBuilder: (context, index) => const SizedBox(height: 12), itemBuilder: (context, index) { final ticket = tickets[index]; final officeName = officeById[ticket.officeId]?.name ?? ticket.officeId; final hasMention = unreadByTicketId[ticket.id] == true; final typingState = ref.watch( typingIndicatorProvider(ticket.id), ); final showTyping = typingState.userIds.isNotEmpty; return ListTile( leading: const Icon(Icons.confirmation_number_outlined), title: Text(ticket.subject), subtitle: Text(officeName), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ _buildStatusChip(context, ticket.status), if (showTyping) ...[ const SizedBox(width: 6), TypingDots( size: 6, color: Theme.of(context).colorScheme.primary, ), ], if (hasMention) const Padding( padding: EdgeInsets.only(left: 8), child: Icon( Icons.circle, size: 10, color: Colors.red, ), ), ], ), onTap: () => context.go('/tickets/${ticket.id}'), ); }, ), ), ], ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, _) => Center(child: Text('Failed to load tickets: $error')), ), ), floatingActionButton: FloatingActionButton.extended( onPressed: () => _showCreateTicketDialog(context, ref), icon: const Icon(Icons.add), label: const Text('New Ticket'), ), ); } Future _showCreateTicketDialog( BuildContext context, WidgetRef ref, ) async { final subjectController = TextEditingController(); final descriptionController = TextEditingController(); Office? selectedOffice; await showDialog( context: context, builder: (dialogContext) { return StatefulBuilder( builder: (context, setState) { return AlertDialog( title: const Text('Create Ticket'), content: Consumer( builder: (context, ref, child) { final officesAsync = ref.watch(officesProvider); return SizedBox( width: 360, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: subjectController, decoration: const InputDecoration( labelText: 'Subject', ), ), const SizedBox(height: 12), TextField( controller: descriptionController, decoration: const InputDecoration( labelText: 'Description', ), maxLines: 3, ), const SizedBox(height: 12), officesAsync.when( data: (offices) { if (offices.isEmpty) { return const Text('No offices assigned.'); } selectedOffice ??= offices.first; return DropdownButtonFormField( key: ValueKey(selectedOffice?.id), initialValue: selectedOffice, items: offices .map( (office) => DropdownMenuItem( value: office, child: Text(office.name), ), ) .toList(), onChanged: (value) => setState(() => selectedOffice = value), decoration: const InputDecoration( labelText: 'Office', ), ); }, loading: () => const LinearProgressIndicator(), error: (error, _) => Text('Failed to load offices: $error'), ), ], ), ); }, ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: () async { final subject = subjectController.text.trim(); final description = descriptionController.text.trim(); if (subject.isEmpty || description.isEmpty || selectedOffice == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Fill out all fields.')), ); return; } await ref .read(ticketsControllerProvider) .createTicket( subject: subject, description: description, officeId: selectedOffice!.id, ); ref.invalidate(ticketsProvider); if (context.mounted) { Navigator.of(dialogContext).pop(); } }, child: const Text('Create'), ), ], ); }, ); }, ); } Map _unreadByTicketId( AsyncValue> notificationsAsync, ) { return notificationsAsync.maybeWhen( data: (items) { final map = {}; for (final item in items) { if (item.ticketId == null) continue; if (item.isUnread) { map[item.ticketId!] = true; } } return map; }, orElse: () => {}, ); } Widget _buildStatusChip(BuildContext context, String status) { return Chip( label: Text(status.toUpperCase()), backgroundColor: _statusColor(context, status), labelStyle: TextStyle( color: _statusTextColor(context, status), fontWeight: FontWeight.w600, ), ); } Color _statusColor(BuildContext context, String status) { return switch (status) { 'pending' => Colors.amber.shade300, 'promoted' => Colors.blue.shade300, 'closed' => Colors.green.shade300, _ => Theme.of(context).colorScheme.surfaceContainerHighest, }; } Color _statusTextColor(BuildContext context, String status) { return switch (status) { 'pending' => Colors.brown.shade900, 'promoted' => Colors.blue.shade900, 'closed' => Colors.green.shade900, _ => Theme.of(context).colorScheme.onSurfaceVariant, }; } }