import 'package:flutter/material.dart'; import '../theme/m3_motion.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/profile.dart'; import '../providers/profile_provider.dart'; import '../providers/tasks_provider.dart'; import '../theme/app_surfaces.dart'; import '../utils/snackbar.dart'; class TaskAssignmentSection extends ConsumerWidget { const TaskAssignmentSection({ super.key, required this.taskId, required this.canAssign, }); final String taskId; final bool canAssign; @override Widget build(BuildContext context, WidgetRef ref) { final profilesAsync = ref.watch(profilesProvider); final tasksAsync = ref.watch(tasksProvider); final assignmentsAsync = ref.watch(taskAssignmentsProvider); final profiles = profilesAsync.valueOrNull ?? []; final tasks = tasksAsync.valueOrNull ?? []; final taskTicketId = tasks .where((task) => task.id == taskId) .map((task) => task.ticketId) .firstOrNull; final assignments = assignmentsAsync.valueOrNull ?? []; final itStaff = profiles.where((profile) => profile.role == 'it_staff').toList() ..sort((a, b) => a.fullName.compareTo(b.fullName)); final assignedForTask = assignments .where((assignment) => assignment.taskId == taskId) .toList(); final assignedIds = assignedForTask.map((a) => a.userId).toSet(); // With concurrent assignments allowed we no longer restrict the // eligible list based on whether the staff member already has an active // task. All IT staff can be assigned to any number of tasks at once. Keep // the original sort order derived from itStaff rather than filtering. final eligibleStaff = List.from(itStaff); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'Assigned IT Staff', style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), ), const Spacer(), if (canAssign) TextButton.icon( onPressed: () => _showAssignmentDialog( context, ref, eligibleStaff, assignedIds, taskTicketId, ), icon: const Icon(Icons.group_add), label: const Text('Assign'), ), ], ), const SizedBox(height: 8), if (assignedForTask.isEmpty) Text( 'No IT staff assigned.', style: Theme.of(context).textTheme.bodyMedium, ) else Wrap( spacing: 8, runSpacing: 6, children: assignedForTask.map((assignment) { final profile = profiles .where((item) => item.id == assignment.userId) .firstOrNull; final label = profile?.fullName.isNotEmpty == true ? profile!.fullName : assignment.userId; return InputChip( label: Text(label), onDeleted: canAssign ? () => ref .read(taskAssignmentsControllerProvider) .removeAssignment( taskId: taskId, userId: assignment.userId, ) : null, ); }).toList(), ), ], ); } Future _showAssignmentDialog( BuildContext context, WidgetRef ref, List eligibleStaff, Set assignedIds, String? taskTicketId, ) async { // If there are no IT staff at all we still need to bail out. We don't // consider vacancy anymore because everyone is eligible, so the only // reason for the dialog to be unusable is an empty staff list. if (eligibleStaff.isEmpty && assignedIds.isEmpty) { await m3ShowDialog( context: context, builder: (dialogContext) { return AlertDialog( shape: AppSurfaces.of(context).dialogShape, title: const Text('Assign IT Staff'), content: const Text('No IT staff available.'), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Close'), ), ], ); }, ); return; } final selection = assignedIds.toSet(); await m3ShowDialog( context: context, builder: (dialogContext) { var isSaving = false; return StatefulBuilder( builder: (context, setState) { return AlertDialog( shape: AppSurfaces.of(context).dialogShape, title: const Text('Assign IT Staff'), contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 12), content: SizedBox( width: 360, child: Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: ListView.builder( shrinkWrap: true, itemCount: eligibleStaff.length, itemBuilder: (context, index) { final staff = eligibleStaff[index]; final name = staff.fullName.isNotEmpty ? staff.fullName : staff.id; final selected = selection.contains(staff.id); return CheckboxListTile( value: selected, title: Text(name), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 2, ), onChanged: isSaving ? null : (value) { setState(() { if (value == true) { selection.add(staff.id); } else { selection.remove(staff.id); } }); }, ); }, ), ), ), actions: [ TextButton( onPressed: isSaving ? null : () => Navigator.of(dialogContext).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: isSaving ? null : () async { setState(() => isSaving = true); try { await ref .read(taskAssignmentsControllerProvider) .replaceAssignments( taskId: taskId, ticketId: taskTicketId, newUserIds: selection.toList(), currentUserIds: assignedIds.toList(), ); if (context.mounted) { showSuccessSnackBar( context, 'Assignment saved successfully', ); } if (context.mounted) { Navigator.of(dialogContext).pop(); } } catch (e) { if (context.mounted) { showErrorSnackBar( context, 'Failed to save assignment', ); } } finally { if (context.mounted) { setState(() => isSaving = false); } } }, child: isSaving ? SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.onPrimary, ), ), ) : const Text('Save'), ), ], ); }, ); }, ); } } extension _FirstOrNull on Iterable { T? get firstOrNull => isEmpty ? null : first; }