import 'dart:convert'; /// Une ligne d'`AuditLog` telle que la rend `GET /api/Audit`. /// /// Le contrôleur renvoie les entités brutes (pas de DTO), donc les noms de champs /// sont ceux de `ManagerService.Data.AuditLog` sérialisés en camelCase. class AuditEntry { final String id; final String entityType; final String entityId; final String action; final String? userId; final String? instanceId; final DateTime timestamp; final String? oldValues; final String? newValues; const AuditEntry({ required this.id, required this.entityType, required this.entityId, required this.action, required this.userId, required this.instanceId, required this.timestamp, required this.oldValues, required this.newValues, }); factory AuditEntry.fromJson(Map json) => AuditEntry( id: (json['id'] ?? '').toString(), entityType: (json['entityType'] ?? '').toString(), entityId: (json['entityId'] ?? '').toString(), action: (json['action'] ?? '').toString(), userId: json['userId'] as String?, instanceId: json['instanceId'] as String?, timestamp: DateTime.tryParse((json['timestamp'] ?? '').toString())?.toLocal() ?? DateTime.fromMillisecondsSinceEpoch(0), oldValues: json['oldValues'] as String?, newValues: json['newValues'] as String?, ); /// Les colonnes réellement touchées, dans l'ordre alphabétique. /// /// `OldValues` est nul sur une création et `NewValues` sur une suppression : /// l'union des deux clés est la seule façon d'obtenir la liste complète sans /// supposer laquelle des deux est remplie. List get changes { final before = _decode(oldValues); final after = _decode(newValues); final keys = {...before.keys, ...after.keys}.toList()..sort(); return keys .map((k) => AuditFieldChange(k, _render(before[k]), _render(after[k]))) .toList(); } /// Un JSON illisible est traité comme absent : la ligne du journal reste /// consultable même si la sérialisation d'une colonne exotique a échoué côté serveur. static Map _decode(String? raw) { if (raw == null || raw.isEmpty) return const {}; try { final decoded = jsonDecode(raw); return decoded is Map ? decoded : const {}; } catch (_) { return const {}; } } /// Les valeurs multilingues arrivent en listes d'objets `{language, value}` : /// les rendre en JSON brut donnerait une ligne illisible dans le détail. static String? _render(dynamic value) { if (value == null) return null; if (value is String) return value; if (value is List) { final translations = value .whereType() .where((m) => m.containsKey('language') && m.containsKey('value')) .map((m) => '${m['language']} : ${m['value']}') .toList(); if (translations.isNotEmpty) return translations.join('\n'); } return jsonEncode(value); } } class AuditFieldChange { final String field; final String? before; final String? after; const AuditFieldChange(this.field, this.before, this.after); } class AuditPage { final int total; final int page; final int limit; final List items; const AuditPage({ required this.total, required this.page, required this.limit, required this.items, }); factory AuditPage.fromJson(Map json) => AuditPage( total: (json['total'] as num?)?.toInt() ?? 0, page: (json['page'] as num?)?.toInt() ?? 1, limit: (json['limit'] as num?)?.toInt() ?? 50, items: ((json['items'] as List?) ?? const []) .map((e) => AuditEntry.fromJson(e as Map)) .toList(), ); int get pageCount => limit == 0 ? 1 : (total + limit - 1) ~/ limit; }