Allow task completion even if signatories are still empty, put an indicator for completed tasks with incomplete details.

This commit is contained in:
2026-02-24 22:08:53 +08:00
parent 892fbee456
commit 546c254326
6 changed files with 212 additions and 23 deletions
+13
View File
@@ -53,6 +53,19 @@ class Task {
// JSON serialized rich text for action taken (Quill Delta JSON encoded)
final String? actionTaken;
/// Helper that indicates whether a completed task still has missing
/// metadata such as signatories or action details. The parameter is used
/// by UI to surface a warning icon/banner when a task has been closed but
/// the user skipped filling out all fields.
bool get hasIncompleteDetails {
if (status != 'completed') return false;
bool empty(String? v) => v == null || v.trim().isEmpty;
return empty(requestedBy) ||
empty(notedBy) ||
empty(receivedBy) ||
empty(actionTaken);
}
factory Task.fromMap(Map<String, dynamic> map) {
return Task(
id: map['id'] as String,
+10 -15
View File
@@ -562,7 +562,9 @@ class TasksController {
.from('tasks')
// include all columns that must be non-null/empty before completing
.select(
'request_type, request_category, requested_by, noted_by, received_by, action_taken',
// signatories are not needed for validation; action_taken is still
// required so we include it alongside the type/category fields.
'request_type, request_category, action_taken',
)
.eq('id', taskId)
.maybeSingle();
@@ -572,9 +574,6 @@ class TasksController {
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>[];
@@ -584,17 +583,13 @@ class TasksController {
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');
}
// signatories are no longer required for completion; they can be
// filled in later. we still require action taken to document what
// was done.
// if you want to enforce action taken you can uncomment this block,
// but current business rule only mandates request metadata. we keep
// action taken non-null for clarity.
if (action == null || (action is String && action.trim().isEmpty)) {
missing.add('action taken');
}
+22
View File
@@ -426,6 +426,28 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
const SizedBox(height: 12),
Text(description),
],
// warning banner for completed tasks with missing metadata
if (task.status == 'completed' && task.hasIncompleteDetails) ...[
const SizedBox(height: 12),
Row(
children: [
const Icon(
Icons.warning_amber_rounded,
color: Colors.orange,
),
const SizedBox(width: 6),
Expanded(
child: Text(
'Task completed but some details are still empty.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
),
],
const SizedBox(height: 16),
// Collapsible tabbed details: Assignees / Type & Category / Signatories
ExpansionTile(
+23 -1
View File
@@ -297,8 +297,21 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
),
TasQColumn<Task>(
header: 'Status',
cellBuilder: (context, task) =>
cellBuilder: (context, task) => Row(
mainAxisSize: MainAxisSize.min,
children: [
_StatusBadge(status: task.status),
if (task.status == 'completed' &&
task.hasIncompleteDetails) ...[
const SizedBox(width: 4),
const Icon(
Icons.warning_amber_rounded,
size: 16,
color: Colors.orange,
),
],
],
),
),
TasQColumn<Task>(
header: 'Timestamp',
@@ -352,6 +365,15 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
mainAxisSize: MainAxisSize.min,
children: [
_StatusBadge(status: task.status),
if (task.status == 'completed' &&
task.hasIncompleteDetails) ...[
const SizedBox(width: 4),
const Icon(
Icons.warning_amber_rounded,
size: 16,
color: Colors.orange,
),
],
if (showTyping) ...[
const SizedBox(width: 6),
TypingDots(