Added Task number

This commit is contained in:
2026-02-21 21:37:36 +08:00
parent 8bb69a80af
commit 6238c701c0
7 changed files with 272 additions and 14 deletions
+3
View File
@@ -6,6 +6,7 @@ class Task {
Task({
required this.id,
required this.ticketId,
required this.taskNumber,
required this.title,
required this.description,
required this.officeId,
@@ -28,6 +29,7 @@ class Task {
final String id;
final String? ticketId;
final String? taskNumber;
final String title;
final String description;
final String? officeId;
@@ -55,6 +57,7 @@ class Task {
return Task(
id: map['id'] as String,
ticketId: map['ticket_id'] as String?,
taskNumber: map['task_number'] as String?,
title: map['title'] as String? ?? 'Task',
description: map['description'] as String? ?? '',
officeId: map['office_id'] as String?,
+82 -8
View File
@@ -153,7 +153,8 @@ final tasksProvider = StreamProvider<List<Task>>((ref) {
.where(
(t) =>
t.title.toLowerCase().contains(q) ||
t.description.toLowerCase().contains(q),
t.description.toLowerCase().contains(q) ||
(t.taskNumber?.toLowerCase().contains(q) ?? false),
)
.toList();
}
@@ -253,15 +254,88 @@ class TasksController {
payload['request_category'] = requestCategory;
}
final data = await _client
.from('tasks')
.insert(payload)
.select('id')
.single();
final taskId = data['id'] as String?;
// Prefer server RPC that atomically generates `task_number` and inserts
// the task; fallback to client-side insert with retry on duplicate-key.
String? taskId;
String? assignedNumber;
try {
final rpcParams = {
'p_title': title,
'p_description': description,
'p_office_id': officeId,
'p_ticket_id': ticketId,
'p_request_type': requestType,
'p_request_type_other': requestTypeOther,
'p_request_category': requestCategory,
'p_creator_id': actorId,
};
// Retry RPC on duplicate-key (23505) errors which may occur
// transiently due to concurrent inserts; prefer RPC always.
const int rpcMaxAttempts = 3;
Map<String, dynamic>? rpcRow;
for (var attempt = 0; attempt < rpcMaxAttempts; attempt++) {
try {
final rpcRes = await _client
.rpc('insert_task_with_number', rpcParams)
.single();
if (rpcRes is Map) {
rpcRow = Map<String, dynamic>.from(rpcRes);
} else if (rpcRes is List &&
rpcRes.isNotEmpty &&
rpcRes.first is Map) {
rpcRow = Map<String, dynamic>.from(rpcRes.first as Map);
}
break;
} catch (err) {
final msg = err.toString();
final isDuplicateKey =
msg.contains('duplicate key value') || msg.contains('23505');
if (!isDuplicateKey || attempt == rpcMaxAttempts - 1) {
rethrow;
}
await Future.delayed(Duration(milliseconds: 150 * (attempt + 1)));
// retry
}
}
if (rpcRow != null) {
taskId = rpcRow['id'] as String?;
assignedNumber = rpcRow['task_number'] as String?;
}
// ignore: avoid_print
print('createTask via RPC assigned number=$assignedNumber id=$taskId');
} catch (e) {
// RPC not available or failed; fallback to client insert with retry
const int maxAttempts = 3;
Map<String, dynamic>? insertData;
for (var attempt = 0; attempt < maxAttempts; attempt++) {
try {
insertData = await _client
.from('tasks')
.insert(payload)
.select('id, task_number')
.single();
break;
} catch (err) {
final msg = err.toString();
final isDuplicateKey =
msg.contains('duplicate key value') || msg.contains('23505');
if (!isDuplicateKey || attempt == maxAttempts - 1) {
rethrow;
}
await Future.delayed(Duration(milliseconds: 150 * (attempt + 1)));
}
}
taskId = insertData == null ? null : insertData['id'] as String?;
assignedNumber = insertData == null
? null
: insertData['task_number'] as String?;
// ignore: avoid_print
print('createTask fallback assigned number=$assignedNumber id=$taskId');
}
if (taskId == null) return;
// Activity log: created
try {
await _client.from('task_activity_logs').insert({
'task_id': taskId,
+8 -2
View File
@@ -315,7 +315,9 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
Align(
alignment: Alignment.center,
child: Text(
task.title.isNotEmpty ? task.title : 'Task ${task.id}',
task.title.isNotEmpty
? task.title
: 'Task ${task.taskNumber ?? task.id}',
textAlign: TextAlign.center,
style: Theme.of(
context,
@@ -339,7 +341,11 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
children: [
_buildStatusChip(context, task, canUpdateStatus),
_MetaBadge(label: 'Office', value: officeName),
_MetaBadge(label: 'Task ID', value: task.id, isMono: true),
_MetaBadge(
label: 'Task #',
value: task.taskNumber ?? task.id,
isMono: true,
),
],
),
if (description.isNotEmpty) ...[
+7 -4
View File
@@ -250,9 +250,10 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
isLoading: false,
columns: [
TasQColumn<Task>(
header: 'Task ID',
header: 'Task #',
technical: true,
cellBuilder: (context, task) => Text(task.id),
cellBuilder: (context, task) =>
Text(task.taskNumber ?? task.id),
),
TasQColumn<Task>(
header: 'Subject',
@@ -326,7 +327,8 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
title: Text(
task.title.isNotEmpty
? task.title
: (ticket?.subject ?? 'Task ${task.id}'),
: (ticket?.subject ??
'Task ${task.taskNumber ?? task.id}'),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -335,7 +337,7 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
const SizedBox(height: 2),
Text('Assigned: $assigned'),
const SizedBox(height: 4),
MonoText('ID ${task.id}'),
MonoText('ID ${task.taskNumber ?? task.id}'),
const SizedBox(height: 2),
Text(_formatTimestamp(task.createdAt)),
],
@@ -663,6 +665,7 @@ List<Task> _applyTaskFilters(
: (ticket?.subject ?? 'Task ${task.id}');
if (query.isNotEmpty &&
!subject.toLowerCase().contains(query) &&
!(task.taskNumber?.toLowerCase().contains(query) ?? false) &&
!task.id.toLowerCase().contains(query)) {
return false;
}