diff --git a/lib/providers/tasks_provider.dart b/lib/providers/tasks_provider.dart index 0fd7c123..9f6c36a9 100644 --- a/lib/providers/tasks_provider.dart +++ b/lib/providers/tasks_provider.dart @@ -556,22 +556,56 @@ class TasksController { required String status, }) async { if (status == 'completed') { - // fetch current metadata to validate + // fetch current metadata to validate several required fields try { final row = await _client .from('tasks') - .select('request_type, request_category') + // include all columns that must be non-null/empty before completing + .select( + 'request_type, request_category, requested_by, noted_by, received_by, action_taken', + ) .eq('id', taskId) .maybeSingle(); - final rt = row is Map ? row['request_type'] : null; - final rc = row is Map ? row['request_category'] : null; - if (rt == null || rc == null) { + if (row is! Map) { + throw Exception('Task not found'); + } + + final rt = row['request_type']; + final rc = row['request_category']; + final requested = row['requested_by']; + final noted = row['noted_by']; + final received = row['received_by']; + final action = row['action_taken']; + + final missing = []; + if (rt == null || (rt is String && rt.trim().isEmpty)) { + missing.add('request type'); + } + if (rc == null || (rc is String && rc.trim().isEmpty)) { + missing.add('request category'); + } + if (requested == null || + (requested is String && requested.trim().isEmpty)) { + missing.add('requested by'); + } + if (noted == null || (noted is String && noted.trim().isEmpty)) { + missing.add('noted by'); + } + if (received == null || + (received is String && received.trim().isEmpty)) { + missing.add('received by'); + } + if (action == null || (action is String && action.trim().isEmpty)) { + missing.add('action taken'); + } + + if (missing.isNotEmpty) { throw Exception( - 'Request type and category must be set before completing a task.', + 'The following fields must be set before completing a task: ${missing.join(', ')}.', ); } } catch (e) { - // rethrow so callers can handle + // rethrow so callers can handle (UI will display message) rethrow; } } diff --git a/lib/screens/tasks/task_detail_screen.dart b/lib/screens/tasks/task_detail_screen.dart index f7154cc7..9c81d7a3 100644 --- a/lib/screens/tasks/task_detail_screen.dart +++ b/lib/screens/tasks/task_detail_screen.dart @@ -2731,9 +2731,16 @@ class _TaskDetailScreenState extends ConsumerState // Update DB only — Supabase realtime stream will emit the // updated task list, so explicit invalidation here causes a // visible loading/refresh and is unnecessary. - await ref - .read(tasksControllerProvider) - .updateTaskStatus(taskId: task.id, status: value); + try { + await ref + .read(tasksControllerProvider) + .updateTaskStatus(taskId: task.id, status: value); + } catch (e) { + // surface validation or other errors to user + if (mounted) { + showErrorSnackBar(context, e.toString()); + } + } }, itemBuilder: (context) => _statusOptions .map( diff --git a/test/tasks_provider_test.dart b/test/tasks_provider_test.dart index de8e0ac4..c9eb41f7 100644 --- a/test/tasks_provider_test.dart +++ b/test/tasks_provider_test.dart @@ -90,8 +90,8 @@ void main() { // note: controller expects SupabaseClient; using dynamic bypass. }); - test('cannot complete a task without request details', () async { - // insert a task with no metadata + test('cannot complete a task without required metadata', () async { + // insert a task with no metadata at all final row = {'id': 'tsk-1', 'status': 'queued'}; fake.tables['tasks']!.add(row); @@ -101,6 +101,43 @@ void main() { ); }); + test('cannot complete when signatories or action taken missing', () async { + // insert a task that has the basic request metadata but nothing else + final row = { + 'id': 'tsk-3', + 'status': 'queued', + 'request_type': 'Repair', + 'request_category': 'Hardware', + }; + fake.tables['tasks']!.add(row); + + // still missing signatories/actionTaken + expect( + () => controller.updateTaskStatus(taskId: 'tsk-3', status: 'completed'), + throwsA(isA()), + ); + + // add signatories but actionTaken still missing + await controller.updateTask( + taskId: 'tsk-3', + requestedBy: 'Alice', + notedBy: 'Bob', + receivedBy: 'Carol', + ); + expect( + () => controller.updateTaskStatus(taskId: 'tsk-3', status: 'completed'), + throwsA(isA()), + ); + + // add action taken (empty JSON string) + await controller.updateTask(taskId: 'tsk-3', actionTaken: '{}'); + await controller.updateTaskStatus(taskId: 'tsk-3', status: 'completed'); + expect( + fake.tables['tasks']!.firstWhere((t) => t['id'] == 'tsk-3')['status'], + 'completed', + ); + }); + test('completing after adding metadata succeeds', () async { // insert a task final row = {'id': 'tsk-2', 'status': 'queued'};