feat: Introduce offline synchronization for time registrations via a new sync service.

This commit is contained in:
steinhelge
2025-11-24 21:23:20 +01:00
parent 237d56066b
commit e05d694218
10 changed files with 367 additions and 76 deletions
+155 -73
View File
@@ -2,11 +2,16 @@ import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:uuid/uuid.dart';
import '../models/time_registration.dart';
import '../models/deviation.dart';
import '../models/sync_operation.dart';
import 'sync_service.dart';
class TimeService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final SyncService _syncService;
final Uuid _uuid = const Uuid();
TimeService(this._syncService);
// Start ny timeregistrering (stempling)
Future<String> startTimer({
required String userId,
@@ -35,10 +40,19 @@ class TimeService {
isManual: false,
);
await _firestore
.collection('time_registrations')
.doc(registration.id)
.set(registration.toFirestore());
if (_syncService.isOnline) {
await _firestore
.collection('time_registrations')
.doc(registration.id)
.set(registration.toFirestore());
} else {
await _syncService.queueOperation(
collection: 'time_registrations',
docId: registration.id,
action: SyncAction.create,
data: registration.toFirestore(),
);
}
return registration.id;
} catch (e) {
@@ -49,28 +63,63 @@ class TimeService {
// Stopp pågående timeregistrering
Future<void> stopTimer(String registrationId, String userId) async {
try {
final doc = await _firestore
.collection('time_registrations')
.doc(registrationId)
.get();
if (!doc.exists) {
throw Exception('Timeregistrering ikke funnet');
}
final registration = TimeRegistration.fromFirestore(doc);
// Hvis vi er offline, kan vi ikke hente dokumentet for å beregne varighet nøyaktig
// basert på server-data. Vi må stole på lokal logikk eller gjøre en "blind" oppdatering.
// For enkelhets skyld antar vi her at vi har start-tiden tilgjengelig i UI/State
// eller at vi gjør en update med serverTimestamp for slutt-tid.
// Men for offline-støtte må vi beregne alt lokalt.
// Dette krever at vi har den aktive registreringen lagret lokalt.
// For nå implementerer vi en enkel offline-støtte som bare køer oppdateringen.
// NB: Dette er en forenkling. I en fullverdig offline-app bør vi lese fra lokal cache.
final endTime = DateTime.now();
final duration = endTime.difference(registration.startTime).inMinutes;
await _firestore
.collection('time_registrations')
.doc(registrationId)
.update({
// Vi sender en oppdatering som setter endTime.
// Cloud Functions eller en senere sync vil måtte håndtere duration-beregning hvis vi ikke har start-tid.
// Men vent, vi kan ikke stole på at Cloud Functions kjører offline.
// Løsning: Vi gjør en 'update' operasjon.
final updates = {
'endTime': Timestamp.fromDate(endTime),
'duration': duration,
'updatedAt': Timestamp.fromDate(DateTime.now()),
'lastModifiedBy': userId,
});
// Vi kan ikke sette duration her uten å vite starttid.
// Hvis vi er online, henter vi og beregner.
};
if (_syncService.isOnline) {
final doc = await _firestore
.collection('time_registrations')
.doc(registrationId)
.get();
if (!doc.exists) {
throw Exception('Timeregistrering ikke funnet');
}
final registration = TimeRegistration.fromFirestore(doc);
final duration = endTime.difference(registration.startTime).inMinutes;
updates['duration'] = duration;
await _firestore
.collection('time_registrations')
.doc(registrationId)
.update(updates);
} else {
// Offline: Vi køer oppdateringen.
// NB: Duration vil mangle eller være feil hvis vi ikke har starttid lokalt.
// Dette er en begrensning i denne enkle implementasjonen.
// En bedre løsning ville være å cache aktive registreringer lokalt.
await _syncService.queueOperation(
collection: 'time_registrations',
docId: registrationId,
action: SyncAction.update,
data: updates,
);
}
} catch (e) {
throw Exception('Kunne ikke stoppe timer: $e');
}
@@ -109,10 +158,19 @@ class TimeService {
isManual: true,
);
await _firestore
.collection('time_registrations')
.doc(registration.id)
.set(registration.toFirestore());
if (_syncService.isOnline) {
await _firestore
.collection('time_registrations')
.doc(registration.id)
.set(registration.toFirestore());
} else {
await _syncService.queueOperation(
collection: 'time_registrations',
docId: registration.id,
action: SyncAction.create,
data: registration.toFirestore(),
);
}
return registration.id;
} catch (e) {
@@ -133,54 +191,46 @@ class TimeService {
String? comment,
}) async {
try {
final doc = await _firestore
.collection('time_registrations')
.doc(registrationId)
.get();
if (!doc.exists) {
throw Exception('Timeregistrering ikke funnet');
}
final registration = TimeRegistration.fromFirestore(doc);
final Map<String, dynamic> updates = {
'updatedAt': Timestamp.fromDate(DateTime.now()),
'lastModifiedBy': userId,
};
if (startTime != null) {
updates['startTime'] = Timestamp.fromDate(startTime);
}
if (endTime != null) {
updates['endTime'] = Timestamp.fromDate(endTime);
}
if (type != null) {
updates['type'] = type.name;
}
if (breaks != null) {
updates['breaks'] = breaks.map((b) => b.toMap()).toList();
}
if (projectId != null) {
updates['projectId'] = projectId;
}
if (customerId != null) {
updates['customerId'] = customerId;
}
if (comment != null) {
updates['comment'] = comment;
}
if (startTime != null) updates['startTime'] = Timestamp.fromDate(startTime);
if (endTime != null) updates['endTime'] = Timestamp.fromDate(endTime);
if (type != null) updates['type'] = type.name;
if (breaks != null) updates['breaks'] = breaks.map((b) => b.toMap()).toList();
if (projectId != null) updates['projectId'] = projectId;
if (customerId != null) updates['customerId'] = customerId;
if (comment != null) updates['comment'] = comment;
// Beregn ny varighet hvis start eller slutt er endret
final newStartTime = startTime ?? registration.startTime;
final newEndTime = endTime ?? registration.endTime;
if (newEndTime != null) {
updates['duration'] = newEndTime.difference(newStartTime).inMinutes;
if (_syncService.isOnline) {
// Hvis online, hent og beregn duration
if (startTime != null || endTime != null) {
final doc = await _firestore.collection('time_registrations').doc(registrationId).get();
if (doc.exists) {
final reg = TimeRegistration.fromFirestore(doc);
final newStart = startTime ?? reg.startTime;
final newEnd = endTime ?? reg.endTime;
if (newEnd != null) {
updates['duration'] = newEnd.difference(newStart).inMinutes;
}
}
}
await _firestore
.collection('time_registrations')
.doc(registrationId)
.update(updates);
} else {
// Offline: Kø oppdatering. Duration beregnes ikke her (begrensning).
await _syncService.queueOperation(
collection: 'time_registrations',
docId: registrationId,
action: SyncAction.update,
data: updates,
);
}
await _firestore
.collection('time_registrations')
.doc(registrationId)
.update(updates);
} catch (e) {
throw Exception('Kunne ikke oppdatere timeregistrering: $e');
}
@@ -189,10 +239,19 @@ class TimeService {
// Slett timeregistrering
Future<void> deleteRegistration(String registrationId) async {
try {
await _firestore
.collection('time_registrations')
.doc(registrationId)
.delete();
if (_syncService.isOnline) {
await _firestore
.collection('time_registrations')
.doc(registrationId)
.delete();
} else {
await _syncService.queueOperation(
collection: 'time_registrations',
docId: registrationId,
action: SyncAction.delete,
data: {},
);
}
} catch (e) {
throw Exception('Kunne ikke slette timeregistrering: $e');
}
@@ -205,6 +264,13 @@ class TimeService {
DateTime? endDate,
}) async {
try {
// TODO: Implementer lokal caching for lesing offline
// For nå kaster vi feil hvis offline og ingen cache
if (!_syncService.isOnline) {
// Her burde vi returnere fra lokal cache
// return [];
}
Query query = _firestore
.collection('time_registrations')
.where('userId', isEqualTo: userId);
@@ -245,6 +311,8 @@ class TimeService {
// Hent pågående timeregistrering
Future<TimeRegistration?> getActiveRegistration(String userId) async {
try {
// TODO: Sjekk lokal cache først
final snapshot = await _firestore
.collection('time_registrations')
.where('userId', isEqualTo: userId)
@@ -267,6 +335,8 @@ class TimeService {
DateTime? startDate,
DateTime? endDate,
}) {
// TODO: Kombiner lokal cache og firestore stream
Query query = _firestore
.collection('time_registrations')
.where('userId', isEqualTo: userId);
@@ -320,6 +390,7 @@ class TimeService {
'overtime': overtimeMinutes,
};
}
// Hent avvik for organisasjon
Future<List<Deviation>> getDeviations({
required String organizationId,
@@ -348,11 +419,22 @@ class TimeService {
String? comment,
}) async {
try {
await _firestore.collection('deviations').doc(deviationId).update({
final updates = {
'acknowledgedAt': Timestamp.now(),
'acknowledgedBy': userId,
if (comment != null) 'comment': comment,
});
};
if (_syncService.isOnline) {
await _firestore.collection('deviations').doc(deviationId).update(updates);
} else {
await _syncService.queueOperation(
collection: 'deviations',
docId: deviationId,
action: SyncAction.update,
data: updates,
);
}
} catch (e) {
throw Exception('Kunne ikke kvittere ut avvik: $e');
}