Offline Support
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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 +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 +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';
|
||||
|
||||
@@ -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 +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';
|
||||
|
||||
@@ -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 +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';
|
||||
|
||||
@@ -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 +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';
|
||||
|
||||
@@ -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 +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 +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 +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';
|
||||
|
||||
@@ -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 +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';
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
@@ -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 +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';
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
@@ -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 +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';
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
@@ -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 +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';
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user