update database
This commit is contained in:
47
lib/features/account/data/models/audit_log_model.dart
Normal file
47
lib/features/account/data/models/audit_log_model.dart
Normal 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(),
|
||||
};
|
||||
}
|
||||
68
lib/features/account/data/models/audit_log_model.g.dart
Normal file
68
lib/features/account/data/models/audit_log_model.g.dart
Normal 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;
|
||||
}
|
||||
50
lib/features/account/data/models/payment_reminder_model.dart
Normal file
50
lib/features/account/data/models/payment_reminder_model.dart
Normal 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(),
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
151
lib/features/account/domain/entities/audit_log.dart
Normal file
151
lib/features/account/domain/entities/audit_log.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
179
lib/features/account/domain/entities/payment_reminder.dart
Normal file
179
lib/features/account/domain/entities/payment_reminder.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
300
lib/features/auth/data/models/user_model.dart
Normal file
300
lib/features/auth/data/models/user_model.dart
Normal 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;
|
||||
}
|
||||
98
lib/features/auth/data/models/user_model.g.dart
Normal file
98
lib/features/auth/data/models/user_model.g.dart
Normal 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;
|
||||
}
|
||||
185
lib/features/auth/data/models/user_session_model.dart
Normal file
185
lib/features/auth/data/models/user_session_model.dart
Normal 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;
|
||||
}
|
||||
71
lib/features/auth/data/models/user_session_model.g.dart
Normal file
71
lib/features/auth/data/models/user_session_model.g.dart
Normal 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;
|
||||
}
|
||||
314
lib/features/auth/domain/entities/user.dart
Normal file
314
lib/features/auth/domain/entities/user.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
142
lib/features/auth/domain/entities/user_session.dart
Normal file
142
lib/features/auth/domain/entities/user_session.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
79
lib/features/cart/data/models/cart_item_model.dart
Normal file
79
lib/features/cart/data/models/cart_item_model.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
59
lib/features/cart/data/models/cart_item_model.g.dart
Normal file
59
lib/features/cart/data/models/cart_item_model.g.dart
Normal 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;
|
||||
}
|
||||
71
lib/features/cart/data/models/cart_model.dart
Normal file
71
lib/features/cart/data/models/cart_model.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
56
lib/features/cart/data/models/cart_model.g.dart
Normal file
56
lib/features/cart/data/models/cart_model.g.dart
Normal 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;
|
||||
}
|
||||
92
lib/features/cart/domain/entities/cart.dart
Normal file
92
lib/features/cart/domain/entities/cart.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
98
lib/features/cart/domain/entities/cart_item.dart
Normal file
98
lib/features/cart/domain/entities/cart_item.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
57
lib/features/chat/data/models/chat_room_model.dart
Normal file
57
lib/features/chat/data/models/chat_room_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
lib/features/chat/data/models/chat_room_model.g.dart
Normal file
68
lib/features/chat/data/models/chat_room_model.g.dart
Normal 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;
|
||||
}
|
||||
67
lib/features/chat/data/models/message_model.dart
Normal file
67
lib/features/chat/data/models/message_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
77
lib/features/chat/data/models/message_model.g.dart
Normal file
77
lib/features/chat/data/models/message_model.g.dart
Normal 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;
|
||||
}
|
||||
186
lib/features/chat/domain/entities/chat_room.dart
Normal file
186
lib/features/chat/domain/entities/chat_room.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
212
lib/features/chat/domain/entities/message.dart
Normal file
212
lib/features/chat/domain/entities/message.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
70
lib/features/loyalty/data/models/gift_catalog_model.dart
Normal file
70
lib/features/loyalty/data/models/gift_catalog_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
83
lib/features/loyalty/data/models/gift_catalog_model.g.dart
Normal file
83
lib/features/loyalty/data/models/gift_catalog_model.g.dart
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
70
lib/features/loyalty/data/models/points_record_model.dart
Normal file
70
lib/features/loyalty/data/models/points_record_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
80
lib/features/loyalty/data/models/points_record_model.g.dart
Normal file
80
lib/features/loyalty/data/models/points_record_model.g.dart
Normal 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;
|
||||
}
|
||||
69
lib/features/loyalty/data/models/redeemed_gift_model.dart
Normal file
69
lib/features/loyalty/data/models/redeemed_gift_model.dart
Normal 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;
|
||||
}
|
||||
86
lib/features/loyalty/data/models/redeemed_gift_model.g.dart
Normal file
86
lib/features/loyalty/data/models/redeemed_gift_model.g.dart
Normal 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;
|
||||
}
|
||||
212
lib/features/loyalty/domain/entities/gift_catalog.dart
Normal file
212
lib/features/loyalty/domain/entities/gift_catalog.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
232
lib/features/loyalty/domain/entities/loyalty_point_entry.dart
Normal file
232
lib/features/loyalty/domain/entities/loyalty_point_entry.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
182
lib/features/loyalty/domain/entities/points_record.dart
Normal file
182
lib/features/loyalty/domain/entities/points_record.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
207
lib/features/loyalty/domain/entities/redeemed_gift.dart
Normal file
207
lib/features/loyalty/domain/entities/redeemed_gift.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
162
lib/features/notifications/domain/entities/notification.dart
Normal file
162
lib/features/notifications/domain/entities/notification.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
86
lib/features/orders/data/models/invoice_model.dart
Normal file
86
lib/features/orders/data/models/invoice_model.dart
Normal 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;
|
||||
}
|
||||
104
lib/features/orders/data/models/invoice_model.g.dart
Normal file
104
lib/features/orders/data/models/invoice_model.g.dart
Normal 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;
|
||||
}
|
||||
40
lib/features/orders/data/models/order_item_model.dart
Normal file
40
lib/features/orders/data/models/order_item_model.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
62
lib/features/orders/data/models/order_item_model.g.dart
Normal file
62
lib/features/orders/data/models/order_item_model.g.dart
Normal 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;
|
||||
}
|
||||
147
lib/features/orders/data/models/order_model.dart
Normal file
147
lib/features/orders/data/models/order_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
92
lib/features/orders/data/models/order_model.g.dart
Normal file
92
lib/features/orders/data/models/order_model.g.dart
Normal 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;
|
||||
}
|
||||
62
lib/features/orders/data/models/payment_line_model.dart
Normal file
62
lib/features/orders/data/models/payment_line_model.dart
Normal 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(),
|
||||
};
|
||||
}
|
||||
83
lib/features/orders/data/models/payment_line_model.g.dart
Normal file
83
lib/features/orders/data/models/payment_line_model.g.dart
Normal 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;
|
||||
}
|
||||
273
lib/features/orders/domain/entities/invoice.dart
Normal file
273
lib/features/orders/domain/entities/invoice.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
321
lib/features/orders/domain/entities/order.dart
Normal file
321
lib/features/orders/domain/entities/order.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
117
lib/features/orders/domain/entities/order_item.dart
Normal file
117
lib/features/orders/domain/entities/order_item.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
237
lib/features/orders/domain/entities/payment_line.dart
Normal file
237
lib/features/orders/domain/entities/payment_line.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
84
lib/features/products/data/models/stock_level_model.dart
Normal file
84
lib/features/products/data/models/stock_level_model.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
56
lib/features/products/data/models/stock_level_model.g.dart
Normal file
56
lib/features/products/data/models/stock_level_model.g.dart
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
106
lib/features/products/domain/entities/stock_level.dart
Normal file
106
lib/features/products/domain/entities/stock_level.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
78
lib/features/projects/data/models/design_request_model.dart
Normal file
78
lib/features/projects/data/models/design_request_model.dart
Normal 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(),
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
249
lib/features/projects/domain/entities/design_request.dart
Normal file
249
lib/features/projects/domain/entities/design_request.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
247
lib/features/projects/domain/entities/project_submission.dart
Normal file
247
lib/features/projects/domain/entities/project_submission.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
45
lib/features/quotes/data/models/quote_item_model.dart
Normal file
45
lib/features/quotes/data/models/quote_item_model.dart
Normal 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;
|
||||
}
|
||||
65
lib/features/quotes/data/models/quote_item_model.g.dart
Normal file
65
lib/features/quotes/data/models/quote_item_model.g.dart
Normal 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;
|
||||
}
|
||||
69
lib/features/quotes/data/models/quote_model.dart
Normal file
69
lib/features/quotes/data/models/quote_model.dart
Normal 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;
|
||||
}
|
||||
86
lib/features/quotes/data/models/quote_model.g.dart
Normal file
86
lib/features/quotes/data/models/quote_model.g.dart
Normal 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;
|
||||
}
|
||||
313
lib/features/quotes/domain/entities/quote.dart
Normal file
313
lib/features/quotes/domain/entities/quote.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
133
lib/features/quotes/domain/entities/quote_item.dart
Normal file
133
lib/features/quotes/domain/entities/quote_item.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
69
lib/features/showrooms/data/models/showroom_model.dart
Normal file
69
lib/features/showrooms/data/models/showroom_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
80
lib/features/showrooms/data/models/showroom_model.g.dart
Normal file
80
lib/features/showrooms/data/models/showroom_model.g.dart
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
163
lib/features/showrooms/domain/entities/showroom.dart
Normal file
163
lib/features/showrooms/domain/entities/showroom.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
65
lib/features/showrooms/domain/entities/showroom_product.dart
Normal file
65
lib/features/showrooms/domain/entities/showroom_product.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user