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,115 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
enum AuditAction {
|
||||
create,
|
||||
update,
|
||||
delete,
|
||||
}
|
||||
|
||||
enum EntityType {
|
||||
timeRegistration,
|
||||
user,
|
||||
tariffProfile,
|
||||
}
|
||||
|
||||
class AuditLog {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String organizationId;
|
||||
final AuditAction action;
|
||||
final EntityType entityType;
|
||||
final String entityId;
|
||||
final Map<String, dynamic>? before;
|
||||
final Map<String, dynamic>? after;
|
||||
final DateTime timestamp;
|
||||
final String? ipAddress;
|
||||
|
||||
AuditLog({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.organizationId,
|
||||
required this.action,
|
||||
required this.entityType,
|
||||
required this.entityId,
|
||||
this.before,
|
||||
this.after,
|
||||
required this.timestamp,
|
||||
this.ipAddress,
|
||||
});
|
||||
|
||||
factory AuditLog.fromFirestore(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
return AuditLog(
|
||||
id: doc.id,
|
||||
userId: data['userId'] ?? '',
|
||||
organizationId: data['organizationId'] ?? '',
|
||||
action: _parseAction(data['action']),
|
||||
entityType: _parseEntityType(data['entityType']),
|
||||
entityId: data['entityId'] ?? '',
|
||||
before: data['changes']?['before'],
|
||||
after: data['changes']?['after'],
|
||||
timestamp: (data['timestamp'] as Timestamp).toDate(),
|
||||
ipAddress: data['ipAddress'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'userId': userId,
|
||||
'organizationId': organizationId,
|
||||
'action': action.name,
|
||||
'entityType': entityType.name,
|
||||
'entityId': entityId,
|
||||
'changes': {
|
||||
'before': before,
|
||||
'after': after,
|
||||
},
|
||||
'timestamp': Timestamp.fromDate(timestamp),
|
||||
'ipAddress': ipAddress,
|
||||
};
|
||||
}
|
||||
|
||||
static AuditAction _parseAction(String? actionString) {
|
||||
switch (actionString) {
|
||||
case 'update':
|
||||
return AuditAction.update;
|
||||
case 'delete':
|
||||
return AuditAction.delete;
|
||||
default:
|
||||
return AuditAction.create;
|
||||
}
|
||||
}
|
||||
|
||||
static EntityType _parseEntityType(String? typeString) {
|
||||
switch (typeString) {
|
||||
case 'user':
|
||||
return EntityType.user;
|
||||
case 'tariffProfile':
|
||||
return EntityType.tariffProfile;
|
||||
default:
|
||||
return EntityType.timeRegistration;
|
||||
}
|
||||
}
|
||||
|
||||
String get actionDisplayName {
|
||||
switch (action) {
|
||||
case AuditAction.create:
|
||||
return 'Opprettet';
|
||||
case AuditAction.update:
|
||||
return 'Endret';
|
||||
case AuditAction.delete:
|
||||
return 'Slettet';
|
||||
}
|
||||
}
|
||||
|
||||
String get entityTypeDisplayName {
|
||||
switch (entityType) {
|
||||
case EntityType.timeRegistration:
|
||||
return 'Timeregistrering';
|
||||
case EntityType.user:
|
||||
return 'Bruker';
|
||||
case EntityType.tariffProfile:
|
||||
return 'Tariffprofil';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
enum DeviationType {
|
||||
dailyMax,
|
||||
weeklyMax,
|
||||
dailyRest,
|
||||
weeklyRest,
|
||||
averageExceeded,
|
||||
nightWork,
|
||||
}
|
||||
|
||||
enum DeviationSeverity {
|
||||
warning,
|
||||
violation,
|
||||
}
|
||||
|
||||
class DeviationMetadata {
|
||||
final double actualValue;
|
||||
final double limitValue;
|
||||
final String? period;
|
||||
|
||||
DeviationMetadata({
|
||||
required this.actualValue,
|
||||
required this.limitValue,
|
||||
this.period,
|
||||
});
|
||||
|
||||
factory DeviationMetadata.fromMap(Map<String, dynamic> map) {
|
||||
return DeviationMetadata(
|
||||
actualValue: (map['actualValue'] ?? 0).toDouble(),
|
||||
limitValue: (map['limitValue'] ?? 0).toDouble(),
|
||||
period: map['period'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'actualValue': actualValue,
|
||||
'limitValue': limitValue,
|
||||
'period': period,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Deviation {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String organizationId;
|
||||
final String timeRegistrationId;
|
||||
final DeviationType type;
|
||||
final DeviationSeverity severity;
|
||||
final String description;
|
||||
final DateTime detectedAt;
|
||||
final DateTime? acknowledgedAt;
|
||||
final String? acknowledgedBy;
|
||||
final DeviationMetadata metadata;
|
||||
|
||||
Deviation({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.organizationId,
|
||||
required this.timeRegistrationId,
|
||||
required this.type,
|
||||
required this.severity,
|
||||
required this.description,
|
||||
required this.detectedAt,
|
||||
this.acknowledgedAt,
|
||||
this.acknowledgedBy,
|
||||
required this.metadata,
|
||||
});
|
||||
|
||||
factory Deviation.fromFirestore(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
return Deviation(
|
||||
id: doc.id,
|
||||
userId: data['userId'] ?? '',
|
||||
organizationId: data['organizationId'] ?? '',
|
||||
timeRegistrationId: data['timeRegistrationId'] ?? '',
|
||||
type: _parseType(data['type']),
|
||||
severity: _parseSeverity(data['severity']),
|
||||
description: data['description'] ?? '',
|
||||
detectedAt: (data['detectedAt'] as Timestamp).toDate(),
|
||||
acknowledgedAt: data['acknowledgedAt'] != null
|
||||
? (data['acknowledgedAt'] as Timestamp).toDate()
|
||||
: null,
|
||||
acknowledgedBy: data['acknowledgedBy'],
|
||||
metadata: DeviationMetadata.fromMap(data['metadata'] ?? {}),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'userId': userId,
|
||||
'organizationId': organizationId,
|
||||
'timeRegistrationId': timeRegistrationId,
|
||||
'type': type.name,
|
||||
'severity': severity.name,
|
||||
'description': description,
|
||||
'detectedAt': Timestamp.fromDate(detectedAt),
|
||||
'acknowledgedAt':
|
||||
acknowledgedAt != null ? Timestamp.fromDate(acknowledgedAt!) : null,
|
||||
'acknowledgedBy': acknowledgedBy,
|
||||
'metadata': metadata.toMap(),
|
||||
};
|
||||
}
|
||||
|
||||
static DeviationType _parseType(String? typeString) {
|
||||
switch (typeString) {
|
||||
case 'dailyMax':
|
||||
return DeviationType.dailyMax;
|
||||
case 'weeklyMax':
|
||||
return DeviationType.weeklyMax;
|
||||
case 'dailyRest':
|
||||
return DeviationType.dailyRest;
|
||||
case 'weeklyRest':
|
||||
return DeviationType.weeklyRest;
|
||||
case 'averageExceeded':
|
||||
return DeviationType.averageExceeded;
|
||||
case 'nightWork':
|
||||
return DeviationType.nightWork;
|
||||
default:
|
||||
return DeviationType.dailyMax;
|
||||
}
|
||||
}
|
||||
|
||||
static DeviationSeverity _parseSeverity(String? severityString) {
|
||||
switch (severityString) {
|
||||
case 'violation':
|
||||
return DeviationSeverity.violation;
|
||||
default:
|
||||
return DeviationSeverity.warning;
|
||||
}
|
||||
}
|
||||
|
||||
String get typeDisplayName {
|
||||
switch (type) {
|
||||
case DeviationType.dailyMax:
|
||||
return 'Maks daglig arbeidstid';
|
||||
case DeviationType.weeklyMax:
|
||||
return 'Maks ukentlig arbeidstid';
|
||||
case DeviationType.dailyRest:
|
||||
return 'Daglig hviletid';
|
||||
case DeviationType.weeklyRest:
|
||||
return 'Ukentlig hviletid';
|
||||
case DeviationType.averageExceeded:
|
||||
return 'Gjennomsnittsberegning overskredet';
|
||||
case DeviationType.nightWork:
|
||||
return 'Nattarbeid';
|
||||
}
|
||||
}
|
||||
|
||||
bool get isAcknowledged => acknowledgedAt != null;
|
||||
|
||||
Deviation copyWith({
|
||||
String? userId,
|
||||
String? organizationId,
|
||||
String? timeRegistrationId,
|
||||
DeviationType? type,
|
||||
DeviationSeverity? severity,
|
||||
String? description,
|
||||
DateTime? detectedAt,
|
||||
DateTime? acknowledgedAt,
|
||||
String? acknowledgedBy,
|
||||
DeviationMetadata? metadata,
|
||||
}) {
|
||||
return Deviation(
|
||||
id: id,
|
||||
userId: userId ?? this.userId,
|
||||
organizationId: organizationId ?? this.organizationId,
|
||||
timeRegistrationId: timeRegistrationId ?? this.timeRegistrationId,
|
||||
type: type ?? this.type,
|
||||
severity: severity ?? this.severity,
|
||||
description: description ?? this.description,
|
||||
detectedAt: detectedAt ?? this.detectedAt,
|
||||
acknowledgedAt: acknowledgedAt ?? this.acknowledgedAt,
|
||||
acknowledgedBy: acknowledgedBy ?? this.acknowledgedBy,
|
||||
metadata: metadata ?? this.metadata,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
class NightWorkRules {
|
||||
final int startHour; // e.g., 21 for 9 PM
|
||||
final int endHour; // e.g., 6 for 6 AM
|
||||
final int maxNightHours;
|
||||
|
||||
NightWorkRules({
|
||||
required this.startHour,
|
||||
required this.endHour,
|
||||
required this.maxNightHours,
|
||||
});
|
||||
|
||||
factory NightWorkRules.fromMap(Map<String, dynamic> map) {
|
||||
return NightWorkRules(
|
||||
startHour: map['startHour'] ?? 21,
|
||||
endHour: map['endHour'] ?? 6,
|
||||
maxNightHours: map['maxNightHours'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'startHour': startHour,
|
||||
'endHour': endHour,
|
||||
'maxNightHours': maxNightHours,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TariffRules {
|
||||
final int maxDailyHours;
|
||||
final int maxWeeklyHours;
|
||||
final int minDailyRest; // in minutes
|
||||
final int minWeeklyRest; // in minutes
|
||||
final bool useAverageCalculation;
|
||||
final int? averagePeriodWeeks;
|
||||
final int? maxAverageHours;
|
||||
final NightWorkRules? nightWorkRules;
|
||||
|
||||
TariffRules({
|
||||
required this.maxDailyHours,
|
||||
required this.maxWeeklyHours,
|
||||
required this.minDailyRest,
|
||||
required this.minWeeklyRest,
|
||||
this.useAverageCalculation = false,
|
||||
this.averagePeriodWeeks,
|
||||
this.maxAverageHours,
|
||||
this.nightWorkRules,
|
||||
});
|
||||
|
||||
factory TariffRules.fromMap(Map<String, dynamic> map) {
|
||||
return TariffRules(
|
||||
maxDailyHours: map['maxDailyHours'] ?? 9,
|
||||
maxWeeklyHours: map['maxWeeklyHours'] ?? 40,
|
||||
minDailyRest: map['minDailyRest'] ?? 660, // 11 hours
|
||||
minWeeklyRest: map['minWeeklyRest'] ?? 2100, // 35 hours
|
||||
useAverageCalculation: map['useAverageCalculation'] ?? false,
|
||||
averagePeriodWeeks: map['averagePeriodWeeks'],
|
||||
maxAverageHours: map['maxAverageHours'],
|
||||
nightWorkRules: map['nightWorkRules'] != null
|
||||
? NightWorkRules.fromMap(map['nightWorkRules'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'maxDailyHours': maxDailyHours,
|
||||
'maxWeeklyHours': maxWeeklyHours,
|
||||
'minDailyRest': minDailyRest,
|
||||
'minWeeklyRest': minWeeklyRest,
|
||||
'useAverageCalculation': useAverageCalculation,
|
||||
'averagePeriodWeeks': averagePeriodWeeks,
|
||||
'maxAverageHours': maxAverageHours,
|
||||
'nightWorkRules': nightWorkRules?.toMap(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TariffProfile {
|
||||
final String id;
|
||||
final String name;
|
||||
final String description;
|
||||
final String organizationId;
|
||||
final TariffRules rules;
|
||||
final bool isDefault;
|
||||
final bool isActive;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
TariffProfile({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.organizationId,
|
||||
required this.rules,
|
||||
this.isDefault = false,
|
||||
this.isActive = true,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory TariffProfile.fromFirestore(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
return TariffProfile(
|
||||
id: doc.id,
|
||||
name: data['name'] ?? '',
|
||||
description: data['description'] ?? '',
|
||||
organizationId: data['organizationId'] ?? '',
|
||||
rules: TariffRules.fromMap(data['rules'] ?? {}),
|
||||
isDefault: data['isDefault'] ?? false,
|
||||
isActive: data['isActive'] ?? true,
|
||||
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'name': name,
|
||||
'description': description,
|
||||
'organizationId': organizationId,
|
||||
'rules': rules.toMap(),
|
||||
'isDefault': isDefault,
|
||||
'isActive': isActive,
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
TariffProfile copyWith({
|
||||
String? name,
|
||||
String? description,
|
||||
String? organizationId,
|
||||
TariffRules? rules,
|
||||
bool? isDefault,
|
||||
bool? isActive,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return TariffProfile(
|
||||
id: id,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
organizationId: organizationId ?? this.organizationId,
|
||||
rules: rules ?? this.rules,
|
||||
isDefault: isDefault ?? this.isDefault,
|
||||
isActive: isActive ?? this.isActive,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
// Predefined profiles
|
||||
static TariffProfile createDefaultAML(String organizationId) {
|
||||
return TariffProfile(
|
||||
id: 'default_aml',
|
||||
name: 'Ingen tariffavtale (AML Standard)',
|
||||
description: 'Standard arbeidsmiljøloven uten tariffavtale',
|
||||
organizationId: organizationId,
|
||||
rules: TariffRules(
|
||||
maxDailyHours: 9,
|
||||
maxWeeklyHours: 40,
|
||||
minDailyRest: 660, // 11 hours
|
||||
minWeeklyRest: 2100, // 35 hours
|
||||
),
|
||||
isDefault: true,
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
static TariffProfile createTariffA(String organizationId) {
|
||||
return TariffProfile(
|
||||
id: 'tariff_a',
|
||||
name: 'Tariffavtale A',
|
||||
description: 'Tariffavtale med utvidet arbeidstid og gjennomsnittsberegning',
|
||||
organizationId: organizationId,
|
||||
rules: TariffRules(
|
||||
maxDailyHours: 10,
|
||||
maxWeeklyHours: 48,
|
||||
minDailyRest: 660, // 11 hours
|
||||
minWeeklyRest: 2100, // 35 hours
|
||||
useAverageCalculation: true,
|
||||
averagePeriodWeeks: 8,
|
||||
maxAverageHours: 48,
|
||||
),
|
||||
isDefault: false,
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
enum RegistrationType {
|
||||
ordinary,
|
||||
overtime,
|
||||
oncall,
|
||||
travel,
|
||||
}
|
||||
|
||||
enum SyncStatus {
|
||||
synced,
|
||||
pending,
|
||||
conflict,
|
||||
}
|
||||
|
||||
class TimeBreak {
|
||||
final DateTime startTime;
|
||||
final DateTime endTime;
|
||||
final int duration; // in minutes
|
||||
|
||||
TimeBreak({
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.duration,
|
||||
});
|
||||
|
||||
factory TimeBreak.fromMap(Map<String, dynamic> map) {
|
||||
return TimeBreak(
|
||||
startTime: (map['startTime'] as Timestamp).toDate(),
|
||||
endTime: (map['endTime'] as Timestamp).toDate(),
|
||||
duration: map['duration'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'startTime': Timestamp.fromDate(startTime),
|
||||
'endTime': Timestamp.fromDate(endTime),
|
||||
'duration': duration,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TimeRegistration {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String organizationId;
|
||||
final DateTime startTime;
|
||||
final DateTime? endTime;
|
||||
final int duration; // in minutes
|
||||
final RegistrationType type;
|
||||
final String? projectId;
|
||||
final String? customerId;
|
||||
final String? comment;
|
||||
final List<TimeBreak> breaks;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final String createdBy;
|
||||
final String? lastModifiedBy;
|
||||
final List<String> deviations;
|
||||
final bool isManual;
|
||||
final SyncStatus syncStatus;
|
||||
|
||||
TimeRegistration({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.organizationId,
|
||||
required this.startTime,
|
||||
this.endTime,
|
||||
required this.duration,
|
||||
this.type = RegistrationType.ordinary,
|
||||
this.projectId,
|
||||
this.customerId,
|
||||
this.comment,
|
||||
this.breaks = const [],
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.createdBy,
|
||||
this.lastModifiedBy,
|
||||
this.deviations = const [],
|
||||
this.isManual = false,
|
||||
this.syncStatus = SyncStatus.synced,
|
||||
});
|
||||
|
||||
factory TimeRegistration.fromFirestore(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
return TimeRegistration(
|
||||
id: doc.id,
|
||||
userId: data['userId'] ?? '',
|
||||
organizationId: data['organizationId'] ?? '',
|
||||
startTime: (data['startTime'] as Timestamp).toDate(),
|
||||
endTime: data['endTime'] != null
|
||||
? (data['endTime'] as Timestamp).toDate()
|
||||
: null,
|
||||
duration: data['duration'] ?? 0,
|
||||
type: _parseType(data['type']),
|
||||
projectId: data['projectId'],
|
||||
customerId: data['customerId'],
|
||||
comment: data['comment'],
|
||||
breaks: (data['breaks'] as List<dynamic>?)
|
||||
?.map((b) => TimeBreak.fromMap(b as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||
createdBy: data['createdBy'] ?? '',
|
||||
lastModifiedBy: data['lastModifiedBy'],
|
||||
deviations: List<String>.from(data['deviations'] ?? []),
|
||||
isManual: data['isManual'] ?? false,
|
||||
syncStatus: _parseSyncStatus(data['syncStatus']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'userId': userId,
|
||||
'organizationId': organizationId,
|
||||
'startTime': Timestamp.fromDate(startTime),
|
||||
'endTime': endTime != null ? Timestamp.fromDate(endTime!) : null,
|
||||
'duration': duration,
|
||||
'type': type.name,
|
||||
'projectId': projectId,
|
||||
'customerId': customerId,
|
||||
'comment': comment,
|
||||
'breaks': breaks.map((b) => b.toMap()).toList(),
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||
'createdBy': createdBy,
|
||||
'lastModifiedBy': lastModifiedBy,
|
||||
'deviations': deviations,
|
||||
'isManual': isManual,
|
||||
'syncStatus': syncStatus.name,
|
||||
};
|
||||
}
|
||||
|
||||
static RegistrationType _parseType(String? typeString) {
|
||||
switch (typeString) {
|
||||
case 'overtime':
|
||||
return RegistrationType.overtime;
|
||||
case 'oncall':
|
||||
return RegistrationType.oncall;
|
||||
case 'travel':
|
||||
return RegistrationType.travel;
|
||||
default:
|
||||
return RegistrationType.ordinary;
|
||||
}
|
||||
}
|
||||
|
||||
static SyncStatus _parseSyncStatus(String? statusString) {
|
||||
switch (statusString) {
|
||||
case 'pending':
|
||||
return SyncStatus.pending;
|
||||
case 'conflict':
|
||||
return SyncStatus.conflict;
|
||||
default:
|
||||
return SyncStatus.synced;
|
||||
}
|
||||
}
|
||||
|
||||
int get totalBreakMinutes {
|
||||
return breaks.fold(0, (sum, b) => sum + b.duration);
|
||||
}
|
||||
|
||||
int get netWorkMinutes {
|
||||
return duration - totalBreakMinutes;
|
||||
}
|
||||
|
||||
bool get isActive {
|
||||
return endTime == null;
|
||||
}
|
||||
|
||||
TimeRegistration copyWith({
|
||||
String? userId,
|
||||
String? organizationId,
|
||||
DateTime? startTime,
|
||||
DateTime? endTime,
|
||||
int? duration,
|
||||
RegistrationType? type,
|
||||
String? projectId,
|
||||
String? customerId,
|
||||
String? comment,
|
||||
List<TimeBreak>? breaks,
|
||||
DateTime? updatedAt,
|
||||
String? lastModifiedBy,
|
||||
List<String>? deviations,
|
||||
bool? isManual,
|
||||
SyncStatus? syncStatus,
|
||||
}) {
|
||||
return TimeRegistration(
|
||||
id: id,
|
||||
userId: userId ?? this.userId,
|
||||
organizationId: organizationId ?? this.organizationId,
|
||||
startTime: startTime ?? this.startTime,
|
||||
endTime: endTime ?? this.endTime,
|
||||
duration: duration ?? this.duration,
|
||||
type: type ?? this.type,
|
||||
projectId: projectId ?? this.projectId,
|
||||
customerId: customerId ?? this.customerId,
|
||||
comment: comment ?? this.comment,
|
||||
breaks: breaks ?? this.breaks,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
createdBy: createdBy,
|
||||
lastModifiedBy: lastModifiedBy ?? this.lastModifiedBy,
|
||||
deviations: deviations ?? this.deviations,
|
||||
isManual: isManual ?? this.isManual,
|
||||
syncStatus: syncStatus ?? this.syncStatus,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
enum UserRole {
|
||||
employee,
|
||||
admin,
|
||||
systemAdmin,
|
||||
}
|
||||
|
||||
class UserModel {
|
||||
final String uid;
|
||||
final String email;
|
||||
final String displayName;
|
||||
final UserRole role;
|
||||
final String tariffProfileId;
|
||||
final String organizationId;
|
||||
final String? departmentId;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final bool isActive;
|
||||
final UserPreferences preferences;
|
||||
|
||||
UserModel({
|
||||
required this.uid,
|
||||
required this.email,
|
||||
required this.displayName,
|
||||
required this.role,
|
||||
required this.tariffProfileId,
|
||||
required this.organizationId,
|
||||
this.departmentId,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.isActive = true,
|
||||
required this.preferences,
|
||||
});
|
||||
|
||||
factory UserModel.fromFirestore(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
return UserModel(
|
||||
uid: doc.id,
|
||||
email: data['email'] ?? '',
|
||||
displayName: data['displayName'] ?? '',
|
||||
role: _parseRole(data['role']),
|
||||
tariffProfileId: data['tariffProfileId'] ?? '',
|
||||
organizationId: data['organizationId'] ?? '',
|
||||
departmentId: data['departmentId'],
|
||||
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||
isActive: data['isActive'] ?? true,
|
||||
preferences: UserPreferences.fromMap(data['preferences'] ?? {}),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'email': email,
|
||||
'displayName': displayName,
|
||||
'role': role.name,
|
||||
'tariffProfileId': tariffProfileId,
|
||||
'organizationId': organizationId,
|
||||
'departmentId': departmentId,
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||
'isActive': isActive,
|
||||
'preferences': preferences.toMap(),
|
||||
};
|
||||
}
|
||||
|
||||
static UserRole _parseRole(String? roleString) {
|
||||
switch (roleString) {
|
||||
case 'admin':
|
||||
return UserRole.admin;
|
||||
case 'systemAdmin':
|
||||
return UserRole.systemAdmin;
|
||||
default:
|
||||
return UserRole.employee;
|
||||
}
|
||||
}
|
||||
|
||||
UserModel copyWith({
|
||||
String? email,
|
||||
String? displayName,
|
||||
UserRole? role,
|
||||
String? tariffProfileId,
|
||||
String? organizationId,
|
||||
String? departmentId,
|
||||
DateTime? updatedAt,
|
||||
bool? isActive,
|
||||
UserPreferences? preferences,
|
||||
}) {
|
||||
return UserModel(
|
||||
uid: uid,
|
||||
email: email ?? this.email,
|
||||
displayName: displayName ?? this.displayName,
|
||||
role: role ?? this.role,
|
||||
tariffProfileId: tariffProfileId ?? this.tariffProfileId,
|
||||
organizationId: organizationId ?? this.organizationId,
|
||||
departmentId: departmentId ?? this.departmentId,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
isActive: isActive ?? this.isActive,
|
||||
preferences: preferences ?? this.preferences,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UserPreferences {
|
||||
final bool notificationsEnabled;
|
||||
final bool emailNotifications;
|
||||
final bool pushNotifications;
|
||||
|
||||
UserPreferences({
|
||||
this.notificationsEnabled = true,
|
||||
this.emailNotifications = true,
|
||||
this.pushNotifications = true,
|
||||
});
|
||||
|
||||
factory UserPreferences.fromMap(Map<String, dynamic> map) {
|
||||
return UserPreferences(
|
||||
notificationsEnabled: map['notificationsEnabled'] ?? true,
|
||||
emailNotifications: map['emailNotifications'] ?? true,
|
||||
pushNotifications: map['pushNotifications'] ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'notificationsEnabled': notificationsEnabled,
|
||||
'emailNotifications': emailNotifications,
|
||||
'pushNotifications': pushNotifications,
|
||||
};
|
||||
}
|
||||
|
||||
UserPreferences copyWith({
|
||||
bool? notificationsEnabled,
|
||||
bool? emailNotifications,
|
||||
bool? pushNotifications,
|
||||
}) {
|
||||
return UserPreferences(
|
||||
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
|
||||
emailNotifications: emailNotifications ?? this.emailNotifications,
|
||||
pushNotifications: pushNotifications ?? this.pushNotifications,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user