Initial Commit: Duty Schedule and Attendance Logbook

This commit is contained in:
2026-03-07 10:16:28 +08:00
parent 73dc735cce
commit c6f536edeb
24 changed files with 3165 additions and 282 deletions
+19
View File
@@ -81,3 +81,22 @@ class AppSetting {
);
}
}
class RamadanConfig {
RamadanConfig({required this.enabled, required this.autoDetect});
final bool enabled;
final bool autoDetect;
factory RamadanConfig.fromJson(Map<String, dynamic> json) {
return RamadanConfig(
enabled: json['enabled'] as bool? ?? false,
autoDetect: json['auto_detect'] as bool? ?? true,
);
}
Map<String, dynamic> toJson() => {
'enabled': enabled,
'auto_detect': autoDetect,
};
}
+43
View File
@@ -0,0 +1,43 @@
import '../utils/app_time.dart';
class AttendanceLog {
AttendanceLog({
required this.id,
required this.userId,
required this.dutyScheduleId,
required this.checkInAt,
required this.checkInLat,
required this.checkInLng,
this.checkOutAt,
this.checkOutLat,
this.checkOutLng,
});
final String id;
final String userId;
final String dutyScheduleId;
final DateTime checkInAt;
final double checkInLat;
final double checkInLng;
final DateTime? checkOutAt;
final double? checkOutLat;
final double? checkOutLng;
bool get isCheckedOut => checkOutAt != null;
factory AttendanceLog.fromMap(Map<String, dynamic> map) {
return AttendanceLog(
id: map['id'] as String,
userId: map['user_id'] as String,
dutyScheduleId: map['duty_schedule_id'] as String,
checkInAt: AppTime.parse(map['check_in_at'] as String),
checkInLat: (map['check_in_lat'] as num).toDouble(),
checkInLng: (map['check_in_lng'] as num).toDouble(),
checkOutAt: map['check_out_at'] == null
? null
: AppTime.parse(map['check_out_at'] as String),
checkOutLat: (map['check_out_lat'] as num?)?.toDouble(),
checkOutLng: (map['check_out_lng'] as num?)?.toDouble(),
);
}
}
+33
View File
@@ -0,0 +1,33 @@
import '../utils/app_time.dart';
class ChatMessage {
ChatMessage({
required this.id,
required this.threadId,
required this.senderId,
required this.body,
required this.createdAt,
});
final String id;
final String threadId;
final String senderId;
final String body;
final DateTime createdAt;
factory ChatMessage.fromMap(Map<String, dynamic> map) {
return ChatMessage(
id: map['id'] as String,
threadId: map['thread_id'] as String,
senderId: map['sender_id'] as String,
body: map['body'] as String,
createdAt: AppTime.parse(map['created_at'] as String),
);
}
Map<String, dynamic> toJson() => {
'thread_id': threadId,
'sender_id': senderId,
'body': body,
};
}
+27
View File
@@ -0,0 +1,27 @@
import '../utils/app_time.dart';
class LivePosition {
LivePosition({
required this.userId,
required this.lat,
required this.lng,
required this.updatedAt,
required this.inPremise,
});
final String userId;
final double lat;
final double lng;
final DateTime updatedAt;
final bool inPremise;
factory LivePosition.fromMap(Map<String, dynamic> map) {
return LivePosition(
userId: map['user_id'] as String,
lat: (map['lat'] as num).toDouble(),
lng: (map['lng'] as num).toDouble(),
updatedAt: AppTime.parse(map['updated_at'] as String),
inPremise: map['in_premise'] as bool? ?? false,
);
}
}
+57
View File
@@ -0,0 +1,57 @@
import '../utils/app_time.dart';
class PassSlip {
PassSlip({
required this.id,
required this.userId,
required this.dutyScheduleId,
required this.reason,
required this.status,
required this.requestedAt,
this.approvedBy,
this.approvedAt,
this.slipStart,
this.slipEnd,
});
final String id;
final String userId;
final String dutyScheduleId;
final String reason;
final String status; // 'pending', 'approved', 'rejected', 'completed'
final DateTime requestedAt;
final String? approvedBy;
final DateTime? approvedAt;
final DateTime? slipStart;
final DateTime? slipEnd;
/// Whether the slip is active (approved but not yet completed).
bool get isActive => status == 'approved' && slipEnd == null;
/// Whether the active slip has exceeded 1 hour.
bool get isExceeded =>
isActive &&
slipStart != null &&
AppTime.now().difference(slipStart!) > const Duration(hours: 1);
factory PassSlip.fromMap(Map<String, dynamic> map) {
return PassSlip(
id: map['id'] as String,
userId: map['user_id'] as String,
dutyScheduleId: map['duty_schedule_id'] as String,
reason: map['reason'] as String,
status: map['status'] as String? ?? 'pending',
requestedAt: AppTime.parse(map['requested_at'] as String),
approvedBy: map['approved_by'] as String?,
approvedAt: map['approved_at'] == null
? null
: AppTime.parse(map['approved_at'] as String),
slipStart: map['slip_start'] == null
? null
: AppTime.parse(map['slip_start'] as String),
slipEnd: map['slip_end'] == null
? null
: AppTime.parse(map['slip_end'] as String),
);
}
}
+11 -1
View File
@@ -1,15 +1,25 @@
class Profile {
Profile({required this.id, required this.role, required this.fullName});
Profile({
required this.id,
required this.role,
required this.fullName,
this.religion = 'catholic',
this.allowTracking = false,
});
final String id;
final String role;
final String fullName;
final String religion;
final bool allowTracking;
factory Profile.fromMap(Map<String, dynamic> map) {
return Profile(
id: map['id'] as String,
role: map['role'] as String? ?? 'standard',
fullName: map['full_name'] as String? ?? '',
religion: map['religion'] as String? ?? 'catholic',
allowTracking: map['allow_tracking'] as bool? ?? false,
);
}
}