443 lines
14 KiB
Dart
443 lines
14 KiB
Dart
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,
|
|
required String organizationId,
|
|
RegistrationType type = RegistrationType.ordinary,
|
|
String? projectId,
|
|
String? customerId,
|
|
String? comment,
|
|
}) async {
|
|
try {
|
|
final registration = TimeRegistration(
|
|
id: _uuid.v4(),
|
|
userId: userId,
|
|
organizationId: organizationId,
|
|
startTime: DateTime.now(),
|
|
endTime: null, // Ikke avsluttet ennå
|
|
duration: 0,
|
|
type: type,
|
|
projectId: projectId,
|
|
customerId: customerId,
|
|
comment: comment,
|
|
breaks: [],
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
createdBy: userId,
|
|
isManual: false,
|
|
);
|
|
|
|
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) {
|
|
throw Exception('Kunne ikke starte timer: $e');
|
|
}
|
|
}
|
|
|
|
// Stopp pågående timeregistrering
|
|
Future<void> stopTimer(String registrationId, String userId) async {
|
|
try {
|
|
// 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();
|
|
|
|
// 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),
|
|
'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');
|
|
}
|
|
}
|
|
|
|
// Opprett manuell timeregistrering
|
|
Future<String> createManualEntry({
|
|
required String userId,
|
|
required String organizationId,
|
|
required DateTime startTime,
|
|
required DateTime endTime,
|
|
RegistrationType type = RegistrationType.ordinary,
|
|
List<TimeBreak> breaks = const [],
|
|
String? projectId,
|
|
String? customerId,
|
|
String? comment,
|
|
}) async {
|
|
try {
|
|
final duration = endTime.difference(startTime).inMinutes;
|
|
|
|
final registration = TimeRegistration(
|
|
id: _uuid.v4(),
|
|
userId: userId,
|
|
organizationId: organizationId,
|
|
startTime: startTime,
|
|
endTime: endTime,
|
|
duration: duration,
|
|
type: type,
|
|
projectId: projectId,
|
|
customerId: customerId,
|
|
comment: comment,
|
|
breaks: breaks,
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
createdBy: userId,
|
|
isManual: true,
|
|
);
|
|
|
|
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) {
|
|
throw Exception('Kunne ikke opprette timeregistrering: $e');
|
|
}
|
|
}
|
|
|
|
// Oppdater eksisterende timeregistrering
|
|
Future<void> updateRegistration({
|
|
required String registrationId,
|
|
required String userId,
|
|
DateTime? startTime,
|
|
DateTime? endTime,
|
|
RegistrationType? type,
|
|
List<TimeBreak>? breaks,
|
|
String? projectId,
|
|
String? customerId,
|
|
String? comment,
|
|
}) async {
|
|
try {
|
|
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 (_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,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
throw Exception('Kunne ikke oppdatere timeregistrering: $e');
|
|
}
|
|
}
|
|
|
|
// Slett timeregistrering
|
|
Future<void> deleteRegistration(String registrationId) async {
|
|
try {
|
|
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');
|
|
}
|
|
}
|
|
|
|
// Hent alle timeregistreringer for bruker i en periode
|
|
Future<List<TimeRegistration>> getRegistrations({
|
|
required String userId,
|
|
DateTime? startDate,
|
|
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);
|
|
|
|
if (startDate != null) {
|
|
query = query.where('startTime',
|
|
isGreaterThanOrEqualTo: Timestamp.fromDate(startDate));
|
|
}
|
|
if (endDate != null) {
|
|
query = query.where('startTime',
|
|
isLessThanOrEqualTo: Timestamp.fromDate(endDate));
|
|
}
|
|
|
|
final snapshot = await query.orderBy('startTime', descending: true).get();
|
|
return snapshot.docs
|
|
.map((doc) => TimeRegistration.fromFirestore(doc))
|
|
.toList();
|
|
} catch (e) {
|
|
throw Exception('Kunne ikke hente timeregistreringer: $e');
|
|
}
|
|
}
|
|
|
|
// Hent timeregistreringer for en spesifikk dag
|
|
Future<List<TimeRegistration>> getRegistrationsByDay({
|
|
required String userId,
|
|
required DateTime date,
|
|
}) async {
|
|
final startOfDay = DateTime(date.year, date.month, date.day);
|
|
final endOfDay = DateTime(date.year, date.month, date.day, 23, 59, 59);
|
|
|
|
return getRegistrations(
|
|
userId: userId,
|
|
startDate: startOfDay,
|
|
endDate: endOfDay,
|
|
);
|
|
}
|
|
|
|
// 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)
|
|
.where('endTime', isNull: true)
|
|
.limit(1)
|
|
.get();
|
|
|
|
if (snapshot.docs.isNotEmpty) {
|
|
return TimeRegistration.fromFirestore(snapshot.docs.first);
|
|
}
|
|
return null;
|
|
} catch (e) {
|
|
throw Exception('Kunne ikke hente aktiv timeregistrering: $e');
|
|
}
|
|
}
|
|
|
|
// Stream av timeregistreringer for bruker
|
|
Stream<List<TimeRegistration>> registrationsStream({
|
|
required String userId,
|
|
DateTime? startDate,
|
|
DateTime? endDate,
|
|
}) {
|
|
// TODO: Kombiner lokal cache og firestore stream
|
|
|
|
Query query = _firestore
|
|
.collection('time_registrations')
|
|
.where('userId', isEqualTo: userId);
|
|
|
|
if (startDate != null) {
|
|
query = query.where('startTime',
|
|
isGreaterThanOrEqualTo: Timestamp.fromDate(startDate));
|
|
}
|
|
if (endDate != null) {
|
|
query = query.where('startTime',
|
|
isLessThanOrEqualTo: Timestamp.fromDate(endDate));
|
|
}
|
|
|
|
return query.orderBy('startTime', descending: true).snapshots().map(
|
|
(snapshot) => snapshot.docs
|
|
.map((doc) => TimeRegistration.fromFirestore(doc))
|
|
.toList(),
|
|
);
|
|
}
|
|
|
|
// Beregn total arbeidstid for en periode
|
|
Future<Map<String, int>> calculateTotalHours({
|
|
required String userId,
|
|
required DateTime startDate,
|
|
required DateTime endDate,
|
|
}) async {
|
|
final registrations = await getRegistrations(
|
|
userId: userId,
|
|
startDate: startDate,
|
|
endDate: endDate,
|
|
);
|
|
|
|
int totalMinutes = 0;
|
|
int ordinaryMinutes = 0;
|
|
int overtimeMinutes = 0;
|
|
|
|
for (final reg in registrations) {
|
|
if (reg.endTime != null) {
|
|
totalMinutes += reg.netWorkMinutes;
|
|
if (reg.type == RegistrationType.ordinary) {
|
|
ordinaryMinutes += reg.netWorkMinutes;
|
|
} else if (reg.type == RegistrationType.overtime) {
|
|
overtimeMinutes += reg.netWorkMinutes;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
'total': totalMinutes,
|
|
'ordinary': ordinaryMinutes,
|
|
'overtime': overtimeMinutes,
|
|
};
|
|
}
|
|
|
|
// Hent avvik for organisasjon
|
|
Future<List<Deviation>> getDeviations({
|
|
required String organizationId,
|
|
bool? onlyUnacknowledged,
|
|
}) async {
|
|
try {
|
|
Query query = _firestore
|
|
.collection('deviations')
|
|
.where('organizationId', isEqualTo: organizationId);
|
|
|
|
if (onlyUnacknowledged == true) {
|
|
query = query.where('acknowledgedAt', isNull: true);
|
|
}
|
|
|
|
final snapshot = await query.orderBy('detectedAt', descending: true).get();
|
|
return snapshot.docs.map((doc) => Deviation.fromFirestore(doc)).toList();
|
|
} catch (e) {
|
|
throw Exception('Kunne ikke hente avvik: $e');
|
|
}
|
|
}
|
|
|
|
// Kvitter ut avvik
|
|
Future<void> acknowledgeDeviation({
|
|
required String deviationId,
|
|
required String userId,
|
|
String? comment,
|
|
}) async {
|
|
try {
|
|
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');
|
|
}
|
|
}
|
|
}
|