Added saving indicator when assigning a task

This commit is contained in:
2026-02-24 21:49:25 +08:00
parent 5979a04254
commit 354b27aad1
2 changed files with 343 additions and 55 deletions
+72 -55
View File
@@ -5,6 +5,7 @@ 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({
@@ -39,36 +40,11 @@ class TaskAssignmentSection extends ConsumerWidget {
.toList();
final assignedIds = assignedForTask.map((a) => a.userId).toSet();
final activeTaskIds = tasks
.where(
(task) => task.status == 'queued' || task.status == 'in_progress',
)
.map((task) => task.id)
.toSet();
final activeAssignmentsByUser = <String, Set<String>>{};
for (final assignment in assignments) {
if (!activeTaskIds.contains(assignment.taskId)) {
continue;
}
activeAssignmentsByUser
.putIfAbsent(assignment.userId, () => <String>{})
.add(assignment.taskId);
}
bool isVacant(String userId) {
final active = activeAssignmentsByUser[userId];
if (active == null || active.isEmpty) {
return true;
}
return active.length == 1 && active.contains(taskId);
}
final eligibleStaff = itStaff
.where(
(profile) => isVacant(profile.id) || assignedIds.contains(profile.id),
)
.toList();
// 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<Profile>.from(itStaff);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -137,6 +113,9 @@ class TaskAssignmentSection extends ConsumerWidget {
Set<String> 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 showDialog<void>(
context: context,
@@ -144,7 +123,7 @@ class TaskAssignmentSection extends ConsumerWidget {
return AlertDialog(
shape: AppSurfaces.of(context).dialogShape,
title: const Text('Assign IT Staff'),
content: const Text('No vacant IT staff available.'),
content: const Text('No IT staff available.'),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
@@ -161,6 +140,7 @@ class TaskAssignmentSection extends ConsumerWidget {
await showDialog<void>(
context: context,
builder: (dialogContext) {
var isSaving = false;
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
@@ -187,15 +167,17 @@ class TaskAssignmentSection extends ConsumerWidget {
horizontal: 12,
vertical: 2,
),
onChanged: (value) {
setState(() {
if (value == true) {
selection.add(staff.id);
} else {
selection.remove(staff.id);
}
});
},
onChanged: isSaving
? null
: (value) {
setState(() {
if (value == true) {
selection.add(staff.id);
} else {
selection.remove(staff.id);
}
});
},
);
},
),
@@ -203,24 +185,59 @@ class TaskAssignmentSection extends ConsumerWidget {
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
onPressed: isSaving
? null
: () => Navigator.of(dialogContext).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () async {
await ref
.read(taskAssignmentsControllerProvider)
.replaceAssignments(
taskId: taskId,
ticketId: taskTicketId,
newUserIds: selection.toList(),
currentUserIds: assignedIds.toList(),
);
if (context.mounted) {
Navigator.of(dialogContext).pop();
}
},
child: const Text('Save'),
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'),
),
],
);