Added validation when task type, category, signatories and action taken are empty upon completing a task

This commit is contained in:
Marc Rejohn Castillano 2026-02-23 21:27:55 +08:00
parent 1074572905
commit 98355c3707
3 changed files with 90 additions and 12 deletions

View File

@ -556,22 +556,56 @@ class TasksController {
required String status, required String status,
}) async { }) async {
if (status == 'completed') { if (status == 'completed') {
// fetch current metadata to validate // fetch current metadata to validate several required fields
try { try {
final row = await _client final row = await _client
.from('tasks') .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) .eq('id', taskId)
.maybeSingle(); .maybeSingle();
final rt = row is Map ? row['request_type'] : null; if (row is! Map<String, dynamic>) {
final rc = row is Map ? row['request_category'] : null; throw Exception('Task not found');
if (rt == null || rc == null) { }
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 = <String>[];
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( 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) { } catch (e) {
// rethrow so callers can handle // rethrow so callers can handle (UI will display message)
rethrow; rethrow;
} }
} }

View File

@ -2731,9 +2731,16 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
// Update DB only Supabase realtime stream will emit the // Update DB only Supabase realtime stream will emit the
// updated task list, so explicit invalidation here causes a // updated task list, so explicit invalidation here causes a
// visible loading/refresh and is unnecessary. // visible loading/refresh and is unnecessary.
await ref try {
.read(tasksControllerProvider) await ref
.updateTaskStatus(taskId: task.id, status: value); .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 itemBuilder: (context) => _statusOptions
.map( .map(

View File

@ -90,8 +90,8 @@ void main() {
// note: controller expects SupabaseClient; using dynamic bypass. // note: controller expects SupabaseClient; using dynamic bypass.
}); });
test('cannot complete a task without request details', () async { test('cannot complete a task without required metadata', () async {
// insert a task with no metadata // insert a task with no metadata at all
final row = {'id': 'tsk-1', 'status': 'queued'}; final row = {'id': 'tsk-1', 'status': 'queued'};
fake.tables['tasks']!.add(row); 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<Exception>()),
);
// 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<Exception>()),
);
// 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 { test('completing after adding metadata succeeds', () async {
// insert a task // insert a task
final row = {'id': 'tsk-2', 'status': 'queued'}; final row = {'id': 'tsk-2', 'status': 'queued'};