Offline Support

This commit is contained in:
2026-04-27 06:20:39 +08:00
parent 7d8851a94a
commit 48cd3bcd60
121 changed files with 14798 additions and 2214 deletions
+1 -111
View File
@@ -1,111 +1 @@
import '../utils/app_time.dart';
class Announcement {
Announcement({
required this.id,
required this.authorId,
required this.title,
required this.body,
required this.visibleRoles,
required this.isTemplate,
this.templateId,
required this.createdAt,
required this.updatedAt,
required this.bannerEnabled,
this.bannerShowAt,
this.bannerHideAt,
this.pushIntervalMinutes,
});
final String id;
final String authorId;
final String title;
final String body;
final List<String> visibleRoles;
final bool isTemplate;
final String? templateId;
final DateTime createdAt;
final DateTime updatedAt;
/// Whether a persistent banner is shown at the top of the announcements screen.
final bool bannerEnabled;
/// When the banner should start showing. [null] means immediately.
final DateTime? bannerShowAt;
/// When the banner should stop showing. [null] means it requires a manual
/// turn-off by the poster or an admin.
final DateTime? bannerHideAt;
/// How often (in minutes) a scheduled push notification is sent while the
/// banner is active. [null] means no scheduled push. Max is 1440 (daily).
final int? pushIntervalMinutes;
/// Whether the banner is currently active (visible) based on the current time.
bool get isBannerActive {
if (!bannerEnabled) return false;
final now = AppTime.now();
if (bannerShowAt != null && now.isBefore(bannerShowAt!)) return false;
if (bannerHideAt != null && now.isAfter(bannerHideAt!)) return false;
return true;
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Announcement &&
runtimeType == other.runtimeType &&
id == other.id &&
authorId == other.authorId &&
title == other.title &&
body == other.body &&
isTemplate == other.isTemplate &&
templateId == other.templateId &&
bannerEnabled == other.bannerEnabled &&
bannerShowAt == other.bannerShowAt &&
bannerHideAt == other.bannerHideAt &&
pushIntervalMinutes == other.pushIntervalMinutes &&
createdAt == other.createdAt &&
updatedAt == other.updatedAt;
@override
int get hashCode => Object.hash(
id,
authorId,
title,
body,
isTemplate,
templateId,
bannerEnabled,
bannerShowAt,
bannerHideAt,
pushIntervalMinutes,
createdAt,
updatedAt,
);
factory Announcement.fromMap(Map<String, dynamic> map) {
final rolesRaw = map['visible_roles'];
final roles = rolesRaw is List ? rolesRaw.cast<String>() : <String>[];
return Announcement(
id: map['id'] as String,
authorId: map['author_id'] as String,
title: map['title'] as String? ?? '',
body: map['body'] as String? ?? '',
visibleRoles: roles,
isTemplate: map['is_template'] as bool? ?? false,
templateId: map['template_id'] as String?,
createdAt: AppTime.parse(map['created_at'] as String),
updatedAt: AppTime.parse(map['updated_at'] as String),
bannerEnabled: map['banner_enabled'] as bool? ?? false,
bannerShowAt: map['banner_show_at'] != null
? AppTime.parse(map['banner_show_at'] as String)
: null,
bannerHideAt: map['banner_hide_at'] != null
? AppTime.parse(map['banner_hide_at'] as String)
: null,
pushIntervalMinutes: map['push_interval_minutes'] as int?,
);
}
}
export 'announcement.model.dart';
+95
View File
@@ -0,0 +1,95 @@
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_sqlite/brick_sqlite.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'announcements'),
)
class Announcement extends OfflineFirstWithSupabaseModel {
final String id;
final String authorId;
final String title;
final String body;
final List<String> visibleRoles;
final bool isTemplate;
final String? templateId;
final DateTime createdAt;
final DateTime updatedAt;
final bool bannerEnabled;
final DateTime? bannerShowAt;
final DateTime? bannerHideAt;
final int? pushIntervalMinutes;
@Supabase(ignore: true)
@Sqlite(ignore: true)
bool get isBannerActive {
if (!bannerEnabled) return false;
final now = AppTime.now();
if (bannerShowAt != null && now.isBefore(bannerShowAt!)) return false;
if (bannerHideAt != null && now.isAfter(bannerHideAt!)) return false;
return true;
}
Announcement({
required this.id,
required this.authorId,
required this.title,
required this.body,
required this.visibleRoles,
required this.isTemplate,
this.templateId,
required this.createdAt,
required this.updatedAt,
required this.bannerEnabled,
this.bannerShowAt,
this.bannerHideAt,
this.pushIntervalMinutes,
});
@Supabase(ignore: true)
@Sqlite(ignore: true)
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Announcement &&
runtimeType == other.runtimeType &&
id == other.id && authorId == other.authorId &&
title == other.title && body == other.body &&
isTemplate == other.isTemplate && templateId == other.templateId &&
bannerEnabled == other.bannerEnabled &&
bannerShowAt == other.bannerShowAt &&
bannerHideAt == other.bannerHideAt &&
pushIntervalMinutes == other.pushIntervalMinutes &&
createdAt == other.createdAt && updatedAt == other.updatedAt;
@Supabase(ignore: true)
@Sqlite(ignore: true)
@override
int get hashCode => Object.hash(id, authorId, title, body, isTemplate,
templateId, bannerEnabled, bannerShowAt, bannerHideAt,
pushIntervalMinutes, createdAt, updatedAt);
factory Announcement.fromMap(Map<String, dynamic> map) {
final rolesRaw = map['visible_roles'];
final roles = rolesRaw is List ? rolesRaw.cast<String>() : <String>[];
return Announcement(
id: map['id'] as String,
authorId: map['author_id'] as String,
title: map['title'] as String? ?? '',
body: map['body'] as String? ?? '',
visibleRoles: roles,
isTemplate: map['is_template'] as bool? ?? false,
templateId: map['template_id'] as String?,
createdAt: AppTime.parse(map['created_at'] as String),
updatedAt: AppTime.parse(map['updated_at'] as String),
bannerEnabled: map['banner_enabled'] as bool? ?? false,
bannerShowAt: map['banner_show_at'] != null
? AppTime.parse(map['banner_show_at'] as String) : null,
bannerHideAt: map['banner_hide_at'] != null
? AppTime.parse(map['banner_hide_at'] as String) : null,
pushIntervalMinutes: map['push_interval_minutes'] as int?,
);
}
}
+1 -47
View File
@@ -1,47 +1 @@
import '../utils/app_time.dart';
class AnnouncementComment {
AnnouncementComment({
required this.id,
required this.announcementId,
required this.authorId,
required this.body,
required this.createdAt,
});
final String id;
final String announcementId;
final String authorId;
final String body;
final DateTime createdAt;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AnnouncementComment &&
runtimeType == other.runtimeType &&
id == other.id &&
announcementId == other.announcementId &&
authorId == other.authorId &&
body == other.body &&
createdAt == other.createdAt;
@override
int get hashCode => Object.hash(
id,
announcementId,
authorId,
body,
createdAt,
);
factory AnnouncementComment.fromMap(Map<String, dynamic> map) {
return AnnouncementComment(
id: map['id'] as String,
announcementId: map['announcement_id'] as String,
authorId: map['author_id'] as String,
body: map['body'] as String? ?? '',
createdAt: AppTime.parse(map['created_at'] as String),
);
}
}
export 'announcement_comment.model.dart';
@@ -0,0 +1,51 @@
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_sqlite/brick_sqlite.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'announcement_comments'),
)
class AnnouncementComment extends OfflineFirstWithSupabaseModel {
final String id;
final String announcementId;
final String authorId;
final String body;
final DateTime createdAt;
AnnouncementComment({
required this.id,
required this.announcementId,
required this.authorId,
required this.body,
required this.createdAt,
});
@Supabase(ignore: true)
@Sqlite(ignore: true)
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AnnouncementComment &&
runtimeType == other.runtimeType &&
id == other.id && announcementId == other.announcementId &&
authorId == other.authorId && body == other.body &&
createdAt == other.createdAt;
@Supabase(ignore: true)
@Sqlite(ignore: true)
@override
int get hashCode =>
Object.hash(id, announcementId, authorId, body, createdAt);
factory AnnouncementComment.fromMap(Map<String, dynamic> map) {
return AnnouncementComment(
id: map['id'] as String,
announcementId: map['announcement_id'] as String,
authorId: map['author_id'] as String,
body: map['body'] as String? ?? '',
createdAt: AppTime.parse(map['created_at'] as String),
);
}
}
+1 -68
View File
@@ -1,68 +1 @@
import '../utils/app_time.dart';
class AttendanceLog {
AttendanceLog({
required this.id,
required this.userId,
required this.dutyScheduleId,
required this.shiftType,
required this.checkInAt,
required this.checkInLat,
required this.checkInLng,
this.checkOutAt,
this.checkOutLat,
this.checkOutLng,
this.justification,
this.checkOutJustification,
this.verificationStatus = 'pending',
this.checkInVerificationPhotoUrl,
this.checkOutVerificationPhotoUrl,
});
final String id;
final String userId;
final String dutyScheduleId;
final String shiftType;
final DateTime checkInAt;
final double checkInLat;
final double checkInLng;
final DateTime? checkOutAt;
final double? checkOutLat;
final double? checkOutLng;
final String? justification;
final String? checkOutJustification;
final String verificationStatus; // pending, verified, unverified, skipped
final String? checkInVerificationPhotoUrl;
final String? checkOutVerificationPhotoUrl;
bool get isCheckedOut => checkOutAt != null;
bool get isVerified => verificationStatus == 'verified';
bool get isUnverified =>
verificationStatus == 'unverified' || verificationStatus == 'skipped';
factory AttendanceLog.fromMap(Map<String, dynamic> map) {
// Support both old single-column and new dual-column schemas.
final legacyUrl = map['verification_photo_url'] as String?;
return AttendanceLog(
id: map['id'] as String,
userId: map['user_id'] as String,
dutyScheduleId: map['duty_schedule_id'] as String,
shiftType: map['shift_type'] as String? ?? 'normal',
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(),
justification: map['justification'] as String?,
checkOutJustification: map['check_out_justification'] as String?,
verificationStatus: map['verification_status'] as String? ?? 'pending',
checkInVerificationPhotoUrl:
map['check_in_verification_photo_url'] as String? ?? legacyUrl,
checkOutVerificationPhotoUrl:
map['check_out_verification_photo_url'] as String?,
);
}
}
export 'attendance_log.model.dart';
+79
View File
@@ -0,0 +1,79 @@
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_sqlite/brick_sqlite.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'attendance_logs'),
)
class AttendanceLog extends OfflineFirstWithSupabaseModel {
final String id;
final String userId;
final String dutyScheduleId;
final String shiftType;
final DateTime checkInAt;
final double checkInLat;
final double checkInLng;
final DateTime? checkOutAt;
final double? checkOutLat;
final double? checkOutLng;
final String? justification;
final String? checkOutJustification;
final String verificationStatus;
final String? checkInVerificationPhotoUrl;
final String? checkOutVerificationPhotoUrl;
@Supabase(ignore: true)
@Sqlite(ignore: true)
bool get isCheckedOut => checkOutAt != null;
@Supabase(ignore: true)
@Sqlite(ignore: true)
bool get isVerified => verificationStatus == 'verified';
@Supabase(ignore: true)
@Sqlite(ignore: true)
bool get isUnverified =>
verificationStatus == 'unverified' || verificationStatus == 'skipped';
AttendanceLog({
required this.id,
required this.userId,
required this.dutyScheduleId,
required this.shiftType,
required this.checkInAt,
required this.checkInLat,
required this.checkInLng,
this.checkOutAt,
this.checkOutLat,
this.checkOutLng,
this.justification,
this.checkOutJustification,
this.verificationStatus = 'pending',
this.checkInVerificationPhotoUrl,
this.checkOutVerificationPhotoUrl,
});
factory AttendanceLog.fromMap(Map<String, dynamic> map) {
final legacyUrl = map['verification_photo_url'] as String?;
return AttendanceLog(
id: map['id'] as String,
userId: map['user_id'] as String,
dutyScheduleId: map['duty_schedule_id'] as String,
shiftType: map['shift_type'] as String? ?? 'normal',
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(),
justification: map['justification'] as String?,
checkOutJustification: map['check_out_justification'] as String?,
verificationStatus: map['verification_status'] as String? ?? 'pending',
checkInVerificationPhotoUrl:
map['check_in_verification_photo_url'] as String? ?? legacyUrl,
checkOutVerificationPhotoUrl:
map['check_out_verification_photo_url'] as String?,
);
}
}
+1 -33
View File
@@ -1,33 +1 @@
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,
};
}
export 'chat_message.model.dart';
+39
View File
@@ -0,0 +1,39 @@
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'chat_messages'),
)
class ChatMessage extends OfflineFirstWithSupabaseModel {
final String id;
final String threadId;
final String senderId;
final String body;
final DateTime createdAt;
ChatMessage({
required this.id,
required this.threadId,
required this.senderId,
required this.body,
required this.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,
};
}
+1 -55
View File
@@ -1,55 +1 @@
import '../utils/app_time.dart';
class DutySchedule {
DutySchedule({
required this.id,
required this.userId,
required this.shiftType,
required this.startTime,
required this.endTime,
required this.status,
required this.createdAt,
required this.checkInAt,
required this.checkInLocation,
required this.relieverIds,
this.swapRequestId,
});
final String id;
final String userId;
final String shiftType;
final DateTime startTime;
final DateTime endTime;
final String status;
final DateTime createdAt;
final DateTime? checkInAt;
final Object? checkInLocation;
final List<String> relieverIds;
final String? swapRequestId;
factory DutySchedule.fromMap(Map<String, dynamic> map) {
final relieversRaw = map['reliever_ids'];
final relievers = relieversRaw is List
? relieversRaw
.where((e) => e != null)
.map((entry) => entry.toString())
.where((s) => s.isNotEmpty)
.toList()
: <String>[];
return DutySchedule(
id: map['id'] as String,
userId: map['user_id'] as String,
shiftType: map['shift_type'] as String? ?? 'normal',
startTime: AppTime.parse(map['start_time'] as String),
endTime: AppTime.parse(map['end_time'] as String),
status: map['status'] as String? ?? 'scheduled',
createdAt: AppTime.parse(map['created_at'] as String),
checkInAt: map['check_in_at'] == null
? null
: AppTime.parse(map['check_in_at'] as String),
checkInLocation: map['check_in_location'],
relieverIds: relievers,
swapRequestId: map['swap_request_id'] as String?,
);
}
}
export 'duty_schedule.model.dart';
+67
View File
@@ -0,0 +1,67 @@
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_sqlite/brick_sqlite.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'duty_schedules'),
)
class DutySchedule extends OfflineFirstWithSupabaseModel {
final String id;
final String userId;
final String shiftType;
final DateTime startTime;
final DateTime endTime;
final String status;
final DateTime createdAt;
final DateTime? checkInAt;
// PostGIS geometry — Brick cannot serialize Object; ignored in local cache.
@Supabase(ignore: true)
@Sqlite(ignore: true)
final Object? checkInLocation;
final List<String> relieverIds;
final String? swapRequestId;
DutySchedule({
required this.id,
required this.userId,
required this.shiftType,
required this.startTime,
required this.endTime,
required this.status,
required this.createdAt,
this.checkInAt,
this.checkInLocation,
required this.relieverIds,
this.swapRequestId,
});
factory DutySchedule.fromMap(Map<String, dynamic> map) {
final relieversRaw = map['reliever_ids'];
final relievers = relieversRaw is List
? relieversRaw
.where((e) => e != null)
.map((entry) => entry.toString())
.where((s) => s.isNotEmpty)
.toList()
: <String>[];
return DutySchedule(
id: map['id'] as String,
userId: map['user_id'] as String,
shiftType: map['shift_type'] as String? ?? 'normal',
startTime: AppTime.parse(map['start_time'] as String),
endTime: AppTime.parse(map['end_time'] as String),
status: map['status'] as String? ?? 'scheduled',
createdAt: AppTime.parse(map['created_at'] as String),
checkInAt: map['check_in_at'] == null
? null
: AppTime.parse(map['check_in_at'] as String),
checkInLocation: map['check_in_location'],
relieverIds: relievers,
swapRequestId: map['swap_request_id'] as String?,
);
}
}
+1 -259
View File
@@ -1,259 +1 @@
import 'dart:convert';
import '../utils/app_time.dart';
/// Available IT services from the form.
class ItServiceType {
static const fbLiveStream = 'fb_live_stream';
static const videoRecording = 'video_recording';
static const technicalAssistance = 'technical_assistance';
static const wifi = 'wifi';
static const others = 'others';
static const all = [
fbLiveStream,
videoRecording,
technicalAssistance,
wifi,
others,
];
static String label(String type) {
switch (type) {
case fbLiveStream:
return 'FB Live Stream';
case videoRecording:
return 'Video Recording';
case technicalAssistance:
return 'Technical Assistance';
case wifi:
return 'WiFi';
case others:
return 'Others';
default:
return type;
}
}
}
/// Status lifecycle for an IT Service Request.
class ItServiceRequestStatus {
static const draft = 'draft';
static const pendingApproval = 'pending_approval';
static const scheduled = 'scheduled';
static const inProgressDryRun = 'in_progress_dry_run';
static const inProgress = 'in_progress';
static const completed = 'completed';
static const cancelled = 'cancelled';
static const all = [
draft,
pendingApproval,
scheduled,
inProgressDryRun,
inProgress,
completed,
cancelled,
];
static String label(String status) {
switch (status) {
case draft:
return 'Draft';
case pendingApproval:
return 'Pending Approval';
case scheduled:
return 'Scheduled';
case inProgressDryRun:
return 'In Progress (Dry Run)';
case inProgress:
return 'In Progress';
case completed:
return 'Completed';
case cancelled:
return 'Cancelled';
default:
return status;
}
}
}
class ItServiceRequest {
ItServiceRequest({
required this.id,
this.requestNumber,
required this.services,
this.servicesOther,
required this.eventName,
this.eventDetails,
this.eventDate,
this.eventEndDate,
this.dryRunDate,
this.dryRunEndDate,
this.contactPerson,
this.contactNumber,
this.remarks,
this.officeId,
this.requestedBy,
this.requestedByUserId,
this.approvedBy,
this.approvedByUserId,
this.approvedAt,
required this.status,
required this.outsidePremiseAllowed,
this.cancellationReason,
this.cancelledAt,
this.creatorId,
required this.createdAt,
required this.updatedAt,
this.completedAt,
this.dateTimeReceived,
this.dateTimeChecked,
});
final String id;
final String? requestNumber;
final List<String> services;
final String? servicesOther;
final String eventName;
final String? eventDetails; // Quill Delta JSON
final DateTime? eventDate;
final DateTime? eventEndDate;
final DateTime? dryRunDate;
final DateTime? dryRunEndDate;
final String? contactPerson;
final String? contactNumber;
final String? remarks; // Quill Delta JSON
final String? officeId;
final String? requestedBy;
final String? requestedByUserId;
final String? approvedBy;
final String? approvedByUserId;
final DateTime? approvedAt;
final String status;
final bool outsidePremiseAllowed;
final String? cancellationReason;
final DateTime? cancelledAt;
final String? creatorId;
final DateTime createdAt;
final DateTime updatedAt;
final DateTime? completedAt;
final DateTime? dateTimeReceived;
final DateTime? dateTimeChecked;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ItServiceRequest &&
runtimeType == other.runtimeType &&
id == other.id &&
requestNumber == other.requestNumber &&
status == other.status &&
updatedAt == other.updatedAt;
@override
int get hashCode => Object.hash(id, requestNumber, status, updatedAt);
/// Whether the request is on a day that would allow geofence override.
bool isGeofenceOverrideActive(DateTime now) {
if (!outsidePremiseAllowed) return false;
final today = DateTime(now.year, now.month, now.day);
// Active on dry run date or event date
if (dryRunDate != null) {
final dryDay = DateTime(
dryRunDate!.year,
dryRunDate!.month,
dryRunDate!.day,
);
if (!today.isBefore(dryDay) &&
today.isBefore(dryDay.add(const Duration(days: 1)))) {
return true;
}
}
if (eventDate != null) {
final eventDay = DateTime(
eventDate!.year,
eventDate!.month,
eventDate!.day,
);
if (!today.isBefore(eventDay) &&
today.isBefore(eventDay.add(const Duration(days: 1)))) {
return true;
}
}
return false;
}
factory ItServiceRequest.fromMap(Map<String, dynamic> map) {
List<String> parseServices(dynamic raw) {
if (raw is List) return raw.map((e) => e.toString()).toList();
if (raw is String) {
// Handle PostgreSQL array format: {a,b,c}
final trimmed = raw.replaceAll('{', '').replaceAll('}', '');
if (trimmed.isEmpty) return [];
return trimmed.split(',').map((e) => e.trim()).toList();
}
return [];
}
String? quillField(dynamic raw) {
if (raw == null) return null;
if (raw is String) return raw;
try {
return jsonEncode(raw);
} catch (_) {
return raw.toString();
}
}
return ItServiceRequest(
id: map['id'] as String,
requestNumber: map['request_number'] as String?,
services: parseServices(map['services']),
servicesOther: map['services_other'] as String?,
eventName: map['event_name'] as String? ?? '',
eventDetails: quillField(map['event_details']),
eventDate: map['event_date'] == null
? null
: AppTime.parse(map['event_date'] as String),
eventEndDate: map['event_end_date'] == null
? null
: AppTime.parse(map['event_end_date'] as String),
dryRunDate: map['dry_run_date'] == null
? null
: AppTime.parse(map['dry_run_date'] as String),
dryRunEndDate: map['dry_run_end_date'] == null
? null
: AppTime.parse(map['dry_run_end_date'] as String),
contactPerson: map['contact_person'] as String?,
contactNumber: map['contact_number'] as String?,
remarks: quillField(map['remarks']),
officeId: map['office_id'] as String?,
requestedBy: map['requested_by'] as String?,
requestedByUserId: map['requested_by_user_id'] as String?,
approvedBy: map['approved_by'] as String?,
approvedByUserId: map['approved_by_user_id'] as String?,
approvedAt: map['approved_at'] == null
? null
: AppTime.parse(map['approved_at'] as String),
status: map['status'] as String? ?? 'draft',
outsidePremiseAllowed: map['outside_premise_allowed'] as bool? ?? false,
cancellationReason: map['cancellation_reason'] as String?,
cancelledAt: map['cancelled_at'] == null
? null
: AppTime.parse(map['cancelled_at'] as String),
creatorId: map['creator_id'] as String?,
createdAt: AppTime.parse(map['created_at'] as String),
updatedAt: AppTime.parse(map['updated_at'] as String),
completedAt: map['completed_at'] == null
? null
: AppTime.parse(map['completed_at'] as String),
dateTimeReceived: map['date_time_received'] == null
? null
: AppTime.parse(map['date_time_received'] as String),
dateTimeChecked: map['date_time_checked'] == null
? null
: AppTime.parse(map['date_time_checked'] as String),
);
}
}
export 'it_service_request.model.dart';
+268
View File
@@ -0,0 +1,268 @@
import 'dart:convert';
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_sqlite/brick_sqlite.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
/// Available IT services from the form.
class ItServiceType {
static const fbLiveStream = 'fb_live_stream';
static const videoRecording = 'video_recording';
static const technicalAssistance = 'technical_assistance';
static const wifi = 'wifi';
static const others = 'others';
static const all = [
fbLiveStream,
videoRecording,
technicalAssistance,
wifi,
others,
];
static String label(String type) {
switch (type) {
case fbLiveStream:
return 'FB Live Stream';
case videoRecording:
return 'Video Recording';
case technicalAssistance:
return 'Technical Assistance';
case wifi:
return 'WiFi';
case others:
return 'Others';
default:
return type;
}
}
}
/// Status lifecycle for an IT Service Request.
class ItServiceRequestStatus {
static const draft = 'draft';
static const pendingApproval = 'pending_approval';
static const scheduled = 'scheduled';
static const inProgressDryRun = 'in_progress_dry_run';
static const inProgress = 'in_progress';
static const completed = 'completed';
static const cancelled = 'cancelled';
static const all = [
draft,
pendingApproval,
scheduled,
inProgressDryRun,
inProgress,
completed,
cancelled,
];
static String label(String status) {
switch (status) {
case draft:
return 'Draft';
case pendingApproval:
return 'Pending Approval';
case scheduled:
return 'Scheduled';
case inProgressDryRun:
return 'In Progress (Dry Run)';
case inProgress:
return 'In Progress';
case completed:
return 'Completed';
case cancelled:
return 'Cancelled';
default:
return status;
}
}
}
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'it_service_requests'),
)
class ItServiceRequest extends OfflineFirstWithSupabaseModel {
final String id;
final String? requestNumber;
final List<String> services;
final String? servicesOther;
final String eventName;
// Quill Delta JSON stored as plain strings.
final String? eventDetails;
final DateTime? eventDate;
final DateTime? eventEndDate;
final DateTime? dryRunDate;
final DateTime? dryRunEndDate;
final String? contactPerson;
final String? contactNumber;
final String? remarks;
final String? officeId;
final String? requestedBy;
final String? requestedByUserId;
final String? approvedBy;
final String? approvedByUserId;
final DateTime? approvedAt;
final String status;
final bool outsidePremiseAllowed;
final String? cancellationReason;
final DateTime? cancelledAt;
final String? creatorId;
final DateTime createdAt;
final DateTime updatedAt;
final DateTime? completedAt;
final DateTime? dateTimeReceived;
final DateTime? dateTimeChecked;
ItServiceRequest({
required this.id,
this.requestNumber,
required this.services,
this.servicesOther,
required this.eventName,
this.eventDetails,
this.eventDate,
this.eventEndDate,
this.dryRunDate,
this.dryRunEndDate,
this.contactPerson,
this.contactNumber,
this.remarks,
this.officeId,
this.requestedBy,
this.requestedByUserId,
this.approvedBy,
this.approvedByUserId,
this.approvedAt,
required this.status,
required this.outsidePremiseAllowed,
this.cancellationReason,
this.cancelledAt,
this.creatorId,
required this.createdAt,
required this.updatedAt,
this.completedAt,
this.dateTimeReceived,
this.dateTimeChecked,
});
@Supabase(ignore: true)
@Sqlite(ignore: true)
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ItServiceRequest &&
runtimeType == other.runtimeType &&
id == other.id &&
requestNumber == other.requestNumber &&
status == other.status &&
updatedAt == other.updatedAt;
@Supabase(ignore: true)
@Sqlite(ignore: true)
@override
int get hashCode => Object.hash(id, requestNumber, status, updatedAt);
bool isGeofenceOverrideActive(DateTime now) {
if (!outsidePremiseAllowed) return false;
final today = DateTime(now.year, now.month, now.day);
if (dryRunDate != null) {
final dryDay = DateTime(
dryRunDate!.year,
dryRunDate!.month,
dryRunDate!.day,
);
if (!today.isBefore(dryDay) &&
today.isBefore(dryDay.add(const Duration(days: 1)))) {
return true;
}
}
if (eventDate != null) {
final eventDay = DateTime(
eventDate!.year,
eventDate!.month,
eventDate!.day,
);
if (!today.isBefore(eventDay) &&
today.isBefore(eventDay.add(const Duration(days: 1)))) {
return true;
}
}
return false;
}
factory ItServiceRequest.fromMap(Map<String, dynamic> map) {
List<String> parseServices(dynamic raw) {
if (raw is List) return raw.map((e) => e.toString()).toList();
if (raw is String) {
final trimmed = raw.replaceAll('{', '').replaceAll('}', '');
if (trimmed.isEmpty) return [];
return trimmed.split(',').map((e) => e.trim()).toList();
}
return [];
}
String? quillField(dynamic raw) {
if (raw == null) return null;
if (raw is String) return raw;
try {
return jsonEncode(raw);
} catch (_) {
return raw.toString();
}
}
return ItServiceRequest(
id: map['id'] as String,
requestNumber: map['request_number'] as String?,
services: parseServices(map['services']),
servicesOther: map['services_other'] as String?,
eventName: map['event_name'] as String? ?? '',
eventDetails: quillField(map['event_details']),
eventDate: map['event_date'] == null
? null
: AppTime.parse(map['event_date'] as String),
eventEndDate: map['event_end_date'] == null
? null
: AppTime.parse(map['event_end_date'] as String),
dryRunDate: map['dry_run_date'] == null
? null
: AppTime.parse(map['dry_run_date'] as String),
dryRunEndDate: map['dry_run_end_date'] == null
? null
: AppTime.parse(map['dry_run_end_date'] as String),
contactPerson: map['contact_person'] as String?,
contactNumber: map['contact_number'] as String?,
remarks: quillField(map['remarks']),
officeId: map['office_id'] as String?,
requestedBy: map['requested_by'] as String?,
requestedByUserId: map['requested_by_user_id'] as String?,
approvedBy: map['approved_by'] as String?,
approvedByUserId: map['approved_by_user_id'] as String?,
approvedAt: map['approved_at'] == null
? null
: AppTime.parse(map['approved_at'] as String),
status: map['status'] as String? ?? 'draft',
outsidePremiseAllowed: map['outside_premise_allowed'] as bool? ?? false,
cancellationReason: map['cancellation_reason'] as String?,
cancelledAt: map['cancelled_at'] == null
? null
: AppTime.parse(map['cancelled_at'] as String),
creatorId: map['creator_id'] as String?,
createdAt: AppTime.parse(map['created_at'] as String),
updatedAt: AppTime.parse(map['updated_at'] as String),
completedAt: map['completed_at'] == null
? null
: AppTime.parse(map['completed_at'] as String),
dateTimeReceived: map['date_time_received'] == null
? null
: AppTime.parse(map['date_time_received'] as String),
dateTimeChecked: map['date_time_checked'] == null
? null
: AppTime.parse(map['date_time_checked'] as String),
);
}
}
+1 -42
View File
@@ -1,42 +1 @@
import 'dart:convert';
import '../utils/app_time.dart';
class ItServiceRequestAction {
ItServiceRequestAction({
required this.id,
required this.requestId,
required this.userId,
this.actionTaken,
required this.createdAt,
required this.updatedAt,
});
final String id;
final String requestId;
final String userId;
final String? actionTaken; // Quill Delta JSON
final DateTime createdAt;
final DateTime updatedAt;
factory ItServiceRequestAction.fromMap(Map<String, dynamic> map) {
String? quillField(dynamic raw) {
if (raw == null) return null;
if (raw is String) return raw;
try {
return jsonEncode(raw);
} catch (_) {
return raw.toString();
}
}
return ItServiceRequestAction(
id: map['id'] as String,
requestId: map['request_id'] as String,
userId: map['user_id'] as String,
actionTaken: quillField(map['action_taken']),
createdAt: AppTime.parse(map['created_at'] as String),
updatedAt: AppTime.parse(map['updated_at'] as String),
);
}
}
export 'it_service_request_action.model.dart';
@@ -0,0 +1,49 @@
import 'dart:convert';
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'it_service_request_actions'),
)
class ItServiceRequestAction extends OfflineFirstWithSupabaseModel {
final String id;
final String requestId;
final String userId;
// Quill Delta JSON stored as plain string.
final String? actionTaken;
final DateTime createdAt;
final DateTime updatedAt;
ItServiceRequestAction({
required this.id,
required this.requestId,
required this.userId,
this.actionTaken,
required this.createdAt,
required this.updatedAt,
});
factory ItServiceRequestAction.fromMap(Map<String, dynamic> map) {
String? quillField(dynamic raw) {
if (raw == null) return null;
if (raw is String) return raw;
try {
return jsonEncode(raw);
} catch (_) {
return raw.toString();
}
}
return ItServiceRequestAction(
id: map['id'] as String,
requestId: map['request_id'] as String,
userId: map['user_id'] as String,
actionTaken: quillField(map['action_taken']),
createdAt: AppTime.parse(map['created_at'] as String),
updatedAt: AppTime.parse(map['updated_at'] as String),
);
}
}
@@ -1,74 +1 @@
import 'dart:convert';
import '../utils/app_time.dart';
class ItServiceRequestActivityLog {
ItServiceRequestActivityLog({
required this.id,
required this.requestId,
this.actorId,
required this.actionType,
this.meta,
required this.createdAt,
});
final String id;
final String requestId;
final String? actorId;
final String actionType;
final Map<String, dynamic>? meta;
final DateTime createdAt;
factory ItServiceRequestActivityLog.fromMap(Map<String, dynamic> map) {
final rawId = map['id'];
final rawRequestId = map['request_id'];
String id = rawId == null ? '' : rawId.toString();
String requestId = rawRequestId == null ? '' : rawRequestId.toString();
final actorId = map['actor_id']?.toString();
final actionType = (map['action_type'] as String?) ?? 'unknown';
Map<String, dynamic>? meta;
final rawMeta = map['meta'];
if (rawMeta is Map<String, dynamic>) {
meta = rawMeta;
} else if (rawMeta is Map) {
try {
meta = rawMeta.map((k, v) => MapEntry(k.toString(), v));
} catch (_) {
meta = null;
}
} else if (rawMeta is String && rawMeta.isNotEmpty) {
try {
final decoded = jsonDecode(rawMeta);
if (decoded is Map<String, dynamic>) {
meta = decoded;
} else if (decoded is Map) {
meta = decoded.map((k, v) => MapEntry(k.toString(), v));
}
} catch (_) {
meta = null;
}
}
DateTime createdAt;
final rawCreated = map['created_at'];
if (rawCreated is String) {
try {
createdAt = AppTime.parse(rawCreated);
} catch (_) {
createdAt = AppTime.now();
}
} else {
createdAt = AppTime.now();
}
return ItServiceRequestActivityLog(
id: id,
requestId: requestId,
actorId: actorId,
actionType: actionType,
meta: meta,
createdAt: createdAt,
);
}
}
export 'it_service_request_activity_log.model.dart';
@@ -0,0 +1,81 @@
import 'dart:convert';
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
// meta: Map<String, dynamic>? is stored as JSON text in SQLite by Brick.
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'it_service_request_activity_logs'),
)
class ItServiceRequestActivityLog extends OfflineFirstWithSupabaseModel {
final String id;
final String requestId;
final String? actorId;
final String actionType;
final Map<String, dynamic>? meta;
final DateTime createdAt;
ItServiceRequestActivityLog({
required this.id,
required this.requestId,
this.actorId,
required this.actionType,
this.meta,
required this.createdAt,
});
factory ItServiceRequestActivityLog.fromMap(Map<String, dynamic> map) {
final rawId = map['id'];
final rawRequestId = map['request_id'];
String id = rawId == null ? '' : rawId.toString();
String requestId = rawRequestId == null ? '' : rawRequestId.toString();
final actorId = map['actor_id']?.toString();
final actionType = (map['action_type'] as String?) ?? 'unknown';
Map<String, dynamic>? meta;
final rawMeta = map['meta'];
if (rawMeta is Map<String, dynamic>) {
meta = rawMeta;
} else if (rawMeta is Map) {
try {
meta = rawMeta.map((k, v) => MapEntry(k.toString(), v));
} catch (_) {
meta = null;
}
} else if (rawMeta is String && rawMeta.isNotEmpty) {
try {
final decoded = jsonDecode(rawMeta);
if (decoded is Map<String, dynamic>) {
meta = decoded;
} else if (decoded is Map) {
meta = decoded.map((k, v) => MapEntry(k.toString(), v));
}
} catch (_) {
meta = null;
}
}
DateTime createdAt;
final rawCreated = map['created_at'];
if (rawCreated is String) {
try {
createdAt = AppTime.parse(rawCreated);
} catch (_) {
createdAt = AppTime.now();
}
} else {
createdAt = AppTime.now();
}
return ItServiceRequestActivityLog(
id: id,
requestId: requestId,
actorId: actorId,
actionType: actionType,
meta: meta,
createdAt: createdAt,
);
}
}
+1 -24
View File
@@ -1,24 +1 @@
import '../utils/app_time.dart';
class ItServiceRequestAssignment {
ItServiceRequestAssignment({
required this.id,
required this.requestId,
required this.userId,
required this.createdAt,
});
final String id;
final String requestId;
final String userId;
final DateTime createdAt;
factory ItServiceRequestAssignment.fromMap(Map<String, dynamic> map) {
return ItServiceRequestAssignment(
id: map['id'] as String,
requestId: map['request_id'] as String,
userId: map['user_id'] as String,
createdAt: AppTime.parse(map['created_at'] as String),
);
}
}
export 'it_service_request_assignment.model.dart';
@@ -0,0 +1,30 @@
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'it_service_request_assignments'),
)
class ItServiceRequestAssignment extends OfflineFirstWithSupabaseModel {
final String id;
final String requestId;
final String userId;
final DateTime createdAt;
ItServiceRequestAssignment({
required this.id,
required this.requestId,
required this.userId,
required this.createdAt,
});
factory ItServiceRequestAssignment.fromMap(Map<String, dynamic> map) {
return ItServiceRequestAssignment(
id: map['id'] as String,
requestId: map['request_id'] as String,
userId: map['user_id'] as String,
createdAt: AppTime.parse(map['created_at'] as String),
);
}
}
+1 -54
View File
@@ -1,54 +1 @@
import '../utils/app_time.dart';
class LeaveOfAbsence {
LeaveOfAbsence({
required this.id,
required this.userId,
required this.leaveType,
required this.justification,
required this.startTime,
required this.endTime,
required this.status,
required this.filedBy,
required this.createdAt,
});
final String id;
final String userId;
final String leaveType;
final String justification;
final DateTime startTime;
final DateTime endTime;
final String status;
final String filedBy;
final DateTime createdAt;
factory LeaveOfAbsence.fromMap(Map<String, dynamic> map) {
return LeaveOfAbsence(
id: map['id'] as String,
userId: map['user_id'] as String,
leaveType: map['leave_type'] as String,
justification: map['justification'] as String,
startTime: AppTime.parse(map['start_time'] as String),
endTime: AppTime.parse(map['end_time'] as String),
status: map['status'] as String? ?? 'pending',
filedBy: map['filed_by'] as String,
createdAt: AppTime.parse(map['created_at'] as String),
);
}
String get leaveTypeLabel {
switch (leaveType) {
case 'emergency_leave':
return 'Emergency Leave';
case 'parental_leave':
return 'Parental Leave';
case 'sick_leave':
return 'Sick Leave';
case 'vacation_leave':
return 'Vacation Leave';
default:
return leaveType;
}
}
}
export 'leave_of_absence.model.dart';
+59
View File
@@ -0,0 +1,59 @@
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_sqlite/brick_sqlite.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
// Supabase table is singular: 'leave_of_absence' (not 'leave_of_absences').
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'leave_of_absence'),
)
class LeaveOfAbsence extends OfflineFirstWithSupabaseModel {
final String id;
final String userId;
final String leaveType;
final String justification;
final DateTime startTime;
final DateTime endTime;
final String status;
final String filedBy;
final DateTime createdAt;
LeaveOfAbsence({
required this.id,
required this.userId,
required this.leaveType,
required this.justification,
required this.startTime,
required this.endTime,
required this.status,
required this.filedBy,
required this.createdAt,
});
factory LeaveOfAbsence.fromMap(Map<String, dynamic> map) {
return LeaveOfAbsence(
id: map['id'] as String,
userId: map['user_id'] as String,
leaveType: map['leave_type'] as String,
justification: map['justification'] as String,
startTime: AppTime.parse(map['start_time'] as String),
endTime: AppTime.parse(map['end_time'] as String),
status: map['status'] as String? ?? 'pending',
filedBy: map['filed_by'] as String,
createdAt: AppTime.parse(map['created_at'] as String),
);
}
@Supabase(ignore: true)
@Sqlite(ignore: true)
String get leaveTypeLabel {
switch (leaveType) {
case 'emergency_leave': return 'Emergency Leave';
case 'parental_leave': return 'Parental Leave';
case 'sick_leave': return 'Sick Leave';
case 'vacation_leave': return 'Vacation Leave';
default: return leaveType;
}
}
}
+1 -55
View File
@@ -1,55 +1 @@
import '../utils/app_time.dart';
class NotificationItem {
NotificationItem({
required this.id,
required this.userId,
required this.actorId,
required this.ticketId,
required this.taskId,
this.leaveId,
this.passSlipId,
required this.itServiceRequestId,
this.announcementId,
required this.messageId,
required this.type,
required this.createdAt,
required this.readAt,
});
final String id;
final String userId;
final String? actorId;
final String? ticketId;
final String? taskId;
final String? leaveId;
final String? passSlipId;
final String? itServiceRequestId;
final String? announcementId;
final int? messageId;
final String type;
final DateTime createdAt;
final DateTime? readAt;
bool get isUnread => readAt == null;
factory NotificationItem.fromMap(Map<String, dynamic> map) {
return NotificationItem(
id: map['id'] as String,
userId: map['user_id'] as String,
actorId: map['actor_id'] as String?,
ticketId: map['ticket_id'] as String?,
taskId: map['task_id'] as String?,
leaveId: map['leave_id'] as String?,
passSlipId: map['pass_slip_id'] as String?,
itServiceRequestId: map['it_service_request_id'] as String?,
announcementId: map['announcement_id'] as String?,
messageId: map['message_id'] as int?,
type: map['type'] as String? ?? 'mention',
createdAt: AppTime.parse(map['created_at'] as String),
readAt: map['read_at'] == null
? null
: AppTime.parse(map['read_at'] as String),
);
}
}
export 'notification_item.model.dart';
+65
View File
@@ -0,0 +1,65 @@
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_sqlite/brick_sqlite.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
// Supabase table is 'notifications' (not 'notification_items').
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'notifications'),
)
class NotificationItem extends OfflineFirstWithSupabaseModel {
final String id;
final String userId;
final String? actorId;
final String? ticketId;
final String? taskId;
final String? leaveId;
final String? passSlipId;
final String? itServiceRequestId;
final String? announcementId;
final int? messageId;
final String type;
final DateTime createdAt;
final DateTime? readAt;
@Supabase(ignore: true)
@Sqlite(ignore: true)
bool get isUnread => readAt == null;
NotificationItem({
required this.id,
required this.userId,
required this.actorId,
required this.ticketId,
required this.taskId,
this.leaveId,
this.passSlipId,
required this.itServiceRequestId,
this.announcementId,
required this.messageId,
required this.type,
required this.createdAt,
required this.readAt,
});
factory NotificationItem.fromMap(Map<String, dynamic> map) {
return NotificationItem(
id: map['id'] as String,
userId: map['user_id'] as String,
actorId: map['actor_id'] as String?,
ticketId: map['ticket_id'] as String?,
taskId: map['task_id'] as String?,
leaveId: map['leave_id'] as String?,
passSlipId: map['pass_slip_id'] as String?,
itServiceRequestId: map['it_service_request_id'] as String?,
announcementId: map['announcement_id'] as String?,
messageId: map['message_id'] as int?,
type: map['type'] as String? ?? 'mention',
createdAt: AppTime.parse(map['created_at'] as String),
readAt: map['read_at'] == null
? null
: AppTime.parse(map['read_at'] as String),
);
}
}
+1 -15
View File
@@ -1,15 +1 @@
class Office {
Office({required this.id, required this.name, this.serviceId});
final String id;
final String name;
final String? serviceId;
factory Office.fromMap(Map<String, dynamic> map) {
return Office(
id: map['id'] as String,
name: map['name'] as String? ?? '',
serviceId: map['service_id'] as String?,
);
}
}
export 'office.model.dart';
+21
View File
@@ -0,0 +1,21 @@
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_supabase/brick_supabase.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'offices'),
)
class Office extends OfflineFirstWithSupabaseModel {
final String id;
final String name;
final String? serviceId;
Office({required this.id, required this.name, this.serviceId});
factory Office.fromMap(Map<String, dynamic> map) {
return Office(
id: map['id'] as String,
name: map['name'] as String? ?? '',
serviceId: map['service_id'] as String?,
);
}
}
+1 -62
View File
@@ -1,62 +1 @@
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,
this.requestedStart,
});
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;
final DateTime? requestedStart;
/// 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),
requestedStart: map['requested_start'] == null
? null
: AppTime.parse(map['requested_start'] as String),
);
}
}
export 'pass_slip.model.dart';
+71
View File
@@ -0,0 +1,71 @@
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_sqlite/brick_sqlite.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'pass_slips'),
)
class PassSlip extends OfflineFirstWithSupabaseModel {
final String id;
final String userId;
final String dutyScheduleId;
final String reason;
final String status;
final DateTime requestedAt;
final String? approvedBy;
final DateTime? approvedAt;
final DateTime? slipStart;
final DateTime? slipEnd;
final DateTime? requestedStart;
@Supabase(ignore: true)
@Sqlite(ignore: true)
bool get isActive => status == 'approved' && slipEnd == null;
@Supabase(ignore: true)
@Sqlite(ignore: true)
bool get isExceeded =>
isActive &&
slipStart != null &&
AppTime.now().difference(slipStart!) > const Duration(hours: 1);
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,
this.requestedStart,
});
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),
requestedStart: map['requested_start'] == null
? null
: AppTime.parse(map['requested_start'] as String),
);
}
}
+1 -38
View File
@@ -1,38 +1 @@
class Profile {
Profile({
required this.id,
required this.role,
required this.fullName,
this.religion = 'catholic',
this.allowTracking = false,
this.avatarUrl,
this.facePhotoUrl,
this.faceEnrolledAt,
});
final String id;
final String role;
final String fullName;
final String religion;
final bool allowTracking;
final String? avatarUrl;
final String? facePhotoUrl;
final DateTime? faceEnrolledAt;
bool get hasFaceEnrolled => facePhotoUrl != null && faceEnrolledAt != null;
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,
avatarUrl: map['avatar_url'] as String?,
facePhotoUrl: map['face_photo_url'] as String?,
faceEnrolledAt: map['face_enrolled_at'] == null
? null
: DateTime.tryParse(map['face_enrolled_at'] as String),
);
}
}
export 'profile.model.dart';
+47
View File
@@ -0,0 +1,47 @@
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_sqlite/brick_sqlite.dart';
import 'package:brick_supabase/brick_supabase.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'profiles'),
)
class Profile extends OfflineFirstWithSupabaseModel {
final String id;
final String role;
final String fullName;
final String religion;
final bool allowTracking;
final String? avatarUrl;
final String? facePhotoUrl;
final DateTime? faceEnrolledAt;
@Supabase(ignore: true)
@Sqlite(ignore: true)
bool get hasFaceEnrolled => facePhotoUrl != null && faceEnrolledAt != null;
Profile({
required this.id,
required this.role,
required this.fullName,
this.religion = 'catholic',
this.allowTracking = false,
this.avatarUrl,
this.facePhotoUrl,
this.faceEnrolledAt,
});
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,
avatarUrl: map['avatar_url'] as String?,
facePhotoUrl: map['face_photo_url'] as String?,
faceEnrolledAt: map['face_enrolled_at'] == null
? null
: DateTime.tryParse(map['face_enrolled_at'] as String),
);
}
}
+1 -10
View File
@@ -1,10 +1 @@
class Service {
Service({required this.id, required this.name});
final String id;
final String name;
factory Service.fromMap(Map<String, dynamic> map) {
return Service(id: map['id'] as String, name: map['name'] as String? ?? '');
}
}
export 'service.model.dart';
+19
View File
@@ -0,0 +1,19 @@
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_supabase/brick_supabase.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'services'),
)
class Service extends OfflineFirstWithSupabaseModel {
final String id;
final String name;
Service({required this.id, required this.name});
factory Service.fromMap(Map<String, dynamic> map) {
return Service(
id: map['id'] as String,
name: map['name'] as String? ?? '',
);
}
}
+1 -62
View File
@@ -1,62 +1 @@
import '../utils/app_time.dart';
class SwapRequest {
SwapRequest({
required this.id,
required this.requesterId,
required this.recipientId,
required this.requesterScheduleId,
required this.targetScheduleId,
required this.status,
required this.createdAt,
required this.updatedAt,
this.chatThreadId,
this.shiftType,
this.shiftStartTime,
this.relieverIds,
this.approvedBy,
});
final String id;
final String requesterId;
final String recipientId;
final String requesterScheduleId; // previously `shiftId`
final String? targetScheduleId;
final String status;
final DateTime createdAt;
final DateTime? updatedAt;
final String? chatThreadId;
final String? shiftType;
final DateTime? shiftStartTime;
final List<String>? relieverIds;
final String? approvedBy;
factory SwapRequest.fromMap(Map<String, dynamic> map) {
return SwapRequest(
id: map['id'] as String,
requesterId: map['requester_id'] as String,
recipientId: map['recipient_id'] as String,
requesterScheduleId:
(map['requester_schedule_id'] as String?) ??
(map['shift_id'] as String),
targetScheduleId: map['target_shift_id'] as String?,
status: map['status'] as String? ?? 'pending',
createdAt: AppTime.parse(map['created_at'] as String),
updatedAt: map['updated_at'] == null
? null
: AppTime.parse(map['updated_at'] as String),
chatThreadId: map['chat_thread_id'] as String?,
shiftType: map['shift_type'] as String?,
shiftStartTime: map['shift_start_time'] == null
? null
: AppTime.parse(map['shift_start_time'] as String),
relieverIds: map['reliever_ids'] is List
? (map['reliever_ids'] as List)
.where((e) => e != null)
.map((e) => e.toString())
.toList()
: const <String>[],
approvedBy: map['approved_by'] as String?,
);
}
}
export 'swap_request.model.dart';
+72
View File
@@ -0,0 +1,72 @@
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'swap_requests'),
)
class SwapRequest extends OfflineFirstWithSupabaseModel {
final String id;
final String requesterId;
final String recipientId;
final String requesterScheduleId;
// DB column is 'target_shift_id' (legacy name differs from Dart field).
@Supabase(name: 'target_shift_id')
final String? targetScheduleId;
final String status;
final DateTime createdAt;
final DateTime? updatedAt;
final String? chatThreadId;
final String? shiftType;
final DateTime? shiftStartTime;
final List<String>? relieverIds;
final String? approvedBy;
SwapRequest({
required this.id,
required this.requesterId,
required this.recipientId,
required this.requesterScheduleId,
required this.targetScheduleId,
required this.status,
required this.createdAt,
required this.updatedAt,
this.chatThreadId,
this.shiftType,
this.shiftStartTime,
this.relieverIds,
this.approvedBy,
});
factory SwapRequest.fromMap(Map<String, dynamic> map) {
return SwapRequest(
id: map['id'] as String,
requesterId: map['requester_id'] as String,
recipientId: map['recipient_id'] as String,
requesterScheduleId:
(map['requester_schedule_id'] as String?) ??
(map['shift_id'] as String),
targetScheduleId: map['target_shift_id'] as String?,
status: map['status'] as String? ?? 'pending',
createdAt: AppTime.parse(map['created_at'] as String),
updatedAt: map['updated_at'] == null
? null
: AppTime.parse(map['updated_at'] as String),
chatThreadId: map['chat_thread_id'] as String?,
shiftType: map['shift_type'] as String?,
shiftStartTime: map['shift_start_time'] == null
? null
: AppTime.parse(map['shift_start_time'] as String),
relieverIds: map['reliever_ids'] is List
? (map['reliever_ids'] as List)
.where((e) => e != null)
.map((e) => e.toString())
.toList()
: const <String>[],
approvedBy: map['approved_by'] as String?,
);
}
}
+1 -186
View File
@@ -1,186 +1 @@
import 'dart:convert';
import '../utils/app_time.dart';
class Task {
Task({
required this.id,
required this.ticketId,
required this.taskNumber,
required this.title,
required this.description,
required this.officeId,
required this.status,
required this.priority,
required this.queueOrder,
required this.createdAt,
required this.creatorId,
required this.startedAt,
required this.completedAt,
// new optional metadata fields
this.requestedBy,
this.notedBy,
this.receivedBy,
this.requestType,
this.requestTypeOther,
this.requestCategory,
this.actionTaken,
this.cancellationReason,
this.cancelledAt,
this.itJobPrinted = false,
this.itJobPrintedAt,
this.itJobReceivedById,
});
final String id;
final String? ticketId;
final String? taskNumber;
final String title;
final String description;
final String? officeId;
final String status;
final int priority;
final int? queueOrder;
final DateTime createdAt;
final String? creatorId;
final DateTime? startedAt;
final DateTime? completedAt;
// Optional client/user metadata
final String? requestedBy;
final String? notedBy;
final String? receivedBy;
/// Optional request metadata added later in lifecycle.
final String? requestType;
final String? requestTypeOther;
final String? requestCategory;
// JSON serialized rich text for action taken (Quill Delta JSON encoded)
final String? actionTaken;
/// Cancellation details when a task was cancelled.
final String? cancellationReason;
final DateTime? cancelledAt;
/// Whether the printed IT Job has been submitted.
final bool itJobPrinted;
final DateTime? itJobPrintedAt;
final String? itJobReceivedById;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Task &&
runtimeType == other.runtimeType &&
id == other.id &&
ticketId == other.ticketId &&
taskNumber == other.taskNumber &&
title == other.title &&
description == other.description &&
officeId == other.officeId &&
status == other.status &&
priority == other.priority &&
queueOrder == other.queueOrder &&
createdAt == other.createdAt &&
creatorId == other.creatorId &&
startedAt == other.startedAt &&
completedAt == other.completedAt &&
requestedBy == other.requestedBy &&
notedBy == other.notedBy &&
receivedBy == other.receivedBy &&
requestType == other.requestType &&
requestTypeOther == other.requestTypeOther &&
requestCategory == other.requestCategory &&
actionTaken == other.actionTaken &&
cancellationReason == other.cancellationReason &&
cancelledAt == other.cancelledAt &&
itJobPrinted == other.itJobPrinted &&
itJobPrintedAt == other.itJobPrintedAt &&
itJobReceivedById == other.itJobReceivedById;
@override
int get hashCode => Object.hash(
id,
ticketId,
taskNumber,
title,
description,
officeId,
status,
priority,
queueOrder,
createdAt,
creatorId,
startedAt,
completedAt,
requestedBy,
notedBy,
receivedBy,
requestType,
requestTypeOther,
requestCategory,
// Object.hash supports max 20 positional args; combine remainder.
Object.hash(actionTaken, cancellationReason, cancelledAt,
itJobPrinted, itJobPrintedAt, itJobReceivedById),
);
/// Helper that indicates whether a completed task still has missing
/// metadata such as signatories or action details. The parameter is used
/// by UI to surface a warning icon/banner when a task has been closed but
/// the user skipped filling out all fields.
bool get hasIncompleteDetails {
if (status != 'completed') return false;
bool empty(String? v) => v == null || v.trim().isEmpty;
return empty(requestedBy) ||
empty(notedBy) ||
empty(receivedBy) ||
empty(actionTaken);
}
factory Task.fromMap(Map<String, dynamic> map) {
return Task(
id: map['id'] as String,
ticketId: map['ticket_id'] as String?,
taskNumber: map['task_number'] as String?,
title: map['title'] as String? ?? 'Task',
description: map['description'] as String? ?? '',
officeId: map['office_id'] as String?,
status: map['status'] as String? ?? 'queued',
priority: map['priority'] as int? ?? 1,
queueOrder: map['queue_order'] as int?,
createdAt: AppTime.parse(map['created_at'] as String),
creatorId: map['creator_id'] as String?,
startedAt: map['started_at'] == null
? null
: AppTime.parse(map['started_at'] as String),
completedAt: map['completed_at'] == null
? null
: AppTime.parse(map['completed_at'] as String),
requestType: map['request_type'] as String?,
requestTypeOther: map['request_type_other'] as String?,
requestCategory: map['request_category'] as String?,
requestedBy: map['requested_by'] as String?,
notedBy: map['noted_by'] as String?,
receivedBy: map['received_by'] as String?,
cancellationReason: map['cancellation_reason'] as String?,
cancelledAt: map['cancelled_at'] == null
? null
: AppTime.parse(map['cancelled_at'] as String),
itJobPrinted: map['it_job_printed'] as bool? ?? false,
itJobPrintedAt: map['it_job_printed_at'] == null
? null
: AppTime.parse(map['it_job_printed_at'] as String),
itJobReceivedById: map['it_job_received_by_id'] as String?,
actionTaken: (() {
final at = map['action_taken'];
if (at == null) return null;
if (at is String) return at;
try {
return jsonEncode(at);
} catch (_) {
return at.toString();
}
})(),
);
}
}
export 'task.model.dart';
+159
View File
@@ -0,0 +1,159 @@
import 'dart:convert';
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_sqlite/brick_sqlite.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'tasks'),
)
class Task extends OfflineFirstWithSupabaseModel {
final String id;
final String? ticketId;
final String? taskNumber;
final String title;
final String description;
final String? officeId;
final String status;
final int priority;
final int? queueOrder;
final DateTime createdAt;
final String? creatorId;
final DateTime? startedAt;
final DateTime? completedAt;
final String? requestedBy;
final String? notedBy;
final String? receivedBy;
final String? requestType;
final String? requestTypeOther;
final String? requestCategory;
// JSON-encoded Quill Delta stored as plain string.
final String? actionTaken;
final String? cancellationReason;
final DateTime? cancelledAt;
final bool itJobPrinted;
final DateTime? itJobPrintedAt;
final String? itJobReceivedById;
Task({
required this.id,
required this.ticketId,
required this.taskNumber,
required this.title,
required this.description,
required this.officeId,
required this.status,
required this.priority,
required this.queueOrder,
required this.createdAt,
required this.creatorId,
required this.startedAt,
required this.completedAt,
this.requestedBy,
this.notedBy,
this.receivedBy,
this.requestType,
this.requestTypeOther,
this.requestCategory,
this.actionTaken,
this.cancellationReason,
this.cancelledAt,
this.itJobPrinted = false,
this.itJobPrintedAt,
this.itJobReceivedById,
});
@Supabase(ignore: true)
@Sqlite(ignore: true)
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Task &&
runtimeType == other.runtimeType &&
id == other.id &&
ticketId == other.ticketId &&
taskNumber == other.taskNumber &&
title == other.title &&
description == other.description &&
officeId == other.officeId &&
status == other.status &&
priority == other.priority &&
queueOrder == other.queueOrder &&
createdAt == other.createdAt &&
creatorId == other.creatorId &&
startedAt == other.startedAt &&
completedAt == other.completedAt &&
requestedBy == other.requestedBy &&
notedBy == other.notedBy &&
receivedBy == other.receivedBy &&
requestType == other.requestType &&
requestTypeOther == other.requestTypeOther &&
requestCategory == other.requestCategory &&
actionTaken == other.actionTaken &&
cancellationReason == other.cancellationReason &&
cancelledAt == other.cancelledAt &&
itJobPrinted == other.itJobPrinted &&
itJobPrintedAt == other.itJobPrintedAt &&
itJobReceivedById == other.itJobReceivedById;
@Supabase(ignore: true)
@Sqlite(ignore: true)
@override
int get hashCode => Object.hash(
id, ticketId, taskNumber, title, description, officeId, status, priority,
queueOrder, createdAt, creatorId, startedAt, completedAt, requestedBy,
notedBy, receivedBy, requestType, requestTypeOther, requestCategory,
Object.hash(actionTaken, cancellationReason, cancelledAt,
itJobPrinted, itJobPrintedAt, itJobReceivedById),
);
@Supabase(ignore: true)
@Sqlite(ignore: true)
bool get hasIncompleteDetails {
if (status != 'completed') return false;
bool empty(String? v) => v == null || v.trim().isEmpty;
return empty(requestedBy) || empty(notedBy) ||
empty(receivedBy) || empty(actionTaken);
}
factory Task.fromMap(Map<String, dynamic> map) {
return Task(
id: map['id'] as String,
ticketId: map['ticket_id'] as String?,
taskNumber: map['task_number'] as String?,
title: map['title'] as String? ?? 'Task',
description: map['description'] as String? ?? '',
officeId: map['office_id'] as String?,
status: map['status'] as String? ?? 'queued',
priority: map['priority'] as int? ?? 1,
queueOrder: map['queue_order'] as int?,
createdAt: AppTime.parse(map['created_at'] as String),
creatorId: map['creator_id'] as String?,
startedAt: map['started_at'] == null
? null : AppTime.parse(map['started_at'] as String),
completedAt: map['completed_at'] == null
? null : AppTime.parse(map['completed_at'] as String),
requestType: map['request_type'] as String?,
requestTypeOther: map['request_type_other'] as String?,
requestCategory: map['request_category'] as String?,
requestedBy: map['requested_by'] as String?,
notedBy: map['noted_by'] as String?,
receivedBy: map['received_by'] as String?,
cancellationReason: map['cancellation_reason'] as String?,
cancelledAt: map['cancelled_at'] == null
? null : AppTime.parse(map['cancelled_at'] as String),
itJobPrinted: map['it_job_printed'] as bool? ?? false,
itJobPrintedAt: map['it_job_printed_at'] == null
? null : AppTime.parse(map['it_job_printed_at'] as String),
itJobReceivedById: map['it_job_received_by_id'] as String?,
actionTaken: (() {
final at = map['action_taken'];
if (at == null) return null;
if (at is String) return at;
try { return jsonEncode(at); } catch (_) { return at.toString(); }
})(),
);
}
}
+1 -106
View File
@@ -1,106 +1 @@
import 'dart:convert';
import '../utils/app_time.dart';
class TaskActivityLog {
TaskActivityLog({
required this.id,
required this.taskId,
this.actorId,
required this.actionType,
this.meta,
required this.createdAt,
});
final String id;
final String taskId;
final String? actorId;
final String actionType; // created, assigned, reassigned, started, completed
final Map<String, dynamic>? meta;
final DateTime createdAt;
factory TaskActivityLog.fromMap(Map<String, dynamic> map) {
// id and task_id may be returned as int or String depending on DB
final rawId = map['id'];
final rawTaskId = map['task_id'];
String id = rawId == null ? '' : rawId.toString();
String taskId = rawTaskId == null ? '' : rawTaskId.toString();
// actor_id is nullable
final actorId = map['actor_id']?.toString();
// action_type fallback
final actionType = (map['action_type'] as String?) ?? 'unknown';
// meta may be a Map, Map<dynamic,dynamic>, JSON-encoded string, List, or null
Map<String, dynamic>? meta;
final rawMeta = map['meta'];
if (rawMeta is Map<String, dynamic>) {
meta = rawMeta;
} else if (rawMeta is Map) {
// convert dynamic-key map to Map<String, dynamic>
try {
meta = rawMeta.map((k, v) => MapEntry(k.toString(), v));
} catch (_) {
meta = null;
}
} else if (rawMeta is String && rawMeta.isNotEmpty) {
try {
final decoded = jsonDecode(rawMeta);
if (decoded is Map<String, dynamic>) {
meta = decoded;
} else if (decoded is Map) {
meta = decoded.map((k, v) => MapEntry(k.toString(), v));
}
} catch (_) {
meta = null;
}
} else {
meta = null;
}
// created_at may be ISO string, DateTime, or numeric (seconds/millis since epoch)
final rawCreated = map['created_at'];
DateTime createdAt;
if (rawCreated is DateTime) {
createdAt = AppTime.toAppTime(rawCreated);
} else if (rawCreated is String) {
try {
createdAt = AppTime.parse(rawCreated);
} catch (_) {
createdAt = AppTime.now();
}
} else if (rawCreated is int) {
// assume seconds or milliseconds
if (rawCreated > 1e12) {
// likely microseconds or nanoseconds - treat as milliseconds
createdAt = AppTime.toAppTime(
DateTime.fromMillisecondsSinceEpoch(rawCreated),
);
} else if (rawCreated > 1e10) {
createdAt = AppTime.toAppTime(
DateTime.fromMillisecondsSinceEpoch(rawCreated),
);
} else {
createdAt = AppTime.toAppTime(
DateTime.fromMillisecondsSinceEpoch(rawCreated * 1000),
);
}
} else if (rawCreated is double) {
final asInt = rawCreated.toInt();
createdAt = AppTime.toAppTime(DateTime.fromMillisecondsSinceEpoch(asInt));
} else {
createdAt = AppTime.now();
}
return TaskActivityLog(
id: id,
taskId: taskId,
actorId: actorId,
actionType: actionType,
meta: meta,
createdAt: createdAt,
);
}
}
export 'task_activity_log.model.dart';
+102
View File
@@ -0,0 +1,102 @@
import 'dart:convert';
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
// meta: Map<String, dynamic>? is stored as JSON text in SQLite by Brick.
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'task_activity_logs'),
)
class TaskActivityLog extends OfflineFirstWithSupabaseModel {
final String id;
final String taskId;
final String? actorId;
final String actionType;
final Map<String, dynamic>? meta;
final DateTime createdAt;
TaskActivityLog({
required this.id,
required this.taskId,
this.actorId,
required this.actionType,
this.meta,
required this.createdAt,
});
factory TaskActivityLog.fromMap(Map<String, dynamic> map) {
final rawId = map['id'];
final rawTaskId = map['task_id'];
String id = rawId == null ? '' : rawId.toString();
String taskId = rawTaskId == null ? '' : rawTaskId.toString();
final actorId = map['actor_id']?.toString();
final actionType = (map['action_type'] as String?) ?? 'unknown';
Map<String, dynamic>? meta;
final rawMeta = map['meta'];
if (rawMeta is Map<String, dynamic>) {
meta = rawMeta;
} else if (rawMeta is Map) {
try {
meta = rawMeta.map((k, v) => MapEntry(k.toString(), v));
} catch (_) {
meta = null;
}
} else if (rawMeta is String && rawMeta.isNotEmpty) {
try {
final decoded = jsonDecode(rawMeta);
if (decoded is Map<String, dynamic>) {
meta = decoded;
} else if (decoded is Map) {
meta = decoded.map((k, v) => MapEntry(k.toString(), v));
}
} catch (_) {
meta = null;
}
}
final rawCreated = map['created_at'];
DateTime createdAt;
if (rawCreated is DateTime) {
createdAt = AppTime.toAppTime(rawCreated);
} else if (rawCreated is String) {
try {
createdAt = AppTime.parse(rawCreated);
} catch (_) {
createdAt = AppTime.now();
}
} else if (rawCreated is int) {
if (rawCreated > 1e12) {
createdAt = AppTime.toAppTime(
DateTime.fromMillisecondsSinceEpoch(rawCreated),
);
} else if (rawCreated > 1e10) {
createdAt = AppTime.toAppTime(
DateTime.fromMillisecondsSinceEpoch(rawCreated),
);
} else {
createdAt = AppTime.toAppTime(
DateTime.fromMillisecondsSinceEpoch(rawCreated * 1000),
);
}
} else if (rawCreated is double) {
createdAt = AppTime.toAppTime(
DateTime.fromMillisecondsSinceEpoch(rawCreated.toInt()),
);
} else {
createdAt = AppTime.now();
}
return TaskActivityLog(
id: id,
taskId: taskId,
actorId: actorId,
actionType: actionType,
meta: meta,
createdAt: createdAt,
);
}
}
+1 -42
View File
@@ -1,42 +1 @@
// Extension to add members property to Team
import '../models/team_member.dart';
extension TeamMembersExtension on Team {
List<String> members(List<TeamMember> allMembers) {
return allMembers
.where((m) => m.teamId == id)
.map((m) => m.userId)
.toList();
}
}
class Team {
Team({
required this.id,
required this.name,
required this.leaderId,
required this.officeIds,
required this.createdAt,
this.color,
});
final String id;
final String name;
final String leaderId;
final List<String> officeIds;
final DateTime createdAt;
final String? color;
factory Team.fromMap(Map<String, dynamic> map) {
return Team(
id: map['id'] as String,
name: map['name'] as String? ?? '',
leaderId: map['leader_id'] as String? ?? '',
officeIds:
(map['office_ids'] as List?)?.map((e) => e.toString()).toList() ??
<String>[],
createdAt: DateTime.parse(map['created_at'] as String),
color: map['color'] as String?,
);
}
}
export 'team.model.dart';
+47
View File
@@ -0,0 +1,47 @@
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_supabase/brick_supabase.dart';
import 'package:tasq/models/team_member.dart';
extension TeamMembersExtension on Team {
List<String> members(List<TeamMember> allMembers) {
return allMembers
.where((m) => m.teamId == id)
.map((m) => m.userId)
.toList();
}
}
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'teams'),
)
class Team extends OfflineFirstWithSupabaseModel {
final String id;
final String name;
final String leaderId;
final List<String> officeIds;
final DateTime createdAt;
final String? color;
Team({
required this.id,
required this.name,
required this.leaderId,
required this.officeIds,
required this.createdAt,
this.color,
});
factory Team.fromMap(Map<String, dynamic> map) {
return Team(
id: map['id'] as String,
name: map['name'] as String? ?? '',
leaderId: map['leader_id'] as String? ?? '',
officeIds:
(map['office_ids'] as List?)?.map((e) => e.toString()).toList() ??
<String>[],
createdAt: DateTime.parse(map['created_at'] as String),
color: map['color'] as String?,
);
}
}
+1 -78
View File
@@ -1,78 +1 @@
import '../utils/app_time.dart';
class Ticket {
Ticket({
required this.id,
required this.subject,
required this.description,
required this.officeId,
required this.status,
required this.createdAt,
required this.creatorId,
required this.respondedAt,
required this.promotedAt,
required this.closedAt,
});
final String id;
final String subject;
final String description;
final String officeId;
final String status;
final DateTime createdAt;
final String? creatorId;
final DateTime? respondedAt;
final DateTime? promotedAt;
final DateTime? closedAt;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Ticket &&
runtimeType == other.runtimeType &&
id == other.id &&
subject == other.subject &&
description == other.description &&
officeId == other.officeId &&
status == other.status &&
createdAt == other.createdAt &&
creatorId == other.creatorId &&
respondedAt == other.respondedAt &&
promotedAt == other.promotedAt &&
closedAt == other.closedAt;
@override
int get hashCode => Object.hash(
id,
subject,
description,
officeId,
status,
createdAt,
creatorId,
respondedAt,
promotedAt,
closedAt,
);
factory Ticket.fromMap(Map<String, dynamic> map) {
return Ticket(
id: map['id'] as String,
subject: map['subject'] as String? ?? '',
description: map['description'] as String? ?? '',
officeId: map['office_id'] as String? ?? '',
status: map['status'] as String? ?? 'pending',
createdAt: AppTime.parse(map['created_at'] as String),
creatorId: map['creator_id'] as String?,
respondedAt: map['responded_at'] == null
? null
: AppTime.parse(map['responded_at'] as String),
promotedAt: map['promoted_at'] == null
? null
: AppTime.parse(map['promoted_at'] as String),
closedAt: map['closed_at'] == null
? null
: AppTime.parse(map['closed_at'] as String),
);
}
}
export 'ticket.model.dart';
+71
View File
@@ -0,0 +1,71 @@
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
import 'package:brick_sqlite/brick_sqlite.dart';
import 'package:brick_supabase/brick_supabase.dart';
import '../utils/app_time.dart';
@ConnectOfflineFirstWithSupabase(
supabaseConfig: SupabaseSerializable(tableName: 'tickets'),
)
class Ticket extends OfflineFirstWithSupabaseModel {
final String id;
final String subject;
final String description;
final String officeId;
final String status;
final DateTime createdAt;
final String? creatorId;
final DateTime? respondedAt;
final DateTime? promotedAt;
final DateTime? closedAt;
Ticket({
required this.id,
required this.subject,
required this.description,
required this.officeId,
required this.status,
required this.createdAt,
required this.creatorId,
required this.respondedAt,
required this.promotedAt,
required this.closedAt,
});
@Supabase(ignore: true)
@Sqlite(ignore: true)
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Ticket &&
runtimeType == other.runtimeType &&
id == other.id && subject == other.subject &&
description == other.description && officeId == other.officeId &&
status == other.status && createdAt == other.createdAt &&
creatorId == other.creatorId && respondedAt == other.respondedAt &&
promotedAt == other.promotedAt && closedAt == other.closedAt;
@Supabase(ignore: true)
@Sqlite(ignore: true)
@override
int get hashCode => Object.hash(id, subject, description, officeId, status,
createdAt, creatorId, respondedAt, promotedAt, closedAt);
factory Ticket.fromMap(Map<String, dynamic> map) {
return Ticket(
id: map['id'] as String,
subject: map['subject'] as String? ?? '',
description: map['description'] as String? ?? '',
officeId: map['office_id'] as String? ?? '',
status: map['status'] as String? ?? 'pending',
createdAt: AppTime.parse(map['created_at'] as String),
creatorId: map['creator_id'] as String?,
respondedAt: map['responded_at'] == null
? null : AppTime.parse(map['responded_at'] as String),
promotedAt: map['promoted_at'] == null
? null : AppTime.parse(map['promoted_at'] as String),
closedAt: map['closed_at'] == null
? null : AppTime.parse(map['closed_at'] as String),
);
}
}