import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../../../core/auth/auth_store.dart';
import '../../../core/storage/local_storage.dart';
import '../../../core/sync/report_asset_sync.dart';
class ReportPhoto {
ReportPhoto({
required this.id,
required this.reportId,
required this.fileName,
required this.mimeType,
Uint8List? bytes,
String? dataBase64,
required this.createdAt,
}) : _bytes = bytes,
_legacyDataBase64 = dataBase64;
final String id;
final String reportId;
final String fileName;
final String mimeType;
final Uint8List? _bytes;
final String? _legacyDataBase64;
final DateTime createdAt;
Uint8List get bytes => _bytes ?? base64Decode(_legacyDataBase64 ?? '');
String get dataBase64 => _legacyDataBase64 ?? base64Encode(bytes);
Map<String, Object?> toJson({bool includeData = true}) => {
'id': id,
'reportId': reportId,
'fileName': fileName,
'mimeType': mimeType,
if (includeData) 'dataBase64': dataBase64,
'createdAt': createdAt.toIso8601String(),
};
factory ReportPhoto.fromJson(Map<String, dynamic> json) => ReportPhoto(
id: json['id'] as String,
reportId: json['reportId'] as String,
fileName: json['fileName'] as String,
mimeType: json['mimeType'] as String? ?? 'image/jpeg',
dataBase64: json['dataBase64'] as String?,
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
class ReportPhotoStore extends ChangeNotifier {
ReportPhotoStore._();
static final instance = ReportPhotoStore._();
static const _storageKey = 'machweis_report_photos_v1';
static const maxPhotoBytes = 6 * 1024 * 1024;
static const maxPhotosPerReport = 8;
final List<ReportPhoto> _photos = [];
List<ReportPhoto> photosFor(String reportId) => List.unmodifiable(
_photos.where((photo) => photo.reportId == reportId).toList()
..sort((a, b) => a.createdAt.compareTo(b.createdAt)),
);
ReportPhoto? photoById(String id) {
final index = _photos.indexWhere((photo) => photo.id == id);
return index == -1 ? null : _photos[index];
}
Future<void> initialize() async {
if (LocalStorage.instance.usesRelationalStore) {
final repository = LocalReportPhotoRepository();
final stored = await repository.readAll();
if (stored.isNotEmpty) {
_photos
..clear()
..addAll(stored);
return;
}
if (AuthStore.instance.session != null) {
_photos.clear();
notifyListeners();
return;
}
await _migrateLegacy(repository);
return;
}
final stored = await LocalStorage.instance.getString(_storageKey);
if (stored == null) return;
try {
final values = jsonDecode(stored) as List<dynamic>;
final restored = <ReportPhoto>[];
for (final value in values) {
final json = value as Map<String, dynamic>;
final id = json['id'] as String;
final bytes =
json['dataBase64'] as String? ??
await LocalStorage.instance.getString(_photoDataKey(id));
if (bytes != null) {
restored.add(ReportPhoto.fromJson({...json, 'dataBase64': bytes}));
}
}
_photos
..clear()
..addAll(restored);
// One-time browser migration: retain binary data under individual keys.
await _persist();
} on Object {
/* preserve recoverable legacy data */
}
}
Future<void> add(ReportPhoto photo) async {
if (!const {
'image/jpeg',
'image/png',
'image/webp',
}.contains(photo.mimeType.toLowerCase())) {
throw ArgumentError('Nur JPEG-, PNG- und WebP-Fotos sind zulässig.');
}
if (photo.bytes.lengthInBytes > maxPhotoBytes) {
throw ArgumentError('Das Foto ist größer als 6 MB.');
}
if (photosFor(photo.reportId).length >= maxPhotosPerReport) {
throw StateError('Pro Nachweis sind höchstens 8 Fotos möglich.');
}
_photos.removeWhere((item) => item.id == photo.id);
_photos.add(photo);
await _persist(photo);
await ReportAssetSyncController.instance.enqueuePhoto(
assetId: photo.id,
reportId: photo.reportId,
fileName: photo.fileName,
mimeType: photo.mimeType,
bytes: photo.bytes,
);
}
Future<void> remove(String id) async {
final index = _photos.indexWhere((photo) => photo.id == id);
if (index == -1) return;
final photo = _photos.removeAt(index);
if (LocalStorage.instance.usesRelationalStore) {
await LocalReportPhotoRepository().remove(id);
} else {
await LocalStorage.instance.remove(_photoDataKey(id));
await _persist();
}
await ReportAssetSyncController.instance.enqueueDelete(
assetId: id,
reportId: photo.reportId,
);
await ReportAssetSyncController.instance.synchronize();
}
Future<void> _migrateLegacy(LocalReportPhotoRepository repository) async {
final stored = await LocalStorage.instance.getString(_storageKey);
if (stored == null) return;
try {
final migrated = (jsonDecode(stored) as List<dynamic>)
.map((value) => ReportPhoto.fromJson(value as Map<String, dynamic>))
.toList();
for (final photo in migrated) {
await repository.save(photo);
}
_photos
..clear()
..addAll(migrated);
} on Object {
/* old JSON remains intact */
}
}
Future<void> _persist([ReportPhoto? photo]) async {
if (LocalStorage.instance.usesRelationalStore) {
if (photo != null) await LocalReportPhotoRepository().save(photo);
notifyListeners();
return;
}
if (photo != null) {
await LocalStorage.instance.setString(
_photoDataKey(photo.id),
photo.dataBase64,
);
}
await LocalStorage.instance.setString(
_storageKey,
jsonEncode(
_photos.map((photo) => photo.toJson(includeData: false)).toList(),
),
);
notifyListeners();
}
static String _photoDataKey(String id) => 'machweis_report_photo_data_$id';
}
class LocalReportPhotoRepository {
Future<List<ReportPhoto>> readAll() async {
final photos = <ReportPhoto>[];
final tenantId = AuthStore.instance.session?.tenantId;
for (final row in await LocalStorage.instance.mobileQuery(
'report_photos',
where: tenantId == null ? 'tenant_id IS NULL' : 'tenant_id = ?',
whereArgs: tenantId == null ? null : [tenantId],
)) {
final bytes = await LocalStorage.instance.photoBytes(row['id'] as String);
if (bytes != null) {
photos.add(
ReportPhoto(
id: row['id'] as String,
reportId: row['report_id'] as String,
fileName: row['file_name'] as String,
mimeType: row['mime_type'] as String,
bytes: bytes,
createdAt: DateTime.parse(row['created_at'] as String),
),
);
}
}
return photos;
}
Future<void> save(ReportPhoto photo) async {
await LocalStorage.instance.replaceRow('report_photos', {
'id': photo.id,
'tenant_id': AuthStore.instance.session?.tenantId,
'report_id': photo.reportId,
'file_name': photo.fileName,
'mime_type': photo.mimeType,
'created_at': photo.createdAt.toIso8601String(),
});
await LocalStorage.instance.replacePhotoBytes(photo.id, photo.bytes);
}
Future<void> remove(String id) {
final tenantId = AuthStore.instance.session?.tenantId;
return LocalStorage.instance.deleteRows(
'report_photos',
where: tenantId == null
? 'id = ? AND tenant_id IS NULL'
: 'id = ? AND tenant_id = ?',
whereArgs: tenantId == null ? [id] : [id, tenantId],
);
}
}
