Initial handling of activity logs
This commit is contained in:
parent
b581bdf7be
commit
56504b9e8a
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import '../utils/app_time.dart';
|
import '../utils/app_time.dart';
|
||||||
|
|
||||||
class TaskActivityLog {
|
class TaskActivityLog {
|
||||||
|
|
@ -18,13 +20,87 @@ class TaskActivityLog {
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
|
|
||||||
factory TaskActivityLog.fromMap(Map<String, dynamic> map) {
|
factory TaskActivityLog.fromMap(Map<String, dynamic> 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<dynamic,dynamic>, JSON-encoded string, List, or null
|
||||||
|
Map<String, dynamic>? meta;
|
||||||
|
final rawMeta = map['meta'];
|
||||||
|
if (rawMeta is Map<String, dynamic>) {
|
||||||
|
meta = rawMeta;
|
||||||
|
} else if (rawMeta is Map) {
|
||||||
|
// convert dynamic-key map to Map<String, dynamic>
|
||||||
|
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<String, dynamic>) {
|
||||||
|
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(
|
return TaskActivityLog(
|
||||||
id: map['id'] as String,
|
id: id,
|
||||||
taskId: map['task_id'] as String,
|
taskId: taskId,
|
||||||
actorId: map['actor_id'] as String?,
|
actorId: actorId,
|
||||||
actionType: map['action_type'] as String? ?? 'unknown',
|
actionType: actionType,
|
||||||
meta: map['meta'] as Map<String, dynamic>?,
|
meta: meta,
|
||||||
createdAt: AppTime.parse(map['created_at'] as String),
|
createdAt: createdAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,39 @@ import 'tickets_provider.dart';
|
||||||
import 'user_offices_provider.dart';
|
import 'user_offices_provider.dart';
|
||||||
import '../utils/app_time.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<Map>.
|
||||||
|
Future<void> _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<String, dynamic>.from(r);
|
||||||
|
m.removeWhere((k, v) => v == null);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.toList();
|
||||||
|
if (sanitized.isEmpty) return;
|
||||||
|
await client.from('task_activity_logs').insert(sanitized);
|
||||||
|
} else if (rows is Map) {
|
||||||
|
final m = Map<String, dynamic>.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.
|
/// Task query parameters for server-side pagination and filtering.
|
||||||
class TaskQuery {
|
class TaskQuery {
|
||||||
/// Creates task query parameters.
|
/// Creates task query parameters.
|
||||||
|
|
@ -337,7 +370,7 @@ class TasksController {
|
||||||
if (taskId == null) return;
|
if (taskId == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _client.from('task_activity_logs').insert({
|
await _insertActivityRows(_client, {
|
||||||
'task_id': taskId,
|
'task_id': taskId,
|
||||||
'actor_id': actorId,
|
'actor_id': actorId,
|
||||||
'action_type': 'created',
|
'action_type': 'created',
|
||||||
|
|
@ -549,13 +582,13 @@ class TasksController {
|
||||||
try {
|
try {
|
||||||
final actorId = _client.auth.currentUser?.id;
|
final actorId = _client.auth.currentUser?.id;
|
||||||
if (status == 'in_progress') {
|
if (status == 'in_progress') {
|
||||||
await _client.from('task_activity_logs').insert({
|
await _insertActivityRows(_client, {
|
||||||
'task_id': taskId,
|
'task_id': taskId,
|
||||||
'actor_id': actorId,
|
'actor_id': actorId,
|
||||||
'action_type': 'started',
|
'action_type': 'started',
|
||||||
});
|
});
|
||||||
} else if (status == 'completed') {
|
} else if (status == 'completed') {
|
||||||
await _client.from('task_activity_logs').insert({
|
await _insertActivityRows(_client, {
|
||||||
'task_id': taskId,
|
'task_id': taskId,
|
||||||
'actor_id': actorId,
|
'actor_id': actorId,
|
||||||
'action_type': 'completed',
|
'action_type': 'completed',
|
||||||
|
|
@ -678,7 +711,7 @@ class TasksController {
|
||||||
if (onDuty.isEmpty) {
|
if (onDuty.isEmpty) {
|
||||||
// record a failed auto-assign attempt for observability
|
// record a failed auto-assign attempt for observability
|
||||||
try {
|
try {
|
||||||
await _client.from('task_activity_logs').insert({
|
await _insertActivityRows(_client, {
|
||||||
'task_id': taskId,
|
'task_id': taskId,
|
||||||
'actor_id': null,
|
'actor_id': null,
|
||||||
'action_type': 'auto_assign_failed',
|
'action_type': 'auto_assign_failed',
|
||||||
|
|
@ -726,7 +759,7 @@ class TasksController {
|
||||||
|
|
||||||
if (candidates.isEmpty) {
|
if (candidates.isEmpty) {
|
||||||
try {
|
try {
|
||||||
await _client.from('task_activity_logs').insert({
|
await _insertActivityRows(_client, {
|
||||||
'task_id': taskId,
|
'task_id': taskId,
|
||||||
'actor_id': null,
|
'actor_id': null,
|
||||||
'action_type': 'auto_assign_failed',
|
'action_type': 'auto_assign_failed',
|
||||||
|
|
@ -752,7 +785,7 @@ class TasksController {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _client.from('task_activity_logs').insert({
|
await _insertActivityRows(_client, {
|
||||||
'task_id': taskId,
|
'task_id': taskId,
|
||||||
'actor_id': null,
|
'actor_id': null,
|
||||||
'action_type': 'assigned',
|
'action_type': 'assigned',
|
||||||
|
|
@ -773,7 +806,7 @@ class TasksController {
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print('autoAssignTask error for task=$taskId: $e\n$st');
|
print('autoAssignTask error for task=$taskId: $e\n$st');
|
||||||
try {
|
try {
|
||||||
await _client.from('task_activity_logs').insert({
|
await _insertActivityRows(_client, {
|
||||||
'task_id': taskId,
|
'task_id': taskId,
|
||||||
'actor_id': null,
|
'actor_id': null,
|
||||||
'action_type': 'auto_assign_failed',
|
'action_type': 'auto_assign_failed',
|
||||||
|
|
@ -859,7 +892,7 @@ class TaskAssignmentsController {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
await _client.from('task_activity_logs').insert(logRows);
|
await _insertActivityRows(_client, logRows);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// non-fatal
|
// non-fatal
|
||||||
}
|
}
|
||||||
|
|
@ -876,7 +909,7 @@ class TaskAssignmentsController {
|
||||||
// Record a reassignment event (who removed -> who added)
|
// Record a reassignment event (who removed -> who added)
|
||||||
try {
|
try {
|
||||||
final actorId = _client.auth.currentUser?.id;
|
final actorId = _client.auth.currentUser?.id;
|
||||||
await _client.from('task_activity_logs').insert({
|
await _insertActivityRows(_client, {
|
||||||
'task_id': taskId,
|
'task_id': taskId,
|
||||||
'actor_id': actorId,
|
'actor_id': actorId,
|
||||||
'action_type': 'reassigned',
|
'action_type': 'reassigned',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user