update database

This commit is contained in:
Phuoc Nguyen
2025-10-24 11:31:48 +07:00
parent f95fa9d0a6
commit c4272f9a21
126 changed files with 23528 additions and 2234 deletions

View File

@@ -0,0 +1,47 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
part 'audit_log_model.g.dart';
@HiveType(typeId: HiveTypeIds.auditLogModel)
class AuditLogModel extends HiveObject {
AuditLogModel({required this.logId, required this.userId, required this.action, required this.entityType, required this.entityId, this.oldValue, this.newValue, this.ipAddress, this.userAgent, required this.timestamp});
@HiveField(0) final int logId;
@HiveField(1) final String userId;
@HiveField(2) final String action;
@HiveField(3) final String entityType;
@HiveField(4) final String entityId;
@HiveField(5) final String? oldValue;
@HiveField(6) final String? newValue;
@HiveField(7) final String? ipAddress;
@HiveField(8) final String? userAgent;
@HiveField(9) final DateTime timestamp;
factory AuditLogModel.fromJson(Map<String, dynamic> json) => AuditLogModel(
logId: json['log_id'] as int,
userId: json['user_id'] as String,
action: json['action'] as String,
entityType: json['entity_type'] as String,
entityId: json['entity_id'] as String,
oldValue: json['old_value'] != null ? jsonEncode(json['old_value']) : null,
newValue: json['new_value'] != null ? jsonEncode(json['new_value']) : null,
ipAddress: json['ip_address'] as String?,
userAgent: json['user_agent'] as String?,
timestamp: DateTime.parse(json['timestamp'] as String),
);
Map<String, dynamic> toJson() => {
'log_id': logId,
'user_id': userId,
'action': action,
'entity_type': entityType,
'entity_id': entityId,
'old_value': oldValue != null ? jsonDecode(oldValue!) : null,
'new_value': newValue != null ? jsonDecode(newValue!) : null,
'ip_address': ipAddress,
'user_agent': userAgent,
'timestamp': timestamp.toIso8601String(),
};
}

View File

@@ -0,0 +1,68 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'audit_log_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class AuditLogModelAdapter extends TypeAdapter<AuditLogModel> {
@override
final typeId = 24;
@override
AuditLogModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return AuditLogModel(
logId: (fields[0] as num).toInt(),
userId: fields[1] as String,
action: fields[2] as String,
entityType: fields[3] as String,
entityId: fields[4] as String,
oldValue: fields[5] as String?,
newValue: fields[6] as String?,
ipAddress: fields[7] as String?,
userAgent: fields[8] as String?,
timestamp: fields[9] as DateTime,
);
}
@override
void write(BinaryWriter writer, AuditLogModel obj) {
writer
..writeByte(10)
..writeByte(0)
..write(obj.logId)
..writeByte(1)
..write(obj.userId)
..writeByte(2)
..write(obj.action)
..writeByte(3)
..write(obj.entityType)
..writeByte(4)
..write(obj.entityId)
..writeByte(5)
..write(obj.oldValue)
..writeByte(6)
..write(obj.newValue)
..writeByte(7)
..write(obj.ipAddress)
..writeByte(8)
..write(obj.userAgent)
..writeByte(9)
..write(obj.timestamp);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AuditLogModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,50 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'payment_reminder_model.g.dart';
@HiveType(typeId: HiveTypeIds.paymentReminderModel)
class PaymentReminderModel extends HiveObject {
PaymentReminderModel({required this.reminderId, required this.invoiceId, required this.userId, required this.reminderType, required this.subject, required this.message, required this.isRead, required this.isSent, this.scheduledAt, this.sentAt, this.readAt});
@HiveField(0) final String reminderId;
@HiveField(1) final String invoiceId;
@HiveField(2) final String userId;
@HiveField(3) final ReminderType reminderType;
@HiveField(4) final String subject;
@HiveField(5) final String message;
@HiveField(6) final bool isRead;
@HiveField(7) final bool isSent;
@HiveField(8) final DateTime? scheduledAt;
@HiveField(9) final DateTime? sentAt;
@HiveField(10) final DateTime? readAt;
factory PaymentReminderModel.fromJson(Map<String, dynamic> json) => PaymentReminderModel(
reminderId: json['reminder_id'] as String,
invoiceId: json['invoice_id'] as String,
userId: json['user_id'] as String,
reminderType: ReminderType.values.firstWhere((e) => e.name == json['reminder_type']),
subject: json['subject'] as String,
message: json['message'] as String,
isRead: json['is_read'] as bool? ?? false,
isSent: json['is_sent'] as bool? ?? false,
scheduledAt: json['scheduled_at'] != null ? DateTime.parse(json['scheduled_at']?.toString() ?? '') : null,
sentAt: json['sent_at'] != null ? DateTime.parse(json['sent_at']?.toString() ?? '') : null,
readAt: json['read_at'] != null ? DateTime.parse(json['read_at']?.toString() ?? '') : null,
);
Map<String, dynamic> toJson() => {
'reminder_id': reminderId,
'invoice_id': invoiceId,
'user_id': userId,
'reminder_type': reminderType.name,
'subject': subject,
'message': message,
'is_read': isRead,
'is_sent': isSent,
'scheduled_at': scheduledAt?.toIso8601String(),
'sent_at': sentAt?.toIso8601String(),
'read_at': readAt?.toIso8601String(),
};
}

View File

@@ -0,0 +1,71 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'payment_reminder_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class PaymentReminderModelAdapter extends TypeAdapter<PaymentReminderModel> {
@override
final typeId = 23;
@override
PaymentReminderModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return PaymentReminderModel(
reminderId: fields[0] as String,
invoiceId: fields[1] as String,
userId: fields[2] as String,
reminderType: fields[3] as ReminderType,
subject: fields[4] as String,
message: fields[5] as String,
isRead: fields[6] as bool,
isSent: fields[7] as bool,
scheduledAt: fields[8] as DateTime?,
sentAt: fields[9] as DateTime?,
readAt: fields[10] as DateTime?,
);
}
@override
void write(BinaryWriter writer, PaymentReminderModel obj) {
writer
..writeByte(11)
..writeByte(0)
..write(obj.reminderId)
..writeByte(1)
..write(obj.invoiceId)
..writeByte(2)
..write(obj.userId)
..writeByte(3)
..write(obj.reminderType)
..writeByte(4)
..write(obj.subject)
..writeByte(5)
..write(obj.message)
..writeByte(6)
..write(obj.isRead)
..writeByte(7)
..write(obj.isSent)
..writeByte(8)
..write(obj.scheduledAt)
..writeByte(9)
..write(obj.sentAt)
..writeByte(10)
..write(obj.readAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PaymentReminderModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,151 @@
/// Domain Entity: Audit Log
///
/// Represents an audit trail entry for system activities.
library;
/// Audit Log Entity
///
/// Contains information about a system action:
/// - User and action details
/// - Entity affected
/// - Change tracking
/// - Session information
class AuditLog {
/// Unique log identifier
final String logId;
/// User ID who performed the action
final String? userId;
/// Action performed (create, update, delete, login, etc.)
final String action;
/// Entity type affected (user, order, product, etc.)
final String? entityType;
/// Entity ID affected
final String? entityId;
/// Old value (before change)
final Map<String, dynamic>? oldValue;
/// New value (after change)
final Map<String, dynamic>? newValue;
/// IP address of the user
final String? ipAddress;
/// User agent string
final String? userAgent;
/// Timestamp of the action
final DateTime timestamp;
const AuditLog({
required this.logId,
this.userId,
required this.action,
this.entityType,
this.entityId,
this.oldValue,
this.newValue,
this.ipAddress,
this.userAgent,
required this.timestamp,
});
/// Check if log has old value
bool get hasOldValue => oldValue != null && oldValue!.isNotEmpty;
/// Check if log has new value
bool get hasNewValue => newValue != null && newValue!.isNotEmpty;
/// Check if action is create
bool get isCreate => action.toLowerCase() == 'create';
/// Check if action is update
bool get isUpdate => action.toLowerCase() == 'update';
/// Check if action is delete
bool get isDelete => action.toLowerCase() == 'delete';
/// Check if action is login
bool get isLogin => action.toLowerCase() == 'login';
/// Check if action is logout
bool get isLogout => action.toLowerCase() == 'logout';
/// Get changed fields
List<String> get changedFields {
if (!hasOldValue || !hasNewValue) return [];
final changed = <String>[];
for (final key in newValue!.keys) {
if (oldValue!.containsKey(key) && oldValue![key] != newValue![key]) {
changed.add(key);
}
}
return changed;
}
/// Get time since action
Duration get timeSinceAction {
return DateTime.now().difference(timestamp);
}
/// Copy with method for immutability
AuditLog copyWith({
String? logId,
String? userId,
String? action,
String? entityType,
String? entityId,
Map<String, dynamic>? oldValue,
Map<String, dynamic>? newValue,
String? ipAddress,
String? userAgent,
DateTime? timestamp,
}) {
return AuditLog(
logId: logId ?? this.logId,
userId: userId ?? this.userId,
action: action ?? this.action,
entityType: entityType ?? this.entityType,
entityId: entityId ?? this.entityId,
oldValue: oldValue ?? this.oldValue,
newValue: newValue ?? this.newValue,
ipAddress: ipAddress ?? this.ipAddress,
userAgent: userAgent ?? this.userAgent,
timestamp: timestamp ?? this.timestamp,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is AuditLog &&
other.logId == logId &&
other.userId == userId &&
other.action == action &&
other.entityType == entityType &&
other.entityId == entityId;
}
@override
int get hashCode {
return Object.hash(
logId,
userId,
action,
entityType,
entityId,
);
}
@override
String toString() {
return 'AuditLog(logId: $logId, userId: $userId, action: $action, '
'entityType: $entityType, entityId: $entityId, timestamp: $timestamp)';
}
}

View File

@@ -0,0 +1,179 @@
/// Domain Entity: Payment Reminder
///
/// Represents a payment reminder for an unpaid invoice.
library;
/// Reminder type enum
enum ReminderType {
/// Initial reminder before due date
initial,
/// Reminder on due date
dueDate,
/// First reminder after due date
firstOverdue,
/// Second reminder after due date
secondOverdue,
/// Final warning
finalWarning;
/// Get display name for reminder type
String get displayName {
switch (this) {
case ReminderType.initial:
return 'Initial Reminder';
case ReminderType.dueDate:
return 'Due Date Reminder';
case ReminderType.firstOverdue:
return 'First Overdue';
case ReminderType.secondOverdue:
return 'Second Overdue';
case ReminderType.finalWarning:
return 'Final Warning';
}
}
}
/// Payment Reminder Entity
///
/// Contains information about a payment reminder:
/// - Invoice reference
/// - Reminder content
/// - Delivery status
/// - Scheduling
class PaymentReminder {
/// Unique reminder identifier
final String reminderId;
/// Invoice ID this reminder is for
final String invoiceId;
/// User ID receiving the reminder
final String userId;
/// Reminder type
final ReminderType reminderType;
/// Reminder subject
final String subject;
/// Reminder message
final String message;
/// Reminder has been read
final bool isRead;
/// Reminder has been sent
final bool isSent;
/// Scheduled send timestamp
final DateTime? scheduledAt;
/// Actual send timestamp
final DateTime? sentAt;
/// Read timestamp
final DateTime? readAt;
const PaymentReminder({
required this.reminderId,
required this.invoiceId,
required this.userId,
required this.reminderType,
required this.subject,
required this.message,
required this.isRead,
required this.isSent,
this.scheduledAt,
this.sentAt,
this.readAt,
});
/// Check if reminder is pending (scheduled but not sent)
bool get isPending => !isSent && scheduledAt != null;
/// Check if reminder is overdue to be sent
bool get isOverdueToSend {
if (isSent || scheduledAt == null) return false;
return DateTime.now().isAfter(scheduledAt!);
}
/// Check if reminder is unread
bool get isUnread => !isRead;
/// Get time until scheduled send
Duration? get timeUntilSend {
if (scheduledAt == null || isSent) return null;
final duration = scheduledAt!.difference(DateTime.now());
return duration.isNegative ? null : duration;
}
/// Get time since sent
Duration? get timeSinceSent {
if (sentAt == null) return null;
return DateTime.now().difference(sentAt!);
}
/// Copy with method for immutability
PaymentReminder copyWith({
String? reminderId,
String? invoiceId,
String? userId,
ReminderType? reminderType,
String? subject,
String? message,
bool? isRead,
bool? isSent,
DateTime? scheduledAt,
DateTime? sentAt,
DateTime? readAt,
}) {
return PaymentReminder(
reminderId: reminderId ?? this.reminderId,
invoiceId: invoiceId ?? this.invoiceId,
userId: userId ?? this.userId,
reminderType: reminderType ?? this.reminderType,
subject: subject ?? this.subject,
message: message ?? this.message,
isRead: isRead ?? this.isRead,
isSent: isSent ?? this.isSent,
scheduledAt: scheduledAt ?? this.scheduledAt,
sentAt: sentAt ?? this.sentAt,
readAt: readAt ?? this.readAt,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is PaymentReminder &&
other.reminderId == reminderId &&
other.invoiceId == invoiceId &&
other.userId == userId &&
other.reminderType == reminderType &&
other.isRead == isRead &&
other.isSent == isSent;
}
@override
int get hashCode {
return Object.hash(
reminderId,
invoiceId,
userId,
reminderType,
isRead,
isSent,
);
}
@override
String toString() {
return 'PaymentReminder(reminderId: $reminderId, invoiceId: $invoiceId, '
'reminderType: $reminderType, isSent: $isSent, isRead: $isRead)';
}
}

View File

@@ -0,0 +1,300 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'user_model.g.dart';
/// User Model
///
/// Hive CE model for caching user data locally.
/// Maps to the 'users' table in the database.
///
/// Type ID: 0
@HiveType(typeId: HiveTypeIds.userModel)
class UserModel extends HiveObject {
UserModel({
required this.userId,
required this.phoneNumber,
this.passwordHash,
required this.fullName,
this.email,
required this.role,
required this.status,
required this.loyaltyTier,
required this.totalPoints,
this.companyInfo,
this.cccd,
this.attachments,
this.address,
this.avatarUrl,
this.referralCode,
this.referredBy,
this.erpnextCustomerId,
required this.createdAt,
this.updatedAt,
this.lastLoginAt,
});
/// User ID (Primary Key)
@HiveField(0)
final String userId;
/// Phone number (unique, used for login)
@HiveField(1)
final String phoneNumber;
/// Password hash (stored encrypted)
@HiveField(2)
final String? passwordHash;
/// Full name of the user
@HiveField(3)
final String fullName;
/// Email address
@HiveField(4)
final String? email;
/// User role (customer, distributor, admin, staff)
@HiveField(5)
final UserRole role;
/// Account status (active, inactive, suspended, pending)
@HiveField(6)
final UserStatus status;
/// Loyalty tier (bronze, silver, gold, platinum, diamond, titan)
@HiveField(7)
final LoyaltyTier loyaltyTier;
/// Total accumulated loyalty points
@HiveField(8)
final int totalPoints;
/// Company information (JSON encoded)
/// Contains: company_name, tax_id, business_type, etc.
@HiveField(9)
final String? companyInfo;
/// Citizen ID (CCCD/CMND)
@HiveField(10)
final String? cccd;
/// Attachments (JSON encoded list)
/// Contains: identity_card_images, business_license, etc.
@HiveField(11)
final String? attachments;
/// Address
@HiveField(12)
final String? address;
/// Avatar URL
@HiveField(13)
final String? avatarUrl;
/// Referral code for this user
@HiveField(14)
final String? referralCode;
/// ID of user who referred this user
@HiveField(15)
final String? referredBy;
/// ERPNext customer ID for integration
@HiveField(16)
final String? erpnextCustomerId;
/// Account creation timestamp
@HiveField(17)
final DateTime createdAt;
/// Last update timestamp
@HiveField(18)
final DateTime? updatedAt;
/// Last login timestamp
@HiveField(19)
final DateTime? lastLoginAt;
// =========================================================================
// JSON SERIALIZATION
// =========================================================================
/// Create UserModel from JSON
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
userId: json['user_id'] as String,
phoneNumber: json['phone_number'] as String,
passwordHash: json['password_hash'] as String?,
fullName: json['full_name'] as String,
email: json['email'] as String?,
role: UserRole.values.firstWhere(
(e) => e.name == (json['role'] as String),
orElse: () => UserRole.customer,
),
status: UserStatus.values.firstWhere(
(e) => e.name == (json['status'] as String),
orElse: () => UserStatus.pending,
),
loyaltyTier: LoyaltyTier.values.firstWhere(
(e) => e.name == (json['loyalty_tier'] as String),
orElse: () => LoyaltyTier.bronze,
),
totalPoints: json['total_points'] as int? ?? 0,
companyInfo: json['company_info'] != null
? jsonEncode(json['company_info'])
: null,
cccd: json['cccd'] as String?,
attachments: json['attachments'] != null
? jsonEncode(json['attachments'])
: null,
address: json['address'] as String?,
avatarUrl: json['avatar_url'] as String?,
referralCode: json['referral_code'] as String?,
referredBy: json['referred_by'] as String?,
erpnextCustomerId: json['erpnext_customer_id'] as String?,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: json['updated_at'] != null
? DateTime.parse(json['updated_at'] as String)
: null,
lastLoginAt: json['last_login_at'] != null
? DateTime.parse(json['last_login_at'] as String)
: null,
);
}
/// Convert UserModel to JSON
Map<String, dynamic> toJson() {
return {
'user_id': userId,
'phone_number': phoneNumber,
'password_hash': passwordHash,
'full_name': fullName,
'email': email,
'role': role.name,
'status': status.name,
'loyalty_tier': loyaltyTier.name,
'total_points': totalPoints,
'company_info': companyInfo != null ? jsonDecode(companyInfo!) : null,
'cccd': cccd,
'attachments': attachments != null ? jsonDecode(attachments!) : null,
'address': address,
'avatar_url': avatarUrl,
'referral_code': referralCode,
'referred_by': referredBy,
'erpnext_customer_id': erpnextCustomerId,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
'last_login_at': lastLoginAt?.toIso8601String(),
};
}
// =========================================================================
// HELPER METHODS
// =========================================================================
/// Get company info as Map
Map<String, dynamic>? get companyInfoMap {
if (companyInfo == null) return null;
try {
return jsonDecode(companyInfo!) as Map<String, dynamic>;
} catch (e) {
return null;
}
}
/// Get attachments as List
List<dynamic>? get attachmentsList {
if (attachments == null) return null;
try {
return jsonDecode(attachments!) as List<dynamic>;
} catch (e) {
return null;
}
}
/// Check if user is active
bool get isActive => status == UserStatus.active;
/// Check if user is admin or staff
bool get isStaff => role == UserRole.admin || role == UserRole.staff;
/// Get user initials for avatar
String get initials {
final parts = fullName.trim().split(' ');
if (parts.length >= 2) {
return '${parts.first[0]}${parts.last[0]}'.toUpperCase();
}
return fullName.isNotEmpty ? fullName[0].toUpperCase() : '?';
}
// =========================================================================
// COPY WITH
// =========================================================================
/// Create a copy with updated fields
UserModel copyWith({
String? userId,
String? phoneNumber,
String? passwordHash,
String? fullName,
String? email,
UserRole? role,
UserStatus? status,
LoyaltyTier? loyaltyTier,
int? totalPoints,
String? companyInfo,
String? cccd,
String? attachments,
String? address,
String? avatarUrl,
String? referralCode,
String? referredBy,
String? erpnextCustomerId,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? lastLoginAt,
}) {
return UserModel(
userId: userId ?? this.userId,
phoneNumber: phoneNumber ?? this.phoneNumber,
passwordHash: passwordHash ?? this.passwordHash,
fullName: fullName ?? this.fullName,
email: email ?? this.email,
role: role ?? this.role,
status: status ?? this.status,
loyaltyTier: loyaltyTier ?? this.loyaltyTier,
totalPoints: totalPoints ?? this.totalPoints,
companyInfo: companyInfo ?? this.companyInfo,
cccd: cccd ?? this.cccd,
attachments: attachments ?? this.attachments,
address: address ?? this.address,
avatarUrl: avatarUrl ?? this.avatarUrl,
referralCode: referralCode ?? this.referralCode,
referredBy: referredBy ?? this.referredBy,
erpnextCustomerId: erpnextCustomerId ?? this.erpnextCustomerId,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
lastLoginAt: lastLoginAt ?? this.lastLoginAt,
);
}
@override
String toString() {
return 'UserModel(userId: $userId, fullName: $fullName, role: $role, tier: $loyaltyTier, points: $totalPoints)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UserModel && other.userId == userId;
}
@override
int get hashCode => userId.hashCode;
}

View File

@@ -0,0 +1,98 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class UserModelAdapter extends TypeAdapter<UserModel> {
@override
final typeId = 0;
@override
UserModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return UserModel(
userId: fields[0] as String,
phoneNumber: fields[1] as String,
passwordHash: fields[2] as String?,
fullName: fields[3] as String,
email: fields[4] as String?,
role: fields[5] as UserRole,
status: fields[6] as UserStatus,
loyaltyTier: fields[7] as LoyaltyTier,
totalPoints: (fields[8] as num).toInt(),
companyInfo: fields[9] as String?,
cccd: fields[10] as String?,
attachments: fields[11] as String?,
address: fields[12] as String?,
avatarUrl: fields[13] as String?,
referralCode: fields[14] as String?,
referredBy: fields[15] as String?,
erpnextCustomerId: fields[16] as String?,
createdAt: fields[17] as DateTime,
updatedAt: fields[18] as DateTime?,
lastLoginAt: fields[19] as DateTime?,
);
}
@override
void write(BinaryWriter writer, UserModel obj) {
writer
..writeByte(20)
..writeByte(0)
..write(obj.userId)
..writeByte(1)
..write(obj.phoneNumber)
..writeByte(2)
..write(obj.passwordHash)
..writeByte(3)
..write(obj.fullName)
..writeByte(4)
..write(obj.email)
..writeByte(5)
..write(obj.role)
..writeByte(6)
..write(obj.status)
..writeByte(7)
..write(obj.loyaltyTier)
..writeByte(8)
..write(obj.totalPoints)
..writeByte(9)
..write(obj.companyInfo)
..writeByte(10)
..write(obj.cccd)
..writeByte(11)
..write(obj.attachments)
..writeByte(12)
..write(obj.address)
..writeByte(13)
..write(obj.avatarUrl)
..writeByte(14)
..write(obj.referralCode)
..writeByte(15)
..write(obj.referredBy)
..writeByte(16)
..write(obj.erpnextCustomerId)
..writeByte(17)
..write(obj.createdAt)
..writeByte(18)
..write(obj.updatedAt)
..writeByte(19)
..write(obj.lastLoginAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,185 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
part 'user_session_model.g.dart';
/// User Session Model
///
/// Hive CE model for caching user session data locally.
/// Maps to the 'user_sessions' table in the database.
///
/// Type ID: 1
@HiveType(typeId: HiveTypeIds.userSessionModel)
class UserSessionModel extends HiveObject {
UserSessionModel({
required this.sessionId,
required this.userId,
required this.deviceId,
this.deviceType,
this.deviceName,
this.ipAddress,
this.userAgent,
this.refreshToken,
required this.expiresAt,
required this.createdAt,
this.lastActivity,
});
/// Session ID (Primary Key)
@HiveField(0)
final String sessionId;
/// User ID (Foreign Key to users)
@HiveField(1)
final String userId;
/// Device ID (unique identifier for the device)
@HiveField(2)
final String deviceId;
/// Device type (android, ios, web, etc.)
@HiveField(3)
final String? deviceType;
/// Device name (e.g., "Samsung Galaxy S21")
@HiveField(4)
final String? deviceName;
/// IP address of the device
@HiveField(5)
final String? ipAddress;
/// User agent string
@HiveField(6)
final String? userAgent;
/// Refresh token for session renewal
@HiveField(7)
final String? refreshToken;
/// Session expiration timestamp
@HiveField(8)
final DateTime expiresAt;
/// Session creation timestamp
@HiveField(9)
final DateTime createdAt;
/// Last activity timestamp
@HiveField(10)
final DateTime? lastActivity;
// =========================================================================
// JSON SERIALIZATION
// =========================================================================
/// Create UserSessionModel from JSON
factory UserSessionModel.fromJson(Map<String, dynamic> json) {
return UserSessionModel(
sessionId: json['session_id'] as String,
userId: json['user_id'] as String,
deviceId: json['device_id'] as String,
deviceType: json['device_type'] as String?,
deviceName: json['device_name'] as String?,
ipAddress: json['ip_address'] as String?,
userAgent: json['user_agent'] as String?,
refreshToken: json['refresh_token'] as String?,
expiresAt: DateTime.parse(json['expires_at'] as String),
createdAt: DateTime.parse(json['created_at'] as String),
lastActivity: json['last_activity'] != null
? DateTime.parse(json['last_activity'] as String)
: null,
);
}
/// Convert UserSessionModel to JSON
Map<String, dynamic> toJson() {
return {
'session_id': sessionId,
'user_id': userId,
'device_id': deviceId,
'device_type': deviceType,
'device_name': deviceName,
'ip_address': ipAddress,
'user_agent': userAgent,
'refresh_token': refreshToken,
'expires_at': expiresAt.toIso8601String(),
'created_at': createdAt.toIso8601String(),
'last_activity': lastActivity?.toIso8601String(),
};
}
// =========================================================================
// HELPER METHODS
// =========================================================================
/// Check if session is expired
bool get isExpired => DateTime.now().isAfter(expiresAt);
/// Check if session is valid (not expired)
bool get isValid => !isExpired;
/// Get session duration
Duration get duration => DateTime.now().difference(createdAt);
/// Get time until expiration
Duration get timeUntilExpiration => expiresAt.difference(DateTime.now());
/// Get session age
Duration get age => DateTime.now().difference(createdAt);
/// Get time since last activity
Duration? get timeSinceLastActivity {
if (lastActivity == null) return null;
return DateTime.now().difference(lastActivity!);
}
// =========================================================================
// COPY WITH
// =========================================================================
/// Create a copy with updated fields
UserSessionModel copyWith({
String? sessionId,
String? userId,
String? deviceId,
String? deviceType,
String? deviceName,
String? ipAddress,
String? userAgent,
String? refreshToken,
DateTime? expiresAt,
DateTime? createdAt,
DateTime? lastActivity,
}) {
return UserSessionModel(
sessionId: sessionId ?? this.sessionId,
userId: userId ?? this.userId,
deviceId: deviceId ?? this.deviceId,
deviceType: deviceType ?? this.deviceType,
deviceName: deviceName ?? this.deviceName,
ipAddress: ipAddress ?? this.ipAddress,
userAgent: userAgent ?? this.userAgent,
refreshToken: refreshToken ?? this.refreshToken,
expiresAt: expiresAt ?? this.expiresAt,
createdAt: createdAt ?? this.createdAt,
lastActivity: lastActivity ?? this.lastActivity,
);
}
@override
String toString() {
return 'UserSessionModel(sessionId: $sessionId, userId: $userId, isValid: $isValid)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UserSessionModel && other.sessionId == sessionId;
}
@override
int get hashCode => sessionId.hashCode;
}

View File

@@ -0,0 +1,71 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_session_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class UserSessionModelAdapter extends TypeAdapter<UserSessionModel> {
@override
final typeId = 1;
@override
UserSessionModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return UserSessionModel(
sessionId: fields[0] as String,
userId: fields[1] as String,
deviceId: fields[2] as String,
deviceType: fields[3] as String?,
deviceName: fields[4] as String?,
ipAddress: fields[5] as String?,
userAgent: fields[6] as String?,
refreshToken: fields[7] as String?,
expiresAt: fields[8] as DateTime,
createdAt: fields[9] as DateTime,
lastActivity: fields[10] as DateTime?,
);
}
@override
void write(BinaryWriter writer, UserSessionModel obj) {
writer
..writeByte(11)
..writeByte(0)
..write(obj.sessionId)
..writeByte(1)
..write(obj.userId)
..writeByte(2)
..write(obj.deviceId)
..writeByte(3)
..write(obj.deviceType)
..writeByte(4)
..write(obj.deviceName)
..writeByte(5)
..write(obj.ipAddress)
..writeByte(6)
..write(obj.userAgent)
..writeByte(7)
..write(obj.refreshToken)
..writeByte(8)
..write(obj.expiresAt)
..writeByte(9)
..write(obj.createdAt)
..writeByte(10)
..write(obj.lastActivity);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserSessionModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,314 @@
/// Domain Entity: User
///
/// Represents a user account in the Worker application.
/// Contains authentication, profile, and loyalty information.
library;
/// User role enum
enum UserRole {
/// Customer/worker user
customer,
/// Sales representative
sales,
/// System administrator
admin,
/// Accountant
accountant,
/// Designer
designer;
}
/// User status enum
enum UserStatus {
/// Account pending approval
pending,
/// Active account
active,
/// Suspended account
suspended,
/// Rejected account
rejected;
}
/// Loyalty tier enum
enum LoyaltyTier {
/// No tier
none,
/// Gold tier (entry level)
gold,
/// Platinum tier (mid level)
platinum,
/// Diamond tier (highest level)
diamond;
/// Get display name for tier
String get displayName {
switch (this) {
case LoyaltyTier.none:
return 'NONE';
case LoyaltyTier.gold:
return 'GOLD';
case LoyaltyTier.platinum:
return 'PLATINUM';
case LoyaltyTier.diamond:
return 'DIAMOND';
}
}
}
/// Company information
class CompanyInfo {
/// Company name
final String? name;
/// Tax identification number
final String? taxId;
/// Company address
final String? address;
/// Business type
final String? businessType;
/// Business license number
final String? licenseNumber;
const CompanyInfo({
this.name,
this.taxId,
this.address,
this.businessType,
this.licenseNumber,
});
/// Create from JSON map
factory CompanyInfo.fromJson(Map<String, dynamic> json) {
return CompanyInfo(
name: json['name'] as String?,
taxId: json['tax_id'] as String?,
address: json['address'] as String?,
businessType: json['business_type'] as String?,
licenseNumber: json['license_number'] as String?,
);
}
/// Convert to JSON map
Map<String, dynamic> toJson() {
return {
'name': name,
'tax_id': taxId,
'address': address,
'business_type': businessType,
'license_number': licenseNumber,
};
}
}
/// User Entity
///
/// Represents a complete user profile including:
/// - Authentication credentials
/// - Personal information
/// - Company details (if applicable)
/// - Loyalty program membership
/// - Referral information
class User {
/// Unique user identifier
final String userId;
/// Phone number (used for login)
final String phoneNumber;
/// Full name
final String fullName;
/// Email address
final String? email;
/// User role
final UserRole role;
/// Account status
final UserStatus status;
/// Current loyalty tier
final LoyaltyTier loyaltyTier;
/// Total loyalty points
final int totalPoints;
/// Company information (optional)
final CompanyInfo? companyInfo;
/// CCCD/ID card number
final String? cccd;
/// Attachment URLs (ID cards, licenses, etc.)
final List<String> attachments;
/// Address
final String? address;
/// Avatar URL
final String? avatarUrl;
/// Referral code (unique for this user)
final String? referralCode;
/// ID of user who referred this user
final String? referredBy;
/// ERPNext customer ID
final String? erpnextCustomerId;
/// Account creation timestamp
final DateTime createdAt;
/// Last update timestamp
final DateTime updatedAt;
/// Last login timestamp
final DateTime? lastLoginAt;
const User({
required this.userId,
required this.phoneNumber,
required this.fullName,
this.email,
required this.role,
required this.status,
required this.loyaltyTier,
required this.totalPoints,
this.companyInfo,
this.cccd,
required this.attachments,
this.address,
this.avatarUrl,
this.referralCode,
this.referredBy,
this.erpnextCustomerId,
required this.createdAt,
required this.updatedAt,
this.lastLoginAt,
});
/// Check if user is active
bool get isActive => status == UserStatus.active;
/// Check if user is pending approval
bool get isPending => status == UserStatus.pending;
/// Check if user has company info
bool get hasCompanyInfo => companyInfo != null && companyInfo!.name != null;
/// Check if user is an admin
bool get isAdmin => role == UserRole.admin;
/// Check if user is a customer
bool get isCustomer => role == UserRole.customer;
/// Get display name for user
String get displayName => fullName;
/// Copy with method for immutability
User copyWith({
String? userId,
String? phoneNumber,
String? fullName,
String? email,
UserRole? role,
UserStatus? status,
LoyaltyTier? loyaltyTier,
int? totalPoints,
CompanyInfo? companyInfo,
String? cccd,
List<String>? attachments,
String? address,
String? avatarUrl,
String? referralCode,
String? referredBy,
String? erpnextCustomerId,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? lastLoginAt,
}) {
return User(
userId: userId ?? this.userId,
phoneNumber: phoneNumber ?? this.phoneNumber,
fullName: fullName ?? this.fullName,
email: email ?? this.email,
role: role ?? this.role,
status: status ?? this.status,
loyaltyTier: loyaltyTier ?? this.loyaltyTier,
totalPoints: totalPoints ?? this.totalPoints,
companyInfo: companyInfo ?? this.companyInfo,
cccd: cccd ?? this.cccd,
attachments: attachments ?? this.attachments,
address: address ?? this.address,
avatarUrl: avatarUrl ?? this.avatarUrl,
referralCode: referralCode ?? this.referralCode,
referredBy: referredBy ?? this.referredBy,
erpnextCustomerId: erpnextCustomerId ?? this.erpnextCustomerId,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
lastLoginAt: lastLoginAt ?? this.lastLoginAt,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is User &&
other.userId == userId &&
other.phoneNumber == phoneNumber &&
other.fullName == fullName &&
other.email == email &&
other.role == role &&
other.status == status &&
other.loyaltyTier == loyaltyTier &&
other.totalPoints == totalPoints &&
other.cccd == cccd &&
other.address == address &&
other.avatarUrl == avatarUrl &&
other.referralCode == referralCode &&
other.referredBy == referredBy &&
other.erpnextCustomerId == erpnextCustomerId;
}
@override
int get hashCode {
return Object.hash(
userId,
phoneNumber,
fullName,
email,
role,
status,
loyaltyTier,
totalPoints,
cccd,
address,
avatarUrl,
referralCode,
referredBy,
erpnextCustomerId,
);
}
@override
String toString() {
return 'User(userId: $userId, phoneNumber: $phoneNumber, fullName: $fullName, '
'role: $role, status: $status, loyaltyTier: $loyaltyTier, totalPoints: $totalPoints)';
}
}

View File

@@ -0,0 +1,142 @@
/// Domain Entity: User Session
///
/// Represents an active user session with device and authentication information.
library;
/// User Session Entity
///
/// Contains information about an active user session:
/// - Device details
/// - Authentication tokens
/// - Session timing
class UserSession {
/// Unique session identifier
final String sessionId;
/// User ID associated with this session
final String userId;
/// Unique device identifier
final String deviceId;
/// Device type (ios, android, web)
final String? deviceType;
/// Device name
final String? deviceName;
/// IP address
final String? ipAddress;
/// User agent string
final String? userAgent;
/// Refresh token for renewing access
final String? refreshToken;
/// Session expiration timestamp
final DateTime expiresAt;
/// Session creation timestamp
final DateTime createdAt;
/// Last activity timestamp
final DateTime? lastActivity;
const UserSession({
required this.sessionId,
required this.userId,
required this.deviceId,
this.deviceType,
this.deviceName,
this.ipAddress,
this.userAgent,
this.refreshToken,
required this.expiresAt,
required this.createdAt,
this.lastActivity,
});
/// Check if session is expired
bool get isExpired => DateTime.now().isAfter(expiresAt);
/// Check if session is expiring soon (within 1 hour)
bool get isExpiringSoon {
final hoursUntilExpiry = expiresAt.difference(DateTime.now()).inHours;
return hoursUntilExpiry > 0 && hoursUntilExpiry <= 1;
}
/// Check if session is active
bool get isActive => !isExpired;
/// Get device display name
String get deviceDisplayName => deviceName ?? deviceType ?? 'Unknown Device';
/// Get time since last activity
Duration? get timeSinceLastActivity {
if (lastActivity == null) return null;
return DateTime.now().difference(lastActivity!);
}
/// Copy with method for immutability
UserSession copyWith({
String? sessionId,
String? userId,
String? deviceId,
String? deviceType,
String? deviceName,
String? ipAddress,
String? userAgent,
String? refreshToken,
DateTime? expiresAt,
DateTime? createdAt,
DateTime? lastActivity,
}) {
return UserSession(
sessionId: sessionId ?? this.sessionId,
userId: userId ?? this.userId,
deviceId: deviceId ?? this.deviceId,
deviceType: deviceType ?? this.deviceType,
deviceName: deviceName ?? this.deviceName,
ipAddress: ipAddress ?? this.ipAddress,
userAgent: userAgent ?? this.userAgent,
refreshToken: refreshToken ?? this.refreshToken,
expiresAt: expiresAt ?? this.expiresAt,
createdAt: createdAt ?? this.createdAt,
lastActivity: lastActivity ?? this.lastActivity,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UserSession &&
other.sessionId == sessionId &&
other.userId == userId &&
other.deviceId == deviceId &&
other.deviceType == deviceType &&
other.deviceName == deviceName &&
other.ipAddress == ipAddress &&
other.refreshToken == refreshToken;
}
@override
int get hashCode {
return Object.hash(
sessionId,
userId,
deviceId,
deviceType,
deviceName,
ipAddress,
refreshToken,
);
}
@override
String toString() {
return 'UserSession(sessionId: $sessionId, userId: $userId, deviceId: $deviceId, '
'deviceType: $deviceType, expiresAt: $expiresAt, isActive: $isActive)';
}
}

View File

@@ -0,0 +1,79 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
part 'cart_item_model.g.dart';
/// Cart Item Model - Type ID: 5
@HiveType(typeId: HiveTypeIds.cartItemModel)
class CartItemModel extends HiveObject {
CartItemModel({
required this.cartItemId,
required this.cartId,
required this.productId,
required this.quantity,
required this.unitPrice,
required this.subtotal,
required this.addedAt,
});
@HiveField(0)
final String cartItemId;
@HiveField(1)
final String cartId;
@HiveField(2)
final String productId;
@HiveField(3)
final double quantity;
@HiveField(4)
final double unitPrice;
@HiveField(5)
final double subtotal;
@HiveField(6)
final DateTime addedAt;
factory CartItemModel.fromJson(Map<String, dynamic> json) {
return CartItemModel(
cartItemId: json['cart_item_id'] as String,
cartId: json['cart_id'] as String,
productId: json['product_id'] as String,
quantity: (json['quantity'] as num).toDouble(),
unitPrice: (json['unit_price'] as num).toDouble(),
subtotal: (json['subtotal'] as num).toDouble(),
addedAt: DateTime.parse(json['added_at'] as String),
);
}
Map<String, dynamic> toJson() => {
'cart_item_id': cartItemId,
'cart_id': cartId,
'product_id': productId,
'quantity': quantity,
'unit_price': unitPrice,
'subtotal': subtotal,
'added_at': addedAt.toIso8601String(),
};
CartItemModel copyWith({
String? cartItemId,
String? cartId,
String? productId,
double? quantity,
double? unitPrice,
double? subtotal,
DateTime? addedAt,
}) => CartItemModel(
cartItemId: cartItemId ?? this.cartItemId,
cartId: cartId ?? this.cartId,
productId: productId ?? this.productId,
quantity: quantity ?? this.quantity,
unitPrice: unitPrice ?? this.unitPrice,
subtotal: subtotal ?? this.subtotal,
addedAt: addedAt ?? this.addedAt,
);
}

View File

@@ -0,0 +1,59 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'cart_item_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class CartItemModelAdapter extends TypeAdapter<CartItemModel> {
@override
final typeId = 5;
@override
CartItemModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return CartItemModel(
cartItemId: fields[0] as String,
cartId: fields[1] as String,
productId: fields[2] as String,
quantity: (fields[3] as num).toDouble(),
unitPrice: (fields[4] as num).toDouble(),
subtotal: (fields[5] as num).toDouble(),
addedAt: fields[6] as DateTime,
);
}
@override
void write(BinaryWriter writer, CartItemModel obj) {
writer
..writeByte(7)
..writeByte(0)
..write(obj.cartItemId)
..writeByte(1)
..write(obj.cartId)
..writeByte(2)
..write(obj.productId)
..writeByte(3)
..write(obj.quantity)
..writeByte(4)
..write(obj.unitPrice)
..writeByte(5)
..write(obj.subtotal)
..writeByte(6)
..write(obj.addedAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CartItemModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,71 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
part 'cart_model.g.dart';
/// Cart Model - Type ID: 4
@HiveType(typeId: HiveTypeIds.cartModel)
class CartModel extends HiveObject {
CartModel({
required this.cartId,
required this.userId,
required this.totalAmount,
required this.isSynced,
required this.lastModified,
required this.createdAt,
});
@HiveField(0)
final String cartId;
@HiveField(1)
final String userId;
@HiveField(2)
final double totalAmount;
@HiveField(3)
final bool isSynced;
@HiveField(4)
final DateTime lastModified;
@HiveField(5)
final DateTime createdAt;
factory CartModel.fromJson(Map<String, dynamic> json) {
return CartModel(
cartId: json['cart_id'] as String,
userId: json['user_id'] as String,
totalAmount: (json['total_amount'] as num).toDouble(),
isSynced: json['is_synced'] as bool? ?? false,
lastModified: DateTime.parse(json['last_modified'] as String),
createdAt: DateTime.parse(json['created_at'] as String),
);
}
Map<String, dynamic> toJson() => {
'cart_id': cartId,
'user_id': userId,
'total_amount': totalAmount,
'is_synced': isSynced,
'last_modified': lastModified.toIso8601String(),
'created_at': createdAt.toIso8601String(),
};
CartModel copyWith({
String? cartId,
String? userId,
double? totalAmount,
bool? isSynced,
DateTime? lastModified,
DateTime? createdAt,
}) => CartModel(
cartId: cartId ?? this.cartId,
userId: userId ?? this.userId,
totalAmount: totalAmount ?? this.totalAmount,
isSynced: isSynced ?? this.isSynced,
lastModified: lastModified ?? this.lastModified,
createdAt: createdAt ?? this.createdAt,
);
}

View File

@@ -0,0 +1,56 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'cart_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class CartModelAdapter extends TypeAdapter<CartModel> {
@override
final typeId = 4;
@override
CartModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return CartModel(
cartId: fields[0] as String,
userId: fields[1] as String,
totalAmount: (fields[2] as num).toDouble(),
isSynced: fields[3] as bool,
lastModified: fields[4] as DateTime,
createdAt: fields[5] as DateTime,
);
}
@override
void write(BinaryWriter writer, CartModel obj) {
writer
..writeByte(6)
..writeByte(0)
..write(obj.cartId)
..writeByte(1)
..write(obj.userId)
..writeByte(2)
..write(obj.totalAmount)
..writeByte(3)
..write(obj.isSynced)
..writeByte(4)
..write(obj.lastModified)
..writeByte(5)
..write(obj.createdAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CartModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,92 @@
/// Domain Entity: Cart
///
/// Represents a shopping cart for a user.
library;
/// Cart Entity
///
/// Contains cart-level information:
/// - User ID
/// - Total amount
/// - Sync status
/// - Timestamps
class Cart {
/// Unique cart identifier
final String cartId;
/// User ID who owns this cart
final String userId;
/// Total cart amount
final double totalAmount;
/// Whether cart is synced with backend
final bool isSynced;
/// Last modification timestamp
final DateTime lastModified;
/// Cart creation timestamp
final DateTime createdAt;
const Cart({
required this.cartId,
required this.userId,
required this.totalAmount,
required this.isSynced,
required this.lastModified,
required this.createdAt,
});
/// Check if cart is empty
bool get isEmpty => totalAmount == 0;
/// Check if cart needs sync
bool get needsSync => !isSynced;
/// Copy with method for immutability
Cart copyWith({
String? cartId,
String? userId,
double? totalAmount,
bool? isSynced,
DateTime? lastModified,
DateTime? createdAt,
}) {
return Cart(
cartId: cartId ?? this.cartId,
userId: userId ?? this.userId,
totalAmount: totalAmount ?? this.totalAmount,
isSynced: isSynced ?? this.isSynced,
lastModified: lastModified ?? this.lastModified,
createdAt: createdAt ?? this.createdAt,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Cart &&
other.cartId == cartId &&
other.userId == userId &&
other.totalAmount == totalAmount &&
other.isSynced == isSynced;
}
@override
int get hashCode {
return Object.hash(
cartId,
userId,
totalAmount,
isSynced,
);
}
@override
String toString() {
return 'Cart(cartId: $cartId, userId: $userId, totalAmount: $totalAmount, '
'isSynced: $isSynced, lastModified: $lastModified)';
}
}

View File

@@ -0,0 +1,98 @@
/// Domain Entity: Cart Item
///
/// Represents a single item in a shopping cart.
library;
/// Cart Item Entity
///
/// Contains item-level information:
/// - Product reference
/// - Quantity
/// - Pricing
class CartItem {
/// Unique cart item identifier
final String cartItemId;
/// Cart ID this item belongs to
final String cartId;
/// Product ID
final String productId;
/// Quantity ordered
final double quantity;
/// Unit price at time of adding to cart
final double unitPrice;
/// Subtotal (quantity * unitPrice)
final double subtotal;
/// Timestamp when item was added
final DateTime addedAt;
const CartItem({
required this.cartItemId,
required this.cartId,
required this.productId,
required this.quantity,
required this.unitPrice,
required this.subtotal,
required this.addedAt,
});
/// Calculate subtotal (for verification)
double get calculatedSubtotal => quantity * unitPrice;
/// Copy with method for immutability
CartItem copyWith({
String? cartItemId,
String? cartId,
String? productId,
double? quantity,
double? unitPrice,
double? subtotal,
DateTime? addedAt,
}) {
return CartItem(
cartItemId: cartItemId ?? this.cartItemId,
cartId: cartId ?? this.cartId,
productId: productId ?? this.productId,
quantity: quantity ?? this.quantity,
unitPrice: unitPrice ?? this.unitPrice,
subtotal: subtotal ?? this.subtotal,
addedAt: addedAt ?? this.addedAt,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is CartItem &&
other.cartItemId == cartItemId &&
other.cartId == cartId &&
other.productId == productId &&
other.quantity == quantity &&
other.unitPrice == unitPrice &&
other.subtotal == subtotal;
}
@override
int get hashCode {
return Object.hash(
cartItemId,
cartId,
productId,
quantity,
unitPrice,
subtotal,
);
}
@override
String toString() {
return 'CartItem(cartItemId: $cartItemId, productId: $productId, '
'quantity: $quantity, unitPrice: $unitPrice, subtotal: $subtotal)';
}
}

View File

@@ -0,0 +1,57 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'chat_room_model.g.dart';
@HiveType(typeId: HiveTypeIds.chatRoomModel)
class ChatRoomModel extends HiveObject {
ChatRoomModel({required this.chatRoomId, required this.roomType, this.relatedQuoteId, this.relatedOrderId, required this.participants, this.roomName, required this.isActive, this.lastActivity, required this.createdAt, this.createdBy});
@HiveField(0) final String chatRoomId;
@HiveField(1) final RoomType roomType;
@HiveField(2) final String? relatedQuoteId;
@HiveField(3) final String? relatedOrderId;
@HiveField(4) final String participants;
@HiveField(5) final String? roomName;
@HiveField(6) final bool isActive;
@HiveField(7) final DateTime? lastActivity;
@HiveField(8) final DateTime createdAt;
@HiveField(9) final String? createdBy;
factory ChatRoomModel.fromJson(Map<String, dynamic> json) => ChatRoomModel(
chatRoomId: json['chat_room_id'] as String,
roomType: RoomType.values.firstWhere((e) => e.name == json['room_type']),
relatedQuoteId: json['related_quote_id'] as String?,
relatedOrderId: json['related_order_id'] as String?,
participants: jsonEncode(json['participants']),
roomName: json['room_name'] as String?,
isActive: json['is_active'] as bool? ?? true,
lastActivity: json['last_activity'] != null ? DateTime.parse(json['last_activity']?.toString() ?? '') : null,
createdAt: DateTime.parse(json['created_at']?.toString() ?? ''),
createdBy: json['created_by'] as String?,
);
Map<String, dynamic> toJson() => {
'chat_room_id': chatRoomId,
'room_type': roomType.name,
'related_quote_id': relatedQuoteId,
'related_order_id': relatedOrderId,
'participants': jsonDecode(participants),
'room_name': roomName,
'is_active': isActive,
'last_activity': lastActivity?.toIso8601String(),
'created_at': createdAt.toIso8601String(),
'created_by': createdBy,
};
List<String>? get participantsList {
try {
final decoded = jsonDecode(participants) as List;
return decoded.map((e) => e.toString()).toList();
} catch (e) {
return null;
}
}
}

View File

@@ -0,0 +1,68 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'chat_room_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ChatRoomModelAdapter extends TypeAdapter<ChatRoomModel> {
@override
final typeId = 18;
@override
ChatRoomModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ChatRoomModel(
chatRoomId: fields[0] as String,
roomType: fields[1] as RoomType,
relatedQuoteId: fields[2] as String?,
relatedOrderId: fields[3] as String?,
participants: fields[4] as String,
roomName: fields[5] as String?,
isActive: fields[6] as bool,
lastActivity: fields[7] as DateTime?,
createdAt: fields[8] as DateTime,
createdBy: fields[9] as String?,
);
}
@override
void write(BinaryWriter writer, ChatRoomModel obj) {
writer
..writeByte(10)
..writeByte(0)
..write(obj.chatRoomId)
..writeByte(1)
..write(obj.roomType)
..writeByte(2)
..write(obj.relatedQuoteId)
..writeByte(3)
..write(obj.relatedOrderId)
..writeByte(4)
..write(obj.participants)
..writeByte(5)
..write(obj.roomName)
..writeByte(6)
..write(obj.isActive)
..writeByte(7)
..write(obj.lastActivity)
..writeByte(8)
..write(obj.createdAt)
..writeByte(9)
..write(obj.createdBy);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ChatRoomModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,67 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'message_model.g.dart';
@HiveType(typeId: HiveTypeIds.messageModel)
class MessageModel extends HiveObject {
MessageModel({required this.messageId, required this.chatRoomId, required this.senderId, required this.contentType, required this.content, this.attachmentUrl, this.productReference, required this.isRead, required this.isEdited, required this.isDeleted, this.readBy, required this.timestamp, this.editedAt});
@HiveField(0) final String messageId;
@HiveField(1) final String chatRoomId;
@HiveField(2) final String senderId;
@HiveField(3) final ContentType contentType;
@HiveField(4) final String content;
@HiveField(5) final String? attachmentUrl;
@HiveField(6) final String? productReference;
@HiveField(7) final bool isRead;
@HiveField(8) final bool isEdited;
@HiveField(9) final bool isDeleted;
@HiveField(10) final String? readBy;
@HiveField(11) final DateTime timestamp;
@HiveField(12) final DateTime? editedAt;
factory MessageModel.fromJson(Map<String, dynamic> json) => MessageModel(
messageId: json['message_id'] as String,
chatRoomId: json['chat_room_id'] as String,
senderId: json['sender_id'] as String,
contentType: ContentType.values.firstWhere((e) => e.name == json['content_type']),
content: json['content'] as String,
attachmentUrl: json['attachment_url'] as String?,
productReference: json['product_reference'] as String?,
isRead: json['is_read'] as bool? ?? false,
isEdited: json['is_edited'] as bool? ?? false,
isDeleted: json['is_deleted'] as bool? ?? false,
readBy: json['read_by'] != null ? jsonEncode(json['read_by']) : null,
timestamp: DateTime.parse(json['timestamp']?.toString() ?? ''),
editedAt: json['edited_at'] != null ? DateTime.parse(json['edited_at']?.toString() ?? '') : null,
);
Map<String, dynamic> toJson() => {
'message_id': messageId,
'chat_room_id': chatRoomId,
'sender_id': senderId,
'content_type': contentType.name,
'content': content,
'attachment_url': attachmentUrl,
'product_reference': productReference,
'is_read': isRead,
'is_edited': isEdited,
'is_deleted': isDeleted,
'read_by': readBy != null ? jsonDecode(readBy!) : null,
'timestamp': timestamp.toIso8601String(),
'edited_at': editedAt?.toIso8601String(),
};
List<String>? get readByList {
if (readBy == null) return null;
try {
final decoded = jsonDecode(readBy!) as List;
return decoded.map((e) => e.toString()).toList();
} catch (e) {
return null;
}
}
}

View File

@@ -0,0 +1,77 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'message_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class MessageModelAdapter extends TypeAdapter<MessageModel> {
@override
final typeId = 19;
@override
MessageModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return MessageModel(
messageId: fields[0] as String,
chatRoomId: fields[1] as String,
senderId: fields[2] as String,
contentType: fields[3] as ContentType,
content: fields[4] as String,
attachmentUrl: fields[5] as String?,
productReference: fields[6] as String?,
isRead: fields[7] as bool,
isEdited: fields[8] as bool,
isDeleted: fields[9] as bool,
readBy: fields[10] as String?,
timestamp: fields[11] as DateTime,
editedAt: fields[12] as DateTime?,
);
}
@override
void write(BinaryWriter writer, MessageModel obj) {
writer
..writeByte(13)
..writeByte(0)
..write(obj.messageId)
..writeByte(1)
..write(obj.chatRoomId)
..writeByte(2)
..write(obj.senderId)
..writeByte(3)
..write(obj.contentType)
..writeByte(4)
..write(obj.content)
..writeByte(5)
..write(obj.attachmentUrl)
..writeByte(6)
..write(obj.productReference)
..writeByte(7)
..write(obj.isRead)
..writeByte(8)
..write(obj.isEdited)
..writeByte(9)
..write(obj.isDeleted)
..writeByte(10)
..write(obj.readBy)
..writeByte(11)
..write(obj.timestamp)
..writeByte(12)
..write(obj.editedAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MessageModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,186 @@
/// Domain Entity: Chat Room
///
/// Represents a chat conversation room.
library;
/// Room type enum
enum RoomType {
/// Direct message between two users
direct,
/// Group chat
group,
/// Support chat with staff
support,
/// Order-related chat
order,
/// Quote-related chat
quote;
/// Get display name for room type
String get displayName {
switch (this) {
case RoomType.direct:
return 'Direct';
case RoomType.group:
return 'Group';
case RoomType.support:
return 'Support';
case RoomType.order:
return 'Order';
case RoomType.quote:
return 'Quote';
}
}
}
/// Chat Room Entity
///
/// Contains information about a chat room:
/// - Room type and participants
/// - Related entities (order, quote)
/// - Activity tracking
class ChatRoom {
/// Unique chat room identifier
final String chatRoomId;
/// Room type
final RoomType roomType;
/// Related quote ID (if quote chat)
final String? relatedQuoteId;
/// Related order ID (if order chat)
final String? relatedOrderId;
/// Participant user IDs
final List<String> participants;
/// Room name (for group chats)
final String? roomName;
/// Room is active
final bool isActive;
/// Last activity timestamp
final DateTime? lastActivity;
/// Room creation timestamp
final DateTime createdAt;
/// User ID who created the room
final String? createdBy;
const ChatRoom({
required this.chatRoomId,
required this.roomType,
this.relatedQuoteId,
this.relatedOrderId,
required this.participants,
this.roomName,
required this.isActive,
this.lastActivity,
required this.createdAt,
this.createdBy,
});
/// Check if room is direct message
bool get isDirect => roomType == RoomType.direct;
/// Check if room is group chat
bool get isGroup => roomType == RoomType.group;
/// Check if room is support chat
bool get isSupport => roomType == RoomType.support;
/// Check if room has order context
bool get hasOrderContext =>
roomType == RoomType.order && relatedOrderId != null;
/// Check if room has quote context
bool get hasQuoteContext =>
roomType == RoomType.quote && relatedQuoteId != null;
/// Get number of participants
int get participantCount => participants.length;
/// Get display name for room
String get displayName {
if (roomName != null && roomName!.isNotEmpty) return roomName!;
if (isSupport) return 'Customer Support';
if (hasOrderContext) return 'Order Chat';
if (hasQuoteContext) return 'Quote Discussion';
return 'Chat';
}
/// Get time since last activity
Duration? get timeSinceLastActivity {
if (lastActivity == null) return null;
return DateTime.now().difference(lastActivity!);
}
/// Check if user is participant
bool isParticipant(String userId) {
return participants.contains(userId);
}
/// Copy with method for immutability
ChatRoom copyWith({
String? chatRoomId,
RoomType? roomType,
String? relatedQuoteId,
String? relatedOrderId,
List<String>? participants,
String? roomName,
bool? isActive,
DateTime? lastActivity,
DateTime? createdAt,
String? createdBy,
}) {
return ChatRoom(
chatRoomId: chatRoomId ?? this.chatRoomId,
roomType: roomType ?? this.roomType,
relatedQuoteId: relatedQuoteId ?? this.relatedQuoteId,
relatedOrderId: relatedOrderId ?? this.relatedOrderId,
participants: participants ?? this.participants,
roomName: roomName ?? this.roomName,
isActive: isActive ?? this.isActive,
lastActivity: lastActivity ?? this.lastActivity,
createdAt: createdAt ?? this.createdAt,
createdBy: createdBy ?? this.createdBy,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ChatRoom &&
other.chatRoomId == chatRoomId &&
other.roomType == roomType &&
other.relatedQuoteId == relatedQuoteId &&
other.relatedOrderId == relatedOrderId &&
other.isActive == isActive;
}
@override
int get hashCode {
return Object.hash(
chatRoomId,
roomType,
relatedQuoteId,
relatedOrderId,
isActive,
);
}
@override
String toString() {
return 'ChatRoom(chatRoomId: $chatRoomId, roomType: $roomType, '
'displayName: $displayName, participantCount: $participantCount, '
'isActive: $isActive)';
}
}

View File

@@ -0,0 +1,212 @@
/// Domain Entity: Message
///
/// Represents a chat message in a conversation.
library;
/// Content type enum
enum ContentType {
/// Plain text message
text,
/// Image message
image,
/// File attachment
file,
/// Product reference
product,
/// System notification
system;
/// Get display name for content type
String get displayName {
switch (this) {
case ContentType.text:
return 'Text';
case ContentType.image:
return 'Image';
case ContentType.file:
return 'File';
case ContentType.product:
return 'Product';
case ContentType.system:
return 'System';
}
}
}
/// Message Entity
///
/// Contains information about a chat message:
/// - Message content
/// - Sender information
/// - Attachments
/// - Read status
/// - Edit history
class Message {
/// Unique message identifier
final String messageId;
/// Chat room ID this message belongs to
final String chatRoomId;
/// Sender user ID
final String senderId;
/// Content type
final ContentType contentType;
/// Message content/text
final String content;
/// Attachment URL (for images/files)
final String? attachmentUrl;
/// Product reference ID (if product message)
final String? productReference;
/// Message is read
final bool isRead;
/// Message has been edited
final bool isEdited;
/// Message has been deleted
final bool isDeleted;
/// User IDs who have read this message
final List<String> readBy;
/// Message timestamp
final DateTime timestamp;
/// Edit timestamp
final DateTime? editedAt;
const Message({
required this.messageId,
required this.chatRoomId,
required this.senderId,
required this.contentType,
required this.content,
this.attachmentUrl,
this.productReference,
required this.isRead,
required this.isEdited,
required this.isDeleted,
required this.readBy,
required this.timestamp,
this.editedAt,
});
/// Check if message is text
bool get isText => contentType == ContentType.text;
/// Check if message is image
bool get isImage => contentType == ContentType.image;
/// Check if message is file
bool get isFile => contentType == ContentType.file;
/// Check if message is product reference
bool get isProductReference => contentType == ContentType.product;
/// Check if message is system notification
bool get isSystemMessage => contentType == ContentType.system;
/// Check if message has attachment
bool get hasAttachment =>
attachmentUrl != null && attachmentUrl!.isNotEmpty;
/// Check if message references a product
bool get hasProductReference =>
productReference != null && productReference!.isNotEmpty;
/// Get number of readers
int get readerCount => readBy.length;
/// Check if user has read this message
bool isReadBy(String userId) {
return readBy.contains(userId);
}
/// Check if message is sent by user
bool isSentBy(String userId) {
return senderId == userId;
}
/// Get time since message was sent
Duration get timeSinceSent {
return DateTime.now().difference(timestamp);
}
/// Copy with method for immutability
Message copyWith({
String? messageId,
String? chatRoomId,
String? senderId,
ContentType? contentType,
String? content,
String? attachmentUrl,
String? productReference,
bool? isRead,
bool? isEdited,
bool? isDeleted,
List<String>? readBy,
DateTime? timestamp,
DateTime? editedAt,
}) {
return Message(
messageId: messageId ?? this.messageId,
chatRoomId: chatRoomId ?? this.chatRoomId,
senderId: senderId ?? this.senderId,
contentType: contentType ?? this.contentType,
content: content ?? this.content,
attachmentUrl: attachmentUrl ?? this.attachmentUrl,
productReference: productReference ?? this.productReference,
isRead: isRead ?? this.isRead,
isEdited: isEdited ?? this.isEdited,
isDeleted: isDeleted ?? this.isDeleted,
readBy: readBy ?? this.readBy,
timestamp: timestamp ?? this.timestamp,
editedAt: editedAt ?? this.editedAt,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Message &&
other.messageId == messageId &&
other.chatRoomId == chatRoomId &&
other.senderId == senderId &&
other.contentType == contentType &&
other.content == content &&
other.isRead == isRead &&
other.isEdited == isEdited &&
other.isDeleted == isDeleted;
}
@override
int get hashCode {
return Object.hash(
messageId,
chatRoomId,
senderId,
contentType,
content,
isRead,
isEdited,
isDeleted,
);
}
@override
String toString() {
return 'Message(messageId: $messageId, senderId: $senderId, '
'contentType: $contentType, isRead: $isRead, timestamp: $timestamp)';
}
}

View File

@@ -9,6 +9,7 @@
library;
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/features/home/domain/entities/member_card.dart';
part 'member_card_model.g.dart';
@@ -20,8 +21,8 @@ part 'member_card_model.g.dart';
/// - Hive local database storage
/// - Converting to/from domain entity
///
/// Hive Type ID: 10 (ensure this doesn't conflict with other models)
@HiveType(typeId: 10)
/// Hive Type ID: 25 (from HiveTypeIds.memberCardModel)
@HiveType(typeId: HiveTypeIds.memberCardModel)
class MemberCardModel extends HiveObject {
/// Member ID
@HiveField(0)

View File

@@ -8,7 +8,7 @@ part of 'member_card_model.dart';
class MemberCardModelAdapter extends TypeAdapter<MemberCardModel> {
@override
final typeId = 10;
final typeId = 25;
@override
MemberCardModel read(BinaryReader reader) {

View File

@@ -9,6 +9,7 @@
library;
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/features/home/domain/entities/promotion.dart';
part 'promotion_model.g.dart';
@@ -20,8 +21,8 @@ part 'promotion_model.g.dart';
/// - Hive local database storage
/// - Converting to/from domain entity
///
/// Hive Type ID: 11 (ensure this doesn't conflict with other models)
@HiveType(typeId: 11)
/// Hive Type ID: 26 (from HiveTypeIds.promotionModel)
@HiveType(typeId: HiveTypeIds.promotionModel)
class PromotionModel extends HiveObject {
/// Promotion ID
@HiveField(0)

View File

@@ -8,7 +8,7 @@ part of 'promotion_model.dart';
class PromotionModelAdapter extends TypeAdapter<PromotionModel> {
@override
final typeId = 11;
final typeId = 26;
@override
PromotionModel read(BinaryReader reader) {

View File

@@ -147,7 +147,7 @@ class HomePage extends ConsumerWidget {
QuickAction(
icon: Icons.grid_view,
label: l10n.products,
onTap: () => context.go(RouteNames.products),
onTap: () => context.pushNamed(RouteNames.products),
),
QuickAction(
icon: Icons.shopping_cart,

View File

@@ -0,0 +1,70 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'gift_catalog_model.g.dart';
@HiveType(typeId: HiveTypeIds.giftCatalogModel)
class GiftCatalogModel extends HiveObject {
GiftCatalogModel({required this.catalogId, required this.name, required this.description, this.imageUrl, required this.category, required this.pointsCost, required this.cashValue, required this.quantityAvailable, required this.quantityRedeemed, this.termsConditions, required this.isActive, this.validFrom, this.validUntil, required this.createdAt, this.updatedAt});
@HiveField(0) final String catalogId;
@HiveField(1) final String name;
@HiveField(2) final String description;
@HiveField(3) final String? imageUrl;
@HiveField(4) final GiftCategory category;
@HiveField(5) final int pointsCost;
@HiveField(6) final double cashValue;
@HiveField(7) final int quantityAvailable;
@HiveField(8) final int quantityRedeemed;
@HiveField(9) final String? termsConditions;
@HiveField(10) final bool isActive;
@HiveField(11) final DateTime? validFrom;
@HiveField(12) final DateTime? validUntil;
@HiveField(13) final DateTime createdAt;
@HiveField(14) final DateTime? updatedAt;
factory GiftCatalogModel.fromJson(Map<String, dynamic> json) => GiftCatalogModel(
catalogId: json['catalog_id'] as String,
name: json['name'] as String,
description: json['description'] as String,
imageUrl: json['image_url'] as String?,
category: GiftCategory.values.firstWhere((e) => e.name == json['category']),
pointsCost: json['points_cost'] as int,
cashValue: (json['cash_value'] as num).toDouble(),
quantityAvailable: json['quantity_available'] as int,
quantityRedeemed: json['quantity_redeemed'] as int? ?? 0,
termsConditions: json['terms_conditions'] as String?,
isActive: json['is_active'] as bool? ?? true,
validFrom: json['valid_from'] != null ? DateTime.parse(json['valid_from']?.toString() ?? '') : null,
validUntil: json['valid_until'] != null ? DateTime.parse(json['valid_until']?.toString() ?? '') : null,
createdAt: DateTime.parse(json['created_at']?.toString() ?? ''),
updatedAt: json['updated_at'] != null ? DateTime.parse(json['updated_at']?.toString() ?? '') : null,
);
Map<String, dynamic> toJson() => {
'catalog_id': catalogId,
'name': name,
'description': description,
'image_url': imageUrl,
'category': category.name,
'points_cost': pointsCost,
'cash_value': cashValue,
'quantity_available': quantityAvailable,
'quantity_redeemed': quantityRedeemed,
'terms_conditions': termsConditions,
'is_active': isActive,
'valid_from': validFrom?.toIso8601String(),
'valid_until': validUntil?.toIso8601String(),
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
};
bool get isAvailable => isActive && quantityAvailable > quantityRedeemed;
bool get isValid {
final now = DateTime.now();
if (validFrom != null && now.isBefore(validFrom!)) return false;
if (validUntil != null && now.isAfter(validUntil!)) return false;
return true;
}
}

View File

@@ -0,0 +1,83 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'gift_catalog_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class GiftCatalogModelAdapter extends TypeAdapter<GiftCatalogModel> {
@override
final typeId = 11;
@override
GiftCatalogModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return GiftCatalogModel(
catalogId: fields[0] as String,
name: fields[1] as String,
description: fields[2] as String,
imageUrl: fields[3] as String?,
category: fields[4] as GiftCategory,
pointsCost: (fields[5] as num).toInt(),
cashValue: (fields[6] as num).toDouble(),
quantityAvailable: (fields[7] as num).toInt(),
quantityRedeemed: (fields[8] as num).toInt(),
termsConditions: fields[9] as String?,
isActive: fields[10] as bool,
validFrom: fields[11] as DateTime?,
validUntil: fields[12] as DateTime?,
createdAt: fields[13] as DateTime,
updatedAt: fields[14] as DateTime?,
);
}
@override
void write(BinaryWriter writer, GiftCatalogModel obj) {
writer
..writeByte(15)
..writeByte(0)
..write(obj.catalogId)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.description)
..writeByte(3)
..write(obj.imageUrl)
..writeByte(4)
..write(obj.category)
..writeByte(5)
..write(obj.pointsCost)
..writeByte(6)
..write(obj.cashValue)
..writeByte(7)
..write(obj.quantityAvailable)
..writeByte(8)
..write(obj.quantityRedeemed)
..writeByte(9)
..write(obj.termsConditions)
..writeByte(10)
..write(obj.isActive)
..writeByte(11)
..write(obj.validFrom)
..writeByte(12)
..write(obj.validUntil)
..writeByte(13)
..write(obj.createdAt)
..writeByte(14)
..write(obj.updatedAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GiftCatalogModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,72 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'loyalty_point_entry_model.g.dart';
@HiveType(typeId: HiveTypeIds.loyaltyPointEntryModel)
class LoyaltyPointEntryModel extends HiveObject {
LoyaltyPointEntryModel({required this.entryId, required this.userId, required this.points, required this.entryType, required this.source, required this.description, this.referenceId, this.referenceType, this.complaint, required this.complaintStatus, required this.balanceAfter, this.expiryDate, required this.timestamp, this.erpnextEntryId});
@HiveField(0) final String entryId;
@HiveField(1) final String userId;
@HiveField(2) final int points;
@HiveField(3) final EntryType entryType;
@HiveField(4) final EntrySource source;
@HiveField(5) final String description;
@HiveField(6) final String? referenceId;
@HiveField(7) final String? referenceType;
@HiveField(8) final String? complaint;
@HiveField(9) final ComplaintStatus complaintStatus;
@HiveField(10) final int balanceAfter;
@HiveField(11) final DateTime? expiryDate;
@HiveField(12) final DateTime timestamp;
@HiveField(13) final String? erpnextEntryId;
factory LoyaltyPointEntryModel.fromJson(Map<String, dynamic> json) => LoyaltyPointEntryModel(
entryId: json['entry_id'] as String,
userId: json['user_id'] as String,
points: json['points'] as int,
entryType: EntryType.values.firstWhere((e) => e.name == json['entry_type']),
source: EntrySource.values.firstWhere((e) => e.name == json['source']),
description: json['description'] as String,
referenceId: json['reference_id'] as String?,
referenceType: json['reference_type'] as String?,
complaint: json['complaint'] != null ? jsonEncode(json['complaint']) : null,
complaintStatus: ComplaintStatus.values.firstWhere((e) => e.name == (json['complaint_status'] ?? 'none')),
balanceAfter: json['balance_after'] as int,
expiryDate: json['expiry_date'] != null ? DateTime.parse(json['expiry_date']?.toString() ?? '') : null,
timestamp: DateTime.parse(json['timestamp']?.toString() ?? ''),
erpnextEntryId: json['erpnext_entry_id'] as String?,
);
Map<String, dynamic> toJson() => {
'entry_id': entryId,
'user_id': userId,
'points': points,
'entry_type': entryType.name,
'source': source.name,
'description': description,
'reference_id': referenceId,
'reference_type': referenceType,
'complaint': complaint != null ? jsonDecode(complaint!) : null,
'complaint_status': complaintStatus.name,
'balance_after': balanceAfter,
'expiry_date': expiryDate?.toIso8601String(),
'timestamp': timestamp.toIso8601String(),
'erpnext_entry_id': erpnextEntryId,
};
Map<String, dynamic>? get complaintMap {
if (complaint == null) return null;
try {
return jsonDecode(complaint!) as Map<String, dynamic>;
} catch (e) {
return null;
}
}
bool get isExpired => expiryDate != null && DateTime.now().isAfter(expiryDate!);
bool get hasComplaint => complaintStatus != ComplaintStatus.none;
}

View File

@@ -0,0 +1,81 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'loyalty_point_entry_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class LoyaltyPointEntryModelAdapter
extends TypeAdapter<LoyaltyPointEntryModel> {
@override
final typeId = 10;
@override
LoyaltyPointEntryModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return LoyaltyPointEntryModel(
entryId: fields[0] as String,
userId: fields[1] as String,
points: (fields[2] as num).toInt(),
entryType: fields[3] as EntryType,
source: fields[4] as EntrySource,
description: fields[5] as String,
referenceId: fields[6] as String?,
referenceType: fields[7] as String?,
complaint: fields[8] as String?,
complaintStatus: fields[9] as ComplaintStatus,
balanceAfter: (fields[10] as num).toInt(),
expiryDate: fields[11] as DateTime?,
timestamp: fields[12] as DateTime,
erpnextEntryId: fields[13] as String?,
);
}
@override
void write(BinaryWriter writer, LoyaltyPointEntryModel obj) {
writer
..writeByte(14)
..writeByte(0)
..write(obj.entryId)
..writeByte(1)
..write(obj.userId)
..writeByte(2)
..write(obj.points)
..writeByte(3)
..write(obj.entryType)
..writeByte(4)
..write(obj.source)
..writeByte(5)
..write(obj.description)
..writeByte(6)
..write(obj.referenceId)
..writeByte(7)
..write(obj.referenceType)
..writeByte(8)
..write(obj.complaint)
..writeByte(9)
..write(obj.complaintStatus)
..writeByte(10)
..write(obj.balanceAfter)
..writeByte(11)
..write(obj.expiryDate)
..writeByte(12)
..write(obj.timestamp)
..writeByte(13)
..write(obj.erpnextEntryId);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is LoyaltyPointEntryModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,70 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'points_record_model.g.dart';
@HiveType(typeId: HiveTypeIds.pointsRecordModel)
class PointsRecordModel extends HiveObject {
PointsRecordModel({required this.recordId, required this.userId, required this.invoiceNumber, required this.storeName, required this.transactionDate, required this.invoiceAmount, this.notes, this.attachments, required this.status, this.rejectReason, this.pointsEarned, required this.submittedAt, this.processedAt, this.processedBy});
@HiveField(0) final String recordId;
@HiveField(1) final String userId;
@HiveField(2) final String invoiceNumber;
@HiveField(3) final String storeName;
@HiveField(4) final DateTime transactionDate;
@HiveField(5) final double invoiceAmount;
@HiveField(6) final String? notes;
@HiveField(7) final String? attachments;
@HiveField(8) final PointsStatus status;
@HiveField(9) final String? rejectReason;
@HiveField(10) final int? pointsEarned;
@HiveField(11) final DateTime submittedAt;
@HiveField(12) final DateTime? processedAt;
@HiveField(13) final String? processedBy;
factory PointsRecordModel.fromJson(Map<String, dynamic> json) => PointsRecordModel(
recordId: json['record_id'] as String,
userId: json['user_id'] as String,
invoiceNumber: json['invoice_number'] as String,
storeName: json['store_name'] as String,
transactionDate: DateTime.parse(json['transaction_date']?.toString() ?? ''),
invoiceAmount: (json['invoice_amount'] as num).toDouble(),
notes: json['notes'] as String?,
attachments: json['attachments'] != null ? jsonEncode(json['attachments']) : null,
status: PointsStatus.values.firstWhere((e) => e.name == json['status']),
rejectReason: json['reject_reason'] as String?,
pointsEarned: json['points_earned'] as int?,
submittedAt: DateTime.parse(json['submitted_at']?.toString() ?? ''),
processedAt: json['processed_at'] != null ? DateTime.parse(json['processed_at']?.toString() ?? '') : null,
processedBy: json['processed_by'] as String?,
);
Map<String, dynamic> toJson() => {
'record_id': recordId,
'user_id': userId,
'invoice_number': invoiceNumber,
'store_name': storeName,
'transaction_date': transactionDate.toIso8601String(),
'invoice_amount': invoiceAmount,
'notes': notes,
'attachments': attachments != null ? jsonDecode(attachments!) : null,
'status': status.name,
'reject_reason': rejectReason,
'points_earned': pointsEarned,
'submitted_at': submittedAt.toIso8601String(),
'processed_at': processedAt?.toIso8601String(),
'processed_by': processedBy,
};
List<String>? get attachmentsList {
if (attachments == null) return null;
try {
final decoded = jsonDecode(attachments!) as List;
return decoded.map((e) => e.toString()).toList();
} catch (e) {
return null;
}
}
}

View File

@@ -0,0 +1,80 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'points_record_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class PointsRecordModelAdapter extends TypeAdapter<PointsRecordModel> {
@override
final typeId = 13;
@override
PointsRecordModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return PointsRecordModel(
recordId: fields[0] as String,
userId: fields[1] as String,
invoiceNumber: fields[2] as String,
storeName: fields[3] as String,
transactionDate: fields[4] as DateTime,
invoiceAmount: (fields[5] as num).toDouble(),
notes: fields[6] as String?,
attachments: fields[7] as String?,
status: fields[8] as PointsStatus,
rejectReason: fields[9] as String?,
pointsEarned: (fields[10] as num?)?.toInt(),
submittedAt: fields[11] as DateTime,
processedAt: fields[12] as DateTime?,
processedBy: fields[13] as String?,
);
}
@override
void write(BinaryWriter writer, PointsRecordModel obj) {
writer
..writeByte(14)
..writeByte(0)
..write(obj.recordId)
..writeByte(1)
..write(obj.userId)
..writeByte(2)
..write(obj.invoiceNumber)
..writeByte(3)
..write(obj.storeName)
..writeByte(4)
..write(obj.transactionDate)
..writeByte(5)
..write(obj.invoiceAmount)
..writeByte(6)
..write(obj.notes)
..writeByte(7)
..write(obj.attachments)
..writeByte(8)
..write(obj.status)
..writeByte(9)
..write(obj.rejectReason)
..writeByte(10)
..write(obj.pointsEarned)
..writeByte(11)
..write(obj.submittedAt)
..writeByte(12)
..write(obj.processedAt)
..writeByte(13)
..write(obj.processedBy);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PointsRecordModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,69 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'redeemed_gift_model.g.dart';
@HiveType(typeId: HiveTypeIds.redeemedGiftModel)
class RedeemedGiftModel extends HiveObject {
RedeemedGiftModel({required this.giftId, required this.userId, required this.catalogId, required this.name, required this.description, this.voucherCode, this.qrCodeImage, required this.giftType, required this.pointsCost, required this.cashValue, this.expiryDate, required this.status, required this.redeemedAt, this.usedAt, this.usedLocation, this.usedReference});
@HiveField(0) final String giftId;
@HiveField(1) final String userId;
@HiveField(2) final String catalogId;
@HiveField(3) final String name;
@HiveField(4) final String description;
@HiveField(5) final String? voucherCode;
@HiveField(6) final String? qrCodeImage;
@HiveField(7) final GiftCategory giftType;
@HiveField(8) final int pointsCost;
@HiveField(9) final double cashValue;
@HiveField(10) final DateTime? expiryDate;
@HiveField(11) final GiftStatus status;
@HiveField(12) final DateTime redeemedAt;
@HiveField(13) final DateTime? usedAt;
@HiveField(14) final String? usedLocation;
@HiveField(15) final String? usedReference;
factory RedeemedGiftModel.fromJson(Map<String, dynamic> json) => RedeemedGiftModel(
giftId: json['gift_id'] as String,
userId: json['user_id'] as String,
catalogId: json['catalog_id'] as String,
name: json['name'] as String,
description: json['description'] as String,
voucherCode: json['voucher_code'] as String?,
qrCodeImage: json['qr_code_image'] as String?,
giftType: GiftCategory.values.firstWhere((e) => e.name == json['gift_type']),
pointsCost: json['points_cost'] as int,
cashValue: (json['cash_value'] as num).toDouble(),
expiryDate: json['expiry_date'] != null ? DateTime.parse(json['expiry_date']?.toString() ?? '') : null,
status: GiftStatus.values.firstWhere((e) => e.name == json['status']),
redeemedAt: DateTime.parse(json['redeemed_at']?.toString() ?? ''),
usedAt: json['used_at'] != null ? DateTime.parse(json['used_at']?.toString() ?? '') : null,
usedLocation: json['used_location'] as String?,
usedReference: json['used_reference'] as String?,
);
Map<String, dynamic> toJson() => {
'gift_id': giftId,
'user_id': userId,
'catalog_id': catalogId,
'name': name,
'description': description,
'voucher_code': voucherCode,
'qr_code_image': qrCodeImage,
'gift_type': giftType.name,
'points_cost': pointsCost,
'cash_value': cashValue,
'expiry_date': expiryDate?.toIso8601String(),
'status': status.name,
'redeemed_at': redeemedAt.toIso8601String(),
'used_at': usedAt?.toIso8601String(),
'used_location': usedLocation,
'used_reference': usedReference,
};
bool get isExpired => expiryDate != null && DateTime.now().isAfter(expiryDate!);
bool get isUsed => status == GiftStatus.used;
bool get isActive => status == GiftStatus.active && !isExpired;
}

View File

@@ -0,0 +1,86 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'redeemed_gift_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class RedeemedGiftModelAdapter extends TypeAdapter<RedeemedGiftModel> {
@override
final typeId = 12;
@override
RedeemedGiftModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RedeemedGiftModel(
giftId: fields[0] as String,
userId: fields[1] as String,
catalogId: fields[2] as String,
name: fields[3] as String,
description: fields[4] as String,
voucherCode: fields[5] as String?,
qrCodeImage: fields[6] as String?,
giftType: fields[7] as GiftCategory,
pointsCost: (fields[8] as num).toInt(),
cashValue: (fields[9] as num).toDouble(),
expiryDate: fields[10] as DateTime?,
status: fields[11] as GiftStatus,
redeemedAt: fields[12] as DateTime,
usedAt: fields[13] as DateTime?,
usedLocation: fields[14] as String?,
usedReference: fields[15] as String?,
);
}
@override
void write(BinaryWriter writer, RedeemedGiftModel obj) {
writer
..writeByte(16)
..writeByte(0)
..write(obj.giftId)
..writeByte(1)
..write(obj.userId)
..writeByte(2)
..write(obj.catalogId)
..writeByte(3)
..write(obj.name)
..writeByte(4)
..write(obj.description)
..writeByte(5)
..write(obj.voucherCode)
..writeByte(6)
..write(obj.qrCodeImage)
..writeByte(7)
..write(obj.giftType)
..writeByte(8)
..write(obj.pointsCost)
..writeByte(9)
..write(obj.cashValue)
..writeByte(10)
..write(obj.expiryDate)
..writeByte(11)
..write(obj.status)
..writeByte(12)
..write(obj.redeemedAt)
..writeByte(13)
..write(obj.usedAt)
..writeByte(14)
..write(obj.usedLocation)
..writeByte(15)
..write(obj.usedReference);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RedeemedGiftModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,212 @@
/// Domain Entity: Gift Catalog
///
/// Represents a redeemable gift in the loyalty program catalog.
library;
/// Gift category enum
enum GiftCategory {
/// Voucher gift
voucher,
/// Physical product
product,
/// Service
service,
/// Discount coupon
discount,
/// Other type
other;
/// Get display name for category
String get displayName {
switch (this) {
case GiftCategory.voucher:
return 'Voucher';
case GiftCategory.product:
return 'Product';
case GiftCategory.service:
return 'Service';
case GiftCategory.discount:
return 'Discount';
case GiftCategory.other:
return 'Other';
}
}
}
/// Gift Catalog Entity
///
/// Contains information about a redeemable gift:
/// - Gift details (name, description, image)
/// - Pricing in points
/// - Availability
/// - Terms and conditions
class GiftCatalog {
/// Unique catalog item identifier
final String catalogId;
/// Gift name
final String name;
/// Gift description
final String? description;
/// Gift image URL
final String? imageUrl;
/// Gift category
final GiftCategory category;
/// Points cost to redeem
final int pointsCost;
/// Cash value equivalent
final double? cashValue;
/// Quantity available for redemption
final int quantityAvailable;
/// Quantity already redeemed
final int quantityRedeemed;
/// Terms and conditions
final String? termsConditions;
/// Gift is active and available
final bool isActive;
/// Valid from date
final DateTime? validFrom;
/// Valid until date
final DateTime? validUntil;
/// Creation timestamp
final DateTime createdAt;
/// Last update timestamp
final DateTime updatedAt;
const GiftCatalog({
required this.catalogId,
required this.name,
this.description,
this.imageUrl,
required this.category,
required this.pointsCost,
this.cashValue,
required this.quantityAvailable,
required this.quantityRedeemed,
this.termsConditions,
required this.isActive,
this.validFrom,
this.validUntil,
required this.createdAt,
required this.updatedAt,
});
/// Check if gift is available for redemption
bool get isAvailable => isActive && quantityRemaining > 0 && isCurrentlyValid;
/// Get remaining quantity
int get quantityRemaining => quantityAvailable - quantityRedeemed;
/// Check if gift is in stock
bool get isInStock => quantityRemaining > 0;
/// Check if gift is currently valid (date range)
bool get isCurrentlyValid {
final now = DateTime.now();
if (validFrom != null && now.isBefore(validFrom!)) return false;
if (validUntil != null && now.isAfter(validUntil!)) return false;
return true;
}
/// Check if gift is coming soon
bool get isComingSoon {
if (validFrom == null) return false;
return DateTime.now().isBefore(validFrom!);
}
/// Check if gift has expired
bool get hasExpired {
if (validUntil == null) return false;
return DateTime.now().isAfter(validUntil!);
}
/// Get redemption percentage
double get redemptionPercentage {
if (quantityAvailable == 0) return 0;
return (quantityRedeemed / quantityAvailable) * 100;
}
/// Copy with method for immutability
GiftCatalog copyWith({
String? catalogId,
String? name,
String? description,
String? imageUrl,
GiftCategory? category,
int? pointsCost,
double? cashValue,
int? quantityAvailable,
int? quantityRedeemed,
String? termsConditions,
bool? isActive,
DateTime? validFrom,
DateTime? validUntil,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return GiftCatalog(
catalogId: catalogId ?? this.catalogId,
name: name ?? this.name,
description: description ?? this.description,
imageUrl: imageUrl ?? this.imageUrl,
category: category ?? this.category,
pointsCost: pointsCost ?? this.pointsCost,
cashValue: cashValue ?? this.cashValue,
quantityAvailable: quantityAvailable ?? this.quantityAvailable,
quantityRedeemed: quantityRedeemed ?? this.quantityRedeemed,
termsConditions: termsConditions ?? this.termsConditions,
isActive: isActive ?? this.isActive,
validFrom: validFrom ?? this.validFrom,
validUntil: validUntil ?? this.validUntil,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is GiftCatalog &&
other.catalogId == catalogId &&
other.name == name &&
other.category == category &&
other.pointsCost == pointsCost &&
other.isActive == isActive;
}
@override
int get hashCode {
return Object.hash(
catalogId,
name,
category,
pointsCost,
isActive,
);
}
@override
String toString() {
return 'GiftCatalog(catalogId: $catalogId, name: $name, category: $category, '
'pointsCost: $pointsCost, quantityRemaining: $quantityRemaining, '
'isAvailable: $isAvailable)';
}
}

View File

@@ -0,0 +1,232 @@
/// Domain Entity: Loyalty Point Entry
///
/// Represents a single loyalty points transaction.
library;
/// Entry type enum
enum EntryType {
/// Points earned
earn,
/// Points spent/redeemed
redeem,
/// Points adjusted by admin
adjustment,
/// Points expired
expiry;
}
/// Entry source enum
enum EntrySource {
/// Points from order purchase
order,
/// Points from referral
referral,
/// Points from gift redemption
redemption,
/// Points from project submission
project,
/// Points from points record
pointsRecord,
/// Manual adjustment by admin
manual,
/// Birthday bonus
birthday,
/// Welcome bonus
welcome,
/// Other source
other;
}
/// Complaint status enum
enum ComplaintStatus {
/// No complaint
none,
/// Complaint submitted
submitted,
/// Complaint under review
reviewing,
/// Complaint approved
approved,
/// Complaint rejected
rejected;
}
/// Loyalty Point Entry Entity
///
/// Contains information about a single points transaction:
/// - Points amount (positive for earn, negative for redeem)
/// - Transaction type and source
/// - Reference to related entity
/// - Complaint handling
class LoyaltyPointEntry {
/// Unique entry identifier
final String entryId;
/// User ID
final String userId;
/// Points amount (positive for earn, negative for redeem)
final int points;
/// Entry type
final EntryType entryType;
/// Source of the points
final EntrySource source;
/// Description of the transaction
final String? description;
/// Reference ID to related entity (order ID, gift ID, etc.)
final String? referenceId;
/// Reference type (order, gift, project, etc.)
final String? referenceType;
/// Complaint details (if any)
final Map<String, dynamic>? complaint;
/// Complaint status
final ComplaintStatus complaintStatus;
/// Balance after this transaction
final int balanceAfter;
/// Points expiry date
final DateTime? expiryDate;
/// Transaction timestamp
final DateTime timestamp;
/// ERPNext entry ID
final String? erpnextEntryId;
const LoyaltyPointEntry({
required this.entryId,
required this.userId,
required this.points,
required this.entryType,
required this.source,
this.description,
this.referenceId,
this.referenceType,
this.complaint,
required this.complaintStatus,
required this.balanceAfter,
this.expiryDate,
required this.timestamp,
this.erpnextEntryId,
});
/// Check if points are earned (positive)
bool get isEarn => points > 0 && entryType == EntryType.earn;
/// Check if points are spent (negative)
bool get isRedeem => points < 0 && entryType == EntryType.redeem;
/// Check if entry has complaint
bool get hasComplaint => complaintStatus != ComplaintStatus.none;
/// Check if complaint is pending
bool get isComplaintPending =>
complaintStatus == ComplaintStatus.submitted ||
complaintStatus == ComplaintStatus.reviewing;
/// Check if points are expired
bool get isExpired {
if (expiryDate == null) return false;
return DateTime.now().isAfter(expiryDate!);
}
/// Check if points are expiring soon (within 30 days)
bool get isExpiringSoon {
if (expiryDate == null) return false;
final daysUntilExpiry = expiryDate!.difference(DateTime.now()).inDays;
return daysUntilExpiry > 0 && daysUntilExpiry <= 30;
}
/// Get absolute points value
int get absolutePoints => points.abs();
/// Copy with method for immutability
LoyaltyPointEntry copyWith({
String? entryId,
String? userId,
int? points,
EntryType? entryType,
EntrySource? source,
String? description,
String? referenceId,
String? referenceType,
Map<String, dynamic>? complaint,
ComplaintStatus? complaintStatus,
int? balanceAfter,
DateTime? expiryDate,
DateTime? timestamp,
String? erpnextEntryId,
}) {
return LoyaltyPointEntry(
entryId: entryId ?? this.entryId,
userId: userId ?? this.userId,
points: points ?? this.points,
entryType: entryType ?? this.entryType,
source: source ?? this.source,
description: description ?? this.description,
referenceId: referenceId ?? this.referenceId,
referenceType: referenceType ?? this.referenceType,
complaint: complaint ?? this.complaint,
complaintStatus: complaintStatus ?? this.complaintStatus,
balanceAfter: balanceAfter ?? this.balanceAfter,
expiryDate: expiryDate ?? this.expiryDate,
timestamp: timestamp ?? this.timestamp,
erpnextEntryId: erpnextEntryId ?? this.erpnextEntryId,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is LoyaltyPointEntry &&
other.entryId == entryId &&
other.userId == userId &&
other.points == points &&
other.entryType == entryType &&
other.source == source &&
other.balanceAfter == balanceAfter;
}
@override
int get hashCode {
return Object.hash(
entryId,
userId,
points,
entryType,
source,
balanceAfter,
);
}
@override
String toString() {
return 'LoyaltyPointEntry(entryId: $entryId, userId: $userId, points: $points, '
'entryType: $entryType, source: $source, balanceAfter: $balanceAfter, '
'timestamp: $timestamp)';
}
}

View File

@@ -0,0 +1,182 @@
/// Domain Entity: Points Record
///
/// Represents a user-submitted invoice for points earning.
library;
/// Points record status enum
enum PointsStatus {
/// Record submitted, pending review
pending,
/// Record approved, points awarded
approved,
/// Record rejected
rejected;
/// Get display name for status
String get displayName {
switch (this) {
case PointsStatus.pending:
return 'Pending';
case PointsStatus.approved:
return 'Approved';
case PointsStatus.rejected:
return 'Rejected';
}
}
}
/// Points Record Entity
///
/// Contains information about a user-submitted invoice:
/// - Invoice details
/// - Submission files/attachments
/// - Review status
/// - Points calculation
class PointsRecord {
/// Unique record identifier
final String recordId;
/// User ID who submitted
final String userId;
/// Invoice number
final String invoiceNumber;
/// Store/vendor name
final String storeName;
/// Transaction date
final DateTime transactionDate;
/// Invoice amount
final double invoiceAmount;
/// Additional notes
final String? notes;
/// Attachment URLs (invoice photos, receipts)
final List<String> attachments;
/// Record status
final PointsStatus status;
/// Rejection reason (if rejected)
final String? rejectReason;
/// Points earned (if approved)
final int? pointsEarned;
/// Submission timestamp
final DateTime submittedAt;
/// Processing timestamp
final DateTime? processedAt;
/// ID of admin who processed
final String? processedBy;
const PointsRecord({
required this.recordId,
required this.userId,
required this.invoiceNumber,
required this.storeName,
required this.transactionDate,
required this.invoiceAmount,
this.notes,
required this.attachments,
required this.status,
this.rejectReason,
this.pointsEarned,
required this.submittedAt,
this.processedAt,
this.processedBy,
});
/// Check if record is pending review
bool get isPending => status == PointsStatus.pending;
/// Check if record is approved
bool get isApproved => status == PointsStatus.approved;
/// Check if record is rejected
bool get isRejected => status == PointsStatus.rejected;
/// Check if record has been processed
bool get isProcessed => status != PointsStatus.pending;
/// Check if record has attachments
bool get hasAttachments => attachments.isNotEmpty;
/// Get processing time duration
Duration? get processingDuration {
if (processedAt == null) return null;
return processedAt!.difference(submittedAt);
}
/// Copy with method for immutability
PointsRecord copyWith({
String? recordId,
String? userId,
String? invoiceNumber,
String? storeName,
DateTime? transactionDate,
double? invoiceAmount,
String? notes,
List<String>? attachments,
PointsStatus? status,
String? rejectReason,
int? pointsEarned,
DateTime? submittedAt,
DateTime? processedAt,
String? processedBy,
}) {
return PointsRecord(
recordId: recordId ?? this.recordId,
userId: userId ?? this.userId,
invoiceNumber: invoiceNumber ?? this.invoiceNumber,
storeName: storeName ?? this.storeName,
transactionDate: transactionDate ?? this.transactionDate,
invoiceAmount: invoiceAmount ?? this.invoiceAmount,
notes: notes ?? this.notes,
attachments: attachments ?? this.attachments,
status: status ?? this.status,
rejectReason: rejectReason ?? this.rejectReason,
pointsEarned: pointsEarned ?? this.pointsEarned,
submittedAt: submittedAt ?? this.submittedAt,
processedAt: processedAt ?? this.processedAt,
processedBy: processedBy ?? this.processedBy,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is PointsRecord &&
other.recordId == recordId &&
other.userId == userId &&
other.invoiceNumber == invoiceNumber &&
other.invoiceAmount == invoiceAmount &&
other.status == status;
}
@override
int get hashCode {
return Object.hash(
recordId,
userId,
invoiceNumber,
invoiceAmount,
status,
);
}
@override
String toString() {
return 'PointsRecord(recordId: $recordId, invoiceNumber: $invoiceNumber, '
'storeName: $storeName, invoiceAmount: $invoiceAmount, status: $status, '
'pointsEarned: $pointsEarned)';
}
}

View File

@@ -0,0 +1,207 @@
/// Domain Entity: Redeemed Gift
///
/// Represents a gift that has been redeemed by a user.
library;
import 'gift_catalog.dart';
/// Gift status enum
enum GiftStatus {
/// Gift is active and can be used
active,
/// Gift has been used
used,
/// Gift has expired
expired,
/// Gift has been cancelled
cancelled;
/// Get display name for status
String get displayName {
switch (this) {
case GiftStatus.active:
return 'Active';
case GiftStatus.used:
return 'Used';
case GiftStatus.expired:
return 'Expired';
case GiftStatus.cancelled:
return 'Cancelled';
}
}
}
/// Redeemed Gift Entity
///
/// Contains information about a redeemed gift:
/// - Gift details
/// - Voucher code and QR code
/// - Usage tracking
/// - Expiry dates
class RedeemedGift {
/// Unique gift identifier
final String giftId;
/// User ID who redeemed the gift
final String userId;
/// Catalog ID of the gift
final String catalogId;
/// Gift name (snapshot at redemption time)
final String name;
/// Gift description
final String? description;
/// Voucher code
final String? voucherCode;
/// QR code image URL
final String? qrCodeImage;
/// Gift type/category
final GiftCategory giftType;
/// Points cost (snapshot at redemption time)
final int pointsCost;
/// Cash value (snapshot at redemption time)
final double? cashValue;
/// Expiry date
final DateTime? expiryDate;
/// Gift status
final GiftStatus status;
/// Redemption timestamp
final DateTime redeemedAt;
/// Usage timestamp
final DateTime? usedAt;
/// Location where gift was used
final String? usedLocation;
/// Reference number when used (e.g., order ID)
final String? usedReference;
const RedeemedGift({
required this.giftId,
required this.userId,
required this.catalogId,
required this.name,
this.description,
this.voucherCode,
this.qrCodeImage,
required this.giftType,
required this.pointsCost,
this.cashValue,
this.expiryDate,
required this.status,
required this.redeemedAt,
this.usedAt,
this.usedLocation,
this.usedReference,
});
/// Check if gift is active
bool get isActive => status == GiftStatus.active;
/// Check if gift is used
bool get isUsed => status == GiftStatus.used;
/// Check if gift is expired
bool get isExpired =>
status == GiftStatus.expired ||
(expiryDate != null && DateTime.now().isAfter(expiryDate!));
/// Check if gift can be used
bool get canBeUsed => isActive && !isExpired;
/// Check if gift is expiring soon (within 7 days)
bool get isExpiringSoon {
if (expiryDate == null || isExpired) return false;
final daysUntilExpiry = expiryDate!.difference(DateTime.now()).inDays;
return daysUntilExpiry > 0 && daysUntilExpiry <= 7;
}
/// Get days until expiry
int? get daysUntilExpiry {
if (expiryDate == null) return null;
final days = expiryDate!.difference(DateTime.now()).inDays;
return days > 0 ? days : 0;
}
/// Copy with method for immutability
RedeemedGift copyWith({
String? giftId,
String? userId,
String? catalogId,
String? name,
String? description,
String? voucherCode,
String? qrCodeImage,
GiftCategory? giftType,
int? pointsCost,
double? cashValue,
DateTime? expiryDate,
GiftStatus? status,
DateTime? redeemedAt,
DateTime? usedAt,
String? usedLocation,
String? usedReference,
}) {
return RedeemedGift(
giftId: giftId ?? this.giftId,
userId: userId ?? this.userId,
catalogId: catalogId ?? this.catalogId,
name: name ?? this.name,
description: description ?? this.description,
voucherCode: voucherCode ?? this.voucherCode,
qrCodeImage: qrCodeImage ?? this.qrCodeImage,
giftType: giftType ?? this.giftType,
pointsCost: pointsCost ?? this.pointsCost,
cashValue: cashValue ?? this.cashValue,
expiryDate: expiryDate ?? this.expiryDate,
status: status ?? this.status,
redeemedAt: redeemedAt ?? this.redeemedAt,
usedAt: usedAt ?? this.usedAt,
usedLocation: usedLocation ?? this.usedLocation,
usedReference: usedReference ?? this.usedReference,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is RedeemedGift &&
other.giftId == giftId &&
other.userId == userId &&
other.catalogId == catalogId &&
other.voucherCode == voucherCode &&
other.status == status;
}
@override
int get hashCode {
return Object.hash(
giftId,
userId,
catalogId,
voucherCode,
status,
);
}
@override
String toString() {
return 'RedeemedGift(giftId: $giftId, name: $name, voucherCode: $voucherCode, '
'status: $status, redeemedAt: $redeemedAt)';
}
}

View File

@@ -0,0 +1,56 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
part 'notification_model.g.dart';
@HiveType(typeId: HiveTypeIds.notificationModel)
class NotificationModel extends HiveObject {
NotificationModel({required this.notificationId, required this.userId, required this.type, required this.title, required this.message, this.data, required this.isRead, required this.isPushed, required this.createdAt, this.readAt});
@HiveField(0) final String notificationId;
@HiveField(1) final String userId;
@HiveField(2) final String type;
@HiveField(3) final String title;
@HiveField(4) final String message;
@HiveField(5) final String? data;
@HiveField(6) final bool isRead;
@HiveField(7) final bool isPushed;
@HiveField(8) final DateTime createdAt;
@HiveField(9) final DateTime? readAt;
factory NotificationModel.fromJson(Map<String, dynamic> json) => NotificationModel(
notificationId: json['notification_id'] as String,
userId: json['user_id'] as String,
type: json['type'] as String,
title: json['title'] as String,
message: json['message'] as String,
data: json['data'] != null ? jsonEncode(json['data']) : null,
isRead: json['is_read'] as bool? ?? false,
isPushed: json['is_pushed'] as bool? ?? false,
createdAt: DateTime.parse(json['created_at']?.toString() ?? ''),
readAt: json['read_at'] != null ? DateTime.parse(json['read_at']?.toString() ?? '') : null,
);
Map<String, dynamic> toJson() => {
'notification_id': notificationId,
'user_id': userId,
'type': type,
'title': title,
'message': message,
'data': data != null ? jsonDecode(data!) : null,
'is_read': isRead,
'is_pushed': isPushed,
'created_at': createdAt.toIso8601String(),
'read_at': readAt?.toIso8601String(),
};
Map<String, dynamic>? get dataMap {
if (data == null) return null;
try {
return jsonDecode(data!) as Map<String, dynamic>;
} catch (e) {
return null;
}
}
}

View File

@@ -0,0 +1,68 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'notification_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class NotificationModelAdapter extends TypeAdapter<NotificationModel> {
@override
final typeId = 20;
@override
NotificationModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return NotificationModel(
notificationId: fields[0] as String,
userId: fields[1] as String,
type: fields[2] as String,
title: fields[3] as String,
message: fields[4] as String,
data: fields[5] as String?,
isRead: fields[6] as bool,
isPushed: fields[7] as bool,
createdAt: fields[8] as DateTime,
readAt: fields[9] as DateTime?,
);
}
@override
void write(BinaryWriter writer, NotificationModel obj) {
writer
..writeByte(10)
..writeByte(0)
..write(obj.notificationId)
..writeByte(1)
..write(obj.userId)
..writeByte(2)
..write(obj.type)
..writeByte(3)
..write(obj.title)
..writeByte(4)
..write(obj.message)
..writeByte(5)
..write(obj.data)
..writeByte(6)
..write(obj.isRead)
..writeByte(7)
..write(obj.isPushed)
..writeByte(8)
..write(obj.createdAt)
..writeByte(9)
..write(obj.readAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NotificationModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,162 @@
/// Domain Entity: Notification
///
/// Represents a notification sent to a user.
library;
/// Notification Entity
///
/// Contains information about a notification:
/// - Notification type and content
/// - Associated data
/// - Read and push status
class Notification {
/// Unique notification identifier
final String notificationId;
/// User ID receiving the notification
final String userId;
/// Notification type (order, loyalty, promotion, system, etc.)
final String type;
/// Notification title
final String title;
/// Notification message/body
final String message;
/// Additional data (JSON object with context-specific information)
final Map<String, dynamic>? data;
/// Notification has been read
final bool isRead;
/// Push notification has been sent
final bool isPushed;
/// Notification creation timestamp
final DateTime createdAt;
/// Read timestamp
final DateTime? readAt;
const Notification({
required this.notificationId,
required this.userId,
required this.type,
required this.title,
required this.message,
this.data,
required this.isRead,
required this.isPushed,
required this.createdAt,
this.readAt,
});
/// Check if notification is unread
bool get isUnread => !isRead;
/// Check if notification is order-related
bool get isOrderNotification => type.toLowerCase().contains('order');
/// Check if notification is loyalty-related
bool get isLoyaltyNotification => type.toLowerCase().contains('loyalty') ||
type.toLowerCase().contains('points');
/// Check if notification is promotion-related
bool get isPromotionNotification =>
type.toLowerCase().contains('promotion') ||
type.toLowerCase().contains('discount');
/// Check if notification is system-related
bool get isSystemNotification => type.toLowerCase().contains('system');
/// Get related entity ID from data
String? get relatedEntityId {
if (data == null) return null;
return data!['entity_id'] as String? ??
data!['order_id'] as String? ??
data!['quote_id'] as String?;
}
/// Get related entity type from data
String? get relatedEntityType {
if (data == null) return null;
return data!['entity_type'] as String?;
}
/// Get time since notification was created
Duration get timeSinceCreated {
return DateTime.now().difference(createdAt);
}
/// Check if notification is recent (less than 24 hours)
bool get isRecent {
return timeSinceCreated.inHours < 24;
}
/// Check if notification is old (more than 7 days)
bool get isOld {
return timeSinceCreated.inDays > 7;
}
/// Copy with method for immutability
Notification copyWith({
String? notificationId,
String? userId,
String? type,
String? title,
String? message,
Map<String, dynamic>? data,
bool? isRead,
bool? isPushed,
DateTime? createdAt,
DateTime? readAt,
}) {
return Notification(
notificationId: notificationId ?? this.notificationId,
userId: userId ?? this.userId,
type: type ?? this.type,
title: title ?? this.title,
message: message ?? this.message,
data: data ?? this.data,
isRead: isRead ?? this.isRead,
isPushed: isPushed ?? this.isPushed,
createdAt: createdAt ?? this.createdAt,
readAt: readAt ?? this.readAt,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Notification &&
other.notificationId == notificationId &&
other.userId == userId &&
other.type == type &&
other.title == title &&
other.message == message &&
other.isRead == isRead &&
other.isPushed == isPushed;
}
@override
int get hashCode {
return Object.hash(
notificationId,
userId,
type,
title,
message,
isRead,
isPushed,
);
}
@override
String toString() {
return 'Notification(notificationId: $notificationId, type: $type, '
'title: $title, isRead: $isRead, createdAt: $createdAt)';
}
}

View File

@@ -0,0 +1,86 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'invoice_model.g.dart';
@HiveType(typeId: HiveTypeIds.invoiceModel)
class InvoiceModel extends HiveObject {
InvoiceModel({required this.invoiceId, required this.invoiceNumber, required this.userId, this.orderId, required this.invoiceType, required this.issueDate, required this.dueDate, required this.currency, required this.subtotalAmount, required this.taxAmount, required this.discountAmount, required this.shippingAmount, required this.totalAmount, required this.amountPaid, required this.amountRemaining, required this.status, this.paymentTerms, this.notes, this.erpnextInvoice, required this.createdAt, this.updatedAt, this.lastReminderSent});
@HiveField(0) final String invoiceId;
@HiveField(1) final String invoiceNumber;
@HiveField(2) final String userId;
@HiveField(3) final String? orderId;
@HiveField(4) final InvoiceType invoiceType;
@HiveField(5) final DateTime issueDate;
@HiveField(6) final DateTime dueDate;
@HiveField(7) final String currency;
@HiveField(8) final double subtotalAmount;
@HiveField(9) final double taxAmount;
@HiveField(10) final double discountAmount;
@HiveField(11) final double shippingAmount;
@HiveField(12) final double totalAmount;
@HiveField(13) final double amountPaid;
@HiveField(14) final double amountRemaining;
@HiveField(15) final InvoiceStatus status;
@HiveField(16) final String? paymentTerms;
@HiveField(17) final String? notes;
@HiveField(18) final String? erpnextInvoice;
@HiveField(19) final DateTime createdAt;
@HiveField(20) final DateTime? updatedAt;
@HiveField(21) final DateTime? lastReminderSent;
factory InvoiceModel.fromJson(Map<String, dynamic> json) => InvoiceModel(
invoiceId: json['invoice_id'] as String,
invoiceNumber: json['invoice_number'] as String,
userId: json['user_id'] as String,
orderId: json['order_id'] as String?,
invoiceType: InvoiceType.values.firstWhere((e) => e.name == json['invoice_type']),
issueDate: DateTime.parse(json['issue_date']?.toString() ?? ''),
dueDate: DateTime.parse(json['due_date']?.toString() ?? ''),
currency: json['currency'] as String? ?? 'VND',
subtotalAmount: (json['subtotal_amount'] as num).toDouble(),
taxAmount: (json['tax_amount'] as num).toDouble(),
discountAmount: (json['discount_amount'] as num).toDouble(),
shippingAmount: (json['shipping_amount'] as num).toDouble(),
totalAmount: (json['total_amount'] as num).toDouble(),
amountPaid: (json['amount_paid'] as num).toDouble(),
amountRemaining: (json['amount_remaining'] as num).toDouble(),
status: InvoiceStatus.values.firstWhere((e) => e.name == json['status']),
paymentTerms: json['payment_terms'] as String?,
notes: json['notes'] as String?,
erpnextInvoice: json['erpnext_invoice'] as String?,
createdAt: DateTime.parse(json['created_at']?.toString() ?? ''),
updatedAt: json['updated_at'] != null ? DateTime.parse(json['updated_at']?.toString() ?? '') : null,
lastReminderSent: json['last_reminder_sent'] != null ? DateTime.parse(json['last_reminder_sent']?.toString() ?? '') : null,
);
Map<String, dynamic> toJson() => {
'invoice_id': invoiceId,
'invoice_number': invoiceNumber,
'user_id': userId,
'order_id': orderId,
'invoice_type': invoiceType.name,
'issue_date': issueDate.toIso8601String(),
'due_date': dueDate.toIso8601String(),
'currency': currency,
'subtotal_amount': subtotalAmount,
'tax_amount': taxAmount,
'discount_amount': discountAmount,
'shipping_amount': shippingAmount,
'total_amount': totalAmount,
'amount_paid': amountPaid,
'amount_remaining': amountRemaining,
'status': status.name,
'payment_terms': paymentTerms,
'notes': notes,
'erpnext_invoice': erpnextInvoice,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
'last_reminder_sent': lastReminderSent?.toIso8601String(),
};
bool get isOverdue => DateTime.now().isAfter(dueDate) && status != InvoiceStatus.paid;
bool get isPaid => status == InvoiceStatus.paid;
}

View File

@@ -0,0 +1,104 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'invoice_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class InvoiceModelAdapter extends TypeAdapter<InvoiceModel> {
@override
final typeId = 8;
@override
InvoiceModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return InvoiceModel(
invoiceId: fields[0] as String,
invoiceNumber: fields[1] as String,
userId: fields[2] as String,
orderId: fields[3] as String?,
invoiceType: fields[4] as InvoiceType,
issueDate: fields[5] as DateTime,
dueDate: fields[6] as DateTime,
currency: fields[7] as String,
subtotalAmount: (fields[8] as num).toDouble(),
taxAmount: (fields[9] as num).toDouble(),
discountAmount: (fields[10] as num).toDouble(),
shippingAmount: (fields[11] as num).toDouble(),
totalAmount: (fields[12] as num).toDouble(),
amountPaid: (fields[13] as num).toDouble(),
amountRemaining: (fields[14] as num).toDouble(),
status: fields[15] as InvoiceStatus,
paymentTerms: fields[16] as String?,
notes: fields[17] as String?,
erpnextInvoice: fields[18] as String?,
createdAt: fields[19] as DateTime,
updatedAt: fields[20] as DateTime?,
lastReminderSent: fields[21] as DateTime?,
);
}
@override
void write(BinaryWriter writer, InvoiceModel obj) {
writer
..writeByte(22)
..writeByte(0)
..write(obj.invoiceId)
..writeByte(1)
..write(obj.invoiceNumber)
..writeByte(2)
..write(obj.userId)
..writeByte(3)
..write(obj.orderId)
..writeByte(4)
..write(obj.invoiceType)
..writeByte(5)
..write(obj.issueDate)
..writeByte(6)
..write(obj.dueDate)
..writeByte(7)
..write(obj.currency)
..writeByte(8)
..write(obj.subtotalAmount)
..writeByte(9)
..write(obj.taxAmount)
..writeByte(10)
..write(obj.discountAmount)
..writeByte(11)
..write(obj.shippingAmount)
..writeByte(12)
..write(obj.totalAmount)
..writeByte(13)
..write(obj.amountPaid)
..writeByte(14)
..write(obj.amountRemaining)
..writeByte(15)
..write(obj.status)
..writeByte(16)
..write(obj.paymentTerms)
..writeByte(17)
..write(obj.notes)
..writeByte(18)
..write(obj.erpnextInvoice)
..writeByte(19)
..write(obj.createdAt)
..writeByte(20)
..write(obj.updatedAt)
..writeByte(21)
..write(obj.lastReminderSent);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is InvoiceModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,40 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
part 'order_item_model.g.dart';
@HiveType(typeId: HiveTypeIds.orderItemModel)
class OrderItemModel extends HiveObject {
OrderItemModel({required this.orderItemId, required this.orderId, required this.productId, required this.quantity, required this.unitPrice, required this.discountPercent, required this.subtotal, this.notes});
@HiveField(0) final String orderItemId;
@HiveField(1) final String orderId;
@HiveField(2) final String productId;
@HiveField(3) final double quantity;
@HiveField(4) final double unitPrice;
@HiveField(5) final double discountPercent;
@HiveField(6) final double subtotal;
@HiveField(7) final String? notes;
factory OrderItemModel.fromJson(Map<String, dynamic> json) => OrderItemModel(
orderItemId: json['order_item_id'] as String,
orderId: json['order_id'] as String,
productId: json['product_id'] as String,
quantity: (json['quantity'] as num).toDouble(),
unitPrice: (json['unit_price'] as num).toDouble(),
discountPercent: (json['discount_percent'] as num).toDouble(),
subtotal: (json['subtotal'] as num).toDouble(),
notes: json['notes'] as String?,
);
Map<String, dynamic> toJson() => {
'order_item_id': orderItemId,
'order_id': orderId,
'product_id': productId,
'quantity': quantity,
'unit_price': unitPrice,
'discount_percent': discountPercent,
'subtotal': subtotal,
'notes': notes,
};
}

View File

@@ -0,0 +1,62 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'order_item_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class OrderItemModelAdapter extends TypeAdapter<OrderItemModel> {
@override
final typeId = 7;
@override
OrderItemModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return OrderItemModel(
orderItemId: fields[0] as String,
orderId: fields[1] as String,
productId: fields[2] as String,
quantity: (fields[3] as num).toDouble(),
unitPrice: (fields[4] as num).toDouble(),
discountPercent: (fields[5] as num).toDouble(),
subtotal: (fields[6] as num).toDouble(),
notes: fields[7] as String?,
);
}
@override
void write(BinaryWriter writer, OrderItemModel obj) {
writer
..writeByte(8)
..writeByte(0)
..write(obj.orderItemId)
..writeByte(1)
..write(obj.orderId)
..writeByte(2)
..write(obj.productId)
..writeByte(3)
..write(obj.quantity)
..writeByte(4)
..write(obj.unitPrice)
..writeByte(5)
..write(obj.discountPercent)
..writeByte(6)
..write(obj.subtotal)
..writeByte(7)
..write(obj.notes);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is OrderItemModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,147 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'order_model.g.dart';
/// Order Model - Type ID: 6
@HiveType(typeId: HiveTypeIds.orderModel)
class OrderModel extends HiveObject {
OrderModel({
required this.orderId,
required this.orderNumber,
required this.userId,
required this.status,
required this.totalAmount,
required this.discountAmount,
required this.taxAmount,
required this.shippingFee,
required this.finalAmount,
this.shippingAddress,
this.billingAddress,
this.expectedDeliveryDate,
this.actualDeliveryDate,
this.notes,
this.cancellationReason,
this.erpnextSalesOrder,
required this.createdAt,
this.updatedAt,
});
@HiveField(0)
final String orderId;
@HiveField(1)
final String orderNumber;
@HiveField(2)
final String userId;
@HiveField(3)
final OrderStatus status;
@HiveField(4)
final double totalAmount;
@HiveField(5)
final double discountAmount;
@HiveField(6)
final double taxAmount;
@HiveField(7)
final double shippingFee;
@HiveField(8)
final double finalAmount;
@HiveField(9)
final String? shippingAddress;
@HiveField(10)
final String? billingAddress;
@HiveField(11)
final DateTime? expectedDeliveryDate;
@HiveField(12)
final DateTime? actualDeliveryDate;
@HiveField(13)
final String? notes;
@HiveField(14)
final String? cancellationReason;
@HiveField(15)
final String? erpnextSalesOrder;
@HiveField(16)
final DateTime createdAt;
@HiveField(17)
final DateTime? updatedAt;
factory OrderModel.fromJson(Map<String, dynamic> json) {
return OrderModel(
orderId: json['order_id'] as String,
orderNumber: json['order_number'] as String,
userId: json['user_id'] as String,
status: OrderStatus.values.firstWhere((e) => e.name == json['status']),
totalAmount: (json['total_amount'] as num).toDouble(),
discountAmount: (json['discount_amount'] as num).toDouble(),
taxAmount: (json['tax_amount'] as num).toDouble(),
shippingFee: (json['shipping_fee'] as num).toDouble(),
finalAmount: (json['final_amount'] as num).toDouble(),
shippingAddress: json['shipping_address'] != null ? jsonEncode(json['shipping_address']) : null,
billingAddress: json['billing_address'] != null ? jsonEncode(json['billing_address']) : null,
expectedDeliveryDate: json['expected_delivery_date'] != null ? DateTime.parse(json['expected_delivery_date']?.toString() ?? '') : null,
actualDeliveryDate: json['actual_delivery_date'] != null ? DateTime.parse(json['actual_delivery_date']?.toString() ?? '') : null,
notes: json['notes'] as String?,
cancellationReason: json['cancellation_reason'] as String?,
erpnextSalesOrder: json['erpnext_sales_order'] as String?,
createdAt: DateTime.parse(json['created_at']?.toString() ?? ''),
updatedAt: json['updated_at'] != null ? DateTime.parse(json['updated_at']?.toString() ?? '') : null,
);
}
Map<String, dynamic> toJson() => {
'order_id': orderId,
'order_number': orderNumber,
'user_id': userId,
'status': status.name,
'total_amount': totalAmount,
'discount_amount': discountAmount,
'tax_amount': taxAmount,
'shipping_fee': shippingFee,
'final_amount': finalAmount,
'shipping_address': shippingAddress != null ? jsonDecode(shippingAddress!) : null,
'billing_address': billingAddress != null ? jsonDecode(billingAddress!) : null,
'expected_delivery_date': expectedDeliveryDate?.toIso8601String(),
'actual_delivery_date': actualDeliveryDate?.toIso8601String(),
'notes': notes,
'cancellation_reason': cancellationReason,
'erpnext_sales_order': erpnextSalesOrder,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
};
Map<String, dynamic>? get shippingAddressMap {
if (shippingAddress == null) return null;
try {
return jsonDecode(shippingAddress!) as Map<String, dynamic>;
} catch (e) {
return null;
}
}
Map<String, dynamic>? get billingAddressMap {
if (billingAddress == null) return null;
try {
return jsonDecode(billingAddress!) as Map<String, dynamic>;
} catch (e) {
return null;
}
}
}

View File

@@ -0,0 +1,92 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'order_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class OrderModelAdapter extends TypeAdapter<OrderModel> {
@override
final typeId = 6;
@override
OrderModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return OrderModel(
orderId: fields[0] as String,
orderNumber: fields[1] as String,
userId: fields[2] as String,
status: fields[3] as OrderStatus,
totalAmount: (fields[4] as num).toDouble(),
discountAmount: (fields[5] as num).toDouble(),
taxAmount: (fields[6] as num).toDouble(),
shippingFee: (fields[7] as num).toDouble(),
finalAmount: (fields[8] as num).toDouble(),
shippingAddress: fields[9] as String?,
billingAddress: fields[10] as String?,
expectedDeliveryDate: fields[11] as DateTime?,
actualDeliveryDate: fields[12] as DateTime?,
notes: fields[13] as String?,
cancellationReason: fields[14] as String?,
erpnextSalesOrder: fields[15] as String?,
createdAt: fields[16] as DateTime,
updatedAt: fields[17] as DateTime?,
);
}
@override
void write(BinaryWriter writer, OrderModel obj) {
writer
..writeByte(18)
..writeByte(0)
..write(obj.orderId)
..writeByte(1)
..write(obj.orderNumber)
..writeByte(2)
..write(obj.userId)
..writeByte(3)
..write(obj.status)
..writeByte(4)
..write(obj.totalAmount)
..writeByte(5)
..write(obj.discountAmount)
..writeByte(6)
..write(obj.taxAmount)
..writeByte(7)
..write(obj.shippingFee)
..writeByte(8)
..write(obj.finalAmount)
..writeByte(9)
..write(obj.shippingAddress)
..writeByte(10)
..write(obj.billingAddress)
..writeByte(11)
..write(obj.expectedDeliveryDate)
..writeByte(12)
..write(obj.actualDeliveryDate)
..writeByte(13)
..write(obj.notes)
..writeByte(14)
..write(obj.cancellationReason)
..writeByte(15)
..write(obj.erpnextSalesOrder)
..writeByte(16)
..write(obj.createdAt)
..writeByte(17)
..write(obj.updatedAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is OrderModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,62 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'payment_line_model.g.dart';
@HiveType(typeId: HiveTypeIds.paymentLineModel)
class PaymentLineModel extends HiveObject {
PaymentLineModel({required this.paymentLineId, required this.invoiceId, required this.paymentNumber, required this.paymentDate, required this.amount, required this.paymentMethod, this.bankName, this.bankAccount, this.referenceNumber, this.notes, required this.status, this.receiptUrl, this.erpnextPaymentEntry, required this.createdAt, this.processedAt});
@HiveField(0) final String paymentLineId;
@HiveField(1) final String invoiceId;
@HiveField(2) final String paymentNumber;
@HiveField(3) final DateTime paymentDate;
@HiveField(4) final double amount;
@HiveField(5) final PaymentMethod paymentMethod;
@HiveField(6) final String? bankName;
@HiveField(7) final String? bankAccount;
@HiveField(8) final String? referenceNumber;
@HiveField(9) final String? notes;
@HiveField(10) final PaymentStatus status;
@HiveField(11) final String? receiptUrl;
@HiveField(12) final String? erpnextPaymentEntry;
@HiveField(13) final DateTime createdAt;
@HiveField(14) final DateTime? processedAt;
factory PaymentLineModel.fromJson(Map<String, dynamic> json) => PaymentLineModel(
paymentLineId: json['payment_line_id'] as String,
invoiceId: json['invoice_id'] as String,
paymentNumber: json['payment_number'] as String,
paymentDate: DateTime.parse(json['payment_date']?.toString() ?? ''),
amount: (json['amount'] as num).toDouble(),
paymentMethod: PaymentMethod.values.firstWhere((e) => e.name == json['payment_method']),
bankName: json['bank_name'] as String?,
bankAccount: json['bank_account'] as String?,
referenceNumber: json['reference_number'] as String?,
notes: json['notes'] as String?,
status: PaymentStatus.values.firstWhere((e) => e.name == json['status']),
receiptUrl: json['receipt_url'] as String?,
erpnextPaymentEntry: json['erpnext_payment_entry'] as String?,
createdAt: DateTime.parse(json['created_at']?.toString() ?? ''),
processedAt: json['processed_at'] != null ? DateTime.parse(json['processed_at']?.toString() ?? '') : null,
);
Map<String, dynamic> toJson() => {
'payment_line_id': paymentLineId,
'invoice_id': invoiceId,
'payment_number': paymentNumber,
'payment_date': paymentDate.toIso8601String(),
'amount': amount,
'payment_method': paymentMethod.name,
'bank_name': bankName,
'bank_account': bankAccount,
'reference_number': referenceNumber,
'notes': notes,
'status': status.name,
'receipt_url': receiptUrl,
'erpnext_payment_entry': erpnextPaymentEntry,
'created_at': createdAt.toIso8601String(),
'processed_at': processedAt?.toIso8601String(),
};
}

View File

@@ -0,0 +1,83 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'payment_line_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class PaymentLineModelAdapter extends TypeAdapter<PaymentLineModel> {
@override
final typeId = 9;
@override
PaymentLineModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return PaymentLineModel(
paymentLineId: fields[0] as String,
invoiceId: fields[1] as String,
paymentNumber: fields[2] as String,
paymentDate: fields[3] as DateTime,
amount: (fields[4] as num).toDouble(),
paymentMethod: fields[5] as PaymentMethod,
bankName: fields[6] as String?,
bankAccount: fields[7] as String?,
referenceNumber: fields[8] as String?,
notes: fields[9] as String?,
status: fields[10] as PaymentStatus,
receiptUrl: fields[11] as String?,
erpnextPaymentEntry: fields[12] as String?,
createdAt: fields[13] as DateTime,
processedAt: fields[14] as DateTime?,
);
}
@override
void write(BinaryWriter writer, PaymentLineModel obj) {
writer
..writeByte(15)
..writeByte(0)
..write(obj.paymentLineId)
..writeByte(1)
..write(obj.invoiceId)
..writeByte(2)
..write(obj.paymentNumber)
..writeByte(3)
..write(obj.paymentDate)
..writeByte(4)
..write(obj.amount)
..writeByte(5)
..write(obj.paymentMethod)
..writeByte(6)
..write(obj.bankName)
..writeByte(7)
..write(obj.bankAccount)
..writeByte(8)
..write(obj.referenceNumber)
..writeByte(9)
..write(obj.notes)
..writeByte(10)
..write(obj.status)
..writeByte(11)
..write(obj.receiptUrl)
..writeByte(12)
..write(obj.erpnextPaymentEntry)
..writeByte(13)
..write(obj.createdAt)
..writeByte(14)
..write(obj.processedAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PaymentLineModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,273 @@
/// Domain Entity: Invoice
///
/// Represents an invoice for an order.
library;
/// Invoice type enum
enum InvoiceType {
/// Standard invoice
standard,
/// Proforma invoice
proforma,
/// Credit note
creditNote,
/// Debit note
debitNote;
}
/// Invoice status enum
enum InvoiceStatus {
/// Draft invoice
draft,
/// Invoice has been submitted
submitted,
/// Partially paid
partiallyPaid,
/// Fully paid
paid,
/// Overdue invoice
overdue,
/// Cancelled invoice
cancelled;
/// Get display name for status
String get displayName {
switch (this) {
case InvoiceStatus.draft:
return 'Draft';
case InvoiceStatus.submitted:
return 'Submitted';
case InvoiceStatus.partiallyPaid:
return 'Partially Paid';
case InvoiceStatus.paid:
return 'Paid';
case InvoiceStatus.overdue:
return 'Overdue';
case InvoiceStatus.cancelled:
return 'Cancelled';
}
}
}
/// Invoice Entity
///
/// Contains complete invoice information:
/// - Invoice identification
/// - Associated order
/// - Amounts and calculations
/// - Payment tracking
/// - Status and dates
class Invoice {
/// Unique invoice identifier
final String invoiceId;
/// Invoice number (human-readable)
final String invoiceNumber;
/// User ID
final String userId;
/// Order ID
final String? orderId;
/// Invoice type
final InvoiceType invoiceType;
/// Issue date
final DateTime issueDate;
/// Due date
final DateTime dueDate;
/// Currency code (e.g., VND, USD)
final String currency;
/// Subtotal amount
final double subtotalAmount;
/// Tax amount
final double taxAmount;
/// Discount amount
final double discountAmount;
/// Shipping amount
final double shippingAmount;
/// Total amount
final double totalAmount;
/// Amount paid so far
final double amountPaid;
/// Amount remaining to be paid
final double amountRemaining;
/// Invoice status
final InvoiceStatus status;
/// Payment terms
final String? paymentTerms;
/// Invoice notes
final String? notes;
/// ERPNext invoice reference
final String? erpnextInvoice;
/// Creation timestamp
final DateTime createdAt;
/// Last update timestamp
final DateTime updatedAt;
/// Last reminder sent timestamp
final DateTime? lastReminderSent;
const Invoice({
required this.invoiceId,
required this.invoiceNumber,
required this.userId,
this.orderId,
required this.invoiceType,
required this.issueDate,
required this.dueDate,
required this.currency,
required this.subtotalAmount,
required this.taxAmount,
required this.discountAmount,
required this.shippingAmount,
required this.totalAmount,
required this.amountPaid,
required this.amountRemaining,
required this.status,
this.paymentTerms,
this.notes,
this.erpnextInvoice,
required this.createdAt,
required this.updatedAt,
this.lastReminderSent,
});
/// Check if invoice is fully paid
bool get isPaid => status == InvoiceStatus.paid || amountRemaining <= 0;
/// Check if invoice is overdue
bool get isOverdue =>
status == InvoiceStatus.overdue ||
(!isPaid && DateTime.now().isAfter(dueDate));
/// Check if invoice is partially paid
bool get isPartiallyPaid =>
amountPaid > 0 && amountPaid < totalAmount;
/// Get payment percentage
double get paymentPercentage {
if (totalAmount == 0) return 0;
return (amountPaid / totalAmount) * 100;
}
/// Get days until due
int get daysUntilDue => dueDate.difference(DateTime.now()).inDays;
/// Get days overdue
int get daysOverdue {
if (!isOverdue) return 0;
return DateTime.now().difference(dueDate).inDays;
}
/// Copy with method for immutability
Invoice copyWith({
String? invoiceId,
String? invoiceNumber,
String? userId,
String? orderId,
InvoiceType? invoiceType,
DateTime? issueDate,
DateTime? dueDate,
String? currency,
double? subtotalAmount,
double? taxAmount,
double? discountAmount,
double? shippingAmount,
double? totalAmount,
double? amountPaid,
double? amountRemaining,
InvoiceStatus? status,
String? paymentTerms,
String? notes,
String? erpnextInvoice,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? lastReminderSent,
}) {
return Invoice(
invoiceId: invoiceId ?? this.invoiceId,
invoiceNumber: invoiceNumber ?? this.invoiceNumber,
userId: userId ?? this.userId,
orderId: orderId ?? this.orderId,
invoiceType: invoiceType ?? this.invoiceType,
issueDate: issueDate ?? this.issueDate,
dueDate: dueDate ?? this.dueDate,
currency: currency ?? this.currency,
subtotalAmount: subtotalAmount ?? this.subtotalAmount,
taxAmount: taxAmount ?? this.taxAmount,
discountAmount: discountAmount ?? this.discountAmount,
shippingAmount: shippingAmount ?? this.shippingAmount,
totalAmount: totalAmount ?? this.totalAmount,
amountPaid: amountPaid ?? this.amountPaid,
amountRemaining: amountRemaining ?? this.amountRemaining,
status: status ?? this.status,
paymentTerms: paymentTerms ?? this.paymentTerms,
notes: notes ?? this.notes,
erpnextInvoice: erpnextInvoice ?? this.erpnextInvoice,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
lastReminderSent: lastReminderSent ?? this.lastReminderSent,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Invoice &&
other.invoiceId == invoiceId &&
other.invoiceNumber == invoiceNumber &&
other.userId == userId &&
other.orderId == orderId &&
other.invoiceType == invoiceType &&
other.totalAmount == totalAmount &&
other.amountPaid == amountPaid &&
other.status == status;
}
@override
int get hashCode {
return Object.hash(
invoiceId,
invoiceNumber,
userId,
orderId,
invoiceType,
totalAmount,
amountPaid,
status,
);
}
@override
String toString() {
return 'Invoice(invoiceId: $invoiceId, invoiceNumber: $invoiceNumber, '
'status: $status, totalAmount: $totalAmount, amountPaid: $amountPaid, '
'amountRemaining: $amountRemaining)';
}
}

View File

@@ -0,0 +1,321 @@
/// Domain Entity: Order
///
/// Represents a customer order.
library;
/// Order status enum
enum OrderStatus {
/// Order has been created but not confirmed
draft,
/// Order has been confirmed
confirmed,
/// Order is being processed
processing,
/// Order is ready for shipping
ready,
/// Order has been shipped
shipped,
/// Order has been delivered
delivered,
/// Order has been completed
completed,
/// Order has been cancelled
cancelled,
/// Order has been returned
returned;
/// Get display name for status
String get displayName {
switch (this) {
case OrderStatus.draft:
return 'Draft';
case OrderStatus.confirmed:
return 'Confirmed';
case OrderStatus.processing:
return 'Processing';
case OrderStatus.ready:
return 'Ready';
case OrderStatus.shipped:
return 'Shipped';
case OrderStatus.delivered:
return 'Delivered';
case OrderStatus.completed:
return 'Completed';
case OrderStatus.cancelled:
return 'Cancelled';
case OrderStatus.returned:
return 'Returned';
}
}
}
/// Address information
class Address {
/// Recipient name
final String? name;
/// Phone number
final String? phone;
/// Street address
final String? street;
/// Ward/commune
final String? ward;
/// District
final String? district;
/// City/province
final String? city;
/// Postal code
final String? postalCode;
const Address({
this.name,
this.phone,
this.street,
this.ward,
this.district,
this.city,
this.postalCode,
});
/// Get full address string
String get fullAddress {
final parts = [
street,
ward,
district,
city,
postalCode,
].where((part) => part != null && part.isNotEmpty).toList();
return parts.join(', ');
}
/// Create from JSON map
factory Address.fromJson(Map<String, dynamic> json) {
return Address(
name: json['name'] as String?,
phone: json['phone'] as String?,
street: json['street'] as String?,
ward: json['ward'] as String?,
district: json['district'] as String?,
city: json['city'] as String?,
postalCode: json['postal_code'] as String?,
);
}
/// Convert to JSON map
Map<String, dynamic> toJson() {
return {
'name': name,
'phone': phone,
'street': street,
'ward': ward,
'district': district,
'city': city,
'postal_code': postalCode,
};
}
}
/// Order Entity
///
/// Contains complete order information:
/// - Order identification
/// - Customer details
/// - Pricing and discounts
/// - Shipping information
/// - Status tracking
class Order {
/// Unique order identifier
final String orderId;
/// Human-readable order number
final String orderNumber;
/// User ID who placed the order
final String userId;
/// Current order status
final OrderStatus status;
/// Total order amount before discounts
final double totalAmount;
/// Discount amount applied
final double discountAmount;
/// Tax amount
final double taxAmount;
/// Shipping fee
final double shippingFee;
/// Final amount to pay
final double finalAmount;
/// Shipping address
final Address? shippingAddress;
/// Billing address
final Address? billingAddress;
/// Expected delivery date
final DateTime? expectedDeliveryDate;
/// Actual delivery date
final DateTime? actualDeliveryDate;
/// Order notes
final String? notes;
/// Cancellation reason
final String? cancellationReason;
/// ERPNext sales order reference
final String? erpnextSalesOrder;
/// Order creation timestamp
final DateTime createdAt;
/// Last update timestamp
final DateTime updatedAt;
const Order({
required this.orderId,
required this.orderNumber,
required this.userId,
required this.status,
required this.totalAmount,
required this.discountAmount,
required this.taxAmount,
required this.shippingFee,
required this.finalAmount,
this.shippingAddress,
this.billingAddress,
this.expectedDeliveryDate,
this.actualDeliveryDate,
this.notes,
this.cancellationReason,
this.erpnextSalesOrder,
required this.createdAt,
required this.updatedAt,
});
/// Check if order is active (not cancelled or completed)
bool get isActive =>
status != OrderStatus.cancelled &&
status != OrderStatus.completed &&
status != OrderStatus.returned;
/// Check if order can be cancelled
bool get canBeCancelled =>
status == OrderStatus.draft ||
status == OrderStatus.confirmed ||
status == OrderStatus.processing;
/// Check if order is delivered
bool get isDelivered =>
status == OrderStatus.delivered || status == OrderStatus.completed;
/// Check if order is cancelled
bool get isCancelled => status == OrderStatus.cancelled;
/// Get discount percentage
double get discountPercentage {
if (totalAmount == 0) return 0;
return (discountAmount / totalAmount) * 100;
}
/// Copy with method for immutability
Order copyWith({
String? orderId,
String? orderNumber,
String? userId,
OrderStatus? status,
double? totalAmount,
double? discountAmount,
double? taxAmount,
double? shippingFee,
double? finalAmount,
Address? shippingAddress,
Address? billingAddress,
DateTime? expectedDeliveryDate,
DateTime? actualDeliveryDate,
String? notes,
String? cancellationReason,
String? erpnextSalesOrder,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return Order(
orderId: orderId ?? this.orderId,
orderNumber: orderNumber ?? this.orderNumber,
userId: userId ?? this.userId,
status: status ?? this.status,
totalAmount: totalAmount ?? this.totalAmount,
discountAmount: discountAmount ?? this.discountAmount,
taxAmount: taxAmount ?? this.taxAmount,
shippingFee: shippingFee ?? this.shippingFee,
finalAmount: finalAmount ?? this.finalAmount,
shippingAddress: shippingAddress ?? this.shippingAddress,
billingAddress: billingAddress ?? this.billingAddress,
expectedDeliveryDate: expectedDeliveryDate ?? this.expectedDeliveryDate,
actualDeliveryDate: actualDeliveryDate ?? this.actualDeliveryDate,
notes: notes ?? this.notes,
cancellationReason: cancellationReason ?? this.cancellationReason,
erpnextSalesOrder: erpnextSalesOrder ?? this.erpnextSalesOrder,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Order &&
other.orderId == orderId &&
other.orderNumber == orderNumber &&
other.userId == userId &&
other.status == status &&
other.totalAmount == totalAmount &&
other.discountAmount == discountAmount &&
other.taxAmount == taxAmount &&
other.shippingFee == shippingFee &&
other.finalAmount == finalAmount;
}
@override
int get hashCode {
return Object.hash(
orderId,
orderNumber,
userId,
status,
totalAmount,
discountAmount,
taxAmount,
shippingFee,
finalAmount,
);
}
@override
String toString() {
return 'Order(orderId: $orderId, orderNumber: $orderNumber, status: $status, '
'finalAmount: $finalAmount, createdAt: $createdAt)';
}
}

View File

@@ -0,0 +1,117 @@
/// Domain Entity: Order Item
///
/// Represents a single line item in an order.
library;
/// Order Item Entity
///
/// Contains item-level information in an order:
/// - Product reference
/// - Quantity and pricing
/// - Discounts
/// - Notes
class OrderItem {
/// Unique order item identifier
final String orderItemId;
/// Order ID this item belongs to
final String orderId;
/// Product ID
final String productId;
/// Quantity ordered
final double quantity;
/// Unit price at time of order
final double unitPrice;
/// Discount percentage applied
final double discountPercent;
/// Subtotal (quantity * unitPrice * (1 - discountPercent/100))
final double subtotal;
/// Item notes
final String? notes;
const OrderItem({
required this.orderItemId,
required this.orderId,
required this.productId,
required this.quantity,
required this.unitPrice,
required this.discountPercent,
required this.subtotal,
this.notes,
});
/// Calculate subtotal before discount
double get subtotalBeforeDiscount => quantity * unitPrice;
/// Calculate discount amount
double get discountAmount =>
subtotalBeforeDiscount * (discountPercent / 100);
/// Calculate subtotal after discount (for verification)
double get calculatedSubtotal => subtotalBeforeDiscount - discountAmount;
/// Check if item has discount
bool get hasDiscount => discountPercent > 0;
/// Copy with method for immutability
OrderItem copyWith({
String? orderItemId,
String? orderId,
String? productId,
double? quantity,
double? unitPrice,
double? discountPercent,
double? subtotal,
String? notes,
}) {
return OrderItem(
orderItemId: orderItemId ?? this.orderItemId,
orderId: orderId ?? this.orderId,
productId: productId ?? this.productId,
quantity: quantity ?? this.quantity,
unitPrice: unitPrice ?? this.unitPrice,
discountPercent: discountPercent ?? this.discountPercent,
subtotal: subtotal ?? this.subtotal,
notes: notes ?? this.notes,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is OrderItem &&
other.orderItemId == orderItemId &&
other.orderId == orderId &&
other.productId == productId &&
other.quantity == quantity &&
other.unitPrice == unitPrice &&
other.discountPercent == discountPercent &&
other.subtotal == subtotal;
}
@override
int get hashCode {
return Object.hash(
orderItemId,
orderId,
productId,
quantity,
unitPrice,
discountPercent,
subtotal,
);
}
@override
String toString() {
return 'OrderItem(orderItemId: $orderItemId, productId: $productId, '
'quantity: $quantity, unitPrice: $unitPrice, subtotal: $subtotal)';
}
}

View File

@@ -0,0 +1,237 @@
/// Domain Entity: Payment Line
///
/// Represents a payment transaction for an invoice.
library;
/// Payment method enum
enum PaymentMethod {
/// Cash payment
cash,
/// Bank transfer
bankTransfer,
/// Credit card
creditCard,
/// E-wallet (Momo, ZaloPay, etc.)
ewallet,
/// Check
check,
/// Other method
other;
/// Get display name for payment method
String get displayName {
switch (this) {
case PaymentMethod.cash:
return 'Cash';
case PaymentMethod.bankTransfer:
return 'Bank Transfer';
case PaymentMethod.creditCard:
return 'Credit Card';
case PaymentMethod.ewallet:
return 'E-Wallet';
case PaymentMethod.check:
return 'Check';
case PaymentMethod.other:
return 'Other';
}
}
}
/// Payment status enum
enum PaymentStatus {
/// Payment pending
pending,
/// Payment is being processed
processing,
/// Payment completed successfully
completed,
/// Payment failed
failed,
/// Payment refunded
refunded,
/// Payment cancelled
cancelled;
/// Get display name for status
String get displayName {
switch (this) {
case PaymentStatus.pending:
return 'Pending';
case PaymentStatus.processing:
return 'Processing';
case PaymentStatus.completed:
return 'Completed';
case PaymentStatus.failed:
return 'Failed';
case PaymentStatus.refunded:
return 'Refunded';
case PaymentStatus.cancelled:
return 'Cancelled';
}
}
}
/// Payment Line Entity
///
/// Contains payment transaction information:
/// - Payment details
/// - Payment method
/// - Bank information
/// - Status tracking
class PaymentLine {
/// Unique payment line identifier
final String paymentLineId;
/// Invoice ID this payment is for
final String invoiceId;
/// Payment number (human-readable)
final String paymentNumber;
/// Payment date
final DateTime paymentDate;
/// Payment amount
final double amount;
/// Payment method
final PaymentMethod paymentMethod;
/// Bank name (for bank transfer)
final String? bankName;
/// Bank account number (for bank transfer)
final String? bankAccount;
/// Reference number (transaction ID, check number, etc.)
final String? referenceNumber;
/// Payment notes
final String? notes;
/// Payment status
final PaymentStatus status;
/// Receipt URL
final String? receiptUrl;
/// ERPNext payment entry reference
final String? erpnextPaymentEntry;
/// Creation timestamp
final DateTime createdAt;
/// Processing timestamp
final DateTime? processedAt;
const PaymentLine({
required this.paymentLineId,
required this.invoiceId,
required this.paymentNumber,
required this.paymentDate,
required this.amount,
required this.paymentMethod,
this.bankName,
this.bankAccount,
this.referenceNumber,
this.notes,
required this.status,
this.receiptUrl,
this.erpnextPaymentEntry,
required this.createdAt,
this.processedAt,
});
/// Check if payment is completed
bool get isCompleted => status == PaymentStatus.completed;
/// Check if payment is pending
bool get isPending => status == PaymentStatus.pending;
/// Check if payment is being processed
bool get isProcessing => status == PaymentStatus.processing;
/// Check if payment failed
bool get isFailed => status == PaymentStatus.failed;
/// Check if payment has receipt
bool get hasReceipt => receiptUrl != null && receiptUrl!.isNotEmpty;
/// Copy with method for immutability
PaymentLine copyWith({
String? paymentLineId,
String? invoiceId,
String? paymentNumber,
DateTime? paymentDate,
double? amount,
PaymentMethod? paymentMethod,
String? bankName,
String? bankAccount,
String? referenceNumber,
String? notes,
PaymentStatus? status,
String? receiptUrl,
String? erpnextPaymentEntry,
DateTime? createdAt,
DateTime? processedAt,
}) {
return PaymentLine(
paymentLineId: paymentLineId ?? this.paymentLineId,
invoiceId: invoiceId ?? this.invoiceId,
paymentNumber: paymentNumber ?? this.paymentNumber,
paymentDate: paymentDate ?? this.paymentDate,
amount: amount ?? this.amount,
paymentMethod: paymentMethod ?? this.paymentMethod,
bankName: bankName ?? this.bankName,
bankAccount: bankAccount ?? this.bankAccount,
referenceNumber: referenceNumber ?? this.referenceNumber,
notes: notes ?? this.notes,
status: status ?? this.status,
receiptUrl: receiptUrl ?? this.receiptUrl,
erpnextPaymentEntry: erpnextPaymentEntry ?? this.erpnextPaymentEntry,
createdAt: createdAt ?? this.createdAt,
processedAt: processedAt ?? this.processedAt,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is PaymentLine &&
other.paymentLineId == paymentLineId &&
other.invoiceId == invoiceId &&
other.paymentNumber == paymentNumber &&
other.amount == amount &&
other.paymentMethod == paymentMethod &&
other.status == status;
}
@override
int get hashCode {
return Object.hash(
paymentLineId,
invoiceId,
paymentNumber,
amount,
paymentMethod,
status,
);
}
@override
String toString() {
return 'PaymentLine(paymentLineId: $paymentLineId, paymentNumber: $paymentNumber, '
'amount: $amount, paymentMethod: $paymentMethod, status: $status)';
}
}

View File

@@ -4,6 +4,7 @@
library;
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/features/products/domain/entities/category.dart';
part 'category_model.g.dart';
@@ -15,8 +16,8 @@ part 'category_model.g.dart';
/// - Hive local database storage
/// - Converting to/from domain entity
///
/// Hive Type ID: 12
@HiveType(typeId: 12)
/// Hive Type ID: 27 (from HiveTypeIds.categoryModel)
@HiveType(typeId: HiveTypeIds.categoryModel)
class CategoryModel extends HiveObject {
/// Unique identifier
@HiveField(0)

View File

@@ -8,7 +8,7 @@ part of 'category_model.dart';
class CategoryModelAdapter extends TypeAdapter<CategoryModel> {
@override
final typeId = 12;
final typeId = 27;
@override
CategoryModel read(BinaryReader reader) {

View File

@@ -1,242 +1,294 @@
/// Data Model: Product
///
/// Data Transfer Object for product information.
/// Handles JSON and Hive serialization/deserialization.
library;
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/features/products/domain/entities/product.dart';
part 'product_model.g.dart';
/// Product Model
///
/// Used for:
/// - JSON serialization/deserialization
/// - Hive local database storage
/// - Converting to/from domain entity
/// Hive CE model for caching product data locally.
/// Maps to the 'products' table in the database.
///
/// Hive Type ID: 1
@HiveType(typeId: 1)
/// Type ID: 2
@HiveType(typeId: HiveTypeIds.productModel)
class ProductModel extends HiveObject {
/// Unique identifier
ProductModel({
required this.productId,
required this.name,
this.description,
required this.basePrice,
this.images,
this.imageCaptions,
this.link360,
this.specifications,
this.category,
this.brand,
this.unit,
required this.isActive,
required this.isFeatured,
this.erpnextItemCode,
required this.createdAt,
this.updatedAt,
});
/// Product ID (Primary Key)
@HiveField(0)
final String id;
final String productId;
/// Product name
@HiveField(1)
final String name;
/// Product SKU
@HiveField(2)
final String sku;
/// Product description
@HiveField(2)
final String? description;
/// Base price
@HiveField(3)
final String description;
final double basePrice;
/// Price per unit (VND)
/// Product images (JSON encoded list of URLs)
@HiveField(4)
final double price;
final String? images;
/// Unit of measurement
/// Image captions (JSON encoded map of image_url -> caption)
@HiveField(5)
final String unit;
final String? imageCaptions;
/// Product image URL
/// 360-degree view link
@HiveField(6)
final String imageUrl;
final String? link360;
/// Category ID
/// Product specifications (JSON encoded)
/// Contains: size, material, color, finish, etc.
@HiveField(7)
final String categoryId;
final String? specifications;
/// Stock availability
/// Product category
@HiveField(8)
final bool inStock;
final String? category;
/// Stock quantity
/// Product brand
@HiveField(9)
final int stockQuantity;
/// Created date (ISO8601 string)
@HiveField(10)
final String createdAt;
/// Sale price (optional)
@HiveField(11)
final double? salePrice;
/// Brand name (optional)
@HiveField(12)
final String? brand;
ProductModel({
required this.id,
required this.name,
required this.sku,
required this.description,
required this.price,
required this.unit,
required this.imageUrl,
required this.categoryId,
required this.inStock,
required this.stockQuantity,
required this.createdAt,
this.salePrice,
this.brand,
});
/// Unit of measurement (m2, box, piece, etc.)
@HiveField(10)
final String? unit;
/// From JSON constructor
/// Whether product is active
@HiveField(11)
final bool isActive;
/// Whether product is featured
@HiveField(12)
final bool isFeatured;
/// ERPNext item code for integration
@HiveField(13)
final String? erpnextItemCode;
/// Product creation timestamp
@HiveField(14)
final DateTime createdAt;
/// Last update timestamp
@HiveField(15)
final DateTime? updatedAt;
// =========================================================================
// JSON SERIALIZATION
// =========================================================================
/// Create ProductModel from JSON
factory ProductModel.fromJson(Map<String, dynamic> json) {
return ProductModel(
id: json['id'] as String,
productId: json['product_id'] as String,
name: json['name'] as String,
sku: json['sku'] as String,
description: json['description'] as String,
price: (json['price'] as num).toDouble(),
unit: json['unit'] as String,
imageUrl: json['imageUrl'] as String,
categoryId: json['categoryId'] as String,
inStock: json['inStock'] as bool,
stockQuantity: json['stockQuantity'] as int,
createdAt: json['createdAt'] as String,
salePrice: json['salePrice'] != null ? (json['salePrice'] as num).toDouble() : null,
description: json['description'] as String?,
basePrice: (json['base_price'] as num).toDouble(),
images: json['images'] != null ? jsonEncode(json['images']) : null,
imageCaptions: json['image_captions'] != null
? jsonEncode(json['image_captions'])
: null,
link360: json['link_360'] as String?,
specifications: json['specifications'] != null
? jsonEncode(json['specifications'])
: null,
category: json['category'] as String?,
brand: json['brand'] as String?,
unit: json['unit'] as String?,
isActive: json['is_active'] as bool? ?? true,
isFeatured: json['is_featured'] as bool? ?? false,
erpnextItemCode: json['erpnext_item_code'] as String?,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: json['updated_at'] != null
? DateTime.parse(json['updated_at'] as String)
: null,
);
}
/// To JSON method
/// Convert ProductModel to JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'product_id': productId,
'name': name,
'sku': sku,
'description': description,
'price': price,
'unit': unit,
'imageUrl': imageUrl,
'categoryId': categoryId,
'inStock': inStock,
'stockQuantity': stockQuantity,
'createdAt': createdAt,
'salePrice': salePrice,
'base_price': basePrice,
'images': images != null ? jsonDecode(images!) : null,
'image_captions':
imageCaptions != null ? jsonDecode(imageCaptions!) : null,
'link_360': link360,
'specifications':
specifications != null ? jsonDecode(specifications!) : null,
'category': category,
'brand': brand,
'unit': unit,
'is_active': isActive,
'is_featured': isFeatured,
'erpnext_item_code': erpnextItemCode,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
};
}
/// Convert to domain entity
Product toEntity() {
return Product(
id: id,
name: name,
sku: sku,
description: description,
price: price,
unit: unit,
imageUrl: imageUrl,
categoryId: categoryId,
inStock: inStock,
stockQuantity: stockQuantity,
createdAt: DateTime.parse(createdAt),
salePrice: salePrice,
brand: brand,
);
// =========================================================================
// HELPER METHODS
// =========================================================================
/// Get images as List
List<String>? get imagesList {
if (images == null) return null;
try {
final decoded = jsonDecode(images!) as List;
return decoded.map((e) => e.toString()).toList();
} catch (e) {
return null;
}
}
/// Create from domain entity
factory ProductModel.fromEntity(Product entity) {
return ProductModel(
id: entity.id,
name: entity.name,
sku: entity.sku,
description: entity.description,
price: entity.price,
unit: entity.unit,
imageUrl: entity.imageUrl,
categoryId: entity.categoryId,
inStock: entity.inStock,
stockQuantity: entity.stockQuantity,
createdAt: entity.createdAt.toIso8601String(),
salePrice: entity.salePrice,
brand: entity.brand,
);
/// Get first image or placeholder
String get primaryImage {
final imgs = imagesList;
if (imgs != null && imgs.isNotEmpty) {
return imgs.first;
}
return ''; // Return empty string, UI should handle placeholder
}
/// Copy with method
/// Get image captions as Map
Map<String, String>? get imageCaptionsMap {
if (imageCaptions == null) return null;
try {
final decoded = jsonDecode(imageCaptions!) as Map<String, dynamic>;
return decoded.map((key, value) => MapEntry(key, value.toString()));
} catch (e) {
return null;
}
}
/// Get specifications as Map
Map<String, dynamic>? get specificationsMap {
if (specifications == null) return null;
try {
return jsonDecode(specifications!) as Map<String, dynamic>;
} catch (e) {
return null;
}
}
/// Get formatted price with currency
String get formattedPrice {
return '${basePrice.toStringAsFixed(0)}đ';
}
/// Check if product has 360 view
bool get has360View => link360 != null && link360!.isNotEmpty;
// =========================================================================
// COPY WITH
// =========================================================================
/// Create a copy with updated fields
ProductModel copyWith({
String? id,
String? productId,
String? name,
String? sku,
String? description,
double? price,
String? unit,
String? imageUrl,
String? categoryId,
bool? inStock,
int? stockQuantity,
String? createdAt,
double? salePrice,
double? basePrice,
String? images,
String? imageCaptions,
String? link360,
String? specifications,
String? category,
String? brand,
String? unit,
bool? isActive,
bool? isFeatured,
String? erpnextItemCode,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return ProductModel(
id: id ?? this.id,
productId: productId ?? this.productId,
name: name ?? this.name,
sku: sku ?? this.sku,
description: description ?? this.description,
price: price ?? this.price,
unit: unit ?? this.unit,
imageUrl: imageUrl ?? this.imageUrl,
categoryId: categoryId ?? this.categoryId,
inStock: inStock ?? this.inStock,
stockQuantity: stockQuantity ?? this.stockQuantity,
createdAt: createdAt ?? this.createdAt,
salePrice: salePrice ?? this.salePrice,
basePrice: basePrice ?? this.basePrice,
images: images ?? this.images,
imageCaptions: imageCaptions ?? this.imageCaptions,
link360: link360 ?? this.link360,
specifications: specifications ?? this.specifications,
category: category ?? this.category,
brand: brand ?? this.brand,
unit: unit ?? this.unit,
isActive: isActive ?? this.isActive,
isFeatured: isFeatured ?? this.isFeatured,
erpnextItemCode: erpnextItemCode ?? this.erpnextItemCode,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
String toString() {
return 'ProductModel(id: $id, name: $name, sku: $sku, price: $price, unit: $unit)';
return 'ProductModel(productId: $productId, name: $name, price: $basePrice)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ProductModel &&
other.id == id &&
other.name == name &&
other.sku == sku &&
other.description == description &&
other.price == price &&
other.unit == unit &&
other.imageUrl == imageUrl &&
other.categoryId == categoryId &&
other.inStock == inStock &&
other.stockQuantity == stockQuantity &&
other.createdAt == createdAt &&
other.salePrice == salePrice &&
other.brand == brand;
return other is ProductModel && other.productId == productId;
}
@override
int get hashCode {
return Object.hash(
id,
name,
sku,
description,
price,
unit,
imageUrl,
categoryId,
inStock,
stockQuantity,
createdAt,
salePrice,
brand,
int get hashCode => productId.hashCode;
// =========================================================================
// ENTITY CONVERSION
// =========================================================================
/// Convert ProductModel to Product entity
Product toEntity() {
return Product(
productId: productId,
name: name,
description: description,
basePrice: basePrice,
images: imagesList ?? [],
imageCaptions: imageCaptionsMap ?? {},
link360: link360,
specifications: specificationsMap ?? {},
category: category,
brand: brand,
unit: unit,
isActive: isActive,
isFeatured: isFeatured,
erpnextItemCode: erpnextItemCode,
createdAt: createdAt,
updatedAt: updatedAt ?? createdAt,
);
}
}

View File

@@ -8,7 +8,7 @@ part of 'product_model.dart';
class ProductModelAdapter extends TypeAdapter<ProductModel> {
@override
final typeId = 1;
final typeId = 2;
@override
ProductModel read(BinaryReader reader) {
@@ -17,52 +17,61 @@ class ProductModelAdapter extends TypeAdapter<ProductModel> {
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ProductModel(
id: fields[0] as String,
productId: fields[0] as String,
name: fields[1] as String,
sku: fields[2] as String,
description: fields[3] as String,
price: (fields[4] as num).toDouble(),
unit: fields[5] as String,
imageUrl: fields[6] as String,
categoryId: fields[7] as String,
inStock: fields[8] as bool,
stockQuantity: (fields[9] as num).toInt(),
createdAt: fields[10] as String,
salePrice: (fields[11] as num?)?.toDouble(),
brand: fields[12] as String?,
description: fields[2] as String?,
basePrice: (fields[3] as num).toDouble(),
images: fields[4] as String?,
imageCaptions: fields[5] as String?,
link360: fields[6] as String?,
specifications: fields[7] as String?,
category: fields[8] as String?,
brand: fields[9] as String?,
unit: fields[10] as String?,
isActive: fields[11] as bool,
isFeatured: fields[12] as bool,
erpnextItemCode: fields[13] as String?,
createdAt: fields[14] as DateTime,
updatedAt: fields[15] as DateTime?,
);
}
@override
void write(BinaryWriter writer, ProductModel obj) {
writer
..writeByte(13)
..writeByte(16)
..writeByte(0)
..write(obj.id)
..write(obj.productId)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.sku)
..writeByte(3)
..write(obj.description)
..writeByte(3)
..write(obj.basePrice)
..writeByte(4)
..write(obj.price)
..write(obj.images)
..writeByte(5)
..write(obj.unit)
..write(obj.imageCaptions)
..writeByte(6)
..write(obj.imageUrl)
..write(obj.link360)
..writeByte(7)
..write(obj.categoryId)
..write(obj.specifications)
..writeByte(8)
..write(obj.inStock)
..write(obj.category)
..writeByte(9)
..write(obj.stockQuantity)
..write(obj.brand)
..writeByte(10)
..write(obj.createdAt)
..write(obj.unit)
..writeByte(11)
..write(obj.salePrice)
..write(obj.isActive)
..writeByte(12)
..write(obj.brand);
..write(obj.isFeatured)
..writeByte(13)
..write(obj.erpnextItemCode)
..writeByte(14)
..write(obj.createdAt)
..writeByte(15)
..write(obj.updatedAt);
}
@override

View File

@@ -0,0 +1,84 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
part 'stock_level_model.g.dart';
/// Stock Level Model
///
/// Hive CE model for caching stock level data locally.
/// Maps to the 'stock_levels' table in the database.
///
/// Type ID: 3
@HiveType(typeId: HiveTypeIds.stockLevelModel)
class StockLevelModel extends HiveObject {
StockLevelModel({
required this.productId,
required this.availableQty,
required this.reservedQty,
required this.orderedQty,
required this.warehouseCode,
required this.lastUpdated,
});
@HiveField(0)
final String productId;
@HiveField(1)
final double availableQty;
@HiveField(2)
final double reservedQty;
@HiveField(3)
final double orderedQty;
@HiveField(4)
final String warehouseCode;
@HiveField(5)
final DateTime lastUpdated;
factory StockLevelModel.fromJson(Map<String, dynamic> json) {
return StockLevelModel(
productId: json['product_id'] as String,
availableQty: (json['available_qty'] as num).toDouble(),
reservedQty: (json['reserved_qty'] as num).toDouble(),
orderedQty: (json['ordered_qty'] as num).toDouble(),
warehouseCode: json['warehouse_code'] as String,
lastUpdated: DateTime.parse(json['last_updated'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'product_id': productId,
'available_qty': availableQty,
'reserved_qty': reservedQty,
'ordered_qty': orderedQty,
'warehouse_code': warehouseCode,
'last_updated': lastUpdated.toIso8601String(),
};
}
double get totalQty => availableQty + reservedQty + orderedQty;
bool get inStock => availableQty > 0;
StockLevelModel copyWith({
String? productId,
double? availableQty,
double? reservedQty,
double? orderedQty,
String? warehouseCode,
DateTime? lastUpdated,
}) {
return StockLevelModel(
productId: productId ?? this.productId,
availableQty: availableQty ?? this.availableQty,
reservedQty: reservedQty ?? this.reservedQty,
orderedQty: orderedQty ?? this.orderedQty,
warehouseCode: warehouseCode ?? this.warehouseCode,
lastUpdated: lastUpdated ?? this.lastUpdated,
);
}
}

View File

@@ -0,0 +1,56 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'stock_level_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class StockLevelModelAdapter extends TypeAdapter<StockLevelModel> {
@override
final typeId = 3;
@override
StockLevelModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return StockLevelModel(
productId: fields[0] as String,
availableQty: (fields[1] as num).toDouble(),
reservedQty: (fields[2] as num).toDouble(),
orderedQty: (fields[3] as num).toDouble(),
warehouseCode: fields[4] as String,
lastUpdated: fields[5] as DateTime,
);
}
@override
void write(BinaryWriter writer, StockLevelModel obj) {
writer
..writeByte(6)
..writeByte(0)
..write(obj.productId)
..writeByte(1)
..write(obj.availableQty)
..writeByte(2)
..write(obj.reservedQty)
..writeByte(3)
..write(obj.orderedQty)
..writeByte(4)
..write(obj.warehouseCode)
..writeByte(5)
..write(obj.lastUpdated);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is StockLevelModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -10,111 +10,155 @@ library;
/// Used across all layers but originates in the domain layer.
class Product {
/// Unique identifier
final String id;
final String productId;
/// Product name (Vietnamese)
final String name;
/// Product SKU (Stock Keeping Unit)
final String sku;
/// Product description
final String description;
final String? description;
/// Price per unit (VND)
final double price;
/// Base price per unit (VND)
final double basePrice;
/// Product images (URLs)
final List<String> images;
/// Image captions
final Map<String, String> imageCaptions;
/// 360-degree view link
final String? link360;
/// Product specifications
final Map<String, dynamic> specifications;
/// Category name
final String? category;
/// Brand name
final String? brand;
/// Unit of measurement (e.g., "m²", "viên", "hộp")
final String unit;
final String? unit;
/// Product image URL
final String imageUrl;
/// Product is active
final bool isActive;
/// Category ID
final String categoryId;
/// Product is featured
final bool isFeatured;
/// Stock availability
final bool inStock;
/// Stock quantity
final int stockQuantity;
/// ERPNext item code
final String? erpnextItemCode;
/// Created date
final DateTime createdAt;
/// Optional sale price
final double? salePrice;
/// Optional brand name
final String? brand;
/// Last updated date
final DateTime updatedAt;
const Product({
required this.id,
required this.productId,
required this.name,
required this.sku,
required this.description,
required this.price,
required this.unit,
required this.imageUrl,
required this.categoryId,
required this.inStock,
required this.stockQuantity,
required this.createdAt,
this.salePrice,
this.description,
required this.basePrice,
required this.images,
required this.imageCaptions,
this.link360,
required this.specifications,
this.category,
this.brand,
this.unit,
required this.isActive,
required this.isFeatured,
this.erpnextItemCode,
required this.createdAt,
required this.updatedAt,
});
/// Get effective price (sale price if available, otherwise regular price)
double get effectivePrice => salePrice ?? price;
/// Get primary image URL
String? get primaryImage => images.isNotEmpty ? images.first : null;
/// Alias for primaryImage (used by UI widgets)
String get imageUrl => primaryImage ?? '';
/// Category ID (alias for category field)
String? get categoryId => category;
/// Check if product has 360 view
bool get has360View => link360 != null && link360!.isNotEmpty;
/// Check if product has multiple images
bool get hasMultipleImages => images.length > 1;
/// Check if product is on sale
bool get isOnSale => salePrice != null && salePrice! < price;
/// TODO: Implement sale price logic when backend supports it
bool get isOnSale => false;
/// Get discount percentage
int get discountPercentage {
if (!isOnSale) return 0;
return (((price - salePrice!) / price) * 100).round();
/// Discount percentage
/// TODO: Calculate from salePrice when backend supports it
int get discountPercentage => 0;
/// Effective price (considering sales)
/// TODO: Use salePrice when backend supports it
double get effectivePrice => basePrice;
/// Check if product is low stock
/// TODO: Implement stock tracking when backend supports it
bool get isLowStock => false;
/// Check if product is in stock
/// Currently using isActive as proxy
bool get inStock => isActive;
/// Get specification value by key
String? getSpecification(String key) {
return specifications[key]?.toString();
}
/// Check if stock is low (less than 10 items)
bool get isLowStock => inStock && stockQuantity < 10;
/// Copy with method for creating modified copies
Product copyWith({
String? id,
String? productId,
String? name,
String? sku,
String? description,
double? price,
String? unit,
String? imageUrl,
String? categoryId,
bool? inStock,
int? stockQuantity,
DateTime? createdAt,
double? salePrice,
double? basePrice,
List<String>? images,
Map<String, String>? imageCaptions,
String? link360,
Map<String, dynamic>? specifications,
String? category,
String? brand,
String? unit,
bool? isActive,
bool? isFeatured,
String? erpnextItemCode,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return Product(
id: id ?? this.id,
productId: productId ?? this.productId,
name: name ?? this.name,
sku: sku ?? this.sku,
description: description ?? this.description,
price: price ?? this.price,
unit: unit ?? this.unit,
imageUrl: imageUrl ?? this.imageUrl,
categoryId: categoryId ?? this.categoryId,
inStock: inStock ?? this.inStock,
stockQuantity: stockQuantity ?? this.stockQuantity,
createdAt: createdAt ?? this.createdAt,
salePrice: salePrice ?? this.salePrice,
basePrice: basePrice ?? this.basePrice,
images: images ?? this.images,
imageCaptions: imageCaptions ?? this.imageCaptions,
link360: link360 ?? this.link360,
specifications: specifications ?? this.specifications,
category: category ?? this.category,
brand: brand ?? this.brand,
unit: unit ?? this.unit,
isActive: isActive ?? this.isActive,
isFeatured: isFeatured ?? this.isFeatured,
erpnextItemCode: erpnextItemCode ?? this.erpnextItemCode,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
String toString() {
return 'Product(id: $id, name: $name, sku: $sku, price: $price, unit: $unit, inStock: $inStock)';
return 'Product(productId: $productId, name: $name, basePrice: $basePrice, '
'category: $category, isActive: $isActive, isFeatured: $isFeatured)';
}
@override
@@ -122,35 +166,31 @@ class Product {
if (identical(this, other)) return true;
return other is Product &&
other.id == id &&
other.productId == productId &&
other.name == name &&
other.sku == sku &&
other.description == description &&
other.price == price &&
other.basePrice == basePrice &&
other.category == category &&
other.brand == brand &&
other.unit == unit &&
other.imageUrl == imageUrl &&
other.categoryId == categoryId &&
other.inStock == inStock &&
other.stockQuantity == stockQuantity &&
other.salePrice == salePrice &&
other.brand == brand;
other.isActive == isActive &&
other.isFeatured == isFeatured &&
other.erpnextItemCode == erpnextItemCode;
}
@override
int get hashCode {
return Object.hash(
id,
productId,
name,
sku,
description,
price,
unit,
imageUrl,
categoryId,
inStock,
stockQuantity,
salePrice,
basePrice,
category,
brand,
unit,
isActive,
isFeatured,
erpnextItemCode,
);
}
}

View File

@@ -0,0 +1,106 @@
/// Domain Entity: Stock Level
///
/// Represents inventory stock level for a product in a warehouse.
library;
/// Stock Level Entity
///
/// Contains inventory information for a product:
/// - Available quantity
/// - Reserved quantity (for pending orders)
/// - Ordered quantity (incoming stock)
/// - Warehouse location
class StockLevel {
/// Product ID
final String productId;
/// Available quantity for sale
final double availableQty;
/// Quantity reserved for orders
final double reservedQty;
/// Quantity on order (incoming)
final double orderedQty;
/// Warehouse code
final String warehouseCode;
/// Last update timestamp
final DateTime lastUpdated;
const StockLevel({
required this.productId,
required this.availableQty,
required this.reservedQty,
required this.orderedQty,
required this.warehouseCode,
required this.lastUpdated,
});
/// Get total quantity (available + reserved + ordered)
double get totalQty => availableQty + reservedQty + orderedQty;
/// Check if product is in stock
bool get isInStock => availableQty > 0;
/// Check if stock is low (less than 10 units)
bool get isLowStock => availableQty > 0 && availableQty < 10;
/// Check if out of stock
bool get isOutOfStock => availableQty <= 0;
/// Get available percentage
double get availablePercentage {
if (totalQty == 0) return 0;
return (availableQty / totalQty) * 100;
}
/// Copy with method for immutability
StockLevel copyWith({
String? productId,
double? availableQty,
double? reservedQty,
double? orderedQty,
String? warehouseCode,
DateTime? lastUpdated,
}) {
return StockLevel(
productId: productId ?? this.productId,
availableQty: availableQty ?? this.availableQty,
reservedQty: reservedQty ?? this.reservedQty,
orderedQty: orderedQty ?? this.orderedQty,
warehouseCode: warehouseCode ?? this.warehouseCode,
lastUpdated: lastUpdated ?? this.lastUpdated,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is StockLevel &&
other.productId == productId &&
other.availableQty == availableQty &&
other.reservedQty == reservedQty &&
other.orderedQty == orderedQty &&
other.warehouseCode == warehouseCode;
}
@override
int get hashCode {
return Object.hash(
productId,
availableQty,
reservedQty,
orderedQty,
warehouseCode,
);
}
@override
String toString() {
return 'StockLevel(productId: $productId, availableQty: $availableQty, '
'reservedQty: $reservedQty, orderedQty: $orderedQty, warehouseCode: $warehouseCode)';
}
}

View File

@@ -5,6 +5,7 @@ library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/products/presentation/providers/categories_provider.dart';
@@ -34,6 +35,10 @@ class ProductsPage extends ConsumerWidget {
return Scaffold(
backgroundColor: AppColors.white,
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => context.pop(),
),
title: const Text('Sản phẩm', style: TextStyle(color: Colors.black)),
elevation: AppBarSpecs.elevation,
backgroundColor: AppColors.white,

View File

@@ -0,0 +1,78 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'design_request_model.g.dart';
@HiveType(typeId: HiveTypeIds.designRequestModel)
class DesignRequestModel extends HiveObject {
DesignRequestModel({required this.requestId, required this.userId, required this.projectName, required this.projectType, required this.area, required this.style, required this.budget, required this.currentSituation, required this.requirements, this.notes, this.attachments, required this.status, this.assignedDesigner, this.finalDesignLink, this.feedback, this.rating, this.estimatedCompletion, required this.createdAt, this.completedAt, this.updatedAt});
@HiveField(0) final String requestId;
@HiveField(1) final String userId;
@HiveField(2) final String projectName;
@HiveField(3) final ProjectType projectType;
@HiveField(4) final double area;
@HiveField(5) final String style;
@HiveField(6) final double budget;
@HiveField(7) final String currentSituation;
@HiveField(8) final String requirements;
@HiveField(9) final String? notes;
@HiveField(10) final String? attachments;
@HiveField(11) final DesignStatus status;
@HiveField(12) final String? assignedDesigner;
@HiveField(13) final String? finalDesignLink;
@HiveField(14) final String? feedback;
@HiveField(15) final int? rating;
@HiveField(16) final DateTime? estimatedCompletion;
@HiveField(17) final DateTime createdAt;
@HiveField(18) final DateTime? completedAt;
@HiveField(19) final DateTime? updatedAt;
factory DesignRequestModel.fromJson(Map<String, dynamic> json) => DesignRequestModel(
requestId: json['request_id'] as String,
userId: json['user_id'] as String,
projectName: json['project_name'] as String,
projectType: ProjectType.values.firstWhere((e) => e.name == json['project_type']),
area: (json['area'] as num).toDouble(),
style: json['style'] as String,
budget: (json['budget'] as num).toDouble(),
currentSituation: json['current_situation'] as String,
requirements: json['requirements'] as String,
notes: json['notes'] as String?,
attachments: json['attachments'] != null ? jsonEncode(json['attachments']) : null,
status: DesignStatus.values.firstWhere((e) => e.name == json['status']),
assignedDesigner: json['assigned_designer'] as String?,
finalDesignLink: json['final_design_link'] as String?,
feedback: json['feedback'] as String?,
rating: json['rating'] as int?,
estimatedCompletion: json['estimated_completion'] != null ? DateTime.parse(json['estimated_completion']?.toString() ?? '') : null,
createdAt: DateTime.parse(json['created_at']?.toString() ?? ''),
completedAt: json['completed_at'] != null ? DateTime.parse(json['completed_at']?.toString() ?? '') : null,
updatedAt: json['updated_at'] != null ? DateTime.parse(json['updated_at']?.toString() ?? '') : null,
);
Map<String, dynamic> toJson() => {
'request_id': requestId,
'user_id': userId,
'project_name': projectName,
'project_type': projectType.name,
'area': area,
'style': style,
'budget': budget,
'current_situation': currentSituation,
'requirements': requirements,
'notes': notes,
'attachments': attachments != null ? jsonDecode(attachments!) : null,
'status': status.name,
'assigned_designer': assignedDesigner,
'final_design_link': finalDesignLink,
'feedback': feedback,
'rating': rating,
'estimated_completion': estimatedCompletion?.toIso8601String(),
'created_at': createdAt.toIso8601String(),
'completed_at': completedAt?.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
};
}

View File

@@ -0,0 +1,98 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'design_request_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class DesignRequestModelAdapter extends TypeAdapter<DesignRequestModel> {
@override
final typeId = 15;
@override
DesignRequestModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return DesignRequestModel(
requestId: fields[0] as String,
userId: fields[1] as String,
projectName: fields[2] as String,
projectType: fields[3] as ProjectType,
area: (fields[4] as num).toDouble(),
style: fields[5] as String,
budget: (fields[6] as num).toDouble(),
currentSituation: fields[7] as String,
requirements: fields[8] as String,
notes: fields[9] as String?,
attachments: fields[10] as String?,
status: fields[11] as DesignStatus,
assignedDesigner: fields[12] as String?,
finalDesignLink: fields[13] as String?,
feedback: fields[14] as String?,
rating: (fields[15] as num?)?.toInt(),
estimatedCompletion: fields[16] as DateTime?,
createdAt: fields[17] as DateTime,
completedAt: fields[18] as DateTime?,
updatedAt: fields[19] as DateTime?,
);
}
@override
void write(BinaryWriter writer, DesignRequestModel obj) {
writer
..writeByte(20)
..writeByte(0)
..write(obj.requestId)
..writeByte(1)
..write(obj.userId)
..writeByte(2)
..write(obj.projectName)
..writeByte(3)
..write(obj.projectType)
..writeByte(4)
..write(obj.area)
..writeByte(5)
..write(obj.style)
..writeByte(6)
..write(obj.budget)
..writeByte(7)
..write(obj.currentSituation)
..writeByte(8)
..write(obj.requirements)
..writeByte(9)
..write(obj.notes)
..writeByte(10)
..write(obj.attachments)
..writeByte(11)
..write(obj.status)
..writeByte(12)
..write(obj.assignedDesigner)
..writeByte(13)
..write(obj.finalDesignLink)
..writeByte(14)
..write(obj.feedback)
..writeByte(15)
..write(obj.rating)
..writeByte(16)
..write(obj.estimatedCompletion)
..writeByte(17)
..write(obj.createdAt)
..writeByte(18)
..write(obj.completedAt)
..writeByte(19)
..write(obj.updatedAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is DesignRequestModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,86 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'project_submission_model.g.dart';
@HiveType(typeId: HiveTypeIds.projectSubmissionModel)
class ProjectSubmissionModel extends HiveObject {
ProjectSubmissionModel({required this.submissionId, required this.userId, required this.projectName, required this.projectAddress, required this.projectValue, required this.projectType, this.beforePhotos, this.afterPhotos, this.invoices, required this.status, this.reviewNotes, this.rejectionReason, this.pointsEarned, required this.submittedAt, this.reviewedAt, this.reviewedBy});
@HiveField(0) final String submissionId;
@HiveField(1) final String userId;
@HiveField(2) final String projectName;
@HiveField(3) final String projectAddress;
@HiveField(4) final double projectValue;
@HiveField(5) final ProjectType projectType;
@HiveField(6) final String? beforePhotos;
@HiveField(7) final String? afterPhotos;
@HiveField(8) final String? invoices;
@HiveField(9) final SubmissionStatus status;
@HiveField(10) final String? reviewNotes;
@HiveField(11) final String? rejectionReason;
@HiveField(12) final int? pointsEarned;
@HiveField(13) final DateTime submittedAt;
@HiveField(14) final DateTime? reviewedAt;
@HiveField(15) final String? reviewedBy;
factory ProjectSubmissionModel.fromJson(Map<String, dynamic> json) => ProjectSubmissionModel(
submissionId: json['submission_id'] as String,
userId: json['user_id'] as String,
projectName: json['project_name'] as String,
projectAddress: json['project_address'] as String,
projectValue: (json['project_value'] as num).toDouble(),
projectType: ProjectType.values.firstWhere((e) => e.name == json['project_type']),
beforePhotos: json['before_photos'] != null ? jsonEncode(json['before_photos']) : null,
afterPhotos: json['after_photos'] != null ? jsonEncode(json['after_photos']) : null,
invoices: json['invoices'] != null ? jsonEncode(json['invoices']) : null,
status: SubmissionStatus.values.firstWhere((e) => e.name == json['status']),
reviewNotes: json['review_notes'] as String?,
rejectionReason: json['rejection_reason'] as String?,
pointsEarned: json['points_earned'] as int?,
submittedAt: DateTime.parse(json['submitted_at']?.toString() ?? ''),
reviewedAt: json['reviewed_at'] != null ? DateTime.parse(json['reviewed_at']?.toString() ?? '') : null,
reviewedBy: json['reviewed_by'] as String?,
);
Map<String, dynamic> toJson() => {
'submission_id': submissionId,
'user_id': userId,
'project_name': projectName,
'project_address': projectAddress,
'project_value': projectValue,
'project_type': projectType.name,
'before_photos': beforePhotos != null ? jsonDecode(beforePhotos!) : null,
'after_photos': afterPhotos != null ? jsonDecode(afterPhotos!) : null,
'invoices': invoices != null ? jsonDecode(invoices!) : null,
'status': status.name,
'review_notes': reviewNotes,
'rejection_reason': rejectionReason,
'points_earned': pointsEarned,
'submitted_at': submittedAt.toIso8601String(),
'reviewed_at': reviewedAt?.toIso8601String(),
'reviewed_by': reviewedBy,
};
List<String>? get beforePhotosList {
if (beforePhotos == null) return null;
try {
final decoded = jsonDecode(beforePhotos!) as List;
return decoded.map((e) => e.toString()).toList();
} catch (e) {
return null;
}
}
List<String>? get afterPhotosList {
if (afterPhotos == null) return null;
try {
final decoded = jsonDecode(afterPhotos!) as List;
return decoded.map((e) => e.toString()).toList();
} catch (e) {
return null;
}
}
}

View File

@@ -0,0 +1,87 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'project_submission_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ProjectSubmissionModelAdapter
extends TypeAdapter<ProjectSubmissionModel> {
@override
final typeId = 14;
@override
ProjectSubmissionModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ProjectSubmissionModel(
submissionId: fields[0] as String,
userId: fields[1] as String,
projectName: fields[2] as String,
projectAddress: fields[3] as String,
projectValue: (fields[4] as num).toDouble(),
projectType: fields[5] as ProjectType,
beforePhotos: fields[6] as String?,
afterPhotos: fields[7] as String?,
invoices: fields[8] as String?,
status: fields[9] as SubmissionStatus,
reviewNotes: fields[10] as String?,
rejectionReason: fields[11] as String?,
pointsEarned: (fields[12] as num?)?.toInt(),
submittedAt: fields[13] as DateTime,
reviewedAt: fields[14] as DateTime?,
reviewedBy: fields[15] as String?,
);
}
@override
void write(BinaryWriter writer, ProjectSubmissionModel obj) {
writer
..writeByte(16)
..writeByte(0)
..write(obj.submissionId)
..writeByte(1)
..write(obj.userId)
..writeByte(2)
..write(obj.projectName)
..writeByte(3)
..write(obj.projectAddress)
..writeByte(4)
..write(obj.projectValue)
..writeByte(5)
..write(obj.projectType)
..writeByte(6)
..write(obj.beforePhotos)
..writeByte(7)
..write(obj.afterPhotos)
..writeByte(8)
..write(obj.invoices)
..writeByte(9)
..write(obj.status)
..writeByte(10)
..write(obj.reviewNotes)
..writeByte(11)
..write(obj.rejectionReason)
..writeByte(12)
..write(obj.pointsEarned)
..writeByte(13)
..write(obj.submittedAt)
..writeByte(14)
..write(obj.reviewedAt)
..writeByte(15)
..write(obj.reviewedBy);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ProjectSubmissionModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,249 @@
/// Domain Entity: Design Request
///
/// Represents a request for design consultation service.
library;
import 'project_submission.dart';
/// Design status enum
enum DesignStatus {
/// Request submitted, pending assignment
pending,
/// Assigned to designer
assigned,
/// Design in progress
inProgress,
/// Design completed
completed,
/// Request cancelled
cancelled;
/// Get display name for status
String get displayName {
switch (this) {
case DesignStatus.pending:
return 'Pending';
case DesignStatus.assigned:
return 'Assigned';
case DesignStatus.inProgress:
return 'In Progress';
case DesignStatus.completed:
return 'Completed';
case DesignStatus.cancelled:
return 'Cancelled';
}
}
}
/// Design Request Entity
///
/// Contains information about a design consultation request:
/// - Project requirements
/// - Design preferences
/// - Budget constraints
/// - Assignment and tracking
class DesignRequest {
/// Unique request identifier
final String requestId;
/// User ID who requested
final String userId;
/// Project name
final String projectName;
/// Project type
final ProjectType projectType;
/// Project area (square meters)
final double area;
/// Design style preference
final String? style;
/// Budget for design/construction
final double? budget;
/// Current situation description
final String? currentSituation;
/// Requirements and wishes
final String? requirements;
/// Additional notes
final String? notes;
/// Attachment URLs (photos, references)
final List<String> attachments;
/// Request status
final DesignStatus status;
/// Assigned designer name
final String? assignedDesigner;
/// Final design link/URL
final String? finalDesignLink;
/// User feedback
final String? feedback;
/// User rating (1-5)
final int? rating;
/// Estimated completion date
final DateTime? estimatedCompletion;
/// Request creation timestamp
final DateTime createdAt;
/// Completion timestamp
final DateTime? completedAt;
/// Last update timestamp
final DateTime updatedAt;
const DesignRequest({
required this.requestId,
required this.userId,
required this.projectName,
required this.projectType,
required this.area,
this.style,
this.budget,
this.currentSituation,
this.requirements,
this.notes,
required this.attachments,
required this.status,
this.assignedDesigner,
this.finalDesignLink,
this.feedback,
this.rating,
this.estimatedCompletion,
required this.createdAt,
this.completedAt,
required this.updatedAt,
});
/// Check if request is pending
bool get isPending => status == DesignStatus.pending;
/// Check if request is assigned
bool get isAssigned =>
status == DesignStatus.assigned || status == DesignStatus.inProgress;
/// Check if request is in progress
bool get isInProgress => status == DesignStatus.inProgress;
/// Check if request is completed
bool get isCompleted => status == DesignStatus.completed;
/// Check if request is cancelled
bool get isCancelled => status == DesignStatus.cancelled;
/// Check if request has attachments
bool get hasAttachments => attachments.isNotEmpty;
/// Check if design is ready
bool get hasDesign =>
finalDesignLink != null && finalDesignLink!.isNotEmpty;
/// Check if user has provided feedback
bool get hasFeedback => feedback != null || rating != null;
/// Get completion duration
Duration? get completionDuration {
if (completedAt == null) return null;
return completedAt!.difference(createdAt);
}
/// Check if overdue
bool get isOverdue {
if (estimatedCompletion == null || isCompleted || isCancelled) {
return false;
}
return DateTime.now().isAfter(estimatedCompletion!);
}
/// Copy with method for immutability
DesignRequest copyWith({
String? requestId,
String? userId,
String? projectName,
ProjectType? projectType,
double? area,
String? style,
double? budget,
String? currentSituation,
String? requirements,
String? notes,
List<String>? attachments,
DesignStatus? status,
String? assignedDesigner,
String? finalDesignLink,
String? feedback,
int? rating,
DateTime? estimatedCompletion,
DateTime? createdAt,
DateTime? completedAt,
DateTime? updatedAt,
}) {
return DesignRequest(
requestId: requestId ?? this.requestId,
userId: userId ?? this.userId,
projectName: projectName ?? this.projectName,
projectType: projectType ?? this.projectType,
area: area ?? this.area,
style: style ?? this.style,
budget: budget ?? this.budget,
currentSituation: currentSituation ?? this.currentSituation,
requirements: requirements ?? this.requirements,
notes: notes ?? this.notes,
attachments: attachments ?? this.attachments,
status: status ?? this.status,
assignedDesigner: assignedDesigner ?? this.assignedDesigner,
finalDesignLink: finalDesignLink ?? this.finalDesignLink,
feedback: feedback ?? this.feedback,
rating: rating ?? this.rating,
estimatedCompletion: estimatedCompletion ?? this.estimatedCompletion,
createdAt: createdAt ?? this.createdAt,
completedAt: completedAt ?? this.completedAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is DesignRequest &&
other.requestId == requestId &&
other.userId == userId &&
other.projectName == projectName &&
other.area == area &&
other.status == status;
}
@override
int get hashCode {
return Object.hash(
requestId,
userId,
projectName,
area,
status,
);
}
@override
String toString() {
return 'DesignRequest(requestId: $requestId, projectName: $projectName, '
'projectType: $projectType, area: $area, status: $status, '
'assignedDesigner: $assignedDesigner)';
}
}

View File

@@ -0,0 +1,247 @@
/// Domain Entity: Project Submission
///
/// Represents a completed project submitted for loyalty points.
library;
/// Project type enum
enum ProjectType {
/// Residential project
residential,
/// Commercial project
commercial,
/// Industrial project
industrial,
/// Public infrastructure
infrastructure,
/// Other type
other;
/// Get display name for project type
String get displayName {
switch (this) {
case ProjectType.residential:
return 'Residential';
case ProjectType.commercial:
return 'Commercial';
case ProjectType.industrial:
return 'Industrial';
case ProjectType.infrastructure:
return 'Infrastructure';
case ProjectType.other:
return 'Other';
}
}
}
/// Submission status enum
enum SubmissionStatus {
/// Submitted, pending review
pending,
/// Under review
reviewing,
/// Approved, points awarded
approved,
/// Rejected
rejected;
/// Get display name for status
String get displayName {
switch (this) {
case SubmissionStatus.pending:
return 'Pending';
case SubmissionStatus.reviewing:
return 'Reviewing';
case SubmissionStatus.approved:
return 'Approved';
case SubmissionStatus.rejected:
return 'Rejected';
}
}
}
/// Project Submission Entity
///
/// Contains information about a completed project:
/// - Project details
/// - Before/after photos
/// - Invoice documentation
/// - Review status
/// - Points earned
class ProjectSubmission {
/// Unique submission identifier
final String submissionId;
/// User ID who submitted
final String userId;
/// Project name
final String projectName;
/// Project address/location
final String? projectAddress;
/// Project value/cost
final double projectValue;
/// Project type
final ProjectType projectType;
/// Before photos URLs
final List<String> beforePhotos;
/// After photos URLs
final List<String> afterPhotos;
/// Invoice/receipt URLs
final List<String> invoices;
/// Submission status
final SubmissionStatus status;
/// Review notes from admin
final String? reviewNotes;
/// Rejection reason (if rejected)
final String? rejectionReason;
/// Points earned (if approved)
final int? pointsEarned;
/// Submission timestamp
final DateTime submittedAt;
/// Review timestamp
final DateTime? reviewedAt;
/// ID of admin who reviewed
final String? reviewedBy;
const ProjectSubmission({
required this.submissionId,
required this.userId,
required this.projectName,
this.projectAddress,
required this.projectValue,
required this.projectType,
required this.beforePhotos,
required this.afterPhotos,
required this.invoices,
required this.status,
this.reviewNotes,
this.rejectionReason,
this.pointsEarned,
required this.submittedAt,
this.reviewedAt,
this.reviewedBy,
});
/// Check if submission is pending
bool get isPending => status == SubmissionStatus.pending;
/// Check if submission is under review
bool get isReviewing => status == SubmissionStatus.reviewing;
/// Check if submission is approved
bool get isApproved => status == SubmissionStatus.approved;
/// Check if submission is rejected
bool get isRejected => status == SubmissionStatus.rejected;
/// Check if submission has been reviewed
bool get isReviewed =>
status == SubmissionStatus.approved || status == SubmissionStatus.rejected;
/// Check if submission has before photos
bool get hasBeforePhotos => beforePhotos.isNotEmpty;
/// Check if submission has after photos
bool get hasAfterPhotos => afterPhotos.isNotEmpty;
/// Check if submission has invoices
bool get hasInvoices => invoices.isNotEmpty;
/// Get total number of photos
int get totalPhotos => beforePhotos.length + afterPhotos.length;
/// Get review duration
Duration? get reviewDuration {
if (reviewedAt == null) return null;
return reviewedAt!.difference(submittedAt);
}
/// Copy with method for immutability
ProjectSubmission copyWith({
String? submissionId,
String? userId,
String? projectName,
String? projectAddress,
double? projectValue,
ProjectType? projectType,
List<String>? beforePhotos,
List<String>? afterPhotos,
List<String>? invoices,
SubmissionStatus? status,
String? reviewNotes,
String? rejectionReason,
int? pointsEarned,
DateTime? submittedAt,
DateTime? reviewedAt,
String? reviewedBy,
}) {
return ProjectSubmission(
submissionId: submissionId ?? this.submissionId,
userId: userId ?? this.userId,
projectName: projectName ?? this.projectName,
projectAddress: projectAddress ?? this.projectAddress,
projectValue: projectValue ?? this.projectValue,
projectType: projectType ?? this.projectType,
beforePhotos: beforePhotos ?? this.beforePhotos,
afterPhotos: afterPhotos ?? this.afterPhotos,
invoices: invoices ?? this.invoices,
status: status ?? this.status,
reviewNotes: reviewNotes ?? this.reviewNotes,
rejectionReason: rejectionReason ?? this.rejectionReason,
pointsEarned: pointsEarned ?? this.pointsEarned,
submittedAt: submittedAt ?? this.submittedAt,
reviewedAt: reviewedAt ?? this.reviewedAt,
reviewedBy: reviewedBy ?? this.reviewedBy,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ProjectSubmission &&
other.submissionId == submissionId &&
other.userId == userId &&
other.projectName == projectName &&
other.projectValue == projectValue &&
other.status == status;
}
@override
int get hashCode {
return Object.hash(
submissionId,
userId,
projectName,
projectValue,
status,
);
}
@override
String toString() {
return 'ProjectSubmission(submissionId: $submissionId, projectName: $projectName, '
'projectValue: $projectValue, projectType: $projectType, status: $status, '
'pointsEarned: $pointsEarned)';
}
}

View File

@@ -0,0 +1,45 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
part 'quote_item_model.g.dart';
@HiveType(typeId: HiveTypeIds.quoteItemModel)
class QuoteItemModel extends HiveObject {
QuoteItemModel({required this.quoteItemId, required this.quoteId, required this.productId, required this.quantity, required this.originalPrice, required this.negotiatedPrice, required this.discountPercent, required this.subtotal, this.notes});
@HiveField(0) final String quoteItemId;
@HiveField(1) final String quoteId;
@HiveField(2) final String productId;
@HiveField(3) final double quantity;
@HiveField(4) final double originalPrice;
@HiveField(5) final double negotiatedPrice;
@HiveField(6) final double discountPercent;
@HiveField(7) final double subtotal;
@HiveField(8) final String? notes;
factory QuoteItemModel.fromJson(Map<String, dynamic> json) => QuoteItemModel(
quoteItemId: json['quote_item_id'] as String,
quoteId: json['quote_id'] as String,
productId: json['product_id'] as String,
quantity: (json['quantity'] as num).toDouble(),
originalPrice: (json['original_price'] as num).toDouble(),
negotiatedPrice: (json['negotiated_price'] as num).toDouble(),
discountPercent: (json['discount_percent'] as num).toDouble(),
subtotal: (json['subtotal'] as num).toDouble(),
notes: json['notes'] as String?,
);
Map<String, dynamic> toJson() => {
'quote_item_id': quoteItemId,
'quote_id': quoteId,
'product_id': productId,
'quantity': quantity,
'original_price': originalPrice,
'negotiated_price': negotiatedPrice,
'discount_percent': discountPercent,
'subtotal': subtotal,
'notes': notes,
};
double get totalDiscount => originalPrice * quantity - subtotal;
}

View File

@@ -0,0 +1,65 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'quote_item_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class QuoteItemModelAdapter extends TypeAdapter<QuoteItemModel> {
@override
final typeId = 17;
@override
QuoteItemModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return QuoteItemModel(
quoteItemId: fields[0] as String,
quoteId: fields[1] as String,
productId: fields[2] as String,
quantity: (fields[3] as num).toDouble(),
originalPrice: (fields[4] as num).toDouble(),
negotiatedPrice: (fields[5] as num).toDouble(),
discountPercent: (fields[6] as num).toDouble(),
subtotal: (fields[7] as num).toDouble(),
notes: fields[8] as String?,
);
}
@override
void write(BinaryWriter writer, QuoteItemModel obj) {
writer
..writeByte(9)
..writeByte(0)
..write(obj.quoteItemId)
..writeByte(1)
..write(obj.quoteId)
..writeByte(2)
..write(obj.productId)
..writeByte(3)
..write(obj.quantity)
..writeByte(4)
..write(obj.originalPrice)
..writeByte(5)
..write(obj.negotiatedPrice)
..writeByte(6)
..write(obj.discountPercent)
..writeByte(7)
..write(obj.subtotal)
..writeByte(8)
..write(obj.notes);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is QuoteItemModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,69 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
part 'quote_model.g.dart';
@HiveType(typeId: HiveTypeIds.quoteModel)
class QuoteModel extends HiveObject {
QuoteModel({required this.quoteId, required this.quoteNumber, required this.userId, required this.status, required this.totalAmount, required this.discountAmount, required this.finalAmount, this.projectName, this.deliveryAddress, this.paymentTerms, this.notes, this.validUntil, this.convertedOrderId, this.erpnextQuotation, required this.createdAt, this.updatedAt});
@HiveField(0) final String quoteId;
@HiveField(1) final String quoteNumber;
@HiveField(2) final String userId;
@HiveField(3) final QuoteStatus status;
@HiveField(4) final double totalAmount;
@HiveField(5) final double discountAmount;
@HiveField(6) final double finalAmount;
@HiveField(7) final String? projectName;
@HiveField(8) final String? deliveryAddress;
@HiveField(9) final String? paymentTerms;
@HiveField(10) final String? notes;
@HiveField(11) final DateTime? validUntil;
@HiveField(12) final String? convertedOrderId;
@HiveField(13) final String? erpnextQuotation;
@HiveField(14) final DateTime createdAt;
@HiveField(15) final DateTime? updatedAt;
factory QuoteModel.fromJson(Map<String, dynamic> json) => QuoteModel(
quoteId: json['quote_id'] as String,
quoteNumber: json['quote_number'] as String,
userId: json['user_id'] as String,
status: QuoteStatus.values.firstWhere((e) => e.name == json['status']),
totalAmount: (json['total_amount'] as num).toDouble(),
discountAmount: (json['discount_amount'] as num).toDouble(),
finalAmount: (json['final_amount'] as num).toDouble(),
projectName: json['project_name'] as String?,
deliveryAddress: json['delivery_address'] != null ? jsonEncode(json['delivery_address']) : null,
paymentTerms: json['payment_terms'] as String?,
notes: json['notes'] as String?,
validUntil: json['valid_until'] != null ? DateTime.parse(json['valid_until']?.toString() ?? '') : null,
convertedOrderId: json['converted_order_id'] as String?,
erpnextQuotation: json['erpnext_quotation'] as String?,
createdAt: DateTime.parse(json['created_at']?.toString() ?? ''),
updatedAt: json['updated_at'] != null ? DateTime.parse(json['updated_at']?.toString() ?? '') : null,
);
Map<String, dynamic> toJson() => {
'quote_id': quoteId,
'quote_number': quoteNumber,
'user_id': userId,
'status': status.name,
'total_amount': totalAmount,
'discount_amount': discountAmount,
'final_amount': finalAmount,
'project_name': projectName,
'delivery_address': deliveryAddress != null ? jsonDecode(deliveryAddress!) : null,
'payment_terms': paymentTerms,
'notes': notes,
'valid_until': validUntil?.toIso8601String(),
'converted_order_id': convertedOrderId,
'erpnext_quotation': erpnextQuotation,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
};
bool get isExpired => validUntil != null && DateTime.now().isAfter(validUntil!);
bool get isConverted => convertedOrderId != null;
}

View File

@@ -0,0 +1,86 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'quote_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class QuoteModelAdapter extends TypeAdapter<QuoteModel> {
@override
final typeId = 16;
@override
QuoteModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return QuoteModel(
quoteId: fields[0] as String,
quoteNumber: fields[1] as String,
userId: fields[2] as String,
status: fields[3] as QuoteStatus,
totalAmount: (fields[4] as num).toDouble(),
discountAmount: (fields[5] as num).toDouble(),
finalAmount: (fields[6] as num).toDouble(),
projectName: fields[7] as String?,
deliveryAddress: fields[8] as String?,
paymentTerms: fields[9] as String?,
notes: fields[10] as String?,
validUntil: fields[11] as DateTime?,
convertedOrderId: fields[12] as String?,
erpnextQuotation: fields[13] as String?,
createdAt: fields[14] as DateTime,
updatedAt: fields[15] as DateTime?,
);
}
@override
void write(BinaryWriter writer, QuoteModel obj) {
writer
..writeByte(16)
..writeByte(0)
..write(obj.quoteId)
..writeByte(1)
..write(obj.quoteNumber)
..writeByte(2)
..write(obj.userId)
..writeByte(3)
..write(obj.status)
..writeByte(4)
..write(obj.totalAmount)
..writeByte(5)
..write(obj.discountAmount)
..writeByte(6)
..write(obj.finalAmount)
..writeByte(7)
..write(obj.projectName)
..writeByte(8)
..write(obj.deliveryAddress)
..writeByte(9)
..write(obj.paymentTerms)
..writeByte(10)
..write(obj.notes)
..writeByte(11)
..write(obj.validUntil)
..writeByte(12)
..write(obj.convertedOrderId)
..writeByte(13)
..write(obj.erpnextQuotation)
..writeByte(14)
..write(obj.createdAt)
..writeByte(15)
..write(obj.updatedAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is QuoteModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,313 @@
/// Domain Entity: Quote
///
/// Represents a price quotation for products/services.
library;
/// Quote status enum
enum QuoteStatus {
/// Quote is in draft state
draft,
/// Quote has been sent to customer
sent,
/// Customer has accepted the quote
accepted,
/// Quote has been rejected
rejected,
/// Quote has expired
expired,
/// Quote has been converted to order
converted;
/// Get display name for status
String get displayName {
switch (this) {
case QuoteStatus.draft:
return 'Draft';
case QuoteStatus.sent:
return 'Sent';
case QuoteStatus.accepted:
return 'Accepted';
case QuoteStatus.rejected:
return 'Rejected';
case QuoteStatus.expired:
return 'Expired';
case QuoteStatus.converted:
return 'Converted';
}
}
}
/// Delivery Address information
class DeliveryAddress {
/// Recipient name
final String? name;
/// Phone number
final String? phone;
/// Street address
final String? street;
/// Ward/commune
final String? ward;
/// District
final String? district;
/// City/province
final String? city;
/// Postal code
final String? postalCode;
const DeliveryAddress({
this.name,
this.phone,
this.street,
this.ward,
this.district,
this.city,
this.postalCode,
});
/// Get full address string
String get fullAddress {
final parts = [
street,
ward,
district,
city,
postalCode,
].where((part) => part != null && part.isNotEmpty).toList();
return parts.join(', ');
}
/// Create from JSON map
factory DeliveryAddress.fromJson(Map<String, dynamic> json) {
return DeliveryAddress(
name: json['name'] as String?,
phone: json['phone'] as String?,
street: json['street'] as String?,
ward: json['ward'] as String?,
district: json['district'] as String?,
city: json['city'] as String?,
postalCode: json['postal_code'] as String?,
);
}
/// Convert to JSON map
Map<String, dynamic> toJson() {
return {
'name': name,
'phone': phone,
'street': street,
'ward': ward,
'district': district,
'city': city,
'postal_code': postalCode,
};
}
}
/// Quote Entity
///
/// Contains complete quotation information:
/// - Quote identification
/// - Customer details
/// - Pricing and terms
/// - Conversion tracking
class Quote {
/// Unique quote identifier
final String quoteId;
/// Quote number (human-readable)
final String quoteNumber;
/// User ID who requested the quote
final String userId;
/// Quote status
final QuoteStatus status;
/// Total amount before discount
final double totalAmount;
/// Discount amount
final double discountAmount;
/// Final amount after discount
final double finalAmount;
/// Project name (if applicable)
final String? projectName;
/// Delivery address
final DeliveryAddress? deliveryAddress;
/// Payment terms
final String? paymentTerms;
/// Quote notes
final String? notes;
/// Valid until date
final DateTime? validUntil;
/// Converted order ID (if converted)
final String? convertedOrderId;
/// ERPNext quotation reference
final String? erpnextQuotation;
/// Quote creation timestamp
final DateTime createdAt;
/// Last update timestamp
final DateTime updatedAt;
const Quote({
required this.quoteId,
required this.quoteNumber,
required this.userId,
required this.status,
required this.totalAmount,
required this.discountAmount,
required this.finalAmount,
this.projectName,
this.deliveryAddress,
this.paymentTerms,
this.notes,
this.validUntil,
this.convertedOrderId,
this.erpnextQuotation,
required this.createdAt,
required this.updatedAt,
});
/// Check if quote is draft
bool get isDraft => status == QuoteStatus.draft;
/// Check if quote is sent
bool get isSent => status == QuoteStatus.sent;
/// Check if quote is accepted
bool get isAccepted => status == QuoteStatus.accepted;
/// Check if quote is rejected
bool get isRejected => status == QuoteStatus.rejected;
/// Check if quote is expired
bool get isExpired =>
status == QuoteStatus.expired ||
(validUntil != null && DateTime.now().isAfter(validUntil!));
/// Check if quote is converted to order
bool get isConverted => status == QuoteStatus.converted;
/// Check if quote can be edited
bool get canBeEdited => isDraft;
/// Check if quote can be sent
bool get canBeSent => isDraft;
/// Check if quote can be converted to order
bool get canBeConverted => isAccepted && !isExpired;
/// Get discount percentage
double get discountPercentage {
if (totalAmount == 0) return 0;
return (discountAmount / totalAmount) * 100;
}
/// Get days until expiry
int? get daysUntilExpiry {
if (validUntil == null) return null;
final days = validUntil!.difference(DateTime.now()).inDays;
return days > 0 ? days : 0;
}
/// Check if expiring soon (within 7 days)
bool get isExpiringSoon {
if (validUntil == null || isExpired) return false;
final days = daysUntilExpiry;
return days != null && days > 0 && days <= 7;
}
/// Copy with method for immutability
Quote copyWith({
String? quoteId,
String? quoteNumber,
String? userId,
QuoteStatus? status,
double? totalAmount,
double? discountAmount,
double? finalAmount,
String? projectName,
DeliveryAddress? deliveryAddress,
String? paymentTerms,
String? notes,
DateTime? validUntil,
String? convertedOrderId,
String? erpnextQuotation,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return Quote(
quoteId: quoteId ?? this.quoteId,
quoteNumber: quoteNumber ?? this.quoteNumber,
userId: userId ?? this.userId,
status: status ?? this.status,
totalAmount: totalAmount ?? this.totalAmount,
discountAmount: discountAmount ?? this.discountAmount,
finalAmount: finalAmount ?? this.finalAmount,
projectName: projectName ?? this.projectName,
deliveryAddress: deliveryAddress ?? this.deliveryAddress,
paymentTerms: paymentTerms ?? this.paymentTerms,
notes: notes ?? this.notes,
validUntil: validUntil ?? this.validUntil,
convertedOrderId: convertedOrderId ?? this.convertedOrderId,
erpnextQuotation: erpnextQuotation ?? this.erpnextQuotation,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Quote &&
other.quoteId == quoteId &&
other.quoteNumber == quoteNumber &&
other.userId == userId &&
other.status == status &&
other.totalAmount == totalAmount &&
other.discountAmount == discountAmount &&
other.finalAmount == finalAmount;
}
@override
int get hashCode {
return Object.hash(
quoteId,
quoteNumber,
userId,
status,
totalAmount,
discountAmount,
finalAmount,
);
}
@override
String toString() {
return 'Quote(quoteId: $quoteId, quoteNumber: $quoteNumber, status: $status, '
'finalAmount: $finalAmount, validUntil: $validUntil)';
}
}

View File

@@ -0,0 +1,133 @@
/// Domain Entity: Quote Item
///
/// Represents a single line item in a quotation.
library;
/// Quote Item Entity
///
/// Contains item-level information in a quote:
/// - Product reference
/// - Quantity and pricing
/// - Price negotiation
/// - Discounts
class QuoteItem {
/// Unique quote item identifier
final String quoteItemId;
/// Quote ID this item belongs to
final String quoteId;
/// Product ID
final String productId;
/// Quantity quoted
final double quantity;
/// Original/list price per unit
final double originalPrice;
/// Negotiated price per unit
final double negotiatedPrice;
/// Discount percentage
final double discountPercent;
/// Subtotal (quantity * negotiatedPrice)
final double subtotal;
/// Item notes
final String? notes;
const QuoteItem({
required this.quoteItemId,
required this.quoteId,
required this.productId,
required this.quantity,
required this.originalPrice,
required this.negotiatedPrice,
required this.discountPercent,
required this.subtotal,
this.notes,
});
/// Calculate subtotal at original price
double get subtotalAtOriginalPrice => quantity * originalPrice;
/// Calculate discount amount per unit
double get discountPerUnit => originalPrice - negotiatedPrice;
/// Calculate total discount amount
double get totalDiscountAmount =>
(quantity * originalPrice) - (quantity * negotiatedPrice);
/// Calculate effective discount percentage
double get effectiveDiscountPercentage {
if (originalPrice == 0) return 0;
return ((originalPrice - negotiatedPrice) / originalPrice) * 100;
}
/// Check if item has discount
bool get hasDiscount => negotiatedPrice < originalPrice;
/// Check if price was negotiated
bool get isNegotiated => negotiatedPrice != originalPrice;
/// Copy with method for immutability
QuoteItem copyWith({
String? quoteItemId,
String? quoteId,
String? productId,
double? quantity,
double? originalPrice,
double? negotiatedPrice,
double? discountPercent,
double? subtotal,
String? notes,
}) {
return QuoteItem(
quoteItemId: quoteItemId ?? this.quoteItemId,
quoteId: quoteId ?? this.quoteId,
productId: productId ?? this.productId,
quantity: quantity ?? this.quantity,
originalPrice: originalPrice ?? this.originalPrice,
negotiatedPrice: negotiatedPrice ?? this.negotiatedPrice,
discountPercent: discountPercent ?? this.discountPercent,
subtotal: subtotal ?? this.subtotal,
notes: notes ?? this.notes,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is QuoteItem &&
other.quoteItemId == quoteItemId &&
other.quoteId == quoteId &&
other.productId == productId &&
other.quantity == quantity &&
other.originalPrice == originalPrice &&
other.negotiatedPrice == negotiatedPrice &&
other.subtotal == subtotal;
}
@override
int get hashCode {
return Object.hash(
quoteItemId,
quoteId,
productId,
quantity,
originalPrice,
negotiatedPrice,
subtotal,
);
}
@override
String toString() {
return 'QuoteItem(quoteItemId: $quoteItemId, productId: $productId, '
'quantity: $quantity, originalPrice: $originalPrice, '
'negotiatedPrice: $negotiatedPrice, subtotal: $subtotal)';
}
}

View File

@@ -0,0 +1,69 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
part 'showroom_model.g.dart';
@HiveType(typeId: HiveTypeIds.showroomModel)
class ShowroomModel extends HiveObject {
ShowroomModel({required this.showroomId, required this.title, required this.description, this.coverImage, this.link360, required this.area, required this.style, required this.location, this.galleryImages, required this.viewCount, required this.isFeatured, required this.isActive, this.publishedAt, this.createdBy});
@HiveField(0) final String showroomId;
@HiveField(1) final String title;
@HiveField(2) final String description;
@HiveField(3) final String? coverImage;
@HiveField(4) final String? link360;
@HiveField(5) final double area;
@HiveField(6) final String style;
@HiveField(7) final String location;
@HiveField(8) final String? galleryImages;
@HiveField(9) final int viewCount;
@HiveField(10) final bool isFeatured;
@HiveField(11) final bool isActive;
@HiveField(12) final DateTime? publishedAt;
@HiveField(13) final String? createdBy;
factory ShowroomModel.fromJson(Map<String, dynamic> json) => ShowroomModel(
showroomId: json['showroom_id'] as String,
title: json['title'] as String,
description: json['description'] as String,
coverImage: json['cover_image'] as String?,
link360: json['link_360'] as String?,
area: (json['area'] as num).toDouble(),
style: json['style'] as String,
location: json['location'] as String,
galleryImages: json['gallery_images'] != null ? jsonEncode(json['gallery_images']) : null,
viewCount: json['view_count'] as int? ?? 0,
isFeatured: json['is_featured'] as bool? ?? false,
isActive: json['is_active'] as bool? ?? true,
publishedAt: json['published_at'] != null ? DateTime.parse(json['published_at']?.toString() ?? '') : null,
createdBy: json['created_by'] as String?,
);
Map<String, dynamic> toJson() => {
'showroom_id': showroomId,
'title': title,
'description': description,
'cover_image': coverImage,
'link_360': link360,
'area': area,
'style': style,
'location': location,
'gallery_images': galleryImages != null ? jsonDecode(galleryImages!) : null,
'view_count': viewCount,
'is_featured': isFeatured,
'is_active': isActive,
'published_at': publishedAt?.toIso8601String(),
'created_by': createdBy,
};
List<String>? get galleryImagesList {
if (galleryImages == null) return null;
try {
final decoded = jsonDecode(galleryImages!) as List;
return decoded.map((e) => e.toString()).toList();
} catch (e) {
return null;
}
}
}

View File

@@ -0,0 +1,80 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'showroom_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ShowroomModelAdapter extends TypeAdapter<ShowroomModel> {
@override
final typeId = 21;
@override
ShowroomModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ShowroomModel(
showroomId: fields[0] as String,
title: fields[1] as String,
description: fields[2] as String,
coverImage: fields[3] as String?,
link360: fields[4] as String?,
area: (fields[5] as num).toDouble(),
style: fields[6] as String,
location: fields[7] as String,
galleryImages: fields[8] as String?,
viewCount: (fields[9] as num).toInt(),
isFeatured: fields[10] as bool,
isActive: fields[11] as bool,
publishedAt: fields[12] as DateTime?,
createdBy: fields[13] as String?,
);
}
@override
void write(BinaryWriter writer, ShowroomModel obj) {
writer
..writeByte(14)
..writeByte(0)
..write(obj.showroomId)
..writeByte(1)
..write(obj.title)
..writeByte(2)
..write(obj.description)
..writeByte(3)
..write(obj.coverImage)
..writeByte(4)
..write(obj.link360)
..writeByte(5)
..write(obj.area)
..writeByte(6)
..write(obj.style)
..writeByte(7)
..write(obj.location)
..writeByte(8)
..write(obj.galleryImages)
..writeByte(9)
..write(obj.viewCount)
..writeByte(10)
..write(obj.isFeatured)
..writeByte(11)
..write(obj.isActive)
..writeByte(12)
..write(obj.publishedAt)
..writeByte(13)
..write(obj.createdBy);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ShowroomModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,25 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
part 'showroom_product_model.g.dart';
@HiveType(typeId: HiveTypeIds.showroomProductModel)
class ShowroomProductModel extends HiveObject {
ShowroomProductModel({required this.showroomId, required this.productId, required this.quantityUsed});
@HiveField(0) final String showroomId;
@HiveField(1) final String productId;
@HiveField(2) final double quantityUsed;
factory ShowroomProductModel.fromJson(Map<String, dynamic> json) => ShowroomProductModel(
showroomId: json['showroom_id'] as String,
productId: json['product_id'] as String,
quantityUsed: (json['quantity_used'] as num).toDouble(),
);
Map<String, dynamic> toJson() => {
'showroom_id': showroomId,
'product_id': productId,
'quantity_used': quantityUsed,
};
}

View File

@@ -0,0 +1,47 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'showroom_product_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ShowroomProductModelAdapter extends TypeAdapter<ShowroomProductModel> {
@override
final typeId = 22;
@override
ShowroomProductModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ShowroomProductModel(
showroomId: fields[0] as String,
productId: fields[1] as String,
quantityUsed: (fields[2] as num).toDouble(),
);
}
@override
void write(BinaryWriter writer, ShowroomProductModel obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.showroomId)
..writeByte(1)
..write(obj.productId)
..writeByte(2)
..write(obj.quantityUsed);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ShowroomProductModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,163 @@
/// Domain Entity: Showroom
///
/// Represents a virtual showroom display.
library;
/// Showroom Entity
///
/// Contains information about a showroom:
/// - Showroom details and description
/// - Media (images, 360 view)
/// - Metadata (style, location, area)
/// - Visibility settings
class Showroom {
/// Unique showroom identifier
final String showroomId;
/// Showroom title
final String title;
/// Showroom description
final String? description;
/// Cover image URL
final String? coverImage;
/// 360-degree view link
final String? link360;
/// Showroom area (square meters)
final double? area;
/// Design style
final String? style;
/// Location/address
final String? location;
/// Gallery image URLs
final List<String> galleryImages;
/// View count
final int viewCount;
/// Showroom is featured
final bool isFeatured;
/// Showroom is active and visible
final bool isActive;
/// Publication timestamp
final DateTime? publishedAt;
/// User ID who created the showroom
final String? createdBy;
const Showroom({
required this.showroomId,
required this.title,
this.description,
this.coverImage,
this.link360,
this.area,
this.style,
this.location,
required this.galleryImages,
required this.viewCount,
required this.isFeatured,
required this.isActive,
this.publishedAt,
this.createdBy,
});
/// Check if showroom is published
bool get isPublished =>
publishedAt != null && DateTime.now().isAfter(publishedAt!);
/// Check if showroom has 360 view
bool get has360View => link360 != null && link360!.isNotEmpty;
/// Check if showroom has gallery
bool get hasGallery => galleryImages.isNotEmpty;
/// Get gallery size
int get gallerySize => galleryImages.length;
/// Check if showroom has cover image
bool get hasCoverImage => coverImage != null && coverImage!.isNotEmpty;
/// Get display image (cover or first gallery image)
String? get displayImage {
if (hasCoverImage) return coverImage;
if (hasGallery) return galleryImages.first;
return null;
}
/// Check if showroom can be viewed
bool get canBeViewed => isActive && isPublished;
/// Copy with method for immutability
Showroom copyWith({
String? showroomId,
String? title,
String? description,
String? coverImage,
String? link360,
double? area,
String? style,
String? location,
List<String>? galleryImages,
int? viewCount,
bool? isFeatured,
bool? isActive,
DateTime? publishedAt,
String? createdBy,
}) {
return Showroom(
showroomId: showroomId ?? this.showroomId,
title: title ?? this.title,
description: description ?? this.description,
coverImage: coverImage ?? this.coverImage,
link360: link360 ?? this.link360,
area: area ?? this.area,
style: style ?? this.style,
location: location ?? this.location,
galleryImages: galleryImages ?? this.galleryImages,
viewCount: viewCount ?? this.viewCount,
isFeatured: isFeatured ?? this.isFeatured,
isActive: isActive ?? this.isActive,
publishedAt: publishedAt ?? this.publishedAt,
createdBy: createdBy ?? this.createdBy,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Showroom &&
other.showroomId == showroomId &&
other.title == title &&
other.style == style &&
other.isFeatured == isFeatured &&
other.isActive == isActive;
}
@override
int get hashCode {
return Object.hash(
showroomId,
title,
style,
isFeatured,
isActive,
);
}
@override
String toString() {
return 'Showroom(showroomId: $showroomId, title: $title, style: $style, '
'area: $area, viewCount: $viewCount, isFeatured: $isFeatured, '
'isActive: $isActive)';
}
}

View File

@@ -0,0 +1,65 @@
/// Domain Entity: Showroom Product
///
/// Represents a product used in a showroom display.
library;
/// Showroom Product Entity
///
/// Contains information about a product featured in a showroom:
/// - Product reference
/// - Quantity used
/// - Showroom context
class ShowroomProduct {
/// Showroom ID
final String showroomId;
/// Product ID
final String productId;
/// Quantity of product used in the showroom
final double quantityUsed;
const ShowroomProduct({
required this.showroomId,
required this.productId,
required this.quantityUsed,
});
/// Copy with method for immutability
ShowroomProduct copyWith({
String? showroomId,
String? productId,
double? quantityUsed,
}) {
return ShowroomProduct(
showroomId: showroomId ?? this.showroomId,
productId: productId ?? this.productId,
quantityUsed: quantityUsed ?? this.quantityUsed,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ShowroomProduct &&
other.showroomId == showroomId &&
other.productId == productId &&
other.quantityUsed == quantityUsed;
}
@override
int get hashCode {
return Object.hash(
showroomId,
productId,
quantityUsed,
);
}
@override
String toString() {
return 'ShowroomProduct(showroomId: $showroomId, productId: $productId, '
'quantityUsed: $quantityUsed)';
}
}