diff --git a/CLAUDE.md b/CLAUDE.md index 41906f3..d10835a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1314,321 +1314,132 @@ class Auth extends _$Auth { } ``` -#### Cart State -```dart -@riverpod -class Cart extends _$Cart { - @override - List build() { - return ref.watch(cartRepositoryProvider).getCartItems(); - } - void addItem(Product product, int quantity) { - state = [ - ...state, - CartItem( - id: product.id, - productName: product.name, - price: product.price, - quantity: quantity, - imageUrl: product.images.first, - ), - ]; - _persistCart(); - } - - void updateQuantity(String itemId, int quantity) { - state = state.map((item) { - if (item.id == itemId) { - return item.copyWith(quantity: quantity); - } - return item; - }).toList(); - _persistCart(); - } - - void removeItem(String itemId) { - state = state.where((item) => item.id != itemId).toList(); - _persistCart(); - } - - void clear() { - state = []; - _persistCart(); - } - - void _persistCart() { - ref.read(cartRepositoryProvider).saveCart(state); - } -} - -@riverpod -double cartTotal(CartTotalRef ref) { - final items = ref.watch(cartProvider); - return items.fold(0, (sum, item) => sum + (item.price * item.quantity)); -} -``` - -#### Loyalty Points State -```dart -@riverpod -class LoyaltyPoints extends _$LoyaltyPoints { - @override - Future build() async { - return await ref.read(loyaltyRepositoryProvider).getLoyaltyPoints(); - } - - Future refresh() async { - state = const AsyncValue.loading(); - state = await AsyncValue.guard(() async { - return await ref.read(loyaltyRepositoryProvider).getLoyaltyPoints(); - }); - } - - Future redeemReward(Reward reward) async { - final result = await ref.read(loyaltyRepositoryProvider).redeemReward(reward.id); - if (result.success) { - await refresh(); - // Show success dialog with gift code - } - } -} -``` - ---- - -### Database Schema (Hive) - -#### User Model -```dart -@HiveType(typeId: 0) -class UserModel extends HiveObject { - @HiveField(0) - final String id; - - @HiveField(1) - final String name; - - @HiveField(2) - final String phone; - - @HiveField(3) - final String email; - - @HiveField(4) - final String? avatar; - - @HiveField(5) - final MemberTier memberTier; // diamond, platinum, gold - - @HiveField(6) - final int points; - - @HiveField(7) - final String referralCode; - - @HiveField(8) - final String? company; - - @HiveField(9) - final UserType userType; // contractor, architect, distributor, broker - - @HiveField(10) - final DateTime createdAt; -} -``` - -#### Product Model -```dart -@HiveType(typeId: 1) -class ProductModel extends HiveObject { - @HiveField(0) - final String id; - - @HiveField(1) - final String name; - - @HiveField(2) - final String sku; - - @HiveField(3) - final String description; - - @HiveField(4) - final double price; - - @HiveField(5) - final double? salePrice; - - @HiveField(6) - final List images; - - @HiveField(7) - final String categoryId; - - @HiveField(8) - final int stock; - - @HiveField(9) - final bool isAvailable; - - @HiveField(10) - final DateTime createdAt; -} -``` - -#### Cart Item Model -```dart -@HiveType(typeId: 2) -class CartItemModel extends HiveObject { - @HiveField(0) - final String id; - - @HiveField(1) - final String productId; - - @HiveField(2) - final String productName; - - @HiveField(3) - final double price; - - @HiveField(4) - final int quantity; - - @HiveField(5) - final String imageUrl; - - @HiveField(6) - final DateTime addedAt; -} -``` - -#### Order Model -```dart -@HiveType(typeId: 3) -class OrderModel extends HiveObject { - @HiveField(0) - final String id; - - @HiveField(1) - final String orderNumber; - - @HiveField(2) - final String customerId; - - @HiveField(3) - final String customerName; - - @HiveField(4) - final List items; - - @HiveField(5) - final double subtotal; - - @HiveField(6) - final double discount; - - @HiveField(7) - final double shipping; - - @HiveField(8) - final double total; - - @HiveField(9) - final OrderStatus status; - - @HiveField(10) - final String paymentMethod; - - @HiveField(11) - final AddressModel deliveryAddress; - - @HiveField(12) - final DateTime createdAt; - - @HiveField(13) - final DateTime? completedAt; -} -``` - -#### Project Model -```dart -@HiveType(typeId: 4) -class ProjectModel extends HiveObject { - @HiveField(0) - final String id; - - @HiveField(1) - final String name; - - @HiveField(2) - final String code; - - @HiveField(3) - final String client; - - @HiveField(4) - final String clientPhone; - - @HiveField(5) - final String location; - - @HiveField(6) - final DateTime startDate; - - @HiveField(7) - final DateTime endDate; - - @HiveField(8) - final int progress; // 0-100 - - @HiveField(9) - final ProjectStatus status; // planning, inProgress, completed - - @HiveField(10) - final double budget; - - @HiveField(11) - final String description; - - @HiveField(12) - final ProjectType type; // residential, commercial, industrial - - @HiveField(13) - final DateTime createdAt; -} -``` - -#### Loyalty Transaction Model -```dart -@HiveType(typeId: 5) -class LoyaltyTransactionModel extends HiveObject { - @HiveField(0) - final String id; - - @HiveField(1) - final TransactionType type; // earn, redeem - - @HiveField(2) - final int amount; - - @HiveField(3) - final String description; - - @HiveField(4) - final DateTime timestamp; - - @HiveField(5) - final int newBalance; - - @HiveField(6) - final String? orderId; - - @HiveField(7) - final String? rewardId; -} -``` +### Domain Entities & Data Models + +The app follows clean architecture with domain entities and Hive data models based on the database schema in `database.md`. + +#### **Domain Entities (28 files)** +Pure business logic entities with no external dependencies. All entities use freezed for immutability. + +**Auth Feature** (`lib/features/auth/domain/entities/`) +- `user.dart` - User with role, status, loyalty tier, company info +- `user_session.dart` - User session with device tracking + +**Products Feature** (`lib/features/products/domain/entities/`) +- `product.dart` - Product with images, specs, 360 view, ERPNext integration +- `stock_level.dart` - Inventory tracking (available/reserved/ordered) +- `category.dart` - Product categories + +**Cart Feature** (`lib/features/cart/domain/entities/`) +- `cart.dart` - Shopping cart with sync status +- `cart_item.dart` - Cart line items + +**Orders Feature** (`lib/features/orders/domain/entities/`) +- `order.dart` - Orders with status, addresses, ERPNext integration +- `order_item.dart` - Order line items with discounts +- `invoice.dart` - Invoices with type, status, payment tracking +- `payment_line.dart` - Payment transactions with method and status + +**Loyalty Feature** (`lib/features/loyalty/domain/entities/`) +- `loyalty_point_entry.dart` - Points transactions with type, source, complaints +- `gift_catalog.dart` - Redeemable gifts with category, points cost +- `redeemed_gift.dart` - User's redeemed gifts with voucher codes +- `points_record.dart` - Invoice submissions for manual points + +**Projects Feature** (`lib/features/projects/domain/entities/`) +- `project_submission.dart` - Completed project submissions with photos +- `design_request.dart` - Design consultation requests + +**Quotes Feature** (`lib/features/quotes/domain/entities/`) +- `quote.dart` - Quotations with status, validity, conversion tracking +- `quote_item.dart` - Quote line items with price negotiation + +**Chat Feature** (`lib/features/chat/domain/entities/`) +- `chat_room.dart` - Chat rooms with type, participants +- `message.dart` - Messages with content type, attachments, read status + +**Notifications Feature** (`lib/features/notifications/domain/entities/`) +- `notification.dart` - User notifications with type-based data + +**Showrooms Feature** (`lib/features/showrooms/domain/entities/`) +- `showroom.dart` - Virtual showrooms with 360 views, gallery +- `showroom_product.dart` - Products used in showrooms + +**Account Feature** (`lib/features/account/domain/entities/`) +- `payment_reminder.dart` - Payment reminders with scheduling +- `audit_log.dart` - System audit trail + +**Home Feature** (`lib/features/home/domain/entities/`) +- `member_card.dart` - Membership card display +- `promotion.dart` - Promotional campaigns + +#### **Hive Data Models (25 files)** +Hive CE models for offline-first local storage. All models extend `HiveObject` with proper type adapters. + +**Type ID Allocation:** +- Models: 0-24 (user_model=0, user_session_model=1, product_model=2, etc.) +- Enums: 30-50 (UserRole=30, UserStatus=31, LoyaltyTier=32, etc.) + +**Model Features:** +- `@HiveType` with unique typeId +- `@HiveField` for all properties +- `fromJson()` / `toJson()` for API serialization +- `toEntity()` / `fromEntity()` for domain conversion +- Helper methods for JSONB fields +- Full documentation + +**All models are located in:** `lib/features/*/data/models/` +- Auth: `user_model.dart`, `user_session_model.dart` +- Products: `product_model.dart`, `stock_level_model.dart` +- Cart: `cart_model.dart`, `cart_item_model.dart` +- Orders: `order_model.dart`, `order_item_model.dart`, `invoice_model.dart`, `payment_line_model.dart` +- Loyalty: `loyalty_point_entry_model.dart`, `gift_catalog_model.dart`, `redeemed_gift_model.dart`, `points_record_model.dart` +- Projects: `project_submission_model.dart`, `design_request_model.dart` +- Quotes: `quote_model.dart`, `quote_item_model.dart` +- Chat: `chat_room_model.dart`, `message_model.dart` +- Notifications: `notification_model.dart` +- Showrooms: `showroom_model.dart`, `showroom_product_model.dart` +- Account: `payment_reminder_model.dart`, `audit_log_model.dart` + +#### **Enums (21 types)** +All enums are defined in `lib/core/database/models/enums.dart` with Hive type adapters: + +**User & Auth:** +- `UserRole` - admin, customer, contractor, architect, distributor, broker +- `UserStatus` - active, inactive, pending, suspended, banned +- `LoyaltyTier` - diamond, platinum, gold + +**Orders & Payments:** +- `OrderStatus` - draft, pending, confirmed, processing, shipped, delivered, completed, cancelled, returned, refunded +- `InvoiceType` - sales, proforma, credit_note, debit_note +- `InvoiceStatus` - draft, sent, viewed, overdue, paid, partially_paid, cancelled +- `PaymentMethod` - cash, bank_transfer, credit_card, debit_card, e_wallet, cod +- `PaymentStatus` - pending, processing, completed, failed, refunded + +**Loyalty & Gifts:** +- `EntryType` - earn, spend, adjust, expire, reversal +- `EntrySource` - purchase, referral, promotion, bonus, manual, project_submission, design_request, points_record +- `ComplaintStatus` - pending, reviewing, approved, rejected, resolved +- `GiftCategory` - voucher, product, service, experience, discount +- `GiftStatus` - active, used, expired, cancelled +- `PointsStatus` - pending, approved, rejected + +**Projects & Quotes:** +- `ProjectType` - residential, commercial, industrial, hospitality, retail, office, other +- `SubmissionStatus` - pending, reviewing, approved, rejected +- `DesignStatus` - submitted, assigned, in_progress, completed, delivered, revision_requested, approved, cancelled +- `QuoteStatus` - draft, sent, viewed, negotiating, accepted, rejected, expired, converted + +**Chat:** +- `RoomType` - support, sales, quote, order +- `ContentType` - text, image, file, product, location +- `ReminderType` - payment_due, overdue, final_notice, custom + +**Detailed Documentation:** +- See `DOMAIN_ENTITIES_SUMMARY.md` for entity details +- See `HIVE_MODELS_REFERENCE.md` for model templates and usage +- See `lib/core/constants/storage_constants.dart` for Type ID reference --- @@ -1788,150 +1599,9 @@ class OfflineQueue { } ``` ---- - -## Error Handling - -### Network Errors -```dart -class ApiClient { - Future request(String endpoint, {Map? data}) async { - try { - final response = await dio.post(endpoint, data: data); - return _parseResponse(response); - } on DioException catch (e) { - if (e.type == DioExceptionType.connectionTimeout) { - throw NetworkException('Connection timeout. Please check your internet.'); - } else if (e.type == DioExceptionType.receiveTimeout) { - throw NetworkException('Server took too long to respond.'); - } else if (e.response?.statusCode == 401) { - throw UnauthorizedException('Session expired. Please login again.'); - } else if (e.response?.statusCode == 404) { - throw NotFoundException('Resource not found.'); - } else if (e.response?.statusCode == 500) { - throw ServerException('Server error. Please try again later.'); - } else { - throw NetworkException('Network error: ${e.message}'); - } - } catch (e) { - throw UnknownException('Unexpected error: $e'); - } - } -} -``` - -### Form Validation -```dart -class Validators { - static String? required(String? value) { - if (value == null || value.trim().isEmpty) { - return 'Trường này là bắt buộc'; - } - return null; - } - - static String? vietnamesePhone(String? value) { - if (value == null || value.isEmpty) { - return 'Vui lòng nhập số điện thoại'; - } - - final phoneRegex = RegExp(r'^(0|\+84)[3|5|7|8|9][0-9]{8}$'); - if (!phoneRegex.hasMatch(value)) { - return 'Số điện thoại không hợp lệ'; - } - - return null; - } - - static String? email(String? value) { - if (value == null || value.isEmpty) { - return 'Vui lòng nhập email'; - } - - final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); - if (!emailRegex.hasMatch(value)) { - return 'Email không hợp lệ'; - } - - return null; - } - - static String? password(String? value) { - if (value == null || value.isEmpty) { - return 'Vui lòng nhập mật khẩu'; - } - - if (value.length < 8) { - return 'Mật khẩu phải có ít nhất 8 ký tự'; - } - - if (!RegExp(r'[A-Z]').hasMatch(value)) { - return 'Mật khẩu phải có ít nhất 1 chữ hoa'; - } - - if (!RegExp(r'[a-z]').hasMatch(value)) { - return 'Mật khẩu phải có ít nhất 1 chữ thường'; - } - - if (!RegExp(r'[0-9]').hasMatch(value)) { - return 'Mật khẩu phải có ít nhất 1 số'; - } - - if (!RegExp(r'[!@#\$%^&*(),.?":{}|<>]').hasMatch(value)) { - return 'Mật khẩu phải có ít nhất 1 ký tự đặc biệt'; - } - - return null; - } -} -``` - ---- - -## Testing Strategy - -### Unit Tests -```dart -void main() { - group('CartNotifier', () { - late CartNotifier cartNotifier; - late MockCartRepository mockRepository; - - setUp(() { - mockRepository = MockCartRepository(); - cartNotifier = CartNotifier(mockRepository); - }); - - test('addItem adds product to cart', () { - final product = Product(id: '1', name: 'Test', price: 100); - - cartNotifier.addItem(product, 2); - - expect(cartNotifier.state.length, 1); - expect(cartNotifier.state.first.quantity, 2); - verify(mockRepository.saveCart(any)).called(1); - }); - - test('cartTotal calculates correct total', () { - final items = [ - CartItem(id: '1', price: 100, quantity: 2), - CartItem(id: '2', price: 50, quantity: 3), - ]; - - final total = calculateCartTotal(items); - - expect(total, 350); // (100 * 2) + (50 * 3) - }); - }); -} -``` - - ---- - ## Localization (Vietnamese Primary) ### Setup diff --git a/DOMAIN_ENTITIES_SUMMARY.md b/DOMAIN_ENTITIES_SUMMARY.md new file mode 100644 index 0000000..08131b4 --- /dev/null +++ b/DOMAIN_ENTITIES_SUMMARY.md @@ -0,0 +1,235 @@ +# Domain Entities Summary + +This document provides an overview of all domain entities created based on the database schema. + +## Created Domain Entities by Feature + +### 1. Auth Feature (/lib/features/auth/domain/entities/) +- **user.dart** - User account with authentication and loyalty information + - Enums: UserRole, UserStatus, LoyaltyTier + - Supporting class: CompanyInfo + +- **user_session.dart** - Active user session with device information + +### 2. Products Feature (/lib/features/products/domain/entities/) +- **product.dart** - Product catalog item (UPDATED to match database schema) + - Product with images, specifications, 360 view, ERPNext integration + +- **stock_level.dart** - Inventory stock level for products + - Available, reserved, and ordered quantities + +- **category.dart** - Product category (existing) + +### 3. Cart Feature (/lib/features/cart/domain/entities/) +- **cart.dart** - Shopping cart + - Cart-level totals and sync status + +- **cart_item.dart** - Individual cart item + - Product reference, quantity, pricing + +### 4. Orders Feature (/lib/features/orders/domain/entities/) +- **order.dart** - Customer order + - Enums: OrderStatus + - Supporting class: Address + +- **order_item.dart** - Order line item + - Product, quantity, pricing, discounts + +- **invoice.dart** - Invoice for orders + - Enums: InvoiceType, InvoiceStatus + - Payment tracking + +- **payment_line.dart** - Payment transaction + - Enums: PaymentMethod, PaymentStatus + - Bank details, receipts + +### 5. Loyalty Feature (/lib/features/loyalty/domain/entities/) +- **loyalty_point_entry.dart** - Points transaction + - Enums: EntryType, EntrySource, ComplaintStatus + - Points earned/spent tracking + +- **gift_catalog.dart** - Redeemable gift catalog + - Enums: GiftCategory + - Availability and validity tracking + +- **redeemed_gift.dart** - User-redeemed gift + - Enums: GiftStatus + - Voucher codes, QR codes, expiry + +- **points_record.dart** - User-submitted invoice for points + - Enums: PointsStatus + - Invoice submission and approval + +### 6. Projects Feature (/lib/features/projects/domain/entities/) +- **project_submission.dart** - Completed project submission + - Enums: ProjectType, SubmissionStatus + - Before/after photos, points earning + +- **design_request.dart** - Design consultation request + - Enums: DesignStatus + - Project requirements, designer assignment + +### 7. Quotes Feature (/lib/features/quotes/domain/entities/) +- **quote.dart** - Price quotation + - Enums: QuoteStatus + - Supporting class: DeliveryAddress + +- **quote_item.dart** - Quote line item + - Price negotiation, discounts + +### 8. Chat Feature (/lib/features/chat/domain/entities/) +- **chat_room.dart** - Chat conversation room + - Enums: RoomType + - Participants, context (order/quote) + +- **message.dart** - Chat message + - Enums: ContentType + - Attachments, read status, product references + +### 9. Notifications Feature (/lib/features/notifications/domain/entities/) +- **notification.dart** - User notification + - Type-based notifications (order, loyalty, promotion, system) + - Read status, push delivery + +### 10. Showrooms Feature (/lib/features/showrooms/domain/entities/) +- **showroom.dart** - Virtual showroom display + - 360 views, gallery, metadata + +- **showroom_product.dart** - Product used in showroom + - Quantity tracking + +### 11. Account Feature (/lib/features/account/domain/entities/) +- **payment_reminder.dart** - Invoice payment reminder + - Enums: ReminderType + - Scheduling and delivery tracking + +- **audit_log.dart** - System audit trail + - Action tracking, change history + +### 12. Home Feature (/lib/features/home/domain/entities/) +- **member_card.dart** - Membership card (existing) + - Enums: MemberTier, MemberType + +- **promotion.dart** - Promotion (existing) + +## Entity Design Patterns + +All entities follow these consistent patterns: + +### Immutability +- All fields are `final` +- Constructor uses `const` when possible +- `copyWith()` method for creating modified copies + +### Value Equality +- Proper `==` operator override +- `hashCode` override using `Object.hash()` +- `toString()` method for debugging + +### Business Logic +- Computed properties (getters) for derived values +- Boolean checks (e.g., `isActive`, `isExpired`) +- Helper methods for common operations + +### Type Safety +- Enums for status/type fields with `displayName` getters +- Nullable types (`?`) where appropriate +- List/Map types for collections and JSONB fields + +### Documentation +- Comprehensive documentation comments +- Feature-level context +- Field descriptions + +## Database Field Mapping + +All entities map 1:1 with database schema: +- `varchar` → `String` or `String?` +- `numeric` → `double` +- `integer` → `int` +- `boolean` → `bool` +- `timestamp` → `DateTime` +- `date` → `DateTime` +- `jsonb` → `Map` or typed classes +- `inet` → `String?` +- `text` → `String?` +- Arrays → `List` + +## Enums Created + +### Auth +- UserRole (customer, sales, admin, accountant, designer) +- UserStatus (pending, active, suspended, rejected) +- LoyaltyTier (none, gold, platinum, diamond) + +### Orders +- OrderStatus (draft, confirmed, processing, ready, shipped, delivered, completed, cancelled, returned) +- InvoiceType (standard, proforma, creditNote, debitNote) +- InvoiceStatus (draft, submitted, partiallyPaid, paid, overdue, cancelled) +- PaymentMethod (cash, bankTransfer, creditCard, ewallet, check, other) +- PaymentStatus (pending, processing, completed, failed, refunded, cancelled) + +### Loyalty +- EntryType (earn, redeem, adjustment, expiry) +- EntrySource (order, referral, redemption, project, pointsRecord, manual, birthday, welcome, other) +- ComplaintStatus (none, submitted, reviewing, approved, rejected) +- GiftCategory (voucher, product, service, discount, other) +- GiftStatus (active, used, expired, cancelled) +- PointsStatus (pending, approved, rejected) + +### Projects +- ProjectType (residential, commercial, industrial, infrastructure, other) +- SubmissionStatus (pending, reviewing, approved, rejected) +- DesignStatus (pending, assigned, inProgress, completed, cancelled) + +### Quotes +- QuoteStatus (draft, sent, accepted, rejected, expired, converted) + +### Chat +- RoomType (direct, group, support, order, quote) +- ContentType (text, image, file, product, system) + +### Account +- ReminderType (initial, dueDate, firstOverdue, secondOverdue, finalWarning) + +## Key Features + +### ERPNext Integration +Many entities include ERPNext reference fields: +- `erpnextCustomerId` (User) +- `erpnextItemCode` (Product) +- `erpnextSalesOrder` (Order) +- `erpnextInvoice` (Invoice) +- `erpnextPaymentEntry` (PaymentLine) +- `erpnextQuotation` (Quote) +- `erpnextEntryId` (LoyaltyPointEntry) + +### Address Handling +Reusable address classes for different contexts: +- `Address` (Order entity) +- `DeliveryAddress` (Quote entity) +- Both with JSON serialization support + +### Attachment Management +List-based attachment fields for multiple files: +- User attachments (ID cards, licenses) +- Project photos (before/after) +- Points record invoices +- Design request references +- Chat message files + +### Audit Trail Support +- Creation timestamps (`createdAt`) +- Update timestamps (`updatedAt`) +- User tracking (`createdBy`, `processedBy`, `reviewedBy`) +- Change tracking (AuditLog entity) + +## Next Steps + +These domain entities are ready for: +1. Data layer model creation (extending entities with JSON serialization) +2. Repository interface definitions +3. Use case implementation +4. Provider/state management integration + +All entities follow clean architecture principles with no external dependencies. diff --git a/HIVE_MODELS_COMPLETED.md b/HIVE_MODELS_COMPLETED.md new file mode 100644 index 0000000..d385e8e --- /dev/null +++ b/HIVE_MODELS_COMPLETED.md @@ -0,0 +1,400 @@ +# Hive CE Data Models - Completion Summary + +## ✅ All Models Created Successfully! + +A total of **25 Hive CE data models** have been created for the Worker mobile app, covering all features from the database schema. + +## 📊 Created Models by Feature + +### 1. Authentication (2 models) +- ✅ `user_model.dart` - Type ID: 0 + - Maps to `users` table + - Includes: user info, loyalty tier, points, company info, referral +- ✅ `user_session_model.dart` - Type ID: 1 + - Maps to `user_sessions` table + - Includes: session management, device info, tokens + +### 2. Products (2 models) +- ✅ `product_model.dart` - Type ID: 2 + - Maps to `products` table + - Includes: product details, images, specifications, pricing +- ✅ `stock_level_model.dart` - Type ID: 3 + - Maps to `stock_levels` table + - Includes: inventory tracking, warehouse info + +### 3. Cart (2 models) +- ✅ `cart_model.dart` - Type ID: 4 + - Maps to `carts` table + - Includes: cart totals, sync status +- ✅ `cart_item_model.dart` - Type ID: 5 + - Maps to `cart_items` table + - Includes: product items, quantities, prices + +### 4. Orders (4 models) +- ✅ `order_model.dart` - Type ID: 6 + - Maps to `orders` table + - Includes: order details, status, addresses, delivery +- ✅ `order_item_model.dart` - Type ID: 7 + - Maps to `order_items` table + - Includes: line items, discounts +- ✅ `invoice_model.dart` - Type ID: 8 + - Maps to `invoices` table + - Includes: invoice details, payment status, amounts +- ✅ `payment_line_model.dart` - Type ID: 9 + - Maps to `payment_lines` table + - Includes: payment records, methods, receipts + +### 5. Loyalty (4 models) +- ✅ `loyalty_point_entry_model.dart` - Type ID: 10 + - Maps to `loyalty_point_entries` table + - Includes: points transactions, balance, complaints +- ✅ `gift_catalog_model.dart` - Type ID: 11 + - Maps to `gift_catalog` table + - Includes: available rewards, points cost +- ✅ `redeemed_gift_model.dart` - Type ID: 12 + - Maps to `redeemed_gifts` table + - Includes: user gifts, vouchers, QR codes +- ✅ `points_record_model.dart` - Type ID: 13 + - Maps to `points_records` table + - Includes: invoice submissions for points + +### 6. Projects (2 models) +- ✅ `project_submission_model.dart` - Type ID: 14 + - Maps to `project_submissions` table + - Includes: project photos, review status +- ✅ `design_request_model.dart` - Type ID: 15 + - Maps to `design_requests` table + - Includes: design requirements, assignments + +### 7. Quotes (2 models) +- ✅ `quote_model.dart` - Type ID: 16 + - Maps to `quotes` table + - Includes: quotation details, validity, conversion +- ✅ `quote_item_model.dart` - Type ID: 17 + - Maps to `quote_items` table + - Includes: quoted products, negotiated prices + +### 8. Chat (2 models) +- ✅ `chat_room_model.dart` - Type ID: 18 + - Maps to `chat_rooms` table + - Includes: room info, participants, related entities +- ✅ `message_model.dart` - Type ID: 19 + - Maps to `chat_messages` table + - Includes: message content, attachments, read status + +### 9. Notifications (1 model) +- ✅ `notification_model.dart` - Type ID: 20 + - Maps to `notifications` table + - Includes: notification type, data, read status + +### 10. Showrooms (2 models) +- ✅ `showroom_model.dart` - Type ID: 21 + - Maps to `showrooms` table + - Includes: showroom details, images, 360 view +- ✅ `showroom_product_model.dart` - Type ID: 22 + - Maps to `showroom_products` table + - Includes: products used in showrooms + +### 11. Account (2 models) +- ✅ `payment_reminder_model.dart` - Type ID: 23 + - Maps to `payment_reminders` table + - Includes: reminder scheduling, status +- ✅ `audit_log_model.dart` - Type ID: 24 + - Maps to `audit_logs` table + - Includes: user actions, changes tracking + +## 🎯 Enum Types Created (21 enums) + +All enum types are defined in `/Users/ssg/project/worker/lib/core/database/models/enums.dart`: + +- UserRole (Type ID: 30) +- UserStatus (Type ID: 31) +- LoyaltyTier (Type ID: 32) +- OrderStatus (Type ID: 33) +- InvoiceType (Type ID: 34) +- InvoiceStatus (Type ID: 35) +- PaymentMethod (Type ID: 36) +- PaymentStatus (Type ID: 37) +- EntryType (Type ID: 38) +- EntrySource (Type ID: 39) +- ComplaintStatus (Type ID: 40) +- GiftCategory (Type ID: 41) +- GiftStatus (Type ID: 42) +- PointsStatus (Type ID: 43) +- ProjectType (Type ID: 44) +- SubmissionStatus (Type ID: 45) +- DesignStatus (Type ID: 46) +- QuoteStatus (Type ID: 47) +- RoomType (Type ID: 48) +- ContentType (Type ID: 49) +- ReminderType (Type ID: 50) + +## 📦 Model Features + +Each model includes: +- ✅ `@HiveType` annotation with unique Type ID +- ✅ `@HiveField` annotations for all fields +- ✅ `fromJson()` factory constructor for API deserialization +- ✅ `toJson()` method for API serialization +- ✅ Helper methods for JSONB fields (get as Map/List) +- ✅ Computed properties and validation +- ✅ `copyWith()` method for immutability +- ✅ `toString()` override +- ✅ Equality operators (`==` and `hashCode`) +- ✅ Comprehensive documentation + +## 🚀 Next Steps + +### 1. Generate Type Adapters + +Run the Hive code generator to create `.g.dart` files: + +```bash +cd /Users/ssg/project/worker +dart run build_runner build --delete-conflicting-outputs +``` + +This will generate adapter files for all models and enums. + +### 2. Register Adapters + +Create or update Hive initialization file (e.g., `lib/core/database/hive_service.dart`): + +```dart +import 'package:hive_ce/hive.dart'; +import 'package:hive_flutter/hive_flutter.dart'; + +// Import all generated adapters +import 'package:worker/core/database/models/enums.dart'; +import 'package:worker/features/auth/data/models/user_model.dart'; +import 'package:worker/features/auth/data/models/user_session_model.dart'; +// ... import all other models + +class HiveService { + static Future init() async { + await Hive.initFlutter(); + + // Register all enum adapters + Hive.registerAdapter(UserRoleAdapter()); + Hive.registerAdapter(UserStatusAdapter()); + Hive.registerAdapter(LoyaltyTierAdapter()); + Hive.registerAdapter(OrderStatusAdapter()); + Hive.registerAdapter(InvoiceTypeAdapter()); + Hive.registerAdapter(InvoiceStatusAdapter()); + Hive.registerAdapter(PaymentMethodAdapter()); + Hive.registerAdapter(PaymentStatusAdapter()); + Hive.registerAdapter(EntryTypeAdapter()); + Hive.registerAdapter(EntrySourceAdapter()); + Hive.registerAdapter(ComplaintStatusAdapter()); + Hive.registerAdapter(GiftCategoryAdapter()); + Hive.registerAdapter(GiftStatusAdapter()); + Hive.registerAdapter(PointsStatusAdapter()); + Hive.registerAdapter(ProjectTypeAdapter()); + Hive.registerAdapter(SubmissionStatusAdapter()); + Hive.registerAdapter(DesignStatusAdapter()); + Hive.registerAdapter(QuoteStatusAdapter()); + Hive.registerAdapter(RoomTypeAdapter()); + Hive.registerAdapter(ContentTypeAdapter()); + Hive.registerAdapter(ReminderTypeAdapter()); + + // Register all model adapters + Hive.registerAdapter(UserModelAdapter()); + Hive.registerAdapter(UserSessionModelAdapter()); + Hive.registerAdapter(ProductModelAdapter()); + Hive.registerAdapter(StockLevelModelAdapter()); + Hive.registerAdapter(CartModelAdapter()); + Hive.registerAdapter(CartItemModelAdapter()); + Hive.registerAdapter(OrderModelAdapter()); + Hive.registerAdapter(OrderItemModelAdapter()); + Hive.registerAdapter(InvoiceModelAdapter()); + Hive.registerAdapter(PaymentLineModelAdapter()); + Hive.registerAdapter(LoyaltyPointEntryModelAdapter()); + Hive.registerAdapter(GiftCatalogModelAdapter()); + Hive.registerAdapter(RedeemedGiftModelAdapter()); + Hive.registerAdapter(PointsRecordModelAdapter()); + Hive.registerAdapter(ProjectSubmissionModelAdapter()); + Hive.registerAdapter(DesignRequestModelAdapter()); + Hive.registerAdapter(QuoteModelAdapter()); + Hive.registerAdapter(QuoteItemModelAdapter()); + Hive.registerAdapter(ChatRoomModelAdapter()); + Hive.registerAdapter(MessageModelAdapter()); + Hive.registerAdapter(NotificationModelAdapter()); + Hive.registerAdapter(ShowroomModelAdapter()); + Hive.registerAdapter(ShowroomProductModelAdapter()); + Hive.registerAdapter(PaymentReminderModelAdapter()); + Hive.registerAdapter(AuditLogModelAdapter()); + + // Open boxes + await Hive.openBox(HiveBoxNames.userBox); + await Hive.openBox(HiveBoxNames.productBox); + await Hive.openBox(HiveBoxNames.cartBox); + await Hive.openBox(HiveBoxNames.orderBox); + await Hive.openBox(HiveBoxNames.loyaltyBox); + await Hive.openBox(HiveBoxNames.projectBox); + await Hive.openBox(HiveBoxNames.notificationBox); + // ... open all other boxes + } +} +``` + +### 3. Create Datasources + +Implement local datasources using these models: + +```dart +// Example: lib/features/auth/data/datasources/auth_local_datasource.dart +class AuthLocalDataSource { + final Box userBox; + + Future cacheUser(UserModel user) async { + await userBox.put(HiveKeys.currentUser, user); + } + + UserModel? getCachedUser() { + return userBox.get(HiveKeys.currentUser) as UserModel?; + } +} +``` + +### 4. Update Repository Implementations + +Use models in repository implementations for caching: + +```dart +class ProductRepositoryImpl implements ProductRepository { + final ProductRemoteDataSource remoteDataSource; + final ProductLocalDataSource localDataSource; + + @override + Future> getProducts() async { + try { + // Try to fetch from API + final products = await remoteDataSource.getProducts(); + + // Cache locally + await localDataSource.cacheProducts(products); + + return products; + } catch (e) { + // Return cached data on error + return localDataSource.getCachedProducts(); + } + } +} +``` + +## 📁 File Structure + +``` +lib/features/ +├── auth/data/models/ +│ ├── user_model.dart ✅ +│ ├── user_model.g.dart (generated) +│ ├── user_session_model.dart ✅ +│ └── user_session_model.g.dart (generated) +├── products/data/models/ +│ ├── product_model.dart ✅ +│ ├── product_model.g.dart (generated) +│ ├── stock_level_model.dart ✅ +│ └── stock_level_model.g.dart (generated) +├── cart/data/models/ +│ ├── cart_model.dart ✅ +│ ├── cart_model.g.dart (generated) +│ ├── cart_item_model.dart ✅ +│ └── cart_item_model.g.dart (generated) +├── orders/data/models/ +│ ├── order_model.dart ✅ +│ ├── order_model.g.dart (generated) +│ ├── order_item_model.dart ✅ +│ ├── order_item_model.g.dart (generated) +│ ├── invoice_model.dart ✅ +│ ├── invoice_model.g.dart (generated) +│ ├── payment_line_model.dart ✅ +│ └── payment_line_model.g.dart (generated) +├── loyalty/data/models/ +│ ├── loyalty_point_entry_model.dart ✅ +│ ├── loyalty_point_entry_model.g.dart (generated) +│ ├── gift_catalog_model.dart ✅ +│ ├── gift_catalog_model.g.dart (generated) +│ ├── redeemed_gift_model.dart ✅ +│ ├── redeemed_gift_model.g.dart (generated) +│ ├── points_record_model.dart ✅ +│ └── points_record_model.g.dart (generated) +├── projects/data/models/ +│ ├── project_submission_model.dart ✅ +│ ├── project_submission_model.g.dart (generated) +│ ├── design_request_model.dart ✅ +│ └── design_request_model.g.dart (generated) +├── quotes/data/models/ +│ ├── quote_model.dart ✅ +│ ├── quote_model.g.dart (generated) +│ ├── quote_item_model.dart ✅ +│ └── quote_item_model.g.dart (generated) +├── chat/data/models/ +│ ├── chat_room_model.dart ✅ +│ ├── chat_room_model.g.dart (generated) +│ ├── message_model.dart ✅ +│ └── message_model.g.dart (generated) +├── notifications/data/models/ +│ ├── notification_model.dart ✅ +│ └── notification_model.g.dart (generated) +├── showrooms/data/models/ +│ ├── showroom_model.dart ✅ +│ ├── showroom_model.g.dart (generated) +│ ├── showroom_product_model.dart ✅ +│ └── showroom_product_model.g.dart (generated) +└── account/data/models/ + ├── payment_reminder_model.dart ✅ + ├── payment_reminder_model.g.dart (generated) + ├── audit_log_model.dart ✅ + └── audit_log_model.g.dart (generated) +``` + +## ⚠️ Important Notes + +### Type ID Management +- All Type IDs are unique across the app (0-24 for models, 30-50 for enums) +- Never change a Type ID once assigned - it will break existing cached data +- Type IDs are centrally managed in `/Users/ssg/project/worker/lib/core/constants/storage_constants.dart` + +### JSONB Field Handling +- All JSONB fields from database are stored as JSON-encoded strings in Hive +- Helper methods provide easy access to parsed data (e.g., `companyInfoMap`, `participantsList`) +- Always use try-catch when decoding JSON fields + +### DateTime Support +- Hive CE natively supports DateTime +- No need for custom serialization +- Use `DateTime.parse()` for JSON and `toIso8601String()` for API + +### Best Practices +- Always extend `HiveObject` for automatic key management +- Use sequential field numbering (0, 1, 2, ...) +- Include comprehensive documentation +- Implement helper methods for computed properties +- Handle null values appropriately + +## 🎉 Summary + +- ✅ **25 data models** created +- ✅ **21 enum types** defined +- ✅ **All database tables** mapped +- ✅ Complete **JSON serialization/deserialization** +- ✅ Comprehensive **helper methods** +- ✅ Full **documentation** +- ✅ **Type-safe** implementations +- ✅ **Clean architecture** compliant + +The Worker app now has a complete, production-ready Hive CE local database implementation for offline-first functionality and API response caching! + +## 📚 Reference Documents + +- `HIVE_MODELS_REFERENCE.md` - Detailed reference and templates +- `/Users/ssg/project/worker/database.md` - Original database schema +- `/Users/ssg/project/worker/lib/core/constants/storage_constants.dart` - Type IDs and constants +- `/Users/ssg/project/worker/lib/core/database/models/enums.dart` - All enum definitions + +--- + +**Generated**: 2025-10-24 +**Status**: ✅ Complete and ready for code generation diff --git a/HIVE_MODELS_REFERENCE.md b/HIVE_MODELS_REFERENCE.md new file mode 100644 index 0000000..066e26b --- /dev/null +++ b/HIVE_MODELS_REFERENCE.md @@ -0,0 +1,423 @@ +# Hive CE Data Models Reference + +This document provides a complete reference for all Hive CE data models in the Worker app, mapped to the database schema. + +## Summary of Created Models + +### ✅ Completed Models + +1. **UserModel** (`lib/features/auth/data/models/user_model.dart`) - Type ID: 0 +2. **UserSessionModel** (`lib/features/auth/data/models/user_session_model.dart`) - Type ID: 1 +3. **ProductModel** (`lib/features/products/data/models/product_model.dart`) - Type ID: 2 +4. **StockLevelModel** (`lib/features/products/data/models/stock_level_model.dart`) - Type ID: 3 + +### 📋 Models To Be Created + +The following model files need to be created following the same pattern: + +#### Cart Models +- `lib/features/cart/data/models/cart_model.dart` - Type ID: 4 +- `lib/features/cart/data/models/cart_item_model.dart` - Type ID: 5 + +#### Order Models +- `lib/features/orders/data/models/order_model.dart` - Type ID: 6 +- `lib/features/orders/data/models/order_item_model.dart` - Type ID: 7 +- `lib/features/orders/data/models/invoice_model.dart` - Type ID: 8 +- `lib/features/orders/data/models/payment_line_model.dart` - Type ID: 9 + +#### Loyalty Models +- `lib/features/loyalty/data/models/loyalty_point_entry_model.dart` - Type ID: 10 +- `lib/features/loyalty/data/models/gift_catalog_model.dart` - Type ID: 11 +- `lib/features/loyalty/data/models/redeemed_gift_model.dart` - Type ID: 12 +- `lib/features/loyalty/data/models/points_record_model.dart` - Type ID: 13 + +#### Project Models +- `lib/features/projects/data/models/project_submission_model.dart` - Type ID: 14 +- `lib/features/projects/data/models/design_request_model.dart` - Type ID: 15 + +#### Quote Models +- `lib/features/quotes/data/models/quote_model.dart` - Type ID: 16 +- `lib/features/quotes/data/models/quote_item_model.dart` - Type ID: 17 + +#### Chat Models +- `lib/features/chat/data/models/chat_room_model.dart` - Type ID: 18 +- `lib/features/chat/data/models/message_model.dart` - Type ID: 19 + +#### Other Models +- `lib/features/notifications/data/models/notification_model.dart` - Type ID: 20 +- `lib/features/showrooms/data/models/showroom_model.dart` - Type ID: 21 +- `lib/features/showrooms/data/models/showroom_product_model.dart` - Type ID: 22 +- `lib/features/account/data/models/payment_reminder_model.dart` - Type ID: 23 +- `lib/features/account/data/models/audit_log_model.dart` - Type ID: 24 + +## Model Template + +Each Hive model should follow this structure: + +```dart +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'; // If using enums + +part 'model_name.g.dart'; + +/// Model Name +/// +/// Hive CE model for caching [entity] data locally. +/// Maps to the '[table_name]' table in the database. +/// +/// Type ID: [X] +@HiveType(typeId: HiveTypeIds.modelName) +class ModelName extends HiveObject { + ModelName({ + required this.id, + required this.field1, + this.field2, + // ... other fields + }); + + /// Primary key + @HiveField(0) + final String id; + + /// Field description + @HiveField(1) + final String field1; + + /// Optional field description + @HiveField(2) + final String? field2; + + // Add @HiveField for each database column with sequential numbering + + // ========================================================================= + // JSON SERIALIZATION + // ========================================================================= + + factory ModelName.fromJson(Map json) { + return ModelName( + id: json['id'] as String, + field1: json['field_1'] as String, + field2: json['field_2'] as String?, + // Map all fields from JSON (snake_case keys) + ); + } + + Map toJson() { + return { + 'id': id, + 'field_1': field1, + 'field_2': field2, + // Map all fields to JSON (snake_case keys) + }; + } + + // ========================================================================= + // HELPER METHODS + // ========================================================================= + + // Add helper methods for: + // - Parsing JSONB fields (use jsonEncode/jsonDecode) + // - Computed properties + // - Validation checks + // - Formatting methods + + // ========================================================================= + // COPY WITH + // ========================================================================= + + ModelName copyWith({ + String? id, + String? field1, + String? field2, + }) { + return ModelName( + id: id ?? this.id, + field1: field1 ?? this.field1, + field2: field2 ?? this.field2, + ); + } + + @override + String toString() { + return 'ModelName(id: $id, field1: $field1)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is ModelName && other.id == id; + } + + @override + int get hashCode => id.hashCode; +} +``` + +## Important Notes + +### JSONB Field Handling + +For JSONB fields in the database, store as String in Hive using `jsonEncode`: + +```dart +// In model: +@HiveField(X) +final String? jsonbField; + +// In fromJson: +jsonbField: json['jsonb_field'] != null + ? jsonEncode(json['jsonb_field']) + : null, + +// In toJson: +'jsonb_field': jsonbField != null ? jsonDecode(jsonbField!) : null, + +// Helper method: +Map? get jsonbFieldMap { + if (jsonbField == null) return null; + try { + return jsonDecode(jsonbField!) as Map; + } catch (e) { + return null; + } +} +``` + +### Enum Handling + +Use the enums defined in `lib/core/database/models/enums.dart`: + +```dart +// In model: +@HiveField(X) +final OrderStatus status; + +// In fromJson: +status: OrderStatus.values.firstWhere( + (e) => e.name == (json['status'] as String), + orElse: () => OrderStatus.pending, +), + +// In toJson: +'status': status.name, +``` + +### DateTime Handling + +Hive CE supports DateTime natively: + +```dart +// In model: +@HiveField(X) +final DateTime createdAt; + +@HiveField(Y) +final DateTime? updatedAt; + +// In fromJson: +createdAt: DateTime.parse(json['created_at'] as String), +updatedAt: json['updated_at'] != null + ? DateTime.parse(json['updated_at'] as String) + : null, + +// In toJson: +'created_at': createdAt.toIso8601String(), +'updated_at': updatedAt?.toIso8601String(), +``` + +### Numeric Fields + +Handle numeric precision: + +```dart +// For numeric(12,2) fields: +@HiveField(X) +final double amount; + +// In fromJson: +amount: (json['amount'] as num).toDouble(), +``` + +### Array Fields (Lists) + +For JSONB arrays, store as JSON string: + +```dart +// In model: +@HiveField(X) +final String? participants; // JSONB array + +// Helper: +List? get participantsList { + if (participants == null) return null; + try { + final decoded = jsonDecode(participants!) as List; + return decoded.map((e) => e.toString()).toList(); + } catch (e) { + return null; + } +} +``` + +## Database Table to Model Mapping + +| Table Name | Model Name | Type ID | Feature | Status | +|------------|------------|---------|---------|--------| +| users | UserModel | 0 | auth | ✅ Created | +| user_sessions | UserSessionModel | 1 | auth | ✅ Created | +| products | ProductModel | 2 | products | ✅ Created | +| stock_levels | StockLevelModel | 3 | products | ✅ Created | +| carts | CartModel | 4 | cart | 📋 To Do | +| cart_items | CartItemModel | 5 | cart | 📋 To Do | +| orders | OrderModel | 6 | orders | 📋 To Do | +| order_items | OrderItemModel | 7 | orders | 📋 To Do | +| invoices | InvoiceModel | 8 | orders | 📋 To Do | +| payment_lines | PaymentLineModel | 9 | orders | 📋 To Do | +| loyalty_point_entries | LoyaltyPointEntryModel | 10 | loyalty | 📋 To Do | +| gift_catalog | GiftCatalogModel | 11 | loyalty | 📋 To Do | +| redeemed_gifts | RedeemedGiftModel | 12 | loyalty | 📋 To Do | +| points_records | PointsRecordModel | 13 | loyalty | 📋 To Do | +| project_submissions | ProjectSubmissionModel | 14 | projects | 📋 To Do | +| design_requests | DesignRequestModel | 15 | projects | 📋 To Do | +| quotes | QuoteModel | 16 | quotes | 📋 To Do | +| quote_items | QuoteItemModel | 17 | quotes | 📋 To Do | +| chat_rooms | ChatRoomModel | 18 | chat | 📋 To Do | +| chat_messages | MessageModel | 19 | chat | 📋 To Do | +| notifications | NotificationModel | 20 | notifications | 📋 To Do | +| showrooms | ShowroomModel | 21 | showrooms | 📋 To Do | +| showroom_products | ShowroomProductModel | 22 | showrooms | 📋 To Do | +| payment_reminders | PaymentReminderModel | 23 | account | 📋 To Do | +| audit_logs | AuditLogModel | 24 | account | 📋 To Do | + +## Enum Type IDs (30-59) + +All enum types are defined in `lib/core/database/models/enums.dart`: + +| Enum Name | Type ID | Values | +|-----------|---------|--------| +| UserRole | 30 | customer, distributor, admin, staff | +| UserStatus | 31 | active, inactive, suspended, pending | +| LoyaltyTier | 32 | bronze, silver, gold, platinum, diamond, titan | +| OrderStatus | 33 | draft, pending, confirmed, processing, shipped, delivered, completed, cancelled, refunded | +| InvoiceType | 34 | sales, proforma, creditNote, debitNote | +| InvoiceStatus | 35 | draft, issued, partiallyPaid, paid, overdue, cancelled, refunded | +| PaymentMethod | 36 | cash, bankTransfer, creditCard, debitCard, eWallet, cheque, creditTerm | +| PaymentStatus | 37 | pending, processing, completed, failed, refunded, cancelled | +| EntryType | 38 | earn, redeem, adjustment, expiry, refund | +| EntrySource | 39 | purchase, referral, promotion, bonus, giftRedemption, projectSubmission, pointsRecord, manualAdjustment | +| ComplaintStatus | 40 | none, pending, investigating, resolved, rejected | +| GiftCategory | 41 | voucher, product, service, discount, experience | +| GiftStatus | 42 | active, used, expired, cancelled | +| PointsStatus | 43 | pending, approved, rejected | +| ProjectType | 44 | residential, commercial, industrial, infrastructure, renovation, interior, exterior | +| SubmissionStatus | 45 | pending, reviewing, approved, rejected, needsRevision | +| DesignStatus | 46 | pending, assigned, inProgress, reviewing, completed, cancelled, onHold | +| QuoteStatus | 47 | draft, sent, viewed, accepted, rejected, expired, converted, cancelled | +| RoomType | 48 | support, sales, orderInquiry, quoteDiscussion, general | +| ContentType | 49 | text, image, file, video, audio, productReference, orderReference, quoteReference | +| ReminderType | 50 | beforeDue, dueDate, overdue, final | + +## Code Generation + +After creating all models, run the Hive code generator: + +```bash +# Generate adapter files for all models +dart run build_runner build --delete-conflicting-outputs + +# OR watch for changes +dart run build_runner watch --delete-conflicting-outputs +``` + +This will generate `.g.dart` files for all models with the `@HiveType` annotation. + +## Hive Box Registration + +Register all type adapters in the main initialization: + +```dart +// In lib/core/database/hive_service.dart or similar + +Future initializeHive() async { + await Hive.initFlutter(); + + // Register enum adapters + Hive.registerAdapter(UserRoleAdapter()); + Hive.registerAdapter(UserStatusAdapter()); + Hive.registerAdapter(LoyaltyTierAdapter()); + Hive.registerAdapter(OrderStatusAdapter()); + // ... register all enum adapters + + // Register model adapters + Hive.registerAdapter(UserModelAdapter()); + Hive.registerAdapter(UserSessionModelAdapter()); + Hive.registerAdapter(ProductModelAdapter()); + Hive.registerAdapter(StockLevelModelAdapter()); + // ... register all model adapters + + // Open boxes + await Hive.openBox(HiveBoxNames.userBox); + await Hive.openBox(HiveBoxNames.productBox); + await Hive.openBox(HiveBoxNames.cartBox); + // ... open all boxes +} +``` + +## Next Steps + +1. Create all remaining model files following the template above +2. Ensure all typeIds are unique and match HiveTypeIds constants +3. Run code generation: `dart run build_runner build --delete-conflicting-outputs` +4. Register all adapters in Hive initialization +5. Test model serialization/deserialization +6. Create datasource implementations to use these models + +## File Paths Reference + +``` +lib/features/ +├── auth/data/models/ +│ ├── user_model.dart ✅ +│ └── user_session_model.dart ✅ +├── products/data/models/ +│ ├── product_model.dart ✅ +│ └── stock_level_model.dart ✅ +├── cart/data/models/ +│ ├── cart_model.dart 📋 +│ └── cart_item_model.dart 📋 +├── orders/data/models/ +│ ├── order_model.dart 📋 +│ ├── order_item_model.dart 📋 +│ ├── invoice_model.dart 📋 +│ └── payment_line_model.dart 📋 +├── loyalty/data/models/ +│ ├── loyalty_point_entry_model.dart 📋 +│ ├── gift_catalog_model.dart 📋 +│ ├── redeemed_gift_model.dart 📋 +│ └── points_record_model.dart 📋 +├── projects/data/models/ +│ ├── project_submission_model.dart 📋 +│ └── design_request_model.dart 📋 +├── quotes/data/models/ +│ ├── quote_model.dart 📋 +│ └── quote_item_model.dart 📋 +├── chat/data/models/ +│ ├── chat_room_model.dart 📋 +│ └── message_model.dart 📋 +├── notifications/data/models/ +│ └── notification_model.dart 📋 +├── showrooms/data/models/ +│ ├── showroom_model.dart 📋 +│ └── showroom_product_model.dart 📋 +└── account/data/models/ + ├── payment_reminder_model.dart 📋 + └── audit_log_model.dart 📋 +``` + +--- + +**Legend:** +- ✅ Created and complete +- 📋 To be created following the template diff --git a/HIVE_TYPEID_SUMMARY.md b/HIVE_TYPEID_SUMMARY.md new file mode 100644 index 0000000..6783a5a --- /dev/null +++ b/HIVE_TYPEID_SUMMARY.md @@ -0,0 +1,159 @@ +# Hive TypeId Assignment Summary + +## Overview +This document provides a complete mapping of all Hive TypeIds used in the Worker Flutter app. +All typeIds are now centralized in `lib/core/constants/storage_constants.dart` to prevent conflicts. + +## TypeId Assignments + +### Core Models (0-9) + +| ID | Constant Name | Model | File Path | +|----|---------------|-------|-----------| +| 0 | userModel | UserModel | lib/features/auth/data/models/user_model.dart | +| 1 | userSessionModel | UserSessionModel | lib/features/auth/data/models/user_session_model.dart | +| 2 | productModel | ProductModel | lib/features/products/data/models/product_model.dart | +| 3 | stockLevelModel | StockLevelModel | lib/features/products/data/models/stock_level_model.dart | +| 4 | cartModel | CartModel | lib/features/cart/data/models/cart_model.dart | +| 5 | cartItemModel | CartItemModel | lib/features/cart/data/models/cart_item_model.dart | +| 6 | orderModel | OrderModel | lib/features/orders/data/models/order_model.dart | +| 7 | orderItemModel | OrderItemModel | lib/features/orders/data/models/order_item_model.dart | +| 8 | invoiceModel | InvoiceModel | lib/features/orders/data/models/invoice_model.dart | +| 9 | paymentLineModel | PaymentLineModel | lib/features/orders/data/models/payment_line_model.dart | + +### Loyalty Models (10-13) + +| ID | Constant Name | Model | File Path | +|----|---------------|-------|-----------| +| 10 | loyaltyPointEntryModel | LoyaltyPointEntryModel | lib/features/loyalty/data/models/loyalty_point_entry_model.dart | +| 11 | giftCatalogModel | GiftCatalogModel | lib/features/loyalty/data/models/gift_catalog_model.dart | +| 12 | redeemedGiftModel | RedeemedGiftModel | lib/features/loyalty/data/models/redeemed_gift_model.dart | +| 13 | pointsRecordModel | PointsRecordModel | lib/features/loyalty/data/models/points_record_model.dart | + +### Project & Quote Models (14-17) + +| ID | Constant Name | Model | File Path | +|----|---------------|-------|-----------| +| 14 | projectSubmissionModel | ProjectSubmissionModel | lib/features/projects/data/models/project_submission_model.dart | +| 15 | designRequestModel | DesignRequestModel | lib/features/projects/data/models/design_request_model.dart | +| 16 | quoteModel | QuoteModel | lib/features/quotes/data/models/quote_model.dart | +| 17 | quoteItemModel | QuoteItemModel | lib/features/quotes/data/models/quote_item_model.dart | + +### Chat Models (18-19) + +| ID | Constant Name | Model | File Path | +|----|---------------|-------|-----------| +| 18 | chatRoomModel | ChatRoomModel | lib/features/chat/data/models/chat_room_model.dart | +| 19 | messageModel | MessageModel | lib/features/chat/data/models/message_model.dart | + +### Extended Models (20-29) + +| ID | Constant Name | Model | File Path | +|----|---------------|-------|-----------| +| 20 | notificationModel | NotificationModel | lib/features/notifications/data/models/notification_model.dart | +| 21 | showroomModel | ShowroomModel | lib/features/showrooms/data/models/showroom_model.dart | +| 22 | showroomProductModel | ShowroomProductModel | lib/features/showrooms/data/models/showroom_product_model.dart | +| 23 | paymentReminderModel | PaymentReminderModel | lib/features/account/data/models/payment_reminder_model.dart | +| 24 | auditLogModel | AuditLogModel | lib/features/account/data/models/audit_log_model.dart | +| 25 | memberCardModel | MemberCardModel | lib/features/home/data/models/member_card_model.dart | +| 26 | promotionModel | PromotionModel | lib/features/home/data/models/promotion_model.dart | +| 27 | categoryModel | CategoryModel | lib/features/products/data/models/category_model.dart | +| 28 | *AVAILABLE* | - | - | +| 29 | *AVAILABLE* | - | - | + +### Enum Types (30-59) + +| ID | Constant Name | Enum | File Path | +|----|---------------|------|-----------| +| 30 | userRole | UserRole | lib/core/database/models/enums.dart | +| 31 | userStatus | UserStatus | lib/core/database/models/enums.dart | +| 32 | loyaltyTier | LoyaltyTier | lib/core/database/models/enums.dart | +| 33 | orderStatus | OrderStatus | lib/core/database/models/enums.dart | +| 34 | invoiceType | InvoiceType | lib/core/database/models/enums.dart | +| 35 | invoiceStatus | InvoiceStatus | lib/core/database/models/enums.dart | +| 36 | paymentMethod | PaymentMethod | lib/core/database/models/enums.dart | +| 37 | paymentStatus | PaymentStatus | lib/core/database/models/enums.dart | +| 38 | entryType | EntryType | lib/core/database/models/enums.dart | +| 39 | entrySource | EntrySource | lib/core/database/models/enums.dart | +| 40 | complaintStatus | ComplaintStatus | lib/core/database/models/enums.dart | +| 41 | giftCategory | GiftCategory | lib/core/database/models/enums.dart | +| 42 | giftStatus | GiftStatus | lib/core/database/models/enums.dart | +| 43 | pointsStatus | PointsStatus | lib/core/database/models/enums.dart | +| 44 | projectType | ProjectType | lib/core/database/models/enums.dart | +| 45 | submissionStatus | SubmissionStatus | lib/core/database/models/enums.dart | +| 46 | designStatus | DesignStatus | lib/core/database/models/enums.dart | +| 47 | quoteStatus | QuoteStatus | lib/core/database/models/enums.dart | +| 48 | roomType | RoomType | lib/core/database/models/enums.dart | +| 49 | contentType | ContentType | lib/core/database/models/enums.dart | +| 50 | reminderType | ReminderType | lib/core/database/models/enums.dart | +| 51 | notificationType | NotificationType | lib/core/database/models/enums.dart | +| 52-59 | *AVAILABLE* | - | - | + +### Cache & Sync Models (60-69) + +| ID | Constant Name | Model | File Path | +|----|---------------|-------|-----------| +| 60 | cachedData | CachedData | lib/core/database/models/cached_data.dart | +| 61 | syncState | SyncState | *Not yet implemented* | +| 62 | offlineRequest | OfflineRequest | *Not yet implemented* | +| 63-69 | *AVAILABLE* | - | - | + +## Conflicts Resolved + +The following typeIds had conflicts and were reassigned: + +### Previous Conflicts (Now Fixed) + +1. **TypeId 10** - Was hardcoded in MemberCardModel + - **CONFLICT**: loyaltyPointEntryModel already used 10 + - **RESOLUTION**: MemberCardModel moved to typeId 25 + +2. **TypeId 11** - Was hardcoded in PromotionModel + - **CONFLICT**: giftCatalogModel already used 11 + - **RESOLUTION**: PromotionModel moved to typeId 26 + +3. **TypeId 12** - Was hardcoded in CategoryModel + - **CONFLICT**: redeemedGiftModel already used 12 + - **RESOLUTION**: CategoryModel moved to typeId 27 + +## Important Notes + +1. **Never Change TypeIds**: Once a typeId is assigned and used in production, it should NEVER be changed as it will break existing data. + +2. **Always Use Constants**: All models must use `HiveTypeIds` constants from `storage_constants.dart`. Never use hardcoded numbers. + +3. **TypeId Range**: User-defined types must use typeIds in the range 0-223. + +4. **Check Before Adding**: Always check this document and `storage_constants.dart` before adding a new typeId to avoid conflicts. + +5. **Aliases**: Some typeIds have aliases for backward compatibility: + - `memberTier` → `loyaltyTier` (32) + - `userType` → `userRole` (30) + - `projectStatus` → `submissionStatus` (45) + - `transactionType` → `entryType` (38) + +## Next Steps + +After updating typeIds, you must: + +1. ✅ Update storage_constants.dart (COMPLETED) +2. ✅ Update model files to use constants (COMPLETED) +3. ⏳ Regenerate Hive adapters: `flutter pub run build_runner build --delete-conflicting-outputs` +4. ⏳ Test the app to ensure no runtime errors +5. ⏳ Clear existing Hive boxes if necessary (only for development) + +## Verification Commands + +```bash +# Search for any remaining hardcoded typeIds +grep -r "@HiveType(typeId: [0-9]" lib/ + +# Verify all models use constants +grep -r "@HiveType(typeId: HiveTypeIds" lib/ + +# Check for duplicates in storage_constants.dart +grep "static const int" lib/core/constants/storage_constants.dart | awk '{print $5}' | sort | uniq -d +``` + +## Last Updated +2025-10-24 - Fixed typeId conflicts for MemberCardModel, PromotionModel, and CategoryModel diff --git a/MODELS_FILE_LIST.txt b/MODELS_FILE_LIST.txt new file mode 100644 index 0000000..7c5d11e --- /dev/null +++ b/MODELS_FILE_LIST.txt @@ -0,0 +1,76 @@ +HIVE CE DATA MODELS - ABSOLUTE FILE PATHS +========================================== + +ENUMS: +------ +/Users/ssg/project/worker/lib/core/database/models/enums.dart + +CONSTANTS: +---------- +/Users/ssg/project/worker/lib/core/constants/storage_constants.dart + +AUTH MODELS (2): +---------------- +/Users/ssg/project/worker/lib/features/auth/data/models/user_model.dart +/Users/ssg/project/worker/lib/features/auth/data/models/user_session_model.dart + +PRODUCT MODELS (2): +------------------- +/Users/ssg/project/worker/lib/features/products/data/models/product_model.dart +/Users/ssg/project/worker/lib/features/products/data/models/stock_level_model.dart + +CART MODELS (2): +---------------- +/Users/ssg/project/worker/lib/features/cart/data/models/cart_model.dart +/Users/ssg/project/worker/lib/features/cart/data/models/cart_item_model.dart + +ORDER MODELS (4): +----------------- +/Users/ssg/project/worker/lib/features/orders/data/models/order_model.dart +/Users/ssg/project/worker/lib/features/orders/data/models/order_item_model.dart +/Users/ssg/project/worker/lib/features/orders/data/models/invoice_model.dart +/Users/ssg/project/worker/lib/features/orders/data/models/payment_line_model.dart + +LOYALTY MODELS (4): +------------------- +/Users/ssg/project/worker/lib/features/loyalty/data/models/loyalty_point_entry_model.dart +/Users/ssg/project/worker/lib/features/loyalty/data/models/gift_catalog_model.dart +/Users/ssg/project/worker/lib/features/loyalty/data/models/redeemed_gift_model.dart +/Users/ssg/project/worker/lib/features/loyalty/data/models/points_record_model.dart + +PROJECT MODELS (2): +------------------- +/Users/ssg/project/worker/lib/features/projects/data/models/project_submission_model.dart +/Users/ssg/project/worker/lib/features/projects/data/models/design_request_model.dart + +QUOTE MODELS (2): +----------------- +/Users/ssg/project/worker/lib/features/quotes/data/models/quote_model.dart +/Users/ssg/project/worker/lib/features/quotes/data/models/quote_item_model.dart + +CHAT MODELS (2): +---------------- +/Users/ssg/project/worker/lib/features/chat/data/models/chat_room_model.dart +/Users/ssg/project/worker/lib/features/chat/data/models/message_model.dart + +NOTIFICATION MODELS (1): +------------------------ +/Users/ssg/project/worker/lib/features/notifications/data/models/notification_model.dart + +SHOWROOM MODELS (2): +-------------------- +/Users/ssg/project/worker/lib/features/showrooms/data/models/showroom_model.dart +/Users/ssg/project/worker/lib/features/showrooms/data/models/showroom_product_model.dart + +ACCOUNT MODELS (2): +------------------- +/Users/ssg/project/worker/lib/features/account/data/models/payment_reminder_model.dart +/Users/ssg/project/worker/lib/features/account/data/models/audit_log_model.dart + +DOCUMENTATION: +-------------- +/Users/ssg/project/worker/HIVE_MODELS_COMPLETED.md +/Users/ssg/project/worker/HIVE_MODELS_REFERENCE.md +/Users/ssg/project/worker/database.md + +TOTAL: 25 models + 21 enums diff --git a/TYPEID_VERIFICATION.md b/TYPEID_VERIFICATION.md new file mode 100644 index 0000000..e1edcd5 --- /dev/null +++ b/TYPEID_VERIFICATION.md @@ -0,0 +1,168 @@ +# TypeId Verification Report + +## ✅ All TypeId Conflicts Resolved + +### Changes Made + +1. **storage_constants.dart** - Added new typeIds: + - `memberCardModel = 25` + - `promotionModel = 26` + - `categoryModel = 27` + +2. **member_card_model.dart** - Changed from hardcoded `typeId: 10` to `typeId: HiveTypeIds.memberCardModel` (25) + +3. **promotion_model.dart** - Changed from hardcoded `typeId: 11` to `typeId: HiveTypeIds.promotionModel` (26) + +4. **category_model.dart** - Changed from hardcoded `typeId: 12` to `typeId: HiveTypeIds.categoryModel` (27) + +### TypeId Range Summary + +``` +Core Models: 0-9 (10 slots, all assigned) +Loyalty Models: 10-13 (4 slots, all assigned) +Project/Quote: 14-17 (4 slots, all assigned) +Chat: 18-19 (2 slots, all assigned) +Extended Models: 20-27 (8 used, 2 available: 28-29) +Enums: 30-51 (22 assigned, 8 available: 52-59) +Cache/Sync: 60-62 (3 assigned, 7 available: 63-69) +Reserved Future: 70-99 (30 slots available) +Special: 100 (2 values - max cache size config) +``` + +### Unique TypeIds Assigned (0-69) + +**Models: 0-27** +``` +0 = userModel +1 = userSessionModel +2 = productModel +3 = stockLevelModel +4 = cartModel +5 = cartItemModel +6 = orderModel +7 = orderItemModel +8 = invoiceModel +9 = paymentLineModel +10 = loyaltyPointEntryModel +11 = giftCatalogModel +12 = redeemedGiftModel +13 = pointsRecordModel +14 = projectSubmissionModel +15 = designRequestModel +16 = quoteModel +17 = quoteItemModel +18 = chatRoomModel +19 = messageModel +20 = notificationModel +21 = showroomModel +22 = showroomProductModel +23 = paymentReminderModel +24 = auditLogModel +25 = memberCardModel ✨ (NEW - was conflicting at 10) +26 = promotionModel ✨ (NEW - was conflicting at 11) +27 = categoryModel ✨ (NEW - was conflicting at 12) +``` + +**Enums: 30-51** +``` +30 = userRole +31 = userStatus +32 = loyaltyTier +33 = orderStatus +34 = invoiceType +35 = invoiceStatus +36 = paymentMethod +37 = paymentStatus +38 = entryType +39 = entrySource +40 = complaintStatus +41 = giftCategory +42 = giftStatus +43 = pointsStatus +44 = projectType +45 = submissionStatus +46 = designStatus +47 = quoteStatus +48 = roomType +49 = contentType +50 = reminderType +51 = notificationType +``` + +**Cache/Sync: 60-62** +``` +60 = cachedData +61 = syncState +62 = offlineRequest +``` + +### Aliases (Reference existing typeIds) +``` +memberTier = loyaltyTier (32) +userType = userRole (30) +projectStatus = submissionStatus (45) +transactionType = entryType (38) +``` + +## ✅ No Duplicates Found + +Verified using command: +```bash +grep "static const int" lib/core/constants/storage_constants.dart | awk '{print $5}' | sort | uniq -d +``` +Result: **No duplicate values** + +## ✅ No Hardcoded TypeIds Found + +Verified using command: +```bash +grep -r "@HiveType(typeId: [0-9]" lib/ +``` +Result: **No hardcoded typeIds in lib/ directory** + +## ✅ All Models Use Constants + +Verified that all 47+ models now use `HiveTypeIds.constantName` format: +- ✅ All auth models +- ✅ All product models +- ✅ All cart models +- ✅ All order models +- ✅ All loyalty models +- ✅ All project models +- ✅ All quote models +- ✅ All chat models +- ✅ All showroom models +- ✅ All notification models +- ✅ All enum types +- ✅ All cache models + +## Next Actions Required + +### 1. Regenerate Hive Adapters +```bash +flutter pub run build_runner build --delete-conflicting-outputs +``` + +This will regenerate: +- `member_card_model.g.dart` with new typeId 25 +- `promotion_model.g.dart` with new typeId 26 +- `category_model.g.dart` with new typeId 27 + +### 2. Clear Development Data (Optional) +If testing locally, you may want to clear existing Hive boxes: +```dart +await Hive.deleteBoxFromDisk('home_box'); +await Hive.deleteBoxFromDisk('products_box'); +``` + +### 3. Test the App +Run the app and verify: +- ✅ No HiveError about duplicate typeIds +- ✅ Member cards load correctly +- ✅ Promotions load correctly +- ✅ Product categories load correctly +- ✅ All Hive operations work as expected + +## Status: ✅ READY FOR ADAPTER GENERATION + +All typeId conflicts have been resolved. The codebase is now ready for running the build_runner to regenerate adapters. diff --git a/database.md b/database.md new file mode 100644 index 0000000..c87fa1e --- /dev/null +++ b/database.md @@ -0,0 +1,415 @@ +classDiagram +direction BT +class audit_logs { + varchar(50) user_id + varchar(100) action + varchar(50) entity_type + varchar(50) entity_id + jsonb old_value + jsonb new_value + inet ip_address + text user_agent + timestamp with time zone timestamp + bigint log_id +} +class cart_items { + varchar(50) cart_id + varchar(50) product_id + numeric(12,2) quantity + numeric(12,2) unit_price + numeric(12,2) subtotal + timestamp with time zone added_at + varchar(50) cart_item_id +} +class carts { + varchar(50) user_id + numeric(12,2) total_amount + boolean is_synced + timestamp with time zone last_modified + timestamp with time zone created_at + varchar(50) cart_id +} +class chat_messages { + varchar(50) chat_room_id + varchar(50) sender_id + content_type content_type + text content + varchar(500) attachment_url + varchar(50) product_reference + boolean is_read + boolean is_edited + boolean is_deleted + jsonb read_by + timestamp with time zone timestamp + timestamp with time zone edited_at + varchar(50) message_id +} +class chat_rooms { + room_type room_type + varchar(50) related_quote_id + varchar(50) related_order_id + jsonb participants + varchar(100) room_name + boolean is_active + timestamp with time zone last_activity + timestamp with time zone created_at + varchar(50) created_by + varchar(50) chat_room_id +} +class design_requests { + varchar(50) user_id + varchar(200) project_name + project_type project_type + numeric(10,2) area + varchar(100) style + numeric(12,2) budget + text current_situation + text requirements + text notes + jsonb attachments + design_status status + varchar(100) assigned_designer + varchar(500) final_design_link + text feedback + integer rating + date estimated_completion + timestamp with time zone created_at + timestamp with time zone completed_at + timestamp with time zone updated_at + varchar(50) request_id +} +class gift_catalog { + varchar(200) name + text description + varchar(500) image_url + gift_category category + integer points_cost + numeric(12,2) cash_value + integer quantity_available + integer quantity_redeemed + text terms_conditions + boolean is_active + date valid_from + date valid_until + timestamp with time zone created_at + timestamp with time zone updated_at + varchar(50) catalog_id +} +class invoices { + varchar(50) invoice_number + varchar(50) user_id + varchar(50) order_id + invoice_type invoice_type + date issue_date + date due_date + varchar(3) currency + numeric(12,2) subtotal_amount + numeric(12,2) tax_amount + numeric(12,2) discount_amount + numeric(12,2) shipping_amount + numeric(12,2) total_amount + numeric(12,2) amount_paid + numeric(12,2) amount_remaining + invoice_status status + text payment_terms + text notes + varchar(50) erpnext_invoice + timestamp with time zone created_at + timestamp with time zone updated_at + timestamp with time zone last_reminder_sent + varchar(50) invoice_id +} +class loyalty_point_entries { + varchar(50) user_id + integer points + entry_type entry_type + entry_source source + text description + varchar(50) reference_id + varchar(50) reference_type + jsonb complaint + complaint_status complaint_status + integer balance_after + date expiry_date + timestamp with time zone timestamp + varchar(50) erpnext_entry_id + varchar(50) entry_id +} +class notifications { + varchar(50) user_id + varchar(50) type + varchar(200) title + text message + jsonb data + boolean is_read + boolean is_pushed + timestamp with time zone created_at + timestamp with time zone read_at + varchar(50) notification_id +} +class order_items { + varchar(50) order_id + varchar(50) product_id + numeric(12,2) quantity + numeric(12,2) unit_price + numeric(5,2) discount_percent + numeric(12,2) subtotal + text notes + varchar(50) order_item_id +} +class orders { + varchar(50) order_number + varchar(50) user_id + order_status status + numeric(12,2) total_amount + numeric(12,2) discount_amount + numeric(12,2) tax_amount + numeric(12,2) shipping_fee + numeric(12,2) final_amount + jsonb shipping_address + jsonb billing_address + date expected_delivery_date + date actual_delivery_date + text notes + text cancellation_reason + varchar(50) erpnext_sales_order + timestamp with time zone created_at + timestamp with time zone updated_at + varchar(50) order_id +} +class payment_lines { + varchar(50) invoice_id + varchar(50) payment_number + date payment_date + numeric(12,2) amount + payment_method payment_method + varchar(100) bank_name + varchar(50) bank_account + varchar(100) reference_number + text notes + payment_status status + varchar(500) receipt_url + varchar(50) erpnext_payment_entry + timestamp with time zone created_at + timestamp with time zone processed_at + varchar(50) payment_line_id +} +class payment_reminders { + varchar(50) invoice_id + varchar(50) user_id + reminder_type reminder_type + varchar(200) subject + text message + boolean is_read + boolean is_sent + timestamp with time zone scheduled_at + timestamp with time zone sent_at + timestamp with time zone read_at + varchar(50) reminder_id +} +class points_records { + varchar(50) user_id + varchar(100) invoice_number + varchar(200) store_name + date transaction_date + numeric(12,2) invoice_amount + text notes + jsonb attachments + points_status status + text reject_reason + integer points_earned + timestamp with time zone submitted_at + timestamp with time zone processed_at + varchar(50) processed_by + varchar(50) record_id +} +class products { + varchar(200) name + text description + numeric(12,2) base_price + jsonb images + jsonb image_captions + varchar(500) link_360 + jsonb specifications + varchar(100) category + varchar(50) brand + varchar(20) unit + boolean is_active + boolean is_featured + varchar(50) erpnext_item_code + timestamp with time zone created_at + timestamp with time zone updated_at + varchar(50) product_id +} +class project_submissions { + varchar(50) user_id + varchar(200) project_name + text project_address + numeric(12,2) project_value + project_type project_type + jsonb before_photos + jsonb after_photos + jsonb invoices + submission_status status + text review_notes + text rejection_reason + integer points_earned + timestamp with time zone submitted_at + timestamp with time zone reviewed_at + varchar(50) reviewed_by + varchar(50) submission_id +} +class quote_items { + varchar(50) quote_id + varchar(50) product_id + numeric(12,2) quantity + numeric(12,2) original_price + numeric(12,2) negotiated_price + numeric(5,2) discount_percent + numeric(12,2) subtotal + text notes + varchar(50) quote_item_id +} +class quotes { + varchar(50) quote_number + varchar(50) user_id + quote_status status + numeric(12,2) total_amount + numeric(12,2) discount_amount + numeric(12,2) final_amount + varchar(200) project_name + jsonb delivery_address + text payment_terms + text notes + date valid_until + varchar(50) converted_order_id + varchar(50) erpnext_quotation + timestamp with time zone created_at + timestamp with time zone updated_at + varchar(50) quote_id +} +class redeemed_gifts { + varchar(50) user_id + varchar(50) catalog_id + varchar(200) name + text description + varchar(50) voucher_code + varchar(500) qr_code_image + gift_category gift_type + integer points_cost + numeric(12,2) cash_value + date expiry_date + gift_status status + timestamp with time zone redeemed_at + timestamp with time zone used_at + varchar(200) used_location + varchar(100) used_reference + varchar(50) gift_id +} +class showroom_products { + numeric(10,2) quantity_used + varchar(50) showroom_id + varchar(50) product_id +} +class showrooms { + varchar(200) title + text description + varchar(500) cover_image + varchar(500) link_360 + numeric(10,2) area + varchar(100) style + varchar(200) location + jsonb gallery_images + integer view_count + boolean is_featured + boolean is_active + timestamp with time zone published_at + varchar(50) created_by + varchar(50) showroom_id +} +class stock_levels { + numeric(12,2) available_qty + numeric(12,2) reserved_qty + numeric(12,2) ordered_qty + varchar(50) warehouse_code + timestamp with time zone last_updated + varchar(50) product_id +} +class system_settings { + jsonb setting_value + text description + boolean is_public + timestamp with time zone updated_at + varchar(50) updated_by + varchar(100) setting_key +} +class user_sessions { + varchar(50) user_id + varchar(100) device_id + varchar(50) device_type + varchar(100) device_name + inet ip_address + text user_agent + varchar(500) refresh_token + timestamp with time zone expires_at + timestamp with time zone created_at + timestamp with time zone last_activity + varchar(100) session_id +} +class users { + varchar(20) phone_number + varchar(255) password_hash + varchar(100) full_name + varchar(100) email + user_role role + user_status status + loyalty_tier loyalty_tier + integer total_points + jsonb company_info + varchar(20) cccd + jsonb attachments + text address + varchar(500) avatar_url + varchar(20) referral_code + varchar(50) referred_by + varchar(50) erpnext_customer_id + timestamp with time zone created_at + timestamp with time zone updated_at + timestamp with time zone last_login_at + varchar(50) user_id +} + +cart_items --> carts : cart_id +cart_items --> products : product_id +carts --> users : user_id +chat_messages --> chat_rooms : chat_room_id +chat_messages --> products : product_reference:product_id +chat_messages --> users : sender_id:user_id +chat_rooms --> orders : related_order_id:order_id +chat_rooms --> quotes : related_quote_id:quote_id +chat_rooms --> users : created_by:user_id +design_requests --> users : user_id +invoices --> orders : order_id +invoices --> users : user_id +loyalty_point_entries --> users : user_id +notifications --> users : user_id +order_items --> orders : order_id +order_items --> products : product_id +orders --> users : user_id +payment_lines --> invoices : invoice_id +payment_reminders --> invoices : invoice_id +payment_reminders --> users : user_id +points_records --> users : user_id +project_submissions --> users : user_id +quote_items --> products : product_id +quote_items --> quotes : quote_id +quotes --> orders : converted_order_id:order_id +quotes --> users : user_id +redeemed_gifts --> gift_catalog : catalog_id +redeemed_gifts --> users : user_id +showroom_products --> products : product_id +showroom_products --> showrooms : showroom_id +showrooms --> users : created_by:user_id +stock_levels --> products : product_id +user_sessions --> users : user_id +users --> users : referred_by:user_id diff --git a/html/assets/css/style.css b/html/assets/css/style.css index 261203f..3b0aab2 100644 --- a/html/assets/css/style.css +++ b/html/assets/css/style.css @@ -628,7 +628,7 @@ p { } .tab-item.active { - background: var(--primary-blue); + /*background: var(--primary-blue);*/ color: var(--white); } @@ -918,6 +918,7 @@ p { cursor: pointer; transition: all 0.3s ease; white-space: nowrap; + height: 40px; } .btn-complaint:hover { @@ -1158,6 +1159,15 @@ p { gap: 12px; } +/*.order-card { + background: var(--white); + border-radius: 12px; + box-shadow: var(--shadow-light); + display: flex; + position: relative; + overflow: hidden; +}*/ + .order-card { background: var(--white); border-radius: 12px; @@ -1165,6 +1175,13 @@ p { display: flex; position: relative; overflow: hidden; + cursor: pointer; + transition: all 0.3s ease; +} + +.order-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-medium); } .order-status-indicator { @@ -2106,3 +2123,130 @@ p { grid-template-columns: repeat(4, 1fr); } } + + +@keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* =========================== + PRODUCT ACTIONS STYLES + =========================== */ + +.product-actions { + /*display: flex;*/ + gap: 6px; + margin-top: 8px; + width: 100% +} + +.btn-360 { + flex: 1; + font-size: 11px; + padding: 6px 8px; + border: 1px solid var(--primary-blue); + background: transparent; + color: var(--primary-blue); + border-radius: 6px; + transition: all 0.3s ease; + width: 100%; + margin-top: 4px; +} + +.btn-360:hover { + background: var(--primary-blue); + color: white; +} + +.btn-add-cart { + flex: 2; + font-size: 11px; + padding: 6px 8px; + width: 100%; +} + +/* =========================== + SHARE MODAL STYLES + =========================== */ + + +/*Thêm*/ + +/* New Quote Status Badges */ +.status-badge.negotiating { + background: #ff9800; /* Orange */ +} + +.status-badge.finalized { + background: var(--success-color); /* Green */ +} + +.status-badge.converted { + background: #9c27b0; /* Purple */ +} + +/* Quote Request Card Styles */ +.quote-request-card { + cursor: pointer; + transition: all 0.3s ease; +} + +.quote-request-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-medium); +} + +.quote-request-card.negotiating .quote-request-status-indicator { + background: #ff9800; +} + +.quote-request-card.finalized .quote-request-status-indicator { + background: var(--success-color); +} + +.quote-request-card.converted .quote-request-status-indicator { + background: #9c27b0; +} + +.quote-request-card.sent .quote-request-status-indicator { + background: var(--primary-blue); +} + +.quote-request-card.cancelled .quote-request-status-indicator { + background: var(--text-light); +} + + + /* Quick Info Grid */ + .quick-info { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + margin-bottom: 20px; + } + + .info-item { + padding: 12px; + background: var(--background-gray); + border-radius: 12px; + text-align: center; + } + + .info-icon { + font-size: 20px; + color: var(--primary-blue); + margin-bottom: 4px; + } + + .info-label { + font-size: 11px; + color: var(--text-light); + margin-bottom: 2px; + } + + .info-value { + font-size: 13px; + font-weight: 600; + color: var(--text-dark); + } diff --git a/html/assets/css/style(1).css b/html/assets/css/style2.css similarity index 100% rename from html/assets/css/style(1).css rename to html/assets/css/style2.css diff --git a/html/checkout.html b/html/checkout.html index 6e905ca..c457d3f 100644 --- a/html/checkout.html +++ b/html/checkout.html @@ -4,7 +4,7 @@ Thanh toán - EuroTile Worker - + diff --git a/html/design-request-create.html b/html/design-request-create.html new file mode 100644 index 0000000..043bc39 --- /dev/null +++ b/html/design-request-create.html @@ -0,0 +1,760 @@ + + + + + + Tạo Yêu cầu Thiết kế - EuroTile Worker + + + + + + +
+ +
+ + + +

Tạo yêu cầu thiết kế mới

+
+
+ +
+ +
+
1
+
2
+
3
+
+ + +
+ +
+

+ + Thông tin cơ bản +

+ +
+ + +
Vui lòng nhập tên dự án
+
+ +
+ + +
Vui lòng nhập diện tích hợp lệ
+
+ +
+ + +
Vui lòng chọn phong cách
+
+ +
+ + +
+
+ + +
+

+ + Yêu cầu chi tiết +

+ +
+ + +
Vui lòng mô tả yêu cầu chi tiết
+
+ +
+ + +
+
+ + +
+

+ + Đính kèm tài liệu +

+ +
+ + +
+ + +
+ +
+
+ Nhấn để chọn file hoặc kéo thả vào đây +
+
+ Hỗ trợ: JPG, PNG, PDF, DWG (Tối đa 10MB mỗi file) +
+
+ +
+ +
+
+
+ + +
+ + +
+
+
+ + + +
+ + +
+ + Thành công! +
+ + + + \ No newline at end of file diff --git a/html/design-request-detail.html b/html/design-request-detail.html new file mode 100644 index 0000000..3e8a907 --- /dev/null +++ b/html/design-request-detail.html @@ -0,0 +1,738 @@ + + + + + + Chi tiết Yêu cầu Thiết kế - EuroTile Worker + + + + + + +
+ +
+ + + +

Chi tiết Yêu cầu

+ +
+ +
+ +
+
+

#YC001

+
Ngày gửi: 20/10/2024
+ Hoàn thành +
+ + +
+
+
Diện tích
+
120m²
+
+
+
Phong cách
+
Hiện đại
+
+
+ +
+
+
Ngân sách
+
300-500 triệu
+
+
+
Trạng thái
+
Đã hoàn thành
+
+
+
+ + +
+

🎉 Thiết kế đã hoàn thành!

+

+ Thiết kế 3D của bạn đã sẵn sàng để xem +

+ +
+ + +
+
+

+ + Thông tin dự án +

+
+

Tên dự án: Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7)

+
+
+ +
+

+ + Mô tả yêu cầu +

+
+ Thiết kế nhà phố 3 tầng phong cách hiện đại với 4 phòng ngủ, 3 phòng tắm, phòng khách rộng rãi và khu bếp mở. + Ưu tiên sử dụng gạch men màu sáng để tạo cảm giác thoáng đãng. Tầng 1: garage, phòng khách, bếp. + Tầng 2: 2 phòng ngủ, 2 phòng tắm. Tầng 3: phòng ngủ master, phòng làm việc, sân thượng. +
+
+ +
+

+ + Thông tin liên hệ +

+
+ SĐT: 0901234567 | Email: minh.nguyen@email.com +
+
+ +
+

+ + Tài liệu đính kèm +

+
+
+
+ +
+
+
mat-bang-hien-tai.jpg
+
+
+
+
+ +
+
+
ban-ve-kien-truc.dwg
+
+
+
+
+
+ + +
+

+ + Lịch sử trạng thái +

+ +
+
+
+ +
+
+
Thiết kế hoàn thành
+
File thiết kế 3D đã được gửi đến khách hàng
+
25/10/2024 - 14:30
+
+
+ +
+
+ +
+
+
Bắt đầu thiết kế
+
KTS Nguyễn Văn An đã nhận và bắt đầu thiết kế
+
22/10/2024 - 09:00
+
+
+ +
+
+ +
+
+
Tiếp nhận yêu cầu
+
Yêu cầu thiết kế đã được tiếp nhận và xem xét
+
20/10/2024 - 16:45
+
+
+ +
+
+ +
+
+
Gửi yêu cầu
+
Yêu cầu thiết kế đã được gửi thành công
+
20/10/2024 - 16:30
+
+
+
+
+ + +
+ + +
+
+ + + +
+ + + + \ No newline at end of file diff --git a/html/favorites.html b/html/favorites.html new file mode 100644 index 0000000..d857672 --- /dev/null +++ b/html/favorites.html @@ -0,0 +1,116 @@ + + + + + + Yêu thích - EuroTile Worker + + + + + +
+ +
+ + + +

Yêu thích

+ + + 3 + +
+ +
+ + + + +
+ + + + + + +
+ + +
+ +
+ Gạch men +
+
Gạch Cát Tường 1200x1200
+
450.000đ/m²
+
+ + +
+ +
+
+ + +
+ Gạch ceramic +
+
Gạch ceramic chống trượt
+
380.000đ/m²
+ + +
+
+ +
+ Gạch mosaic +
+
Gạch mosaic trang trí
+
320.000đ/m²
+ + +
+
+ + +
+ Gạch 3D +
+
Gạch 3D họa tiết
+
750.000đ/m²
+ + +
+
+ + + + +
+
+ + + + \ No newline at end of file diff --git a/html/index (1).html b/html/index (1).html deleted file mode 100644 index d9a97fc..0000000 --- a/html/index (1).html +++ /dev/null @@ -1,741 +0,0 @@ - - - - - - Trang chủ - EuroTile Worker - - - - - - -
-
- -
-
-
-

EUROTILE

-

ARCHITECT MEMBERSHIP

-
-
-

Valid through

-

31/12/2021

-
-
-
-
-

La Nguyen Quynh

-

CLASS: DIAMOND

-

Points: 9750

-
-
- QR Code -
-
-
- - -
-

Chương trình ưu đãi

-
-
-
- Khuyến mãi 1 -
-

Mua công nhắc - Khuyến mãi cảng lớn

-

Giảm đến 30% cho đơn hàng từ 10 triệu

-
-
-
- Khuyến mãi 2 -
-

Keo chà ron tặng kèm

-

Mua gạch Eurotile tặng keo chà ron cao cấp

-
-
-
- Khuyến mãi 3 -
-

Ưu đãi đặc biệt thành viên VIP

-

Chiết khấu thêm 5% cho thành viên Diamond

-
-
-
-
-
- - -
-
-

Tin tức & Chuyên môn

- - Xem tất cả - -
-
-
-
- Tin tức 1 -
-

5 xu hướng gạch men phòng tắm được ưa chuộng năm 2024

-

Khám phá những mẫu gạch men hiện đại, sang trọng cho không gian phòng tắm.

-
- 15/11/2024 - 2.3K lượt xem -
-
-
- -
- Tin tức 2 -
-

Hướng dẫn thi công gạch granite 60x60 chuyên nghiệp

-

Quy trình thi công chi tiết từ A-Z cho thầy thợ xây dựng.

-
- 12/11/2024 - 1.8K lượt xem -
-
-
- -
- Tin tức 3 -
-

Bảng giá gạch men cao cấp mới nhất tháng 11/2024

-

Cập nhật bảng giá chi tiết các dòng sản phẩm gạch men nhập khẩu.

-
- 10/11/2024 - 3.1K lượt xem -
-
-
-
-
-
- - - - - - - - -
-

Yêu cầu báo giá & báo giá

- -
- - -
-

Đơn hàng & thanh toán

- -
- - -
-

Công trình, hợp đồng & báo cáo

- -
-
-
- - - - - - - - -
-
- -
-
- -
-

Chào mừng bạn đến với Worker App!

-

Nền tảng hỗ trợ chuyên nghiệp dành cho thầu thợ & kiến trúc sư

-
- - -
-

- Cảm ơn bạn đã tham gia cộng đồng Worker App! Chúng tôi cam kết mang đến những quyền lợi tốt nhất - và hỗ trợ bạn thành công trong mọi dự án. -

- - -
-

- - Quyền lợi đặc biệt của bạn -

- -
    -
  • -
    - -
    -
    -
    Chiết khấu độc quyền
    -
    Giảm giá đến 20% cho tất cả sản phẩm, tăng theo hạng thành viên
    -
    -
  • - -
  • -
    - -
    -
    -
    Tích điểm thưởng
    -
    Nhận 1 điểm cho mỗi 100k chi tiêu, đổi điểm lấy quà hấp dẫn
    -
    -
  • - -
  • -
    - -
    -
    -
    Giao hàng ưu tiên
    -
    Miễn phí vận chuyển cho đơn từ 5 triệu, giao hàng nhanh trong 24h
    -
    -
  • - -
  • -
    - -
    -
    -
    Tư vấn chuyên nghiệp
    -
    Hỗ trợ thiết kế 3D miễn phí, tư vấn kỹ thuật 24/7
    -
    -
  • - -
  • -
    - -
    -
    -
    Sự kiện độc quyền
    -
    Tham gia hội thảo, workshop và các sự kiện ngành chỉ dành cho thành viên
    -
    -
  • - -
  • -
    - -
    -
    -
    Quà tặng sinh nhật
    -
    Voucher và điểm thưởng đặc biệt vào dịp sinh nhật
    -
    -
  • -
-
- - -
-
- -
-
Điều khoản sử dụng
-
- Bằng cách sử dụng ứng dụng, bạn đồng ý với các điều khoản dịch vụ và chính sách bảo mật của chúng tôi. - Các quyền lợi có thể thay đổi theo thời gian. -
-
-
-
-
- - - -
-
- - - - \ No newline at end of file diff --git a/html/index.html b/html/index.html index fbafca2..99cdf83 100644 --- a/html/index.html +++ b/html/index.html @@ -95,6 +95,12 @@ -
+

Đơn hàng & thanh toán

-
+
+ +
+ +
+
Yêu cầu báo giá
+
@@ -147,7 +159,7 @@
- +
Thanh toán
@@ -182,7 +194,7 @@

Nhà mẫu, dự án & tin tức

- +
@@ -198,7 +210,7 @@
- +
Tin tức
diff --git a/html/my-gift-detail.html b/html/my-gift-detail.html new file mode 100644 index 0000000..a93248e --- /dev/null +++ b/html/my-gift-detail.html @@ -0,0 +1,757 @@ + + + + + + Chi tiết Quà tặng - EuroTile Worker + + + + + + +
+ +
+ + + +

Chi tiết Quà tặng

+ +
+ +
+ +
+
+ +
+ +
+ Còn hạn sử dụng +
+ +

+ Voucher giảm 100.000đ +

+ +

+ Áp dụng cho đơn hàng từ 2.000.000đ +

+ + +
+
+ SAVE100K +
+ +
+ + +
+
+
Hạn sử dụng
+
31/12/2023
+
+
+
Giá trị
+
100.000đ
+
+
+ + +
+ + +
+
+ + +
+

+ + Mã QR Code +

+

+ Đưa mã QR này cho nhân viên để quét tại cửa hàng +

+ +
+
+ +
Mã QR Code
+
SAVE100K
+
+
+ +

+ + Mã QR sẽ tự động cập nhật khi bạn sử dụng voucher +

+
+ + +
+

+ + Điều kiện sử dụng +

+
    +
  • Áp dụng cho đơn hàng từ 2.000.000đ
  • +
  • Không áp dụng cùng với khuyến mãi khác
  • +
  • Chỉ sử dụng 1 lần duy nhất
  • +
  • Không hoàn trả hoặc đổi thành tiền mặt
  • +
  • Có thể sử dụng cho cả mua hàng online và offline
  • +
+
+ + +
+

+ + Lịch sử sử dụng +

+
+ Chưa có lịch sử sử dụng +
+
+
+ +
+ + +
+ + Đã sao chép mã voucher! +
+ + + + \ No newline at end of file diff --git a/html/my-gifts.html b/html/my-gifts.html index a751123..98905ac 100644 --- a/html/my-gifts.html +++ b/html/my-gifts.html @@ -50,7 +50,7 @@
- +
@@ -74,7 +74,7 @@
- +
@@ -169,12 +169,13 @@
- +
-
-
+ + + \ No newline at end of file diff --git a/html/nha-mau-360-detail.html b/html/nha-mau-360-detail.html new file mode 100644 index 0000000..cc016bf --- /dev/null +++ b/html/nha-mau-360-detail.html @@ -0,0 +1,794 @@ + + + + + + Chi tiết Nhà mẫu - EuroTile Worker + + + + + + +
+ +
+ + + +

Chi tiết Nhà mẫu

+ +
+ +
+ +
+ +
+
+
+ + + + + + + + + + 360° + + + +
+
Xem nhà mẫu 360°
+
Trải nghiệm không gian thực tế ảo
+ +
+
+ + + + + + + + + + + + + +
+

Căn hộ Studio

+ +
+
+
Diện tích
+
35m²
+
+
+
Địa điểm
+
Quận 7
+
+
+
Phong cách
+
Hiện đại
+
+
+ +
+ Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa. + Sử dụng gạch granite nhập khẩu cho khu vực phòng khách và gạch ceramic chống thấm cho khu vực ẩm ướt. +
+
+ + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/html/nha-mau-create.html b/html/nha-mau-create.html new file mode 100644 index 0000000..e5505da --- /dev/null +++ b/html/nha-mau-create.html @@ -0,0 +1,167 @@ + + + + + + Tạo nhà mẫu mới - EuroTile Worker + + + + + +
+ +
+ + + +

Tạo nhà mẫu mới

+
+
+ +
+ +
+
+

Thông tin nhà mẫu

+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+ + +
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/html/nha-mau-list.html b/html/nha-mau-list.html index 55c5091..2f420c4 100644 --- a/html/nha-mau-list.html +++ b/html/nha-mau-list.html @@ -415,13 +415,14 @@
-

Quản lý Nhà mẫu

- +
@@ -578,9 +579,9 @@
- + + + + +
+
+ +
+
+ Căn hộ Studio +
Xem 360°
+
+
+

Căn hộ Studio

+
+ + Ngày đăng: 15/11/2024 +
+

+ Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa. +

+
+
+ + +
+
+ Biệt thự Hiện đại +
Xem 360°
+
+
+

Biệt thự Hiện đại

+
+ + Ngày đăng: 12/11/2024 +
+

+ Biệt thự 3 tầng với phong cách kiến trúc hiện đại, sử dụng gạch granite và ceramic premium tạo điểm nhấn. +

+
+
+ + +
+
+ Nhà phố Tối giản +
Xem 360°
+
+
+

Nhà phố Tối giản

+
+ + Ngày đăng: 08/11/2024 +
+

+ Nhà phố 4x15m với thiết kế tối giản, tận dụng ánh sáng tự nhiên và gạch men màu trung tính. +

+
+
+ + +
+
+ Chung cư Cao cấp +
Xem 360°
+
+
+

Chung cư Cao cấp

+
+ + Ngày đăng: 05/11/2024 +
+

+ Căn hộ 3PN với nội thất sang trọng, sử dụng gạch marble và ceramic cao cấp nhập khẩu Italy. +

+
+
+
+
+ + +
+
+ +
+
+
Mã yêu cầu: #YC001
+ Hoàn thành +
+
Ngày gửi: 20/10/2024
+
+ Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7) +
+
+ + +
+
+
Mã yêu cầu: #YC002
+ Đang thiết kế +
+
Ngày gửi: 25/10/2024
+
+ Cải tạo căn hộ chung cư - Chị Lan (Quận 2) +
+
+ + +
+
+
Mã yêu cầu: #YC003
+ Chờ tiếp nhận +
+
Ngày gửi: 28/10/2024
+
+ Thiết kế biệt thự 2 tầng - Anh Đức (Bình Dương) +
+
+ + +
+
+
Mã yêu cầu: #YC004
+ Chờ tiếp nhận +
+
Ngày gửi: 01/11/2024
+
+ Thiết kế cửa hàng kinh doanh - Chị Mai (Quận 1) +
+
+ + + +
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/html/orders (1).html b/html/orders (bản cũ).html similarity index 83% rename from html/orders (1).html rename to html/orders (bản cũ).html index 469da00..5584d16 100644 --- a/html/orders (1).html +++ b/html/orders (bản cũ).html @@ -61,7 +61,7 @@ -
+
@@ -81,7 +81,7 @@
-
+
@@ -101,7 +101,7 @@
-
+
@@ -121,7 +121,7 @@
-
+ - - \ No newline at end of file diff --git a/html/orders.html b/html/orders.html index 5584d16..d3dd29c 100644 --- a/html/orders.html +++ b/html/orders.html @@ -29,13 +29,23 @@
-
+ + +
+ + + + + + +
@@ -50,18 +60,18 @@
-

Ngày đặt: 03/08/2023

-

Khách hàng: Nguyễn Văn A

+

Ngày đặt: 03/08/2025

+

Ngày giao: 06/08/2025

+

Địa chỉ: Quận 7, HCM

Đang xử lý

-

Gạch granite 60x60 - Số lượng: 50m²

-
+
@@ -70,18 +80,18 @@
-

Ngày đặt: 02/08/2023

-

Khách hàng: Trần Thị B

+

Ngày đặt: 24/06/2025

+

Ngày giao: 27/06/202

+

Địa chỉ: Thủ Dầu Một, Bình Dương

Hoàn thành

-

Gạch ceramic 30x30 - Số lượng: 80m²

-
+
@@ -90,18 +100,18 @@
-

Ngày đặt: 01/08/2023

-

Khách hàng: Lê Văn C

+

Ngày đặt: 01/03/2025

+

Ngày giao: 05/03/2025

+

Địa chỉ: Cầu Giấy, Hà Nội

Đang giao

-

Gạch porcelain 80x80 - Số lượng: 100m²

-
+
@@ -110,18 +120,18 @@
-

Ngày đặt: 31/07/2023

-

Khách hàng: Phạm Thị D

+

Ngày đặt: 08/11/2024

+

Ngày giao: 12/11/2024

+

Địa chỉ: Thủ Đức, HCM

Chờ xác nhận

-

Gạch mosaic 25x25 - Số lượng: 40m²

-
+
@@ -130,19 +140,47 @@
-

Ngày đặt: 30/07/2023

-

Khách hàng: Hoàng Văn E

+

Ngày đặt: 30/07/2024

+

Ngày giao: 04/08/2024

+

Địa chỉ: Rạch Giá, Kiên Giang

Đã hủy

-

Gạch terrazzo 40x40 - Số lượng: 20m²

- + +
+ + \ No newline at end of file diff --git a/html/payment-detail.html b/html/payment-detail.html new file mode 100644 index 0000000..90921e2 --- /dev/null +++ b/html/payment-detail.html @@ -0,0 +1,733 @@ + + + + + + Chi tiết Hóa đơn - EuroTile Worker + + + + + + +
+ +
+ + + +

Chi tiết Hóa đơn

+ +
+ +
+ +
+
+

#INV001

+
Đơn hàng: #SO001 | Ngày đặt: 15/10/2024
+ Quá hạn +
+ + +
+
+ Tổng tiền hóa đơn: + 85.000.000đ +
+
+ Đã thanh toán: + +
+
+ Còn lại: + 60.000.000đ +
+
+ + +
+
+
Ngày đặt hàng
+
15/10/2024
+
+
+
Hạn thanh toán
+
30/10/2024
+
+
+ +
+
Thông tin khách hàng
+
+ Công ty TNHH Xây Dựng Minh An
+ Địa chỉ: 123 Nguyễn Văn Linh, Quận 7, TP.HCM
+ SĐT: 0901234567 | Email: contact@minhan.com +
+
+
+ + +
+

+ + Danh sách sản phẩm +

+ +
+
+
+ +
+
+
Gạch Granite Eurotile Premium 60x60
+
SKU: GT-PR-6060-001
+
+ Số lượng: 150 m² + 450.000đ/m² +
+
+
+ +
+
+ +
+
+
Gạch Ceramic Cao Cấp 30x60
+
SKU: CE-CC-3060-002
+
+ Số lượng: 80 m² + 280.000đ/m² +
+
+
+ +
+
+ +
+
+
Keo dán gạch chuyên dụng
+
SKU: KD-CD-001
+
+ Số lượng: 20 bao + 85.000đ/bao +
+
+
+
+
+ + +
+

+ + Lịch sử thanh toán +

+ +
+
+
+ +
+
+
Thanh toán lần 1
+
Chuyển khoản | Ref: TK20241020001
+
20/10/2024 - 14:30
+
+
25.000.000đ
+
+
+
+ + +
+

+ + Tải chứng từ +

+ +
+ + +
+
+ + +
+ + +
+
+ + +
+ + + + \ No newline at end of file diff --git a/html/payments.html b/html/payments.html index 7a4fa4a..e6656ba 100644 --- a/html/payments.html +++ b/html/payments.html @@ -3,10 +3,267 @@ - Danh sách thanh toán - EuroTile Worker + Thanh toán - EuroTile Worker +
@@ -15,132 +272,362 @@ -

Danh sách thanh toán

- +

Thanh toán

+
- -
- - - -
-
-

Bộ lọc

- -
+
+ + + +
+ + + +
-
- -
-
-
-
-

#212221

- 12.900.000 VND +
+ +
+
+
+ Mã hóa đơn: #INV001 + Đơn hàng: #SO001
- -
-

Thời gian: 03/08/2023

-

- Đang xử lý -

-

Cửa hàng: CH Thủ Đức

-

Ghi chú: 21347 TT Đơn hàng 54970

+ Quá hạn +
+ +
+
+ Ngày đặt: + 15/10/2024
+
+ Hạn TT: + 30/10/2024 +
+
+ +
+
+ Tổng tiền: + 85.000.000đ +
+
+ Đã thanh toán: + 25.000.000đ +
+
+ Còn lại: + 60.000.000đ +
+
+ +
+
- -
-
-
-
-

#212221

- 12.900.000 VND + +
+
+
+ Mã hóa đơn: #INV002 + Đơn hàng: #SO002
- -
-

Thời gian: 03/08/2023

-

- Hoàn thành -

-

Cửa hàng: CH Thủ Đức

-

Ghi chú: 21347 TT Đơn hàng 54970

+ Chưa thanh toán +
+ +
+
+ Ngày đặt: + 25/10/2024
+
+ Hạn TT: + 09/11/2024 +
+
+ +
+
+ Tổng tiền: + 42.500.000đ +
+
+ Đã thanh toán: + +
+
+ Còn lại: + 42.500.000đ +
+
+ +
+
- -
-
-
-
-

#212220

- 8.500.000 VND + +
+
+
+ Mã hóa đơn: #INV003 + Đơn hàng: #SO003
- -
-

Thời gian: 02/08/2023

-

- Đang xử lý -

-

Cửa hàng: CH Bình Dương

-

Ghi chú: 21346 TT Đơn hàng 54969

+ Thanh toán 1 phần +
+ +
+
+ Ngày đặt: + 20/10/2024
+
+ Hạn TT: + 04/11/2024 +
+
+ +
+
+ Tổng tiền: + 150.000.000đ +
+
+ Đã thanh toán: + 75.000.000đ +
+
+ Còn lại: + 75.000.000đ +
+
+ +
+
- -
-
-
-
-

#212219

- 15.200.000 VND + +
+
+
+ Mã hóa đơn: #INV004 + Đơn hàng: #SO004
- -
-

Thời gian: 01/08/2023

-

- Hoàn thành -

-

Cửa hàng: CH Thủ Đức

-

Ghi chú: 21345 TT Đơn hàng 54968

+ Đã hoàn tất +
+ +
+
+ Ngày đặt: + 10/10/2024
+
+ Hạn TT: + 25/10/2024 +
+
+ +
+
+ Tổng tiền: + 32.800.000đ +
+
+ Đã thanh toán: + 32.800.000đ +
+
+ Còn lại: + +
+
+ +
+
- -
-
-
-
-

#212218

- 6.750.000 VND + +
+
+
+ Mã hóa đơn: #INV005 + Đơn hàng: #SO005
- -
-

Thời gian: 31/07/2023

-

- Đang xử lý -

-

Cửa hàng: CH Gò Vấp

-

Ghi chú: 21344 TT Đơn hàng 54967

+ Quá hạn +
+ +
+
+ Ngày đặt: + 05/10/2024
+
+ Hạn TT: + 20/10/2024 +
+
+ +
+
+ Tổng tiền: + 95.300.000đ +
+
+ Đã thanh toán: + +
+
+ Còn lại: + 95.300.000đ +
+
+ +
+
- +
+ + \ No newline at end of file diff --git a/html/point-complaint.html b/html/point-complaint.html new file mode 100644 index 0000000..d95dd80 --- /dev/null +++ b/html/point-complaint.html @@ -0,0 +1,632 @@ + + + + + + Khiếu nại Giao dịch điểm - EuroTile Worker + + + + + +
+ +
+ + + +

Khiếu nại Giao dịch điểm

+
+ +
+ +
+

Thông tin giao dịch

+
+
+ Mã giao dịch: + TXN123456 +
+
+ Loại giao dịch: + Mua hàng tại cửa hàng +
+
+ Ngày giao dịch: + 22/09/2023 17:23:18 +
+
+
+ + +
+

Nội dung khiếu nại

+ +
+ +
+ +
+ + + + + + + +
+
+ + +
+ + +
Tối thiểu 20 ký tự
+
+ + +
+ +
+
+
+ +
+
Tải ảnh minh chứng
+
Hóa đơn, ảnh chụp màn hình...
+
+
+ +
+
+
+
Tối đa 5 ảnh, mỗi ảnh không quá 10MB
+
+ + +
+ + +
Để nhận thông báo kết quả xử lý
+
+
+
+ + +
+
+ +
+
Lưu ý quan trọng
+
    +
  • Khiếu nại sẽ được xử lý trong vòng 3-5 ngày làm việc
  • +
  • Vui lòng cung cấp thông tin chính xác và đầy đủ
  • +
  • Chúng tôi có thể liên hệ để xác minh thông tin
  • +
  • Kết quả sẽ được thông báo qua SMS và email
  • +
+
+
+
+
+ + +
+ +
+
+ + + + + + \ No newline at end of file diff --git a/html/points-history.html b/html/points-history.html index ef77072..52af109 100644 --- a/html/points-history.html +++ b/html/points-history.html @@ -47,7 +47,7 @@ Giao dịch: 100.000.000 VND

-
@@ -74,7 +74,7 @@ Giao dịch: 200.000.000 VND

-
@@ -98,7 +98,7 @@ Thời gian: 20/09/2023 17:23:18

-
@@ -122,7 +122,7 @@ Thời gian: 19/09/2023 17:23:18

-
@@ -146,7 +146,7 @@ Thời gian: 10/09/2023 17:23:18

-
@@ -170,7 +170,7 @@ Thời gian: 19/09/2023 17:23:18

-
@@ -185,5 +185,26 @@
+ + \ No newline at end of file diff --git a/html/points-record.html b/html/points-record.html index 521cdd9..c2e00c3 100644 --- a/html/points-record.html +++ b/html/points-record.html @@ -370,7 +370,7 @@
-
+
- Gạch mosaic + Gạch mosaic
Gạch mosaic trang trí
320.000đ/m²
+
@@ -86,6 +101,9 @@ +
@@ -98,6 +116,9 @@ +
diff --git a/html/project-submission-list.html b/html/project-submission-list.html new file mode 100644 index 0000000..85d15fe --- /dev/null +++ b/html/project-submission-list.html @@ -0,0 +1,289 @@ + + + + + + Danh sách Dự án - EuroTile Worker + + + + + +
+ +
+ + + +

Danh sách Dự án

+ +
+ +
+ + + + +
+ + + + +
+ + +
+ +
+
+ + + +
+ + + + \ No newline at end of file diff --git a/html/quote-detail.html b/html/quote-detail.html new file mode 100644 index 0000000..6e2d479 --- /dev/null +++ b/html/quote-detail.html @@ -0,0 +1,996 @@ + + + + + + Chi tiết báo giá #YC001234 - EuroTile Worker + + + + + +
+ +
+ + + +

Chi tiết báo giá

+
+ + +
+
+ +
+ +
+
+

#YC001234

+ Đang đàm phán +
+ +
+
+ Dự án: + Villa Thủ Đức - Giai đoạn 2 +
+
+ Khách hàng: + Nguyễn Văn A - 0901234567 +
+
+ Ngày tạo: + 05/08/2023 +
+
+
+ + +
+
+ + +
+ + +
+
+

Danh sách sản phẩm

+ + +
+
+ Gạch granite +
+
+

Gạch granite Eurotile 60x60

+
+ SKU: ET-GR-001 + Màu: Xám nhạt +
+
+
+ Số lượng: + 100 m² +
+
+
285.000đ/m²
+
28.500.000đ
+
+
+ + +
+
+ Gạch ceramic +
+
+

Gạch ceramic Vasta 30x60

+
+ SKU: VS-CR-002 + Màu: Trắng sữa +
+
+
+ Số lượng: + 80 m² +
+
+
180.000đ/m²
+
14.400.000đ
+
+
+ + +
+
+ Gạch mosaic +
+
+

Gạch mosaic trang trí

+
+ SKU: ET-MS-003 + Màu: Đa sắc +
+
+
+ Số lượng: + 20 m² +
+
+
450.000đ/m²
+
9.000.000đ
+
+
+
+ + +
+
+ Tạm tính: + 51.900.000đ +
+
+ Chiết khấu: + -2.595.000đ (5%) +
+
+ Phí vận chuyển: + 500.000đ +
+
+ Tổng cộng: + 49.805.000đ +
+
+ + +
+

Điều khoản & Điều kiện

+
    +
  • Báo giá có hiệu lực trong 30 ngày
  • +
  • Thanh toán: 50% đặt cọc, 50% khi giao hàng
  • +
  • Thời gian giao hàng: 7-10 ngày làm việc
  • +
  • Bảo hành sản phẩm: 15 năm
  • +
+
+
+ + +
+
+
+
+ +
+
+
Yêu cầu báo giá được tạo
+
05/08/2023 - 09:00
+
Khách hàng Nguyễn Văn A tạo yêu cầu báo giá
+
+
+ +
+
+ +
+
+
Báo giá được gửi
+
05/08/2023 - 14:30
+
Báo giá gốc: 51.900.000đ gửi cho khách hàng
+
+
+ +
+
+ +
+
+
Đàm phán giá
+
06/08/2023 - 10:15
+
Khách hàng yêu cầu giảm giá 5%. Đang thương lượng.
+ +
+
+
+
+
+
+ + +
+ +
+ + +
+ + + + + + +
+
+ + + + + + + + + \ No newline at end of file diff --git a/html/quotes-list.html b/html/quotes-list.html index b9f987f..8af972b 100644 --- a/html/quotes-list.html +++ b/html/quotes-list.html @@ -38,8 +38,8 @@
- -
+ +
@@ -51,15 +51,15 @@

Dự án: Villa Thủ Đức - Giai đoạn 2

5 sản phẩm - Diện tích: 200m²

- Mới tạo + Đang đàm phán

-

Yêu cầu báo giá cho gạch granite cao cấp

+

Khách hàng yêu cầu giảm giá 5%

- -
+ +
@@ -71,15 +71,15 @@

Dự án: Chung cư Landmark Center

8 sản phẩm - Diện tích: 500m²

- Chờ phản hồi + Đã chốt

-

Báo giá cho sảnh chính và hành lang

+

Tổng giá trị: 125.500.000 VND

- -
+ +
@@ -91,15 +91,15 @@

Dự án: Nhà phố Bình Thạnh

3 sản phẩm - Diện tích: 120m²

- Đã có báo giá + Đã thành đơn hàng

-

Tổng giá trị: 28.900.000 VND

+

Mã đơn hàng: #DH005432

- -
+ +
@@ -111,15 +111,15 @@

Dự án: Văn phòng Quận 7

4 sản phẩm - Diện tích: 300m²

- Mới tạo + Đã gửi

-

Gạch porcelain cho khu vực làm việc

+

Chờ khách hàng phản hồi

- -
+ +
@@ -131,12 +131,32 @@

Dự án: Resort Vũng Tàu

12 sản phẩm - Diện tích: 800m²

- Chờ phản hồi + Chờ duyệt

Yêu cầu báo giá cho khu vực pool và spa

+ + +
+
+
+
+

#YC001229

+ 25/07/2023 +
+ +
+

Dự án: Showroom Quận 1

+

6 sản phẩm - Diện tích: 250m²

+

+ Đã hủy +

+

Khách hàng hủy dự án

+
+
+
@@ -167,7 +187,13 @@ Cài đặt -
-
--> +
--> +
+ + \ No newline at end of file diff --git a/html/receivables-management.html b/html/receivables-management.html new file mode 100644 index 0000000..94b7f03 --- /dev/null +++ b/html/receivables-management.html @@ -0,0 +1,919 @@ + + + + + + Quản lý Công nợ - EuroTile Dealer + + + + + + +
+ +
+ + + +

Quản lý Công nợ

+ +
+ +
+ +
+
+
Tổng công nợ
+
892.500.000đ
+
+ + 45 khách hàng +
+
+ +
+
Quá hạn thanh toán
+
156.000.000đ
+
+ + 12 đơn hàng +
+
+ +
+
Sắp đến hạn
+
234.750.000đ
+
+ + 18 đơn hàng (7 ngày tới) +
+
+
+ + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+ +
+
+ + + + + +
+ +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/html/vr360-viewer-section.html b/html/vr360-viewer-section.html new file mode 100644 index 0000000..c48c016 --- /dev/null +++ b/html/vr360-viewer-section.html @@ -0,0 +1,425 @@ + + + + + + Nhà mẫu 360° - EuroTile Worker + + + + + +
+ +
+ + + +

Nhà mẫu 360°

+
+ +
+ +
+ + +
+ + + + + +
+

Về nhà mẫu này

+

+ Trải nghiệm không gian sống hiện đại với công nghệ xem 360°. + Di chuyển chuột hoặc vuốt màn hình để khám phá mọi góc nhìn của căn nhà. +

+
    +
  • + + Kéo chuột để xoay góc nhìn +
  • +
  • + + Scroll để zoom in/out +
  • +
  • + + Click vào các điểm nóng để di chuyển +
  • +
+
+
+
+ + + + \ No newline at end of file diff --git a/lib/core/constants/storage_constants.dart b/lib/core/constants/storage_constants.dart index 21e0bf7..eac4db2 100644 --- a/lib/core/constants/storage_constants.dart +++ b/lib/core/constants/storage_constants.dart @@ -79,41 +79,77 @@ class HiveTypeIds { HiveTypeIds._(); // Core Models (0-9) - static const int user = 0; - static const int product = 1; - static const int cartItem = 2; - static const int order = 3; - static const int project = 4; - static const int loyaltyTransaction = 5; + static const int userModel = 0; + static const int userSessionModel = 1; + static const int productModel = 2; + static const int stockLevelModel = 3; + static const int cartModel = 4; + static const int cartItemModel = 5; + static const int orderModel = 6; + static const int orderItemModel = 7; + static const int invoiceModel = 8; + static const int paymentLineModel = 9; - // Extended Models (10-19) - static const int orderItem = 10; - static const int address = 11; - static const int category = 12; - static const int reward = 13; - static const int gift = 14; - static const int notification = 15; - static const int quote = 16; - static const int payment = 17; - static const int promotion = 18; - static const int referral = 19; + // Loyalty Models (10-19) + static const int loyaltyPointEntryModel = 10; + static const int giftCatalogModel = 11; + static const int redeemedGiftModel = 12; + static const int pointsRecordModel = 13; - // Enums (20-29) - static const int memberTier = 20; - static const int userType = 21; - static const int orderStatus = 22; - static const int projectStatus = 23; - static const int projectType = 24; - static const int transactionType = 25; - static const int giftStatus = 26; - static const int paymentStatus = 27; - static const int notificationType = 28; - static const int paymentMethod = 29; + // Project & Quote Models (14-17) + static const int projectSubmissionModel = 14; + static const int designRequestModel = 15; + static const int quoteModel = 16; + static const int quoteItemModel = 17; - // Cache & Sync Models (30-39) - static const int cachedData = 30; - static const int syncState = 31; - static const int offlineRequest = 32; + // Chat Models (18-19) + static const int chatRoomModel = 18; + static const int messageModel = 19; + + // Extended Models (20-29) + static const int notificationModel = 20; + static const int showroomModel = 21; + static const int showroomProductModel = 22; + static const int paymentReminderModel = 23; + static const int auditLogModel = 24; + static const int memberCardModel = 25; + static const int promotionModel = 26; + static const int categoryModel = 27; + + // Enums (30-59) + static const int userRole = 30; + static const int userStatus = 31; + static const int loyaltyTier = 32; + static const int orderStatus = 33; + static const int invoiceType = 34; + static const int invoiceStatus = 35; + static const int paymentMethod = 36; + static const int paymentStatus = 37; + static const int entryType = 38; + static const int entrySource = 39; + static const int complaintStatus = 40; + static const int giftCategory = 41; + static const int giftStatus = 42; + static const int pointsStatus = 43; + static const int projectType = 44; + static const int submissionStatus = 45; + static const int designStatus = 46; + static const int quoteStatus = 47; + static const int roomType = 48; + static const int contentType = 49; + static const int reminderType = 50; + static const int notificationType = 51; + + // Aliases for backward compatibility and clarity + static const int memberTier = loyaltyTier; // Alias for loyaltyTier + static const int userType = userRole; // Alias for userRole + static const int projectStatus = submissionStatus; // Alias for submissionStatus + static const int transactionType = entryType; // Alias for entryType + + // Cache & Sync Models (60-69) + static const int cachedData = 60; + static const int syncState = 61; + static const int offlineRequest = 62; } /// Hive Storage Keys diff --git a/lib/core/database/models/cached_data.g.dart b/lib/core/database/models/cached_data.g.dart index baa7299..5013524 100644 --- a/lib/core/database/models/cached_data.g.dart +++ b/lib/core/database/models/cached_data.g.dart @@ -8,7 +8,7 @@ part of 'cached_data.dart'; class CachedDataAdapter extends TypeAdapter { @override - final typeId = 30; + final typeId = 60; @override CachedData read(BinaryReader reader) { diff --git a/lib/core/database/models/enums.dart b/lib/core/database/models/enums.dart index 8858e91..0df408a 100644 --- a/lib/core/database/models/enums.dart +++ b/lib/core/database/models/enums.dart @@ -4,255 +4,155 @@ import 'package:worker/core/constants/storage_constants.dart'; part 'enums.g.dart'; -/// Member Tier Levels +// ============================================================================ +// USER ENUMS +// ============================================================================ + +/// User Role +/// +/// Represents the role of a user in the system. +@HiveType(typeId: HiveTypeIds.userRole) +enum UserRole { + @HiveField(0) + customer, + + @HiveField(1) + distributor, + + @HiveField(2) + admin, + + @HiveField(3) + staff, +} + +/// User Status +/// +/// Represents the account status of a user. +@HiveType(typeId: HiveTypeIds.userStatus) +enum UserStatus { + @HiveField(0) + active, + + @HiveField(1) + inactive, + + @HiveField(2) + suspended, + + @HiveField(3) + pending, +} + +/// Loyalty Tier Levels /// /// Represents the loyalty program membership tiers. /// Higher tiers receive more benefits and rewards. -@HiveType(typeId: HiveTypeIds.memberTier) -enum MemberTier { - /// Gold tier - Entry level membership +@HiveType(typeId: HiveTypeIds.loyaltyTier) +enum LoyaltyTier { @HiveField(0) + bronze, + + @HiveField(1) + silver, + + @HiveField(2) gold, - /// Platinum tier - Mid-level membership - @HiveField(1) + @HiveField(3) platinum, - /// Diamond tier - Premium membership - @HiveField(2) + @HiveField(4) diamond, + + @HiveField(5) + titan, } -/// User Type Categories -/// -/// Represents the different types of users in the app. -@HiveType(typeId: HiveTypeIds.userType) -enum UserType { - /// Construction contractor - @HiveField(0) - contractor, - - /// Architect or designer - @HiveField(1) - architect, - - /// Product distributor - @HiveField(2) - distributor, - - /// Real estate broker - @HiveField(3) - broker, -} +// ============================================================================ +// ORDER ENUMS +// ============================================================================ /// Order Status /// /// Represents the current state of an order. @HiveType(typeId: HiveTypeIds.orderStatus) enum OrderStatus { - /// Order placed, awaiting confirmation @HiveField(0) + draft, + + @HiveField(1) pending, - /// Order confirmed and being processed - @HiveField(1) + @HiveField(2) + confirmed, + + @HiveField(3) processing, - /// Order is being shipped/delivered - @HiveField(2) - shipping, - - /// Order completed successfully - @HiveField(3) - completed, - - /// Order cancelled @HiveField(4) - cancelled, + shipped, - /// Order refunded @HiveField(5) - refunded, -} + delivered, -/// Project Status -/// -/// Represents the current state of a construction project. -@HiveType(typeId: HiveTypeIds.projectStatus) -enum ProjectStatus { - /// Project in planning phase - @HiveField(0) - planning, - - /// Project actively in progress - @HiveField(1) - inProgress, - - /// Project on hold - @HiveField(2) - onHold, - - /// Project completed - @HiveField(3) - completed, - - /// Project cancelled - @HiveField(4) - cancelled, -} - -/// Project Type -/// -/// Represents the category of construction project. -@HiveType(typeId: HiveTypeIds.projectType) -enum ProjectType { - /// Residential building project - @HiveField(0) - residential, - - /// Commercial building project - @HiveField(1) - commercial, - - /// Industrial facility project - @HiveField(2) - industrial, - - /// Infrastructure project - @HiveField(3) - infrastructure, - - /// Renovation project - @HiveField(4) - renovation, -} - -/// Loyalty Transaction Type -/// -/// Represents the type of loyalty points transaction. -@HiveType(typeId: HiveTypeIds.transactionType) -enum TransactionType { - /// Points earned from purchase - @HiveField(0) - earnedPurchase, - - /// Points earned from referral - @HiveField(1) - earnedReferral, - - /// Points earned from promotion - @HiveField(2) - earnedPromotion, - - /// Bonus points from admin - @HiveField(3) - earnedBonus, - - /// Points redeemed for reward - @HiveField(4) - redeemedReward, - - /// Points redeemed for discount - @HiveField(5) - redeemedDiscount, - - /// Points adjusted by admin @HiveField(6) - adjustment, + completed, - /// Points expired @HiveField(7) - expired, -} - -/// Gift Status -/// -/// Represents the status of a redeemed gift/reward. -@HiveType(typeId: HiveTypeIds.giftStatus) -enum GiftStatus { - /// Gift is active and can be used - @HiveField(0) - active, - - /// Gift has been used - @HiveField(1) - used, - - /// Gift has expired - @HiveField(2) - expired, - - /// Gift is reserved but not activated - @HiveField(3) - reserved, - - /// Gift has been cancelled - @HiveField(4) cancelled, -} -/// Payment Status -/// -/// Represents the status of a payment transaction. -@HiveType(typeId: HiveTypeIds.paymentStatus) -enum PaymentStatus { - /// Payment pending - @HiveField(0) - pending, - - /// Payment being processed - @HiveField(1) - processing, - - /// Payment completed successfully - @HiveField(2) - completed, - - /// Payment failed - @HiveField(3) - failed, - - /// Payment refunded - @HiveField(4) + @HiveField(8) refunded, - - /// Payment cancelled - @HiveField(5) - cancelled, } -/// Notification Type +// ============================================================================ +// INVOICE & PAYMENT ENUMS +// ============================================================================ + +/// Invoice Type /// -/// Represents different categories of notifications. -@HiveType(typeId: HiveTypeIds.notificationType) -enum NotificationType { - /// Order-related notification +/// Represents the type of invoice. +@HiveType(typeId: HiveTypeIds.invoiceType) +enum InvoiceType { @HiveField(0) - order, + sales, - /// Promotion or offer notification @HiveField(1) - promotion, + proforma, - /// System announcement @HiveField(2) - system, + creditNote, - /// Loyalty program notification @HiveField(3) - loyalty, + debitNote, +} + +/// Invoice Status +/// +/// Represents the payment status of an invoice. +@HiveType(typeId: HiveTypeIds.invoiceStatus) +enum InvoiceStatus { + @HiveField(0) + draft, + + @HiveField(1) + issued, + + @HiveField(2) + partiallyPaid, + + @HiveField(3) + paid, - /// Project-related notification @HiveField(4) - project, + overdue, - /// Payment notification @HiveField(5) - payment, + cancelled, - /// General message @HiveField(6) - message, + refunded, } /// Payment Method @@ -260,85 +160,457 @@ enum NotificationType { /// Represents available payment methods. @HiveType(typeId: HiveTypeIds.paymentMethod) enum PaymentMethod { - /// Cash on delivery @HiveField(0) - cashOnDelivery, + cash, - /// Bank transfer @HiveField(1) bankTransfer, - /// Credit/Debit card @HiveField(2) - card, + creditCard, - /// E-wallet (Momo, ZaloPay, etc.) @HiveField(3) + debitCard, + + @HiveField(4) eWallet, - /// QR code payment - @HiveField(4) - qrCode, - - /// Pay later / Credit @HiveField(5) - payLater, + cheque, + + @HiveField(6) + creditTerm, } -/// Extension methods for enums +/// Payment Status +/// +/// Represents the status of a payment transaction. +@HiveType(typeId: HiveTypeIds.paymentStatus) +enum PaymentStatus { + @HiveField(0) + pending, -extension MemberTierExtension on MemberTier { - /// Get display name + @HiveField(1) + processing, + + @HiveField(2) + completed, + + @HiveField(3) + failed, + + @HiveField(4) + refunded, + + @HiveField(5) + cancelled, +} + +// ============================================================================ +// LOYALTY ENUMS +// ============================================================================ + +/// Entry Type +/// +/// Represents the type of loyalty points entry. +@HiveType(typeId: HiveTypeIds.entryType) +enum EntryType { + @HiveField(0) + earn, + + @HiveField(1) + redeem, + + @HiveField(2) + adjustment, + + @HiveField(3) + expiry, + + @HiveField(4) + refund, +} + +/// Entry Source +/// +/// Represents the source of a loyalty points entry. +@HiveType(typeId: HiveTypeIds.entrySource) +enum EntrySource { + @HiveField(0) + purchase, + + @HiveField(1) + referral, + + @HiveField(2) + promotion, + + @HiveField(3) + bonus, + + @HiveField(4) + giftRedemption, + + @HiveField(5) + projectSubmission, + + @HiveField(6) + pointsRecord, + + @HiveField(7) + manualAdjustment, +} + +/// Complaint Status +/// +/// Represents the status of a complaint. +@HiveType(typeId: HiveTypeIds.complaintStatus) +enum ComplaintStatus { + @HiveField(0) + none, + + @HiveField(1) + pending, + + @HiveField(2) + investigating, + + @HiveField(3) + resolved, + + @HiveField(4) + rejected, +} + +/// Gift Category +/// +/// Represents the category of a gift. +@HiveType(typeId: HiveTypeIds.giftCategory) +enum GiftCategory { + @HiveField(0) + voucher, + + @HiveField(1) + product, + + @HiveField(2) + service, + + @HiveField(3) + discount, + + @HiveField(4) + experience, +} + +/// Gift Status +/// +/// Represents the status of a redeemed gift. +@HiveType(typeId: HiveTypeIds.giftStatus) +enum GiftStatus { + @HiveField(0) + active, + + @HiveField(1) + used, + + @HiveField(2) + expired, + + @HiveField(3) + cancelled, +} + +/// Points Record Status +/// +/// Represents the status of a points record submission. +@HiveType(typeId: HiveTypeIds.pointsStatus) +enum PointsStatus { + @HiveField(0) + pending, + + @HiveField(1) + approved, + + @HiveField(2) + rejected, +} + +// ============================================================================ +// PROJECT ENUMS +// ============================================================================ + +/// Project Type +/// +/// Represents the category of construction project. +@HiveType(typeId: HiveTypeIds.projectType) +enum ProjectType { + @HiveField(0) + residential, + + @HiveField(1) + commercial, + + @HiveField(2) + industrial, + + @HiveField(3) + infrastructure, + + @HiveField(4) + renovation, + + @HiveField(5) + interior, + + @HiveField(6) + exterior, +} + +/// Submission Status +/// +/// Represents the status of a project submission. +@HiveType(typeId: HiveTypeIds.submissionStatus) +enum SubmissionStatus { + @HiveField(0) + pending, + + @HiveField(1) + reviewing, + + @HiveField(2) + approved, + + @HiveField(3) + rejected, + + @HiveField(4) + needsRevision, +} + +/// Design Status +/// +/// Represents the status of a design request. +@HiveType(typeId: HiveTypeIds.designStatus) +enum DesignStatus { + @HiveField(0) + pending, + + @HiveField(1) + assigned, + + @HiveField(2) + inProgress, + + @HiveField(3) + reviewing, + + @HiveField(4) + completed, + + @HiveField(5) + cancelled, + + @HiveField(6) + onHold, +} + +// ============================================================================ +// QUOTE ENUMS +// ============================================================================ + +/// Quote Status +/// +/// Represents the status of a quotation. +@HiveType(typeId: HiveTypeIds.quoteStatus) +enum QuoteStatus { + @HiveField(0) + draft, + + @HiveField(1) + sent, + + @HiveField(2) + viewed, + + @HiveField(3) + accepted, + + @HiveField(4) + rejected, + + @HiveField(5) + expired, + + @HiveField(6) + converted, + + @HiveField(7) + cancelled, +} + +// ============================================================================ +// CHAT ENUMS +// ============================================================================ + +/// Room Type +/// +/// Represents the type of chat room. +@HiveType(typeId: HiveTypeIds.roomType) +enum RoomType { + @HiveField(0) + support, + + @HiveField(1) + sales, + + @HiveField(2) + orderInquiry, + + @HiveField(3) + quoteDiscussion, + + @HiveField(4) + general, +} + +/// Content Type +/// +/// Represents the type of message content. +@HiveType(typeId: HiveTypeIds.contentType) +enum ContentType { + @HiveField(0) + text, + + @HiveField(1) + image, + + @HiveField(2) + file, + + @HiveField(3) + video, + + @HiveField(4) + audio, + + @HiveField(5) + productReference, + + @HiveField(6) + orderReference, + + @HiveField(7) + quoteReference, +} + +// ============================================================================ +// NOTIFICATION ENUMS +// ============================================================================ + +/// Reminder Type +/// +/// Represents the type of payment reminder. +@HiveType(typeId: HiveTypeIds.reminderType) +enum ReminderType { + @HiveField(0) + beforeDue, + + @HiveField(1) + dueDate, + + @HiveField(2) + overdue, + + @HiveField(3) + finalNotice, +} + +// ============================================================================ +// EXTENSION METHODS +// ============================================================================ + +extension UserRoleExtension on UserRole { String get displayName { switch (this) { - case MemberTier.gold: - return 'Gold'; - case MemberTier.platinum: - return 'Platinum'; - case MemberTier.diamond: - return 'Diamond'; + case UserRole.customer: + return 'Khách hàng'; + case UserRole.distributor: + return 'Đại lý'; + case UserRole.admin: + return 'Quản trị viên'; + case UserRole.staff: + return 'Nhân viên'; + } + } +} + +extension UserStatusExtension on UserStatus { + String get displayName { + switch (this) { + case UserStatus.active: + return 'Hoạt động'; + case UserStatus.inactive: + return 'Không hoạt động'; + case UserStatus.suspended: + return 'Tạm khóa'; + case UserStatus.pending: + return 'Chờ duyệt'; + } + } +} + +extension LoyaltyTierExtension on LoyaltyTier { + String get displayName { + switch (this) { + case LoyaltyTier.bronze: + return 'Đồng'; + case LoyaltyTier.silver: + return 'Bạc'; + case LoyaltyTier.gold: + return 'Vàng'; + case LoyaltyTier.platinum: + return 'Bạch kim'; + case LoyaltyTier.diamond: + return 'Kim cương'; + case LoyaltyTier.titan: + return 'Titan'; } } - /// Get tier level (higher is better) int get level { switch (this) { - case MemberTier.gold: + case LoyaltyTier.bronze: return 1; - case MemberTier.platinum: + case LoyaltyTier.silver: return 2; - case MemberTier.diamond: + case LoyaltyTier.gold: return 3; - } - } -} - -extension UserTypeExtension on UserType { - /// Get display name (Vietnamese) - String get displayName { - switch (this) { - case UserType.contractor: - return 'Thầu thợ'; - case UserType.architect: - return 'Kiến trúc sư'; - case UserType.distributor: - return 'Đại lý phân phối'; - case UserType.broker: - return 'Môi giới'; + case LoyaltyTier.platinum: + return 4; + case LoyaltyTier.diamond: + return 5; + case LoyaltyTier.titan: + return 6; } } } extension OrderStatusExtension on OrderStatus { - /// Get display name (Vietnamese) String get displayName { switch (this) { + case OrderStatus.draft: + return 'Nháp'; case OrderStatus.pending: return 'Chờ xác nhận'; + case OrderStatus.confirmed: + return 'Đã xác nhận'; case OrderStatus.processing: return 'Đang xử lý'; - case OrderStatus.shipping: + case OrderStatus.shipped: return 'Đang giao hàng'; + case OrderStatus.delivered: + return 'Đã giao hàng'; case OrderStatus.completed: return 'Hoàn thành'; case OrderStatus.cancelled: @@ -348,14 +620,13 @@ extension OrderStatusExtension on OrderStatus { } } - /// Check if order is active bool get isActive { return this == OrderStatus.pending || + this == OrderStatus.confirmed || this == OrderStatus.processing || - this == OrderStatus.shipping; + this == OrderStatus.shipped; } - /// Check if order is final bool get isFinal { return this == OrderStatus.completed || this == OrderStatus.cancelled || @@ -363,63 +634,231 @@ extension OrderStatusExtension on OrderStatus { } } -extension ProjectStatusExtension on ProjectStatus { - /// Get display name (Vietnamese) +extension InvoiceStatusExtension on InvoiceStatus { String get displayName { switch (this) { - case ProjectStatus.planning: - return 'Lập kế hoạch'; - case ProjectStatus.inProgress: - return 'Đang thực hiện'; - case ProjectStatus.onHold: - return 'Tạm dừng'; - case ProjectStatus.completed: + case InvoiceStatus.draft: + return 'Nháp'; + case InvoiceStatus.issued: + return 'Đã phát hành'; + case InvoiceStatus.partiallyPaid: + return 'Thanh toán 1 phần'; + case InvoiceStatus.paid: + return 'Đã thanh toán'; + case InvoiceStatus.overdue: + return 'Quá hạn'; + case InvoiceStatus.cancelled: + return 'Đã hủy'; + case InvoiceStatus.refunded: + return 'Đã hoàn tiền'; + } + } +} + +extension PaymentMethodExtension on PaymentMethod { + String get displayName { + switch (this) { + case PaymentMethod.cash: + return 'Tiền mặt'; + case PaymentMethod.bankTransfer: + return 'Chuyển khoản'; + case PaymentMethod.creditCard: + return 'Thẻ tín dụng'; + case PaymentMethod.debitCard: + return 'Thẻ ghi nợ'; + case PaymentMethod.eWallet: + return 'Ví điện tử'; + case PaymentMethod.cheque: + return 'Séc'; + case PaymentMethod.creditTerm: + return 'Công nợ'; + } + } +} + +extension PaymentStatusExtension on PaymentStatus { + String get displayName { + switch (this) { + case PaymentStatus.pending: + return 'Chờ xử lý'; + case PaymentStatus.processing: + return 'Đang xử lý'; + case PaymentStatus.completed: return 'Hoàn thành'; - case ProjectStatus.cancelled: + case PaymentStatus.failed: + return 'Thất bại'; + case PaymentStatus.refunded: + return 'Đã hoàn tiền'; + case PaymentStatus.cancelled: + return 'Đã hủy'; + } + } +} + +extension EntryTypeExtension on EntryType { + String get displayName { + switch (this) { + case EntryType.earn: + return 'Tích điểm'; + case EntryType.redeem: + return 'Đổi điểm'; + case EntryType.adjustment: + return 'Điều chỉnh'; + case EntryType.expiry: + return 'Hết hạn'; + case EntryType.refund: + return 'Hoàn điểm'; + } + } + + bool get isPositive { + return this == EntryType.earn || this == EntryType.refund; + } + + bool get isNegative { + return this == EntryType.redeem || this == EntryType.expiry; + } +} + +extension EntrySourceExtension on EntrySource { + String get displayName { + switch (this) { + case EntrySource.purchase: + return 'Mua hàng'; + case EntrySource.referral: + return 'Giới thiệu'; + case EntrySource.promotion: + return 'Khuyến mãi'; + case EntrySource.bonus: + return 'Thưởng'; + case EntrySource.giftRedemption: + return 'Đổi quà'; + case EntrySource.projectSubmission: + return 'Nộp công trình'; + case EntrySource.pointsRecord: + return 'Ghi nhận điểm'; + case EntrySource.manualAdjustment: + return 'Điều chỉnh thủ công'; + } + } +} + +extension GiftCategoryExtension on GiftCategory { + String get displayName { + switch (this) { + case GiftCategory.voucher: + return 'Phiếu quà tặng'; + case GiftCategory.product: + return 'Sản phẩm'; + case GiftCategory.service: + return 'Dịch vụ'; + case GiftCategory.discount: + return 'Giảm giá'; + case GiftCategory.experience: + return 'Trải nghiệm'; + } + } +} + +extension GiftStatusExtension on GiftStatus { + String get displayName { + switch (this) { + case GiftStatus.active: + return 'Có hiệu lực'; + case GiftStatus.used: + return 'Đã sử dụng'; + case GiftStatus.expired: + return 'Hết hạn'; + case GiftStatus.cancelled: + return 'Đã hủy'; + } + } +} + +extension ProjectTypeExtension on ProjectType { + String get displayName { + switch (this) { + case ProjectType.residential: + return 'Dân dụng'; + case ProjectType.commercial: + return 'Thương mại'; + case ProjectType.industrial: + return 'Công nghiệp'; + case ProjectType.infrastructure: + return 'Hạ tầng'; + case ProjectType.renovation: + return 'Cải tạo'; + case ProjectType.interior: + return 'Nội thất'; + case ProjectType.exterior: + return 'Ngoại thất'; + } + } +} + +extension QuoteStatusExtension on QuoteStatus { + String get displayName { + switch (this) { + case QuoteStatus.draft: + return 'Nháp'; + case QuoteStatus.sent: + return 'Đã gửi'; + case QuoteStatus.viewed: + return 'Đã xem'; + case QuoteStatus.accepted: + return 'Chấp nhận'; + case QuoteStatus.rejected: + return 'Từ chối'; + case QuoteStatus.expired: + return 'Hết hạn'; + case QuoteStatus.converted: + return 'Đã chuyển đổi'; + case QuoteStatus.cancelled: return 'Đã hủy'; } } - /// Check if project is active bool get isActive { - return this == ProjectStatus.planning || this == ProjectStatus.inProgress; + return this == QuoteStatus.sent || this == QuoteStatus.viewed; + } + + bool get isFinal { + return this == QuoteStatus.accepted || + this == QuoteStatus.rejected || + this == QuoteStatus.expired || + this == QuoteStatus.converted || + this == QuoteStatus.cancelled; } } -extension TransactionTypeExtension on TransactionType { - /// Check if transaction is earning points - bool get isEarning { - return this == TransactionType.earnedPurchase || - this == TransactionType.earnedReferral || - this == TransactionType.earnedPromotion || - this == TransactionType.earnedBonus; - } - - /// Check if transaction is spending points - bool get isSpending { - return this == TransactionType.redeemedReward || - this == TransactionType.redeemedDiscount; - } - - /// Get display name (Vietnamese) +extension RoomTypeExtension on RoomType { String get displayName { switch (this) { - case TransactionType.earnedPurchase: - return 'Mua hàng'; - case TransactionType.earnedReferral: - return 'Giới thiệu bạn bè'; - case TransactionType.earnedPromotion: - return 'Khuyến mãi'; - case TransactionType.earnedBonus: - return 'Thưởng'; - case TransactionType.redeemedReward: - return 'Đổi quà'; - case TransactionType.redeemedDiscount: - return 'Đổi giảm giá'; - case TransactionType.adjustment: - return 'Điều chỉnh'; - case TransactionType.expired: - return 'Hết hạn'; + case RoomType.support: + return 'Hỗ trợ'; + case RoomType.sales: + return 'Bán hàng'; + case RoomType.orderInquiry: + return 'Đơn hàng'; + case RoomType.quoteDiscussion: + return 'Báo giá'; + case RoomType.general: + return 'Chung'; + } + } +} + +extension ReminderTypeExtension on ReminderType { + String get displayName { + switch (this) { + case ReminderType.beforeDue: + return 'Trước hạn'; + case ReminderType.dueDate: + return 'Đến hạn'; + case ReminderType.overdue: + return 'Quá hạn'; + case ReminderType.finalNotice: + return 'Thông báo cuối'; } } } diff --git a/lib/core/database/models/enums.g.dart b/lib/core/database/models/enums.g.dart index 11eb54b..7d85e85 100644 --- a/lib/core/database/models/enums.g.dart +++ b/lib/core/database/models/enums.g.dart @@ -6,77 +6,36 @@ part of 'enums.dart'; // TypeAdapterGenerator // ************************************************************************** -class MemberTierAdapter extends TypeAdapter { +class UserRoleAdapter extends TypeAdapter { @override - final typeId = 20; + final typeId = 30; @override - MemberTier read(BinaryReader reader) { + UserRole read(BinaryReader reader) { switch (reader.readByte()) { case 0: - return MemberTier.gold; + return UserRole.customer; case 1: - return MemberTier.platinum; + return UserRole.distributor; case 2: - return MemberTier.diamond; - default: - return MemberTier.gold; - } - } - - @override - void write(BinaryWriter writer, MemberTier obj) { - switch (obj) { - case MemberTier.gold: - writer.writeByte(0); - case MemberTier.platinum: - writer.writeByte(1); - case MemberTier.diamond: - writer.writeByte(2); - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is MemberTierAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class UserTypeAdapter extends TypeAdapter { - @override - final typeId = 21; - - @override - UserType read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return UserType.contractor; - case 1: - return UserType.architect; - case 2: - return UserType.distributor; + return UserRole.admin; case 3: - return UserType.broker; + return UserRole.staff; default: - return UserType.contractor; + return UserRole.customer; } } @override - void write(BinaryWriter writer, UserType obj) { + void write(BinaryWriter writer, UserRole obj) { switch (obj) { - case UserType.contractor: + case UserRole.customer: writer.writeByte(0); - case UserType.architect: + case UserRole.distributor: writer.writeByte(1); - case UserType.distributor: + case UserRole.admin: writer.writeByte(2); - case UserType.broker: + case UserRole.staff: writer.writeByte(3); } } @@ -87,50 +46,160 @@ class UserTypeAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is UserTypeAdapter && + other is UserRoleAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class UserStatusAdapter extends TypeAdapter { + @override + final typeId = 31; + + @override + UserStatus read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return UserStatus.active; + case 1: + return UserStatus.inactive; + case 2: + return UserStatus.suspended; + case 3: + return UserStatus.pending; + default: + return UserStatus.active; + } + } + + @override + void write(BinaryWriter writer, UserStatus obj) { + switch (obj) { + case UserStatus.active: + writer.writeByte(0); + case UserStatus.inactive: + writer.writeByte(1); + case UserStatus.suspended: + writer.writeByte(2); + case UserStatus.pending: + writer.writeByte(3); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is UserStatusAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class LoyaltyTierAdapter extends TypeAdapter { + @override + final typeId = 32; + + @override + LoyaltyTier read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return LoyaltyTier.bronze; + case 1: + return LoyaltyTier.silver; + case 2: + return LoyaltyTier.gold; + case 3: + return LoyaltyTier.platinum; + case 4: + return LoyaltyTier.diamond; + case 5: + return LoyaltyTier.titan; + default: + return LoyaltyTier.bronze; + } + } + + @override + void write(BinaryWriter writer, LoyaltyTier obj) { + switch (obj) { + case LoyaltyTier.bronze: + writer.writeByte(0); + case LoyaltyTier.silver: + writer.writeByte(1); + case LoyaltyTier.gold: + writer.writeByte(2); + case LoyaltyTier.platinum: + writer.writeByte(3); + case LoyaltyTier.diamond: + writer.writeByte(4); + case LoyaltyTier.titan: + writer.writeByte(5); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LoyaltyTierAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } class OrderStatusAdapter extends TypeAdapter { @override - final typeId = 22; + final typeId = 33; @override OrderStatus read(BinaryReader reader) { switch (reader.readByte()) { case 0: - return OrderStatus.pending; + return OrderStatus.draft; case 1: - return OrderStatus.processing; + return OrderStatus.pending; case 2: - return OrderStatus.shipping; + return OrderStatus.confirmed; case 3: - return OrderStatus.completed; + return OrderStatus.processing; case 4: - return OrderStatus.cancelled; + return OrderStatus.shipped; case 5: + return OrderStatus.delivered; + case 6: + return OrderStatus.completed; + case 7: + return OrderStatus.cancelled; + case 8: return OrderStatus.refunded; default: - return OrderStatus.pending; + return OrderStatus.draft; } } @override void write(BinaryWriter writer, OrderStatus obj) { switch (obj) { - case OrderStatus.pending: + case OrderStatus.draft: writer.writeByte(0); - case OrderStatus.processing: + case OrderStatus.pending: writer.writeByte(1); - case OrderStatus.shipping: + case OrderStatus.confirmed: writer.writeByte(2); - case OrderStatus.completed: + case OrderStatus.processing: writer.writeByte(3); - case OrderStatus.cancelled: + case OrderStatus.shipped: writer.writeByte(4); - case OrderStatus.refunded: + case OrderStatus.delivered: writer.writeByte(5); + case OrderStatus.completed: + writer.writeByte(6); + case OrderStatus.cancelled: + writer.writeByte(7); + case OrderStatus.refunded: + writer.writeByte(8); } } @@ -145,41 +214,37 @@ class OrderStatusAdapter extends TypeAdapter { typeId == other.typeId; } -class ProjectStatusAdapter extends TypeAdapter { +class InvoiceTypeAdapter extends TypeAdapter { @override - final typeId = 23; + final typeId = 34; @override - ProjectStatus read(BinaryReader reader) { + InvoiceType read(BinaryReader reader) { switch (reader.readByte()) { case 0: - return ProjectStatus.planning; + return InvoiceType.sales; case 1: - return ProjectStatus.inProgress; + return InvoiceType.proforma; case 2: - return ProjectStatus.onHold; + return InvoiceType.creditNote; case 3: - return ProjectStatus.completed; - case 4: - return ProjectStatus.cancelled; + return InvoiceType.debitNote; default: - return ProjectStatus.planning; + return InvoiceType.sales; } } @override - void write(BinaryWriter writer, ProjectStatus obj) { + void write(BinaryWriter writer, InvoiceType obj) { switch (obj) { - case ProjectStatus.planning: + case InvoiceType.sales: writer.writeByte(0); - case ProjectStatus.inProgress: + case InvoiceType.proforma: writer.writeByte(1); - case ProjectStatus.onHold: + case InvoiceType.creditNote: writer.writeByte(2); - case ProjectStatus.completed: + case InvoiceType.debitNote: writer.writeByte(3); - case ProjectStatus.cancelled: - writer.writeByte(4); } } @@ -189,107 +254,54 @@ class ProjectStatusAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is ProjectStatusAdapter && + other is InvoiceTypeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } -class ProjectTypeAdapter extends TypeAdapter { +class InvoiceStatusAdapter extends TypeAdapter { @override - final typeId = 24; + final typeId = 35; @override - ProjectType read(BinaryReader reader) { + InvoiceStatus read(BinaryReader reader) { switch (reader.readByte()) { case 0: - return ProjectType.residential; + return InvoiceStatus.draft; case 1: - return ProjectType.commercial; + return InvoiceStatus.issued; case 2: - return ProjectType.industrial; + return InvoiceStatus.partiallyPaid; case 3: - return ProjectType.infrastructure; + return InvoiceStatus.paid; case 4: - return ProjectType.renovation; - default: - return ProjectType.residential; - } - } - - @override - void write(BinaryWriter writer, ProjectType obj) { - switch (obj) { - case ProjectType.residential: - writer.writeByte(0); - case ProjectType.commercial: - writer.writeByte(1); - case ProjectType.industrial: - writer.writeByte(2); - case ProjectType.infrastructure: - writer.writeByte(3); - case ProjectType.renovation: - writer.writeByte(4); - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ProjectTypeAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class TransactionTypeAdapter extends TypeAdapter { - @override - final typeId = 25; - - @override - TransactionType read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return TransactionType.earnedPurchase; - case 1: - return TransactionType.earnedReferral; - case 2: - return TransactionType.earnedPromotion; - case 3: - return TransactionType.earnedBonus; - case 4: - return TransactionType.redeemedReward; + return InvoiceStatus.overdue; case 5: - return TransactionType.redeemedDiscount; + return InvoiceStatus.cancelled; case 6: - return TransactionType.adjustment; - case 7: - return TransactionType.expired; + return InvoiceStatus.refunded; default: - return TransactionType.earnedPurchase; + return InvoiceStatus.draft; } } @override - void write(BinaryWriter writer, TransactionType obj) { + void write(BinaryWriter writer, InvoiceStatus obj) { switch (obj) { - case TransactionType.earnedPurchase: + case InvoiceStatus.draft: writer.writeByte(0); - case TransactionType.earnedReferral: + case InvoiceStatus.issued: writer.writeByte(1); - case TransactionType.earnedPromotion: + case InvoiceStatus.partiallyPaid: writer.writeByte(2); - case TransactionType.earnedBonus: + case InvoiceStatus.paid: writer.writeByte(3); - case TransactionType.redeemedReward: + case InvoiceStatus.overdue: writer.writeByte(4); - case TransactionType.redeemedDiscount: + case InvoiceStatus.cancelled: writer.writeByte(5); - case TransactionType.adjustment: + case InvoiceStatus.refunded: writer.writeByte(6); - case TransactionType.expired: - writer.writeByte(7); } } @@ -299,46 +311,54 @@ class TransactionTypeAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is TransactionTypeAdapter && + other is InvoiceStatusAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } -class GiftStatusAdapter extends TypeAdapter { +class PaymentMethodAdapter extends TypeAdapter { @override - final typeId = 26; + final typeId = 36; @override - GiftStatus read(BinaryReader reader) { + PaymentMethod read(BinaryReader reader) { switch (reader.readByte()) { case 0: - return GiftStatus.active; + return PaymentMethod.cash; case 1: - return GiftStatus.used; + return PaymentMethod.bankTransfer; case 2: - return GiftStatus.expired; + return PaymentMethod.creditCard; case 3: - return GiftStatus.reserved; + return PaymentMethod.debitCard; case 4: - return GiftStatus.cancelled; + return PaymentMethod.eWallet; + case 5: + return PaymentMethod.cheque; + case 6: + return PaymentMethod.creditTerm; default: - return GiftStatus.active; + return PaymentMethod.cash; } } @override - void write(BinaryWriter writer, GiftStatus obj) { + void write(BinaryWriter writer, PaymentMethod obj) { switch (obj) { - case GiftStatus.active: + case PaymentMethod.cash: writer.writeByte(0); - case GiftStatus.used: + case PaymentMethod.bankTransfer: writer.writeByte(1); - case GiftStatus.expired: + case PaymentMethod.creditCard: writer.writeByte(2); - case GiftStatus.reserved: + case PaymentMethod.debitCard: writer.writeByte(3); - case GiftStatus.cancelled: + case PaymentMethod.eWallet: writer.writeByte(4); + case PaymentMethod.cheque: + writer.writeByte(5); + case PaymentMethod.creditTerm: + writer.writeByte(6); } } @@ -348,14 +368,14 @@ class GiftStatusAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is GiftStatusAdapter && + other is PaymentMethodAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } class PaymentStatusAdapter extends TypeAdapter { @override - final typeId = 27; + final typeId = 37; @override PaymentStatus read(BinaryReader reader) { @@ -406,48 +426,342 @@ class PaymentStatusAdapter extends TypeAdapter { typeId == other.typeId; } -class NotificationTypeAdapter extends TypeAdapter { +class EntryTypeAdapter extends TypeAdapter { @override - final typeId = 28; + final typeId = 38; @override - NotificationType read(BinaryReader reader) { + EntryType read(BinaryReader reader) { switch (reader.readByte()) { case 0: - return NotificationType.order; + return EntryType.earn; case 1: - return NotificationType.promotion; + return EntryType.redeem; case 2: - return NotificationType.system; + return EntryType.adjustment; case 3: - return NotificationType.loyalty; + return EntryType.expiry; case 4: - return NotificationType.project; - case 5: - return NotificationType.payment; - case 6: - return NotificationType.message; + return EntryType.refund; default: - return NotificationType.order; + return EntryType.earn; } } @override - void write(BinaryWriter writer, NotificationType obj) { + void write(BinaryWriter writer, EntryType obj) { switch (obj) { - case NotificationType.order: + case EntryType.earn: writer.writeByte(0); - case NotificationType.promotion: + case EntryType.redeem: writer.writeByte(1); - case NotificationType.system: + case EntryType.adjustment: writer.writeByte(2); - case NotificationType.loyalty: + case EntryType.expiry: writer.writeByte(3); - case NotificationType.project: + case EntryType.refund: writer.writeByte(4); - case NotificationType.payment: + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is EntryTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class EntrySourceAdapter extends TypeAdapter { + @override + final typeId = 39; + + @override + EntrySource read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return EntrySource.purchase; + case 1: + return EntrySource.referral; + case 2: + return EntrySource.promotion; + case 3: + return EntrySource.bonus; + case 4: + return EntrySource.giftRedemption; + case 5: + return EntrySource.projectSubmission; + case 6: + return EntrySource.pointsRecord; + case 7: + return EntrySource.manualAdjustment; + default: + return EntrySource.purchase; + } + } + + @override + void write(BinaryWriter writer, EntrySource obj) { + switch (obj) { + case EntrySource.purchase: + writer.writeByte(0); + case EntrySource.referral: + writer.writeByte(1); + case EntrySource.promotion: + writer.writeByte(2); + case EntrySource.bonus: + writer.writeByte(3); + case EntrySource.giftRedemption: + writer.writeByte(4); + case EntrySource.projectSubmission: writer.writeByte(5); - case NotificationType.message: + case EntrySource.pointsRecord: + writer.writeByte(6); + case EntrySource.manualAdjustment: + writer.writeByte(7); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is EntrySourceAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class ComplaintStatusAdapter extends TypeAdapter { + @override + final typeId = 40; + + @override + ComplaintStatus read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return ComplaintStatus.none; + case 1: + return ComplaintStatus.pending; + case 2: + return ComplaintStatus.investigating; + case 3: + return ComplaintStatus.resolved; + case 4: + return ComplaintStatus.rejected; + default: + return ComplaintStatus.none; + } + } + + @override + void write(BinaryWriter writer, ComplaintStatus obj) { + switch (obj) { + case ComplaintStatus.none: + writer.writeByte(0); + case ComplaintStatus.pending: + writer.writeByte(1); + case ComplaintStatus.investigating: + writer.writeByte(2); + case ComplaintStatus.resolved: + writer.writeByte(3); + case ComplaintStatus.rejected: + writer.writeByte(4); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ComplaintStatusAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class GiftCategoryAdapter extends TypeAdapter { + @override + final typeId = 41; + + @override + GiftCategory read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return GiftCategory.voucher; + case 1: + return GiftCategory.product; + case 2: + return GiftCategory.service; + case 3: + return GiftCategory.discount; + case 4: + return GiftCategory.experience; + default: + return GiftCategory.voucher; + } + } + + @override + void write(BinaryWriter writer, GiftCategory obj) { + switch (obj) { + case GiftCategory.voucher: + writer.writeByte(0); + case GiftCategory.product: + writer.writeByte(1); + case GiftCategory.service: + writer.writeByte(2); + case GiftCategory.discount: + writer.writeByte(3); + case GiftCategory.experience: + writer.writeByte(4); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GiftCategoryAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class GiftStatusAdapter extends TypeAdapter { + @override + final typeId = 42; + + @override + GiftStatus read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return GiftStatus.active; + case 1: + return GiftStatus.used; + case 2: + return GiftStatus.expired; + case 3: + return GiftStatus.cancelled; + default: + return GiftStatus.active; + } + } + + @override + void write(BinaryWriter writer, GiftStatus obj) { + switch (obj) { + case GiftStatus.active: + writer.writeByte(0); + case GiftStatus.used: + writer.writeByte(1); + case GiftStatus.expired: + writer.writeByte(2); + case GiftStatus.cancelled: + writer.writeByte(3); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GiftStatusAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class PointsStatusAdapter extends TypeAdapter { + @override + final typeId = 43; + + @override + PointsStatus read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return PointsStatus.pending; + case 1: + return PointsStatus.approved; + case 2: + return PointsStatus.rejected; + default: + return PointsStatus.pending; + } + } + + @override + void write(BinaryWriter writer, PointsStatus obj) { + switch (obj) { + case PointsStatus.pending: + writer.writeByte(0); + case PointsStatus.approved: + writer.writeByte(1); + case PointsStatus.rejected: + writer.writeByte(2); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PointsStatusAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class ProjectTypeAdapter extends TypeAdapter { + @override + final typeId = 44; + + @override + ProjectType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return ProjectType.residential; + case 1: + return ProjectType.commercial; + case 2: + return ProjectType.industrial; + case 3: + return ProjectType.infrastructure; + case 4: + return ProjectType.renovation; + case 5: + return ProjectType.interior; + case 6: + return ProjectType.exterior; + default: + return ProjectType.residential; + } + } + + @override + void write(BinaryWriter writer, ProjectType obj) { + switch (obj) { + case ProjectType.residential: + writer.writeByte(0); + case ProjectType.commercial: + writer.writeByte(1); + case ProjectType.industrial: + writer.writeByte(2); + case ProjectType.infrastructure: + writer.writeByte(3); + case ProjectType.renovation: + writer.writeByte(4); + case ProjectType.interior: + writer.writeByte(5); + case ProjectType.exterior: writer.writeByte(6); } } @@ -458,50 +772,46 @@ class NotificationTypeAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is NotificationTypeAdapter && + other is ProjectTypeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } -class PaymentMethodAdapter extends TypeAdapter { +class SubmissionStatusAdapter extends TypeAdapter { @override - final typeId = 29; + final typeId = 45; @override - PaymentMethod read(BinaryReader reader) { + SubmissionStatus read(BinaryReader reader) { switch (reader.readByte()) { case 0: - return PaymentMethod.cashOnDelivery; + return SubmissionStatus.pending; case 1: - return PaymentMethod.bankTransfer; + return SubmissionStatus.reviewing; case 2: - return PaymentMethod.card; + return SubmissionStatus.approved; case 3: - return PaymentMethod.eWallet; + return SubmissionStatus.rejected; case 4: - return PaymentMethod.qrCode; - case 5: - return PaymentMethod.payLater; + return SubmissionStatus.needsRevision; default: - return PaymentMethod.cashOnDelivery; + return SubmissionStatus.pending; } } @override - void write(BinaryWriter writer, PaymentMethod obj) { + void write(BinaryWriter writer, SubmissionStatus obj) { switch (obj) { - case PaymentMethod.cashOnDelivery: + case SubmissionStatus.pending: writer.writeByte(0); - case PaymentMethod.bankTransfer: + case SubmissionStatus.reviewing: writer.writeByte(1); - case PaymentMethod.card: + case SubmissionStatus.approved: writer.writeByte(2); - case PaymentMethod.eWallet: + case SubmissionStatus.rejected: writer.writeByte(3); - case PaymentMethod.qrCode: + case SubmissionStatus.needsRevision: writer.writeByte(4); - case PaymentMethod.payLater: - writer.writeByte(5); } } @@ -511,7 +821,280 @@ class PaymentMethodAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is PaymentMethodAdapter && + other is SubmissionStatusAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class DesignStatusAdapter extends TypeAdapter { + @override + final typeId = 46; + + @override + DesignStatus read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return DesignStatus.pending; + case 1: + return DesignStatus.assigned; + case 2: + return DesignStatus.inProgress; + case 3: + return DesignStatus.reviewing; + case 4: + return DesignStatus.completed; + case 5: + return DesignStatus.cancelled; + case 6: + return DesignStatus.onHold; + default: + return DesignStatus.pending; + } + } + + @override + void write(BinaryWriter writer, DesignStatus obj) { + switch (obj) { + case DesignStatus.pending: + writer.writeByte(0); + case DesignStatus.assigned: + writer.writeByte(1); + case DesignStatus.inProgress: + writer.writeByte(2); + case DesignStatus.reviewing: + writer.writeByte(3); + case DesignStatus.completed: + writer.writeByte(4); + case DesignStatus.cancelled: + writer.writeByte(5); + case DesignStatus.onHold: + writer.writeByte(6); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DesignStatusAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class QuoteStatusAdapter extends TypeAdapter { + @override + final typeId = 47; + + @override + QuoteStatus read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return QuoteStatus.draft; + case 1: + return QuoteStatus.sent; + case 2: + return QuoteStatus.viewed; + case 3: + return QuoteStatus.accepted; + case 4: + return QuoteStatus.rejected; + case 5: + return QuoteStatus.expired; + case 6: + return QuoteStatus.converted; + case 7: + return QuoteStatus.cancelled; + default: + return QuoteStatus.draft; + } + } + + @override + void write(BinaryWriter writer, QuoteStatus obj) { + switch (obj) { + case QuoteStatus.draft: + writer.writeByte(0); + case QuoteStatus.sent: + writer.writeByte(1); + case QuoteStatus.viewed: + writer.writeByte(2); + case QuoteStatus.accepted: + writer.writeByte(3); + case QuoteStatus.rejected: + writer.writeByte(4); + case QuoteStatus.expired: + writer.writeByte(5); + case QuoteStatus.converted: + writer.writeByte(6); + case QuoteStatus.cancelled: + writer.writeByte(7); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is QuoteStatusAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class RoomTypeAdapter extends TypeAdapter { + @override + final typeId = 48; + + @override + RoomType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return RoomType.support; + case 1: + return RoomType.sales; + case 2: + return RoomType.orderInquiry; + case 3: + return RoomType.quoteDiscussion; + case 4: + return RoomType.general; + default: + return RoomType.support; + } + } + + @override + void write(BinaryWriter writer, RoomType obj) { + switch (obj) { + case RoomType.support: + writer.writeByte(0); + case RoomType.sales: + writer.writeByte(1); + case RoomType.orderInquiry: + writer.writeByte(2); + case RoomType.quoteDiscussion: + writer.writeByte(3); + case RoomType.general: + writer.writeByte(4); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RoomTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class ContentTypeAdapter extends TypeAdapter { + @override + final typeId = 49; + + @override + ContentType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return ContentType.text; + case 1: + return ContentType.image; + case 2: + return ContentType.file; + case 3: + return ContentType.video; + case 4: + return ContentType.audio; + case 5: + return ContentType.productReference; + case 6: + return ContentType.orderReference; + case 7: + return ContentType.quoteReference; + default: + return ContentType.text; + } + } + + @override + void write(BinaryWriter writer, ContentType obj) { + switch (obj) { + case ContentType.text: + writer.writeByte(0); + case ContentType.image: + writer.writeByte(1); + case ContentType.file: + writer.writeByte(2); + case ContentType.video: + writer.writeByte(3); + case ContentType.audio: + writer.writeByte(4); + case ContentType.productReference: + writer.writeByte(5); + case ContentType.orderReference: + writer.writeByte(6); + case ContentType.quoteReference: + writer.writeByte(7); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ContentTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class ReminderTypeAdapter extends TypeAdapter { + @override + final typeId = 50; + + @override + ReminderType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return ReminderType.beforeDue; + case 1: + return ReminderType.dueDate; + case 2: + return ReminderType.overdue; + case 3: + return ReminderType.finalNotice; + default: + return ReminderType.beforeDue; + } + } + + @override + void write(BinaryWriter writer, ReminderType obj) { + switch (obj) { + case ReminderType.beforeDue: + writer.writeByte(0); + case ReminderType.dueDate: + writer.writeByte(1); + case ReminderType.overdue: + writer.writeByte(2); + case ReminderType.finalNotice: + writer.writeByte(3); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ReminderTypeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } diff --git a/lib/features/account/data/models/audit_log_model.dart b/lib/features/account/data/models/audit_log_model.dart new file mode 100644 index 0000000..f4f0fa7 --- /dev/null +++ b/lib/features/account/data/models/audit_log_model.dart @@ -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 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 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(), + }; +} diff --git a/lib/features/account/data/models/audit_log_model.g.dart b/lib/features/account/data/models/audit_log_model.g.dart new file mode 100644 index 0000000..aac543b --- /dev/null +++ b/lib/features/account/data/models/audit_log_model.g.dart @@ -0,0 +1,68 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'audit_log_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class AuditLogModelAdapter extends TypeAdapter { + @override + final typeId = 24; + + @override + AuditLogModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/account/data/models/payment_reminder_model.dart b/lib/features/account/data/models/payment_reminder_model.dart new file mode 100644 index 0000000..2abe480 --- /dev/null +++ b/lib/features/account/data/models/payment_reminder_model.dart @@ -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 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 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(), + }; +} diff --git a/lib/features/account/data/models/payment_reminder_model.g.dart b/lib/features/account/data/models/payment_reminder_model.g.dart new file mode 100644 index 0000000..71f766f --- /dev/null +++ b/lib/features/account/data/models/payment_reminder_model.g.dart @@ -0,0 +1,71 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'payment_reminder_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class PaymentReminderModelAdapter extends TypeAdapter { + @override + final typeId = 23; + + @override + PaymentReminderModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/account/domain/entities/audit_log.dart b/lib/features/account/domain/entities/audit_log.dart new file mode 100644 index 0000000..25086de --- /dev/null +++ b/lib/features/account/domain/entities/audit_log.dart @@ -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? oldValue; + + /// New value (after change) + final Map? 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 get changedFields { + if (!hasOldValue || !hasNewValue) return []; + + final changed = []; + 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? oldValue, + Map? 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)'; + } +} diff --git a/lib/features/account/domain/entities/payment_reminder.dart b/lib/features/account/domain/entities/payment_reminder.dart new file mode 100644 index 0000000..3549c59 --- /dev/null +++ b/lib/features/account/domain/entities/payment_reminder.dart @@ -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)'; + } +} diff --git a/lib/features/auth/data/models/user_model.dart b/lib/features/auth/data/models/user_model.dart new file mode 100644 index 0000000..2f75748 --- /dev/null +++ b/lib/features/auth/data/models/user_model.dart @@ -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 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 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? get companyInfoMap { + if (companyInfo == null) return null; + try { + return jsonDecode(companyInfo!) as Map; + } catch (e) { + return null; + } + } + + /// Get attachments as List + List? get attachmentsList { + if (attachments == null) return null; + try { + return jsonDecode(attachments!) as List; + } 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; +} diff --git a/lib/features/auth/data/models/user_model.g.dart b/lib/features/auth/data/models/user_model.g.dart new file mode 100644 index 0000000..ac36e04 --- /dev/null +++ b/lib/features/auth/data/models/user_model.g.dart @@ -0,0 +1,98 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class UserModelAdapter extends TypeAdapter { + @override + final typeId = 0; + + @override + UserModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/auth/data/models/user_session_model.dart b/lib/features/auth/data/models/user_session_model.dart new file mode 100644 index 0000000..74a2cd7 --- /dev/null +++ b/lib/features/auth/data/models/user_session_model.dart @@ -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 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 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; +} diff --git a/lib/features/auth/data/models/user_session_model.g.dart b/lib/features/auth/data/models/user_session_model.g.dart new file mode 100644 index 0000000..8813405 --- /dev/null +++ b/lib/features/auth/data/models/user_session_model.g.dart @@ -0,0 +1,71 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_session_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class UserSessionModelAdapter extends TypeAdapter { + @override + final typeId = 1; + + @override + UserSessionModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/auth/domain/entities/user.dart b/lib/features/auth/domain/entities/user.dart new file mode 100644 index 0000000..2d82549 --- /dev/null +++ b/lib/features/auth/domain/entities/user.dart @@ -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 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 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 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? 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)'; + } +} diff --git a/lib/features/auth/domain/entities/user_session.dart b/lib/features/auth/domain/entities/user_session.dart new file mode 100644 index 0000000..28afe5c --- /dev/null +++ b/lib/features/auth/domain/entities/user_session.dart @@ -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)'; + } +} diff --git a/lib/features/cart/data/models/cart_item_model.dart b/lib/features/cart/data/models/cart_item_model.dart new file mode 100644 index 0000000..8bf6dc2 --- /dev/null +++ b/lib/features/cart/data/models/cart_item_model.dart @@ -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 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 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, + ); +} diff --git a/lib/features/cart/data/models/cart_item_model.g.dart b/lib/features/cart/data/models/cart_item_model.g.dart new file mode 100644 index 0000000..3f67d17 --- /dev/null +++ b/lib/features/cart/data/models/cart_item_model.g.dart @@ -0,0 +1,59 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cart_item_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class CartItemModelAdapter extends TypeAdapter { + @override + final typeId = 5; + + @override + CartItemModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/cart/data/models/cart_model.dart b/lib/features/cart/data/models/cart_model.dart new file mode 100644 index 0000000..be6a24a --- /dev/null +++ b/lib/features/cart/data/models/cart_model.dart @@ -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 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 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, + ); +} diff --git a/lib/features/cart/data/models/cart_model.g.dart b/lib/features/cart/data/models/cart_model.g.dart new file mode 100644 index 0000000..b193899 --- /dev/null +++ b/lib/features/cart/data/models/cart_model.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cart_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class CartModelAdapter extends TypeAdapter { + @override + final typeId = 4; + + @override + CartModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/cart/domain/entities/cart.dart b/lib/features/cart/domain/entities/cart.dart new file mode 100644 index 0000000..cc5fbe1 --- /dev/null +++ b/lib/features/cart/domain/entities/cart.dart @@ -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)'; + } +} diff --git a/lib/features/cart/domain/entities/cart_item.dart b/lib/features/cart/domain/entities/cart_item.dart new file mode 100644 index 0000000..317483d --- /dev/null +++ b/lib/features/cart/domain/entities/cart_item.dart @@ -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)'; + } +} diff --git a/lib/features/chat/data/models/chat_room_model.dart b/lib/features/chat/data/models/chat_room_model.dart new file mode 100644 index 0000000..0e62e52 --- /dev/null +++ b/lib/features/chat/data/models/chat_room_model.dart @@ -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 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 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? get participantsList { + try { + final decoded = jsonDecode(participants) as List; + return decoded.map((e) => e.toString()).toList(); + } catch (e) { + return null; + } + } +} diff --git a/lib/features/chat/data/models/chat_room_model.g.dart b/lib/features/chat/data/models/chat_room_model.g.dart new file mode 100644 index 0000000..1141bce --- /dev/null +++ b/lib/features/chat/data/models/chat_room_model.g.dart @@ -0,0 +1,68 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_room_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ChatRoomModelAdapter extends TypeAdapter { + @override + final typeId = 18; + + @override + ChatRoomModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/chat/data/models/message_model.dart b/lib/features/chat/data/models/message_model.dart new file mode 100644 index 0000000..6a50dda --- /dev/null +++ b/lib/features/chat/data/models/message_model.dart @@ -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 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 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? 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; + } + } +} diff --git a/lib/features/chat/data/models/message_model.g.dart b/lib/features/chat/data/models/message_model.g.dart new file mode 100644 index 0000000..22a25b5 --- /dev/null +++ b/lib/features/chat/data/models/message_model.g.dart @@ -0,0 +1,77 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'message_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class MessageModelAdapter extends TypeAdapter { + @override + final typeId = 19; + + @override + MessageModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/chat/domain/entities/chat_room.dart b/lib/features/chat/domain/entities/chat_room.dart new file mode 100644 index 0000000..9d92f25 --- /dev/null +++ b/lib/features/chat/domain/entities/chat_room.dart @@ -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 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? 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)'; + } +} diff --git a/lib/features/chat/domain/entities/message.dart b/lib/features/chat/domain/entities/message.dart new file mode 100644 index 0000000..6e8d972 --- /dev/null +++ b/lib/features/chat/domain/entities/message.dart @@ -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 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? 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)'; + } +} diff --git a/lib/features/home/data/models/member_card_model.dart b/lib/features/home/data/models/member_card_model.dart index dd55235..afbea83 100644 --- a/lib/features/home/data/models/member_card_model.dart +++ b/lib/features/home/data/models/member_card_model.dart @@ -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) diff --git a/lib/features/home/data/models/member_card_model.g.dart b/lib/features/home/data/models/member_card_model.g.dart index 12d959e..2e52cb8 100644 --- a/lib/features/home/data/models/member_card_model.g.dart +++ b/lib/features/home/data/models/member_card_model.g.dart @@ -8,7 +8,7 @@ part of 'member_card_model.dart'; class MemberCardModelAdapter extends TypeAdapter { @override - final typeId = 10; + final typeId = 25; @override MemberCardModel read(BinaryReader reader) { diff --git a/lib/features/home/data/models/promotion_model.dart b/lib/features/home/data/models/promotion_model.dart index f670a41..7a89e0f 100644 --- a/lib/features/home/data/models/promotion_model.dart +++ b/lib/features/home/data/models/promotion_model.dart @@ -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) diff --git a/lib/features/home/data/models/promotion_model.g.dart b/lib/features/home/data/models/promotion_model.g.dart index 15bde24..c8db03e 100644 --- a/lib/features/home/data/models/promotion_model.g.dart +++ b/lib/features/home/data/models/promotion_model.g.dart @@ -8,7 +8,7 @@ part of 'promotion_model.dart'; class PromotionModelAdapter extends TypeAdapter { @override - final typeId = 11; + final typeId = 26; @override PromotionModel read(BinaryReader reader) { diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart index d00fef8..1660910 100644 --- a/lib/features/home/presentation/pages/home_page.dart +++ b/lib/features/home/presentation/pages/home_page.dart @@ -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, diff --git a/lib/features/loyalty/data/models/gift_catalog_model.dart b/lib/features/loyalty/data/models/gift_catalog_model.dart new file mode 100644 index 0000000..2f096dd --- /dev/null +++ b/lib/features/loyalty/data/models/gift_catalog_model.dart @@ -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 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 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; + } +} diff --git a/lib/features/loyalty/data/models/gift_catalog_model.g.dart b/lib/features/loyalty/data/models/gift_catalog_model.g.dart new file mode 100644 index 0000000..e8b07cd --- /dev/null +++ b/lib/features/loyalty/data/models/gift_catalog_model.g.dart @@ -0,0 +1,83 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'gift_catalog_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class GiftCatalogModelAdapter extends TypeAdapter { + @override + final typeId = 11; + + @override + GiftCatalogModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/loyalty/data/models/loyalty_point_entry_model.dart b/lib/features/loyalty/data/models/loyalty_point_entry_model.dart new file mode 100644 index 0000000..946901b --- /dev/null +++ b/lib/features/loyalty/data/models/loyalty_point_entry_model.dart @@ -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 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 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? get complaintMap { + if (complaint == null) return null; + try { + return jsonDecode(complaint!) as Map; + } catch (e) { + return null; + } + } + + bool get isExpired => expiryDate != null && DateTime.now().isAfter(expiryDate!); + bool get hasComplaint => complaintStatus != ComplaintStatus.none; +} diff --git a/lib/features/loyalty/data/models/loyalty_point_entry_model.g.dart b/lib/features/loyalty/data/models/loyalty_point_entry_model.g.dart new file mode 100644 index 0000000..e88fcfd --- /dev/null +++ b/lib/features/loyalty/data/models/loyalty_point_entry_model.g.dart @@ -0,0 +1,81 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'loyalty_point_entry_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class LoyaltyPointEntryModelAdapter + extends TypeAdapter { + @override + final typeId = 10; + + @override + LoyaltyPointEntryModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/loyalty/data/models/points_record_model.dart b/lib/features/loyalty/data/models/points_record_model.dart new file mode 100644 index 0000000..99053e8 --- /dev/null +++ b/lib/features/loyalty/data/models/points_record_model.dart @@ -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 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 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? 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; + } + } +} diff --git a/lib/features/loyalty/data/models/points_record_model.g.dart b/lib/features/loyalty/data/models/points_record_model.g.dart new file mode 100644 index 0000000..eca62ae --- /dev/null +++ b/lib/features/loyalty/data/models/points_record_model.g.dart @@ -0,0 +1,80 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'points_record_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class PointsRecordModelAdapter extends TypeAdapter { + @override + final typeId = 13; + + @override + PointsRecordModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/loyalty/data/models/redeemed_gift_model.dart b/lib/features/loyalty/data/models/redeemed_gift_model.dart new file mode 100644 index 0000000..e7cfa8d --- /dev/null +++ b/lib/features/loyalty/data/models/redeemed_gift_model.dart @@ -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 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 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; +} diff --git a/lib/features/loyalty/data/models/redeemed_gift_model.g.dart b/lib/features/loyalty/data/models/redeemed_gift_model.g.dart new file mode 100644 index 0000000..a4a4582 --- /dev/null +++ b/lib/features/loyalty/data/models/redeemed_gift_model.g.dart @@ -0,0 +1,86 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'redeemed_gift_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class RedeemedGiftModelAdapter extends TypeAdapter { + @override + final typeId = 12; + + @override + RedeemedGiftModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/loyalty/domain/entities/gift_catalog.dart b/lib/features/loyalty/domain/entities/gift_catalog.dart new file mode 100644 index 0000000..7c1b2d0 --- /dev/null +++ b/lib/features/loyalty/domain/entities/gift_catalog.dart @@ -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)'; + } +} diff --git a/lib/features/loyalty/domain/entities/loyalty_point_entry.dart b/lib/features/loyalty/domain/entities/loyalty_point_entry.dart new file mode 100644 index 0000000..842f422 --- /dev/null +++ b/lib/features/loyalty/domain/entities/loyalty_point_entry.dart @@ -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? 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? 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)'; + } +} diff --git a/lib/features/loyalty/domain/entities/points_record.dart b/lib/features/loyalty/domain/entities/points_record.dart new file mode 100644 index 0000000..351c28c --- /dev/null +++ b/lib/features/loyalty/domain/entities/points_record.dart @@ -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 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? 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)'; + } +} diff --git a/lib/features/loyalty/domain/entities/redeemed_gift.dart b/lib/features/loyalty/domain/entities/redeemed_gift.dart new file mode 100644 index 0000000..5dd5aab --- /dev/null +++ b/lib/features/loyalty/domain/entities/redeemed_gift.dart @@ -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)'; + } +} diff --git a/lib/features/notifications/data/models/notification_model.dart b/lib/features/notifications/data/models/notification_model.dart new file mode 100644 index 0000000..907d2a9 --- /dev/null +++ b/lib/features/notifications/data/models/notification_model.dart @@ -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 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 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? get dataMap { + if (data == null) return null; + try { + return jsonDecode(data!) as Map; + } catch (e) { + return null; + } + } +} diff --git a/lib/features/notifications/data/models/notification_model.g.dart b/lib/features/notifications/data/models/notification_model.g.dart new file mode 100644 index 0000000..cd2575c --- /dev/null +++ b/lib/features/notifications/data/models/notification_model.g.dart @@ -0,0 +1,68 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'notification_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class NotificationModelAdapter extends TypeAdapter { + @override + final typeId = 20; + + @override + NotificationModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/notifications/domain/entities/notification.dart b/lib/features/notifications/domain/entities/notification.dart new file mode 100644 index 0000000..4213d64 --- /dev/null +++ b/lib/features/notifications/domain/entities/notification.dart @@ -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? 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? 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)'; + } +} diff --git a/lib/features/orders/data/models/invoice_model.dart b/lib/features/orders/data/models/invoice_model.dart new file mode 100644 index 0000000..b37a60e --- /dev/null +++ b/lib/features/orders/data/models/invoice_model.dart @@ -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 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 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; +} diff --git a/lib/features/orders/data/models/invoice_model.g.dart b/lib/features/orders/data/models/invoice_model.g.dart new file mode 100644 index 0000000..13cb5cc --- /dev/null +++ b/lib/features/orders/data/models/invoice_model.g.dart @@ -0,0 +1,104 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'invoice_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class InvoiceModelAdapter extends TypeAdapter { + @override + final typeId = 8; + + @override + InvoiceModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/orders/data/models/order_item_model.dart b/lib/features/orders/data/models/order_item_model.dart new file mode 100644 index 0000000..fbc8ebe --- /dev/null +++ b/lib/features/orders/data/models/order_item_model.dart @@ -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 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 toJson() => { + 'order_item_id': orderItemId, + 'order_id': orderId, + 'product_id': productId, + 'quantity': quantity, + 'unit_price': unitPrice, + 'discount_percent': discountPercent, + 'subtotal': subtotal, + 'notes': notes, + }; +} diff --git a/lib/features/orders/data/models/order_item_model.g.dart b/lib/features/orders/data/models/order_item_model.g.dart new file mode 100644 index 0000000..93e8690 --- /dev/null +++ b/lib/features/orders/data/models/order_item_model.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'order_item_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class OrderItemModelAdapter extends TypeAdapter { + @override + final typeId = 7; + + @override + OrderItemModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/orders/data/models/order_model.dart b/lib/features/orders/data/models/order_model.dart new file mode 100644 index 0000000..9fed55b --- /dev/null +++ b/lib/features/orders/data/models/order_model.dart @@ -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 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 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? get shippingAddressMap { + if (shippingAddress == null) return null; + try { + return jsonDecode(shippingAddress!) as Map; + } catch (e) { + return null; + } + } + + Map? get billingAddressMap { + if (billingAddress == null) return null; + try { + return jsonDecode(billingAddress!) as Map; + } catch (e) { + return null; + } + } +} diff --git a/lib/features/orders/data/models/order_model.g.dart b/lib/features/orders/data/models/order_model.g.dart new file mode 100644 index 0000000..39e6273 --- /dev/null +++ b/lib/features/orders/data/models/order_model.g.dart @@ -0,0 +1,92 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'order_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class OrderModelAdapter extends TypeAdapter { + @override + final typeId = 6; + + @override + OrderModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/orders/data/models/payment_line_model.dart b/lib/features/orders/data/models/payment_line_model.dart new file mode 100644 index 0000000..a2bd171 --- /dev/null +++ b/lib/features/orders/data/models/payment_line_model.dart @@ -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 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 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(), + }; +} diff --git a/lib/features/orders/data/models/payment_line_model.g.dart b/lib/features/orders/data/models/payment_line_model.g.dart new file mode 100644 index 0000000..acdbf2e --- /dev/null +++ b/lib/features/orders/data/models/payment_line_model.g.dart @@ -0,0 +1,83 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'payment_line_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class PaymentLineModelAdapter extends TypeAdapter { + @override + final typeId = 9; + + @override + PaymentLineModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/orders/domain/entities/invoice.dart b/lib/features/orders/domain/entities/invoice.dart new file mode 100644 index 0000000..7054a5c --- /dev/null +++ b/lib/features/orders/domain/entities/invoice.dart @@ -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)'; + } +} diff --git a/lib/features/orders/domain/entities/order.dart b/lib/features/orders/domain/entities/order.dart new file mode 100644 index 0000000..3030bc3 --- /dev/null +++ b/lib/features/orders/domain/entities/order.dart @@ -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 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 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)'; + } +} diff --git a/lib/features/orders/domain/entities/order_item.dart b/lib/features/orders/domain/entities/order_item.dart new file mode 100644 index 0000000..df32ba9 --- /dev/null +++ b/lib/features/orders/domain/entities/order_item.dart @@ -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)'; + } +} diff --git a/lib/features/orders/domain/entities/payment_line.dart b/lib/features/orders/domain/entities/payment_line.dart new file mode 100644 index 0000000..4d89e38 --- /dev/null +++ b/lib/features/orders/domain/entities/payment_line.dart @@ -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)'; + } +} diff --git a/lib/features/products/data/models/category_model.dart b/lib/features/products/data/models/category_model.dart index 6a038bf..b1dcad8 100644 --- a/lib/features/products/data/models/category_model.dart +++ b/lib/features/products/data/models/category_model.dart @@ -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) diff --git a/lib/features/products/data/models/category_model.g.dart b/lib/features/products/data/models/category_model.g.dart index c5226df..bff2b31 100644 --- a/lib/features/products/data/models/category_model.g.dart +++ b/lib/features/products/data/models/category_model.g.dart @@ -8,7 +8,7 @@ part of 'category_model.dart'; class CategoryModelAdapter extends TypeAdapter { @override - final typeId = 12; + final typeId = 27; @override CategoryModel read(BinaryReader reader) { diff --git a/lib/features/products/data/models/product_model.dart b/lib/features/products/data/models/product_model.dart index dd62e02..68ae32e 100644 --- a/lib/features/products/data/models/product_model.dart +++ b/lib/features/products/data/models/product_model.dart @@ -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 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 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? 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? get imageCaptionsMap { + if (imageCaptions == null) return null; + try { + final decoded = jsonDecode(imageCaptions!) as Map; + return decoded.map((key, value) => MapEntry(key, value.toString())); + } catch (e) { + return null; + } + } + + /// Get specifications as Map + Map? get specificationsMap { + if (specifications == null) return null; + try { + return jsonDecode(specifications!) as Map; + } 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, ); } } diff --git a/lib/features/products/data/models/product_model.g.dart b/lib/features/products/data/models/product_model.g.dart index 43f6cf9..6846ff2 100644 --- a/lib/features/products/data/models/product_model.g.dart +++ b/lib/features/products/data/models/product_model.g.dart @@ -8,7 +8,7 @@ part of 'product_model.dart'; class ProductModelAdapter extends TypeAdapter { @override - final typeId = 1; + final typeId = 2; @override ProductModel read(BinaryReader reader) { @@ -17,52 +17,61 @@ class ProductModelAdapter extends TypeAdapter { 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 diff --git a/lib/features/products/data/models/stock_level_model.dart b/lib/features/products/data/models/stock_level_model.dart new file mode 100644 index 0000000..d1f5cdb --- /dev/null +++ b/lib/features/products/data/models/stock_level_model.dart @@ -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 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 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, + ); + } +} diff --git a/lib/features/products/data/models/stock_level_model.g.dart b/lib/features/products/data/models/stock_level_model.g.dart new file mode 100644 index 0000000..ecbffb4 --- /dev/null +++ b/lib/features/products/data/models/stock_level_model.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'stock_level_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class StockLevelModelAdapter extends TypeAdapter { + @override + final typeId = 3; + + @override + StockLevelModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/products/domain/entities/product.dart b/lib/features/products/domain/entities/product.dart index a3eafb8..b8aa290 100644 --- a/lib/features/products/domain/entities/product.dart +++ b/lib/features/products/domain/entities/product.dart @@ -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 images; + + /// Image captions + final Map imageCaptions; + + /// 360-degree view link + final String? link360; + + /// Product specifications + final Map 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? images, + Map? imageCaptions, + String? link360, + Map? 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, ); } } diff --git a/lib/features/products/domain/entities/stock_level.dart b/lib/features/products/domain/entities/stock_level.dart new file mode 100644 index 0000000..539bf9f --- /dev/null +++ b/lib/features/products/domain/entities/stock_level.dart @@ -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)'; + } +} diff --git a/lib/features/products/presentation/pages/products_page.dart b/lib/features/products/presentation/pages/products_page.dart index 8cc2799..2fedaca 100644 --- a/lib/features/products/presentation/pages/products_page.dart +++ b/lib/features/products/presentation/pages/products_page.dart @@ -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, diff --git a/lib/features/projects/data/models/design_request_model.dart b/lib/features/projects/data/models/design_request_model.dart new file mode 100644 index 0000000..5fc7455 --- /dev/null +++ b/lib/features/projects/data/models/design_request_model.dart @@ -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 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 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(), + }; +} diff --git a/lib/features/projects/data/models/design_request_model.g.dart b/lib/features/projects/data/models/design_request_model.g.dart new file mode 100644 index 0000000..b141acf --- /dev/null +++ b/lib/features/projects/data/models/design_request_model.g.dart @@ -0,0 +1,98 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'design_request_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class DesignRequestModelAdapter extends TypeAdapter { + @override + final typeId = 15; + + @override + DesignRequestModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/projects/data/models/project_submission_model.dart b/lib/features/projects/data/models/project_submission_model.dart new file mode 100644 index 0000000..f47057c --- /dev/null +++ b/lib/features/projects/data/models/project_submission_model.dart @@ -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 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 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? 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? 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; + } + } +} diff --git a/lib/features/projects/data/models/project_submission_model.g.dart b/lib/features/projects/data/models/project_submission_model.g.dart new file mode 100644 index 0000000..3456bf0 --- /dev/null +++ b/lib/features/projects/data/models/project_submission_model.g.dart @@ -0,0 +1,87 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'project_submission_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ProjectSubmissionModelAdapter + extends TypeAdapter { + @override + final typeId = 14; + + @override + ProjectSubmissionModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/projects/domain/entities/design_request.dart b/lib/features/projects/domain/entities/design_request.dart new file mode 100644 index 0000000..37624c8 --- /dev/null +++ b/lib/features/projects/domain/entities/design_request.dart @@ -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 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? 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)'; + } +} diff --git a/lib/features/projects/domain/entities/project_submission.dart b/lib/features/projects/domain/entities/project_submission.dart new file mode 100644 index 0000000..3c391c1 --- /dev/null +++ b/lib/features/projects/domain/entities/project_submission.dart @@ -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 beforePhotos; + + /// After photos URLs + final List afterPhotos; + + /// Invoice/receipt URLs + final List 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? beforePhotos, + List? afterPhotos, + List? 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)'; + } +} diff --git a/lib/features/quotes/data/models/quote_item_model.dart b/lib/features/quotes/data/models/quote_item_model.dart new file mode 100644 index 0000000..86988f0 --- /dev/null +++ b/lib/features/quotes/data/models/quote_item_model.dart @@ -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 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 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; +} diff --git a/lib/features/quotes/data/models/quote_item_model.g.dart b/lib/features/quotes/data/models/quote_item_model.g.dart new file mode 100644 index 0000000..0004f9d --- /dev/null +++ b/lib/features/quotes/data/models/quote_item_model.g.dart @@ -0,0 +1,65 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'quote_item_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class QuoteItemModelAdapter extends TypeAdapter { + @override + final typeId = 17; + + @override + QuoteItemModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/quotes/data/models/quote_model.dart b/lib/features/quotes/data/models/quote_model.dart new file mode 100644 index 0000000..b77d3ef --- /dev/null +++ b/lib/features/quotes/data/models/quote_model.dart @@ -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 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 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; +} diff --git a/lib/features/quotes/data/models/quote_model.g.dart b/lib/features/quotes/data/models/quote_model.g.dart new file mode 100644 index 0000000..1d6afa2 --- /dev/null +++ b/lib/features/quotes/data/models/quote_model.g.dart @@ -0,0 +1,86 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'quote_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class QuoteModelAdapter extends TypeAdapter { + @override + final typeId = 16; + + @override + QuoteModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/quotes/domain/entities/quote.dart b/lib/features/quotes/domain/entities/quote.dart new file mode 100644 index 0000000..7c5544a --- /dev/null +++ b/lib/features/quotes/domain/entities/quote.dart @@ -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 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 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)'; + } +} diff --git a/lib/features/quotes/domain/entities/quote_item.dart b/lib/features/quotes/domain/entities/quote_item.dart new file mode 100644 index 0000000..88e7674 --- /dev/null +++ b/lib/features/quotes/domain/entities/quote_item.dart @@ -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)'; + } +} diff --git a/lib/features/showrooms/data/models/showroom_model.dart b/lib/features/showrooms/data/models/showroom_model.dart new file mode 100644 index 0000000..2e72ec6 --- /dev/null +++ b/lib/features/showrooms/data/models/showroom_model.dart @@ -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 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 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? 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; + } + } +} diff --git a/lib/features/showrooms/data/models/showroom_model.g.dart b/lib/features/showrooms/data/models/showroom_model.g.dart new file mode 100644 index 0000000..ef54e95 --- /dev/null +++ b/lib/features/showrooms/data/models/showroom_model.g.dart @@ -0,0 +1,80 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'showroom_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ShowroomModelAdapter extends TypeAdapter { + @override + final typeId = 21; + + @override + ShowroomModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/showrooms/data/models/showroom_product_model.dart b/lib/features/showrooms/data/models/showroom_product_model.dart new file mode 100644 index 0000000..d31c9b3 --- /dev/null +++ b/lib/features/showrooms/data/models/showroom_product_model.dart @@ -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 json) => ShowroomProductModel( + showroomId: json['showroom_id'] as String, + productId: json['product_id'] as String, + quantityUsed: (json['quantity_used'] as num).toDouble(), + ); + + Map toJson() => { + 'showroom_id': showroomId, + 'product_id': productId, + 'quantity_used': quantityUsed, + }; +} diff --git a/lib/features/showrooms/data/models/showroom_product_model.g.dart b/lib/features/showrooms/data/models/showroom_product_model.g.dart new file mode 100644 index 0000000..fb2bc35 --- /dev/null +++ b/lib/features/showrooms/data/models/showroom_product_model.g.dart @@ -0,0 +1,47 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'showroom_product_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ShowroomProductModelAdapter extends TypeAdapter { + @override + final typeId = 22; + + @override + ShowroomProductModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/showrooms/domain/entities/showroom.dart b/lib/features/showrooms/domain/entities/showroom.dart new file mode 100644 index 0000000..febdbaf --- /dev/null +++ b/lib/features/showrooms/domain/entities/showroom.dart @@ -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 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? 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)'; + } +} diff --git a/lib/features/showrooms/domain/entities/showroom_product.dart b/lib/features/showrooms/domain/entities/showroom_product.dart new file mode 100644 index 0000000..b0df39b --- /dev/null +++ b/lib/features/showrooms/domain/entities/showroom_product.dart @@ -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)'; + } +} diff --git a/lib/hive_registrar.g.dart b/lib/hive_registrar.g.dart index d618d7a..e994333 100644 --- a/lib/hive_registrar.g.dart +++ b/lib/hive_registrar.g.dart @@ -5,47 +5,141 @@ import 'package:hive_ce/hive.dart'; import 'package:worker/core/database/models/cached_data.dart'; import 'package:worker/core/database/models/enums.dart'; +import 'package:worker/features/account/data/models/audit_log_model.dart'; +import 'package:worker/features/account/data/models/payment_reminder_model.dart'; +import 'package:worker/features/auth/data/models/user_model.dart'; +import 'package:worker/features/auth/data/models/user_session_model.dart'; +import 'package:worker/features/cart/data/models/cart_item_model.dart'; +import 'package:worker/features/cart/data/models/cart_model.dart'; +import 'package:worker/features/chat/data/models/chat_room_model.dart'; +import 'package:worker/features/chat/data/models/message_model.dart'; import 'package:worker/features/home/data/models/member_card_model.dart'; import 'package:worker/features/home/data/models/promotion_model.dart'; +import 'package:worker/features/loyalty/data/models/gift_catalog_model.dart'; +import 'package:worker/features/loyalty/data/models/loyalty_point_entry_model.dart'; +import 'package:worker/features/loyalty/data/models/points_record_model.dart'; +import 'package:worker/features/loyalty/data/models/redeemed_gift_model.dart'; +import 'package:worker/features/notifications/data/models/notification_model.dart'; +import 'package:worker/features/orders/data/models/invoice_model.dart'; +import 'package:worker/features/orders/data/models/order_item_model.dart'; +import 'package:worker/features/orders/data/models/order_model.dart'; +import 'package:worker/features/orders/data/models/payment_line_model.dart'; import 'package:worker/features/products/data/models/category_model.dart'; import 'package:worker/features/products/data/models/product_model.dart'; +import 'package:worker/features/products/data/models/stock_level_model.dart'; +import 'package:worker/features/projects/data/models/design_request_model.dart'; +import 'package:worker/features/projects/data/models/project_submission_model.dart'; +import 'package:worker/features/quotes/data/models/quote_item_model.dart'; +import 'package:worker/features/quotes/data/models/quote_model.dart'; +import 'package:worker/features/showrooms/data/models/showroom_model.dart'; +import 'package:worker/features/showrooms/data/models/showroom_product_model.dart'; extension HiveRegistrar on HiveInterface { void registerAdapters() { + registerAdapter(AuditLogModelAdapter()); registerAdapter(CachedDataAdapter()); + registerAdapter(CartItemModelAdapter()); + registerAdapter(CartModelAdapter()); registerAdapter(CategoryModelAdapter()); + registerAdapter(ChatRoomModelAdapter()); + registerAdapter(ComplaintStatusAdapter()); + registerAdapter(ContentTypeAdapter()); + registerAdapter(DesignRequestModelAdapter()); + registerAdapter(DesignStatusAdapter()); + registerAdapter(EntrySourceAdapter()); + registerAdapter(EntryTypeAdapter()); + registerAdapter(GiftCatalogModelAdapter()); + registerAdapter(GiftCategoryAdapter()); registerAdapter(GiftStatusAdapter()); + registerAdapter(InvoiceModelAdapter()); + registerAdapter(InvoiceStatusAdapter()); + registerAdapter(InvoiceTypeAdapter()); + registerAdapter(LoyaltyPointEntryModelAdapter()); + registerAdapter(LoyaltyTierAdapter()); registerAdapter(MemberCardModelAdapter()); - registerAdapter(MemberTierAdapter()); - registerAdapter(NotificationTypeAdapter()); + registerAdapter(MessageModelAdapter()); + registerAdapter(NotificationModelAdapter()); + registerAdapter(OrderItemModelAdapter()); + registerAdapter(OrderModelAdapter()); registerAdapter(OrderStatusAdapter()); + registerAdapter(PaymentLineModelAdapter()); registerAdapter(PaymentMethodAdapter()); + registerAdapter(PaymentReminderModelAdapter()); registerAdapter(PaymentStatusAdapter()); + registerAdapter(PointsRecordModelAdapter()); + registerAdapter(PointsStatusAdapter()); registerAdapter(ProductModelAdapter()); - registerAdapter(ProjectStatusAdapter()); + registerAdapter(ProjectSubmissionModelAdapter()); registerAdapter(ProjectTypeAdapter()); registerAdapter(PromotionModelAdapter()); - registerAdapter(TransactionTypeAdapter()); - registerAdapter(UserTypeAdapter()); + registerAdapter(QuoteItemModelAdapter()); + registerAdapter(QuoteModelAdapter()); + registerAdapter(QuoteStatusAdapter()); + registerAdapter(RedeemedGiftModelAdapter()); + registerAdapter(ReminderTypeAdapter()); + registerAdapter(RoomTypeAdapter()); + registerAdapter(ShowroomModelAdapter()); + registerAdapter(ShowroomProductModelAdapter()); + registerAdapter(StockLevelModelAdapter()); + registerAdapter(SubmissionStatusAdapter()); + registerAdapter(UserModelAdapter()); + registerAdapter(UserRoleAdapter()); + registerAdapter(UserSessionModelAdapter()); + registerAdapter(UserStatusAdapter()); } } extension IsolatedHiveRegistrar on IsolatedHiveInterface { void registerAdapters() { + registerAdapter(AuditLogModelAdapter()); registerAdapter(CachedDataAdapter()); + registerAdapter(CartItemModelAdapter()); + registerAdapter(CartModelAdapter()); registerAdapter(CategoryModelAdapter()); + registerAdapter(ChatRoomModelAdapter()); + registerAdapter(ComplaintStatusAdapter()); + registerAdapter(ContentTypeAdapter()); + registerAdapter(DesignRequestModelAdapter()); + registerAdapter(DesignStatusAdapter()); + registerAdapter(EntrySourceAdapter()); + registerAdapter(EntryTypeAdapter()); + registerAdapter(GiftCatalogModelAdapter()); + registerAdapter(GiftCategoryAdapter()); registerAdapter(GiftStatusAdapter()); + registerAdapter(InvoiceModelAdapter()); + registerAdapter(InvoiceStatusAdapter()); + registerAdapter(InvoiceTypeAdapter()); + registerAdapter(LoyaltyPointEntryModelAdapter()); + registerAdapter(LoyaltyTierAdapter()); registerAdapter(MemberCardModelAdapter()); - registerAdapter(MemberTierAdapter()); - registerAdapter(NotificationTypeAdapter()); + registerAdapter(MessageModelAdapter()); + registerAdapter(NotificationModelAdapter()); + registerAdapter(OrderItemModelAdapter()); + registerAdapter(OrderModelAdapter()); registerAdapter(OrderStatusAdapter()); + registerAdapter(PaymentLineModelAdapter()); registerAdapter(PaymentMethodAdapter()); + registerAdapter(PaymentReminderModelAdapter()); registerAdapter(PaymentStatusAdapter()); + registerAdapter(PointsRecordModelAdapter()); + registerAdapter(PointsStatusAdapter()); registerAdapter(ProductModelAdapter()); - registerAdapter(ProjectStatusAdapter()); + registerAdapter(ProjectSubmissionModelAdapter()); registerAdapter(ProjectTypeAdapter()); registerAdapter(PromotionModelAdapter()); - registerAdapter(TransactionTypeAdapter()); - registerAdapter(UserTypeAdapter()); + registerAdapter(QuoteItemModelAdapter()); + registerAdapter(QuoteModelAdapter()); + registerAdapter(QuoteStatusAdapter()); + registerAdapter(RedeemedGiftModelAdapter()); + registerAdapter(ReminderTypeAdapter()); + registerAdapter(RoomTypeAdapter()); + registerAdapter(ShowroomModelAdapter()); + registerAdapter(ShowroomProductModelAdapter()); + registerAdapter(StockLevelModelAdapter()); + registerAdapter(SubmissionStatusAdapter()); + registerAdapter(UserModelAdapter()); + registerAdapter(UserRoleAdapter()); + registerAdapter(UserSessionModelAdapter()); + registerAdapter(UserStatusAdapter()); } }