import 'package:notas/models/note.dart'; import 'package:notas/models/category.dart'; import 'dart:convert'; // DTOs para sincronización con el servidor class SyncRequest { SyncRequest({DateTime? lastSyncAt, required this.changes}) : lastSyncAt = lastSyncAt ?? DateTime.utc(1970, 1, 1); final DateTime lastSyncAt; final SyncChanges changes; Map toJson() { return { 'lastSyncAt': lastSyncAt.toIso8601String(), 'changes': changes.toJson(), }; } } class SyncChanges { const SyncChanges({this.categories = const [], this.notes = const []}); final List categories; final List notes; Map toJson() { return { if (categories.isNotEmpty) 'categories': categories.map((c) => c.toJson()).toList(), if (notes.isNotEmpty) 'notes': notes.map((n) => n.toJson()).toList(), }; } } class SyncChangesResponse { const SyncChangesResponse({ this.categories = const [], this.notes = const [], }); final List categories; final List notes; factory SyncChangesResponse.fromJson(Map json) { final List categoriesJson = json['categories'] as List? ?? []; final List notesJson = json['notes'] as List? ?? []; return SyncChangesResponse( categories: categoriesJson .map((c) => SyncCategoryResponse.fromJson(c as Map)) .toList(), notes: notesJson .map((n) => SyncNoteResponse.fromJson(n as Map)) .toList(), ); } } String _readStringValue(dynamic value) { if (value is String) { return value; } if (value == null) { throw FormatException('Expected String value but found null'); } return jsonEncode(value); } String _readOptionalStringValue(dynamic value) { if (value == null) { return ''; } return _readStringValue(value); } int _readIntValue(dynamic value) { if (value is int) { return value; } if (value is String) { final int? parsed = int.tryParse(value); if (parsed != null) { return parsed; } } throw FormatException('Expected int value but found $value'); } class SyncCategoryPayload { const SyncCategoryPayload({ required this.id, required this.encryptedName, required this.serverVersion, this.isDeleted = false, this.colorValue, this.iconCodePoint, required this.updatedAt, }); final String id; final String encryptedName; final int serverVersion; final bool isDeleted; final int? colorValue; final int? iconCodePoint; final DateTime updatedAt; factory SyncCategoryPayload.fromCategory( Category category, { required String encryptedName, }) { return SyncCategoryPayload( id: category.id, encryptedName: encryptedName, serverVersion: category.serverVersion, isDeleted: category.isDeleted, colorValue: category.colorValue, iconCodePoint: category.iconCodePoint, updatedAt: category.updatedAt, ); } Map toJson() { return { 'id': id, 'encrypted_name': encryptedName, 'serverVersion': serverVersion, 'isDeleted': isDeleted, if (colorValue != null) 'colorValue': colorValue!.toSigned(32), if (iconCodePoint != null) 'iconCodePoint': iconCodePoint, 'updatedAt': updatedAt.toIso8601String(), }; } } class SyncNotePayload { const SyncNotePayload({ required this.id, this.categoryId, required this.encryptedTitle, required this.encryptedBody, required this.serverVersion, this.position = 0.0, this.isDeleted = false, this.isPermanentlyDeleted = false, required this.updatedAt, }); final String id; final String? categoryId; final String encryptedTitle; final String encryptedBody; final int serverVersion; final double position; final bool isDeleted; final bool isPermanentlyDeleted; final DateTime updatedAt; factory SyncNotePayload.fromNote( Note note, { required String encryptedTitle, required String encryptedBody, bool isPermanentlyDeleted = false, }) { return SyncNotePayload( id: note.id, categoryId: note.categoryId, encryptedTitle: encryptedTitle, encryptedBody: encryptedBody, serverVersion: note.serverVersion, position: note.position, isDeleted: note.isDeleted, isPermanentlyDeleted: isPermanentlyDeleted, updatedAt: note.updatedAt, ); } Map toJson() { return { 'id': id, if (categoryId != null) 'categoryId': categoryId, 'encrypted_title': encryptedTitle, 'encrypted_body': encryptedBody, 'serverVersion': serverVersion, if (position != 0) 'position': position, if (isDeleted) 'isDeleted': isDeleted, if (isPermanentlyDeleted) 'isPermanentlyDeleted': isPermanentlyDeleted, 'updatedAt': updatedAt.toIso8601String(), }; } } class SyncResponse { const SyncResponse({ required this.serverTimestamp, required this.synced, required this.changes, }); final DateTime serverTimestamp; final bool synced; final SyncChangesResponse changes; factory SyncResponse.fromJson(Map json) { return SyncResponse( serverTimestamp: DateTime.parse(json['serverTimestamp'] as String), synced: json['synced'] as bool? ?? false, changes: SyncChangesResponse.fromJson( json['changes'] as Map? ?? {}, ), ); } } class SyncCategoryResponse { const SyncCategoryResponse({ required this.id, required this.encryptedName, required this.serverVersion, this.isDeleted = false, this.colorValue, this.iconCodePoint, required this.updatedAt, }); final String id; final String encryptedName; final int serverVersion; final bool isDeleted; final int? colorValue; final int? iconCodePoint; final DateTime updatedAt; factory SyncCategoryResponse.fromJson(Map json) { return SyncCategoryResponse( id: _readStringValue(json['id']), encryptedName: _readStringValue(json['encrypted_name']), serverVersion: _readIntValue(json['serverVersion']), isDeleted: json['isDeleted'] as bool? ?? false, colorValue: json['colorValue'] == null ? null : _readIntValue(json['colorValue']), iconCodePoint: json['iconCodePoint'] == null ? null : _readIntValue(json['iconCodePoint']), updatedAt: DateTime.parse(json['updatedAt'] as String), ); } Category toCategory({required String name}) { return Category( id: id, name: name, serverVersion: serverVersion, isDeleted: isDeleted, colorValue: colorValue, iconCodePoint: iconCodePoint, updatedAt: updatedAt, isDirty: false, ); } } class SyncNoteResponse { const SyncNoteResponse({ required this.id, this.categoryId, required this.encryptedTitle, required this.encryptedBody, required this.serverVersion, this.position = 0.0, this.isDeleted = false, this.isPermanentlyDeleted = false, required this.updatedAt, }); final String id; final String? categoryId; final String encryptedTitle; final String encryptedBody; final int serverVersion; final double position; final bool isDeleted; final bool isPermanentlyDeleted; final DateTime updatedAt; factory SyncNoteResponse.fromJson(Map json) { return SyncNoteResponse( id: _readStringValue(json['id']), categoryId: json['categoryId'] == null ? null : _readStringValue(json['categoryId']), encryptedTitle: _readOptionalStringValue(json['encrypted_title']), encryptedBody: _readOptionalStringValue(json['encrypted_body']), serverVersion: _readIntValue(json['serverVersion']), position: (json['position'] as num?)?.toDouble() ?? 0, isDeleted: json['isDeleted'] as bool? ?? false, isPermanentlyDeleted: json['isPermanentlyDeleted'] as bool? ?? false, updatedAt: DateTime.parse(json['updatedAt'] as String), ); } Note toNote() { return Note( id: id, title: isPermanentlyDeleted ? '' : 'Encrypted', body: isPermanentlyDeleted ? '' : 'Encrypted', createdAt: updatedAt, updatedAt: updatedAt, position: position, serverVersion: serverVersion, isDeleted: isDeleted, categoryId: categoryId, isDirty: false, ); } }