Move push notification to client instead of db trigger

This commit is contained in:
2026-02-28 16:19:08 +08:00
parent dab43a7f30
commit 0a8e388757
8 changed files with 698 additions and 226 deletions
+234 -25
View File
@@ -396,8 +396,9 @@ class TasksController {
taskId = rpcRow['id'] as String?;
assignedNumber = rpcRow['task_number'] as String?;
}
// ignore: avoid_print
print('createTask via RPC assigned number=$assignedNumber id=$taskId');
debugPrint(
'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;
@@ -424,8 +425,9 @@ class TasksController {
assignedNumber = insertData == null
? null
: insertData['task_number'] as String?;
// ignore: avoid_print
print('createTask fallback assigned number=$assignedNumber id=$taskId');
debugPrint(
'createTask fallback assigned number=$assignedNumber id=$taskId',
);
}
if (taskId == null) return;
@@ -445,8 +447,7 @@ class TasksController {
await _autoAssignTask(taskId: taskId, officeId: officeId ?? '');
} catch (e, st) {
// keep creation successful but surface the error in logs for debugging
// ignore: avoid_print
print('autoAssignTask failed for task=$taskId: $e\n$st');
debugPrint('autoAssignTask failed for task=$taskId: $e\n$st');
}
unawaited(_notifyCreated(taskId: taskId, actorId: actorId));
@@ -467,8 +468,7 @@ class TasksController {
'tasks/$taskId/${DateTime.now().millisecondsSinceEpoch}.$extension';
try {
// debug: show upload path
// ignore: avoid_print
print('uploadActionImage uploading to path: $path');
debugPrint('uploadActionImage uploading to path: $path');
// perform the upload and capture whatever the SDK returns (it varies by platform)
final dynamic res;
if (kIsWeb) {
@@ -486,8 +486,7 @@ class TasksController {
await localFile.create();
await localFile.writeAsBytes(bytes);
} catch (e) {
// ignore: avoid_print
print('uploadActionImage failed writing temp file: $e');
debugPrint('uploadActionImage failed writing temp file: $e');
return null;
}
res = await _client.storage
@@ -498,9 +497,9 @@ class TasksController {
} catch (_) {}
}
// debug: inspect the response object/type
// ignore: avoid_print
print('uploadActionImage response type=${res.runtimeType} value=$res');
debugPrint(
'uploadActionImage response type=${res.runtimeType} value=$res',
);
// Some SDK methods return a simple String (path) on success, others
// return a StorageResponse with an error field. Avoid calling .error on a
@@ -509,18 +508,15 @@ class TasksController {
// treat as success
} else if (res is Map && res['error'] != null) {
// older versions might return a plain map
// ignore: avoid_print
print('uploadActionImage upload error: ${res['error']}');
debugPrint('uploadActionImage upload error: ${res['error']}');
return null;
} else if (res != null && res.error != null) {
// StorageResponse case
// ignore: avoid_print
print('uploadActionImage upload error: ${res.error}');
debugPrint('uploadActionImage upload error: ${res.error}');
return null;
}
} catch (e) {
// ignore: avoid_print
print('uploadActionImage failed upload: $e');
debugPrint('uploadActionImage failed upload: $e');
return null;
}
try {
@@ -528,8 +524,7 @@ class TasksController {
.from(_actionImageBucket)
.getPublicUrl(path);
// debug: log full response
// ignore: avoid_print
print('uploadActionImage getPublicUrl response: $urlRes');
debugPrint('uploadActionImage getPublicUrl response: $urlRes');
String? url;
if (urlRes is String) {
@@ -554,8 +549,7 @@ class TasksController {
return '$supabaseUrl/storage/v1/object/public/$_actionImageBucket/$path'
.trim();
} catch (e) {
// ignore: avoid_print
print('uploadActionImage getPublicUrl error: $e');
debugPrint('uploadActionImage getPublicUrl error: $e');
return null;
}
}
@@ -581,6 +575,90 @@ class TasksController {
)
.toList();
await _client.from('notifications').insert(rows);
// Send FCM pushes with meaningful text
try {
// resolve actor display name if possible
String actorName = 'Someone';
if (actorId != null && actorId.isNotEmpty) {
try {
final p = await _client
.from('profiles')
.select('full_name,display_name,name')
.eq('id', actorId)
.maybeSingle();
if (p != null) {
if (p['full_name'] != null) {
actorName = p['full_name'].toString();
} else if (p['display_name'] != null) {
actorName = p['display_name'].toString();
} else if (p['name'] != null) {
actorName = p['name'].toString();
}
}
} catch (_) {}
}
// fetch task_number and office_id for nicer message and deep-linking
String? taskNumber;
String? officeId;
try {
final t = await _client
.from('tasks')
.select('task_number, office_id')
.eq('id', taskId)
.maybeSingle();
if (t != null) {
if (t['task_number'] != null)
taskNumber = t['task_number'].toString();
if (t['office_id'] != null) officeId = t['office_id'].toString();
}
} catch (_) {}
// resolve office name when available
String? officeName;
if (officeId != null && officeId.isNotEmpty) {
try {
final o = await _client
.from('offices')
.select('name')
.eq('id', officeId)
.maybeSingle();
if (o != null && o['name'] != null)
officeName = o['name'].toString();
} catch (_) {}
}
final title = officeName != null
? '$actorName created a new task in $officeName'
: '$actorName created a new task';
final body = taskNumber != null
? (officeName != null
? '$actorName created task #$taskNumber in $officeName'
: '$actorName created task #$taskNumber')
: (officeName != null
? '$actorName created a new task in $officeName'
: '$actorName created a new task');
final dataPayload = <String, dynamic>{
'type': 'created',
if (taskNumber != null) 'task_number': taskNumber,
if (officeId != null) 'office_id': officeId,
if (officeName != null) 'office_name': officeName,
};
await _client.functions.invoke(
'send_fcm',
body: {
'user_ids': recipients,
'title': title,
'body': body,
'data': dataPayload,
},
);
} catch (e) {
// non-fatal: push failure should not break flow
debugPrint('notifyCreated push error: $e');
}
} catch (_) {
return;
}
@@ -919,11 +997,63 @@ class TasksController {
'task_id': taskId,
'type': 'assignment',
});
// send push for auto-assignment
try {
final actorName = 'Dispatcher';
final title = '$actorName assigned you a task';
final body = '$actorName assigned you a task';
// fetch task_number and office for nicer deep-linking when available
String? taskNumber;
String? officeId;
try {
final t = await _client
.from('tasks')
.select('task_number, office_id')
.eq('id', taskId)
.maybeSingle();
if (t != null) {
if (t['task_number'] != null)
taskNumber = t['task_number'].toString();
if (t['office_id'] != null) officeId = t['office_id'].toString();
}
} catch (_) {}
String? officeName;
if (officeId != null && officeId.isNotEmpty) {
try {
final o = await _client
.from('offices')
.select('name')
.eq('id', officeId)
.maybeSingle();
if (o != null && o['name'] != null)
officeName = o['name'].toString();
} catch (_) {}
}
final dataPayload = <String, dynamic>{
'type': 'assignment',
if (taskNumber != null) 'task_number': taskNumber,
if (officeId != null) 'office_id': officeId,
if (officeName != null) 'office_name': officeName,
};
await _client.functions.invoke(
'send_fcm',
body: {
'user_ids': [chosen.userId],
'title': title,
'body': body,
'data': dataPayload,
},
);
} catch (e) {
debugPrint('autoAssign push error: $e');
}
} catch (_) {}
} catch (e, st) {
// Log error for visibility and record a failed auto-assign activity
// ignore: avoid_print
print('autoAssignTask error for task=$taskId: $e\n$st');
debugPrint('autoAssignTask error for task=$taskId: $e\n$st');
try {
await _insertActivityRows(_client, {
'task_id': taskId,
@@ -1060,6 +1190,85 @@ class TaskAssignmentsController {
)
.toList();
await _client.from('notifications').insert(rows);
// send FCM pushes for explicit assignments
try {
String actorName = 'Someone';
if (actorId != null && actorId.isNotEmpty) {
try {
final p = await _client
.from('profiles')
.select('full_name,display_name,name')
.eq('id', actorId)
.maybeSingle();
if (p != null) {
if (p['full_name'] != null)
actorName = p['full_name'].toString();
else if (p['display_name'] != null)
actorName = p['display_name'].toString();
else if (p['name'] != null)
actorName = p['name'].toString();
}
} catch (_) {}
}
final title = '$actorName assigned you to a task';
final body = '$actorName has assigned you to a task';
// fetch task_number and office for nicer deep-linking when available
String? taskNumber;
String? officeId;
try {
final t = await _client
.from('tasks')
.select('task_number, office_id')
.eq('id', taskId)
.maybeSingle();
if (t != null) {
if (t['task_number'] != null)
taskNumber = t['task_number'].toString();
if (t['office_id'] != null) officeId = t['office_id'].toString();
}
} catch (_) {}
String? officeName;
if (officeId != null && officeId.isNotEmpty) {
try {
final o = await _client
.from('offices')
.select('name')
.eq('id', officeId)
.maybeSingle();
if (o != null && o['name'] != null)
officeName = o['name'].toString();
} catch (_) {}
}
final dataPayload = <String, dynamic>{
'type': 'assignment',
if (taskNumber != null) 'task_number': taskNumber,
if (ticketId != null) 'ticket_id': ticketId,
if (officeId != null) 'office_id': officeId,
if (officeName != null) 'office_name': officeName,
};
// include office name in title/body when possible
final displayTitle = officeName != null
? '$title in $officeName'
: title;
final displayBody = officeName != null ? '$body in $officeName' : body;
await _client.functions.invoke(
'send_fcm',
body: {
'user_ids': userIds,
'title': displayTitle,
'body': displayBody,
'data': dataPayload,
},
);
} catch (e) {
debugPrint('notifyAssigned push error: $e');
}
} catch (_) {
return;
}