Initial commit: TimeReg Flutter app med Firebase backend
- Opprettet Flutter-prosjekt med alle nødvendige avhengigheter - Implementert datamodeller (User, TimeRegistration, TariffProfile, Deviation, AuditLog) - Implementert tjenester (AuthService, TimeService) - Implementert Riverpod providers for state management - Opprettet autentiseringsskjermer (login, signup, reset password) - Opprettet hjemmeskjerm med timer-funksjonalitet - Opprettet placeholder-skjermer for historikk, rapporter og profil - Lagt til norsk dokumentasjon i README
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import '../models/user_model.dart';
|
||||
|
||||
class AuthService {
|
||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
|
||||
// Hent nåværende bruker
|
||||
User? get currentUser => _auth.currentUser;
|
||||
|
||||
// Stream av autentiseringstilstand
|
||||
Stream<User?> get authStateChanges => _auth.authStateChanges();
|
||||
|
||||
// Registrer ny bruker med e-post og passord
|
||||
Future<UserCredential> signUpWithEmail({
|
||||
required String email,
|
||||
required String password,
|
||||
required String displayName,
|
||||
required String organizationId,
|
||||
String? departmentId,
|
||||
}) async {
|
||||
try {
|
||||
// Opprett bruker i Firebase Auth
|
||||
final userCredential = await _auth.createUserWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
// Oppdater displayName
|
||||
await userCredential.user?.updateDisplayName(displayName);
|
||||
|
||||
// Opprett brukerdata i Firestore
|
||||
await _createUserDocument(
|
||||
uid: userCredential.user!.uid,
|
||||
email: email,
|
||||
displayName: displayName,
|
||||
organizationId: organizationId,
|
||||
departmentId: departmentId,
|
||||
);
|
||||
|
||||
return userCredential;
|
||||
} on FirebaseAuthException catch (e) {
|
||||
throw _handleAuthException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Logg inn med e-post og passord
|
||||
Future<UserCredential> signInWithEmail({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
try {
|
||||
return await _auth.signInWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
} on FirebaseAuthException catch (e) {
|
||||
throw _handleAuthException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Tilbakestill passord
|
||||
Future<void> resetPassword(String email) async {
|
||||
try {
|
||||
await _auth.sendPasswordResetEmail(email: email);
|
||||
} on FirebaseAuthException catch (e) {
|
||||
throw _handleAuthException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Logg ut
|
||||
Future<void> signOut() async {
|
||||
await _auth.signOut();
|
||||
}
|
||||
|
||||
// Hent brukerdata fra Firestore
|
||||
Future<UserModel?> getUserData(String uid) async {
|
||||
try {
|
||||
final doc = await _firestore.collection('users').doc(uid).get();
|
||||
if (doc.exists) {
|
||||
return UserModel.fromFirestore(doc);
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
throw Exception('Kunne ikke hente brukerdata: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Stream av brukerdata
|
||||
Stream<UserModel?> userDataStream(String uid) {
|
||||
return _firestore
|
||||
.collection('users')
|
||||
.doc(uid)
|
||||
.snapshots()
|
||||
.map((doc) => doc.exists ? UserModel.fromFirestore(doc) : null);
|
||||
}
|
||||
|
||||
// Opprett brukerdokument i Firestore
|
||||
Future<void> _createUserDocument({
|
||||
required String uid,
|
||||
required String email,
|
||||
required String displayName,
|
||||
required String organizationId,
|
||||
String? departmentId,
|
||||
}) async {
|
||||
// Hent standard tariffprofil for organisasjonen
|
||||
final defaultProfile = await _getDefaultTariffProfile(organizationId);
|
||||
|
||||
final userData = UserModel(
|
||||
uid: uid,
|
||||
email: email,
|
||||
displayName: displayName,
|
||||
role: UserRole.employee,
|
||||
tariffProfileId: defaultProfile,
|
||||
organizationId: organizationId,
|
||||
departmentId: departmentId,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
preferences: UserPreferences(),
|
||||
);
|
||||
|
||||
await _firestore.collection('users').doc(uid).set(userData.toFirestore());
|
||||
}
|
||||
|
||||
// Hent standard tariffprofil for organisasjon
|
||||
Future<String> _getDefaultTariffProfile(String organizationId) async {
|
||||
final query = await _firestore
|
||||
.collection('tariff_profiles')
|
||||
.where('organizationId', isEqualTo: organizationId)
|
||||
.where('isDefault', isEqualTo: true)
|
||||
.limit(1)
|
||||
.get();
|
||||
|
||||
if (query.docs.isNotEmpty) {
|
||||
return query.docs.first.id;
|
||||
}
|
||||
|
||||
// Hvis ingen standard profil finnes, returner en placeholder
|
||||
return 'default_aml';
|
||||
}
|
||||
|
||||
// Håndter Firebase Auth exceptions
|
||||
String _handleAuthException(FirebaseAuthException e) {
|
||||
switch (e.code) {
|
||||
case 'weak-password':
|
||||
return 'Passordet er for svakt';
|
||||
case 'email-already-in-use':
|
||||
return 'E-postadressen er allerede i bruk';
|
||||
case 'invalid-email':
|
||||
return 'Ugyldig e-postadresse';
|
||||
case 'user-not-found':
|
||||
return 'Ingen bruker funnet med denne e-postadressen';
|
||||
case 'wrong-password':
|
||||
return 'Feil passord';
|
||||
case 'user-disabled':
|
||||
return 'Denne brukerkontoen er deaktivert';
|
||||
case 'too-many-requests':
|
||||
return 'For mange forsøk. Prøv igjen senere';
|
||||
case 'operation-not-allowed':
|
||||
return 'Denne operasjonen er ikke tillatt';
|
||||
default:
|
||||
return 'En feil oppstod: ${e.message}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../models/time_registration.dart';
|
||||
|
||||
class TimeService {
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
final Uuid _uuid = const Uuid();
|
||||
|
||||
// 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,
|
||||
);
|
||||
|
||||
await _firestore
|
||||
.collection('time_registrations')
|
||||
.doc(registration.id)
|
||||
.set(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 {
|
||||
final doc = await _firestore
|
||||
.collection('time_registrations')
|
||||
.doc(registrationId)
|
||||
.get();
|
||||
|
||||
if (!doc.exists) {
|
||||
throw Exception('Timeregistrering ikke funnet');
|
||||
}
|
||||
|
||||
final registration = TimeRegistration.fromFirestore(doc);
|
||||
final endTime = DateTime.now();
|
||||
final duration = endTime.difference(registration.startTime).inMinutes;
|
||||
|
||||
await _firestore
|
||||
.collection('time_registrations')
|
||||
.doc(registrationId)
|
||||
.update({
|
||||
'endTime': Timestamp.fromDate(endTime),
|
||||
'duration': duration,
|
||||
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||
'lastModifiedBy': userId,
|
||||
});
|
||||
} 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,
|
||||
);
|
||||
|
||||
await _firestore
|
||||
.collection('time_registrations')
|
||||
.doc(registration.id)
|
||||
.set(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 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
await _firestore
|
||||
.collection('time_registrations')
|
||||
.doc(registrationId)
|
||||
.update(updates);
|
||||
} catch (e) {
|
||||
throw Exception('Kunne ikke oppdatere timeregistrering: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Slett timeregistrering
|
||||
Future<void> deleteRegistration(String registrationId) async {
|
||||
try {
|
||||
await _firestore
|
||||
.collection('time_registrations')
|
||||
.doc(registrationId)
|
||||
.delete();
|
||||
} 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 {
|
||||
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 {
|
||||
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,
|
||||
}) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user