From 56504b9e8aa9462331f065923525a888f10aa466 Mon Sep 17 00:00:00 2001 From: Marc Rejohn Castillano Date: Sun, 22 Feb 2026 14:14:16 +0800 Subject: [PATCH] Initial handling of activity logs --- lib/models/task_activity_log.dart | 88 ++++++++++++++++++++++++++++--- lib/providers/tasks_provider.dart | 51 ++++++++++++++---- 2 files changed, 124 insertions(+), 15 deletions(-) diff --git a/lib/models/task_activity_log.dart b/lib/models/task_activity_log.dart index 665deb0b..8d71b0e8 100644 --- a/lib/models/task_activity_log.dart +++ b/lib/models/task_activity_log.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import '../utils/app_time.dart'; class TaskActivityLog { @@ -18,13 +20,87 @@ class TaskActivityLog { final DateTime createdAt; factory TaskActivityLog.fromMap(Map map) { + // id and task_id may be returned as int or String depending on DB + final rawId = map['id']; + final rawTaskId = map['task_id']; + + String id = rawId == null ? '' : rawId.toString(); + String taskId = rawTaskId == null ? '' : rawTaskId.toString(); + + // actor_id is nullable + final actorId = map['actor_id']?.toString(); + + // action_type fallback + final actionType = (map['action_type'] as String?) ?? 'unknown'; + + // meta may be a Map, Map, JSON-encoded string, List, or null + Map? meta; + final rawMeta = map['meta']; + if (rawMeta is Map) { + meta = rawMeta; + } else if (rawMeta is Map) { + // convert dynamic-key map to Map + try { + meta = rawMeta.map((k, v) => MapEntry(k.toString(), v)); + } catch (_) { + meta = null; + } + } else if (rawMeta is String && rawMeta.isNotEmpty) { + try { + final decoded = jsonDecode(rawMeta); + if (decoded is Map) { + meta = decoded; + } else if (decoded is Map) { + meta = decoded.map((k, v) => MapEntry(k.toString(), v)); + } + } catch (_) { + meta = null; + } + } else { + meta = null; + } + + // created_at may be ISO string, DateTime, or numeric (seconds/millis since epoch) + final rawCreated = map['created_at']; + DateTime createdAt; + if (rawCreated is DateTime) { + createdAt = AppTime.toAppTime(rawCreated); + } else if (rawCreated is String) { + try { + createdAt = AppTime.parse(rawCreated); + } catch (_) { + createdAt = AppTime.now(); + } + } else if (rawCreated is int) { + // assume seconds or milliseconds + if (rawCreated > 1e12) { + // likely microseconds or nanoseconds - treat as milliseconds + createdAt = AppTime.toAppTime( + DateTime.fromMillisecondsSinceEpoch(rawCreated), + ); + } else if (rawCreated > 1e10) { + createdAt = AppTime.toAppTime( + DateTime.fromMillisecondsSinceEpoch(rawCreated), + ); + } else { + createdAt = AppTime.toAppTime( + DateTime.fromMillisecondsSinceEpoch(rawCreated * 1000), + ); + } + } else if (rawCreated is double) { + final asInt = rawCreated.toInt(); + createdAt = AppTime.toAppTime(DateTime.fromMillisecondsSinceEpoch(asInt)); + } else { + createdAt = AppTime.now(); + } + return TaskActivityLog( - id: map['id'] as String, - taskId: map['task_id'] as String, - actorId: map['actor_id'] as String?, - actionType: map['action_type'] as String? ?? 'unknown', - meta: map['meta'] as Map?, - createdAt: AppTime.parse(map['created_at'] as String), + id: id, + taskId: taskId, + actorId: actorId, + actionType: actionType, + meta: meta, + createdAt: createdAt, ); } } diff --git a/lib/providers/tasks_provider.dart b/lib/providers/tasks_provider.dart index 7b6731c9..0fd7c123 100644 --- a/lib/providers/tasks_provider.dart +++ b/lib/providers/tasks_provider.dart @@ -16,6 +16,39 @@ import 'tickets_provider.dart'; import 'user_offices_provider.dart'; import '../utils/app_time.dart'; +// Helper to insert activity log rows while sanitizing nulls and +// avoiding exceptions from malformed payloads. Accepts either a Map +// or a List. +Future _insertActivityRows(dynamic client, dynamic rows) async { + try { + if (rows == null) return; + if (rows is List) { + final sanitized = rows + .map((r) { + if (r is Map) { + final m = Map.from(r); + m.removeWhere((k, v) => v == null); + return m; + } + return null; + }) + .whereType>() + .toList(); + if (sanitized.isEmpty) return; + await client.from('task_activity_logs').insert(sanitized); + } else if (rows is Map) { + final m = Map.from(rows); + m.removeWhere((k, v) => v == null); + await client.from('task_activity_logs').insert(m); + } + } catch (e) { + // Log for debugging but don't rethrow to avoid breaking caller flows + try { + debugPrint('[insertActivityRows] insert failed: $e'); + } catch (_) {} + } +} + /// Task query parameters for server-side pagination and filtering. class TaskQuery { /// Creates task query parameters. @@ -337,7 +370,7 @@ class TasksController { if (taskId == null) return; try { - await _client.from('task_activity_logs').insert({ + await _insertActivityRows(_client, { 'task_id': taskId, 'actor_id': actorId, 'action_type': 'created', @@ -549,13 +582,13 @@ class TasksController { try { final actorId = _client.auth.currentUser?.id; if (status == 'in_progress') { - await _client.from('task_activity_logs').insert({ + await _insertActivityRows(_client, { 'task_id': taskId, 'actor_id': actorId, 'action_type': 'started', }); } else if (status == 'completed') { - await _client.from('task_activity_logs').insert({ + await _insertActivityRows(_client, { 'task_id': taskId, 'actor_id': actorId, 'action_type': 'completed', @@ -678,7 +711,7 @@ class TasksController { if (onDuty.isEmpty) { // record a failed auto-assign attempt for observability try { - await _client.from('task_activity_logs').insert({ + await _insertActivityRows(_client, { 'task_id': taskId, 'actor_id': null, 'action_type': 'auto_assign_failed', @@ -726,7 +759,7 @@ class TasksController { if (candidates.isEmpty) { try { - await _client.from('task_activity_logs').insert({ + await _insertActivityRows(_client, { 'task_id': taskId, 'actor_id': null, 'action_type': 'auto_assign_failed', @@ -752,7 +785,7 @@ class TasksController { }); try { - await _client.from('task_activity_logs').insert({ + await _insertActivityRows(_client, { 'task_id': taskId, 'actor_id': null, 'action_type': 'assigned', @@ -773,7 +806,7 @@ class TasksController { // ignore: avoid_print print('autoAssignTask error for task=$taskId: $e\n$st'); try { - await _client.from('task_activity_logs').insert({ + await _insertActivityRows(_client, { 'task_id': taskId, 'actor_id': null, 'action_type': 'auto_assign_failed', @@ -859,7 +892,7 @@ class TaskAssignmentsController { }, ) .toList(); - await _client.from('task_activity_logs').insert(logRows); + await _insertActivityRows(_client, logRows); } catch (_) { // non-fatal } @@ -876,7 +909,7 @@ class TaskAssignmentsController { // Record a reassignment event (who removed -> who added) try { final actorId = _client.auth.currentUser?.id; - await _client.from('task_activity_logs').insert({ + await _insertActivityRows(_client, { 'task_id': taskId, 'actor_id': actorId, 'action_type': 'reassigned',