update database
This commit is contained in:
580
CLAUDE.md
580
CLAUDE.md
@@ -1314,321 +1314,132 @@ class Auth extends _$Auth {
|
||||
}
|
||||
```
|
||||
|
||||
#### Cart State
|
||||
```dart
|
||||
@riverpod
|
||||
class Cart extends _$Cart {
|
||||
@override
|
||||
List<CartItem> 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<LoyaltyPointsData> build() async {
|
||||
return await ref.read(loyaltyRepositoryProvider).getLoyaltyPoints();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
return await ref.read(loyaltyRepositoryProvider).getLoyaltyPoints();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> 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<String> 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<OrderItemModel> 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<T> request<T>(String endpoint, {Map<String, dynamic>? data}) async {
|
||||
try {
|
||||
final response = await dio.post(endpoint, data: data);
|
||||
return _parseResponse<T>(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
|
||||
|
||||
Reference in New Issue
Block a user