md
This commit is contained in:
347
CLAUDE.md
347
CLAUDE.md
@@ -87,6 +87,86 @@ You have access to these expert subagents - USE THEM PROACTIVELY:
|
|||||||
- Support Vietnamese language (primary) and English (secondary)
|
- Support Vietnamese language (primary) and English (secondary)
|
||||||
- Mobile-first design optimized for phone screens
|
- Mobile-first design optimized for phone screens
|
||||||
|
|
||||||
|
### Hive Best Practices
|
||||||
|
**IMPORTANT: Box Type Management**
|
||||||
|
When working with Hive boxes, always use `Box<dynamic>` in data sources and apply `.whereType<T>()` for type-safe queries:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// ✅ CORRECT - Use Box<dynamic> with type filtering
|
||||||
|
Box<dynamic> get _box {
|
||||||
|
return Hive.box<dynamic>(HiveBoxNames.favoriteBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<FavoriteModel>> getAllFavorites(String userId) async {
|
||||||
|
try {
|
||||||
|
final favorites = _box.values
|
||||||
|
.whereType<FavoriteModel>() // Type-safe filtering
|
||||||
|
.where((fav) => fav.userId == userId)
|
||||||
|
.toList();
|
||||||
|
return favorites;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[DataSource] Error: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ INCORRECT - Will cause HiveError
|
||||||
|
Box<FavoriteModel> get _box {
|
||||||
|
return Hive.box<FavoriteModel>(HiveBoxNames.favoriteBox);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reason**: Hive boxes are opened as `Box<dynamic>` in the central HiveService. Re-opening with a specific type causes `HiveError: The box is already open and of type Box<dynamic>`.
|
||||||
|
|
||||||
|
### AppBar Standardization
|
||||||
|
**ALL AppBars must follow this standard pattern** (reference: `products_page.dart`):
|
||||||
|
|
||||||
|
```dart
|
||||||
|
AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||||
|
onPressed: () => context.pop(),
|
||||||
|
),
|
||||||
|
title: const Text('Page Title', style: TextStyle(color: Colors.black)),
|
||||||
|
elevation: AppBarSpecs.elevation,
|
||||||
|
backgroundColor: AppColors.white,
|
||||||
|
foregroundColor: AppColors.grey900,
|
||||||
|
centerTitle: false,
|
||||||
|
actions: [
|
||||||
|
// Custom actions here
|
||||||
|
const SizedBox(width: AppSpacing.sm), // Always end with spacing
|
||||||
|
],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Requirements**:
|
||||||
|
- Black back arrow with explicit color
|
||||||
|
- Black text title with TextStyle
|
||||||
|
- Left-aligned title (`centerTitle: false`)
|
||||||
|
- White background (`AppColors.white`)
|
||||||
|
- Use `AppBarSpecs.elevation` (not hardcoded values)
|
||||||
|
- Always add `SizedBox(width: AppSpacing.sm)` after actions
|
||||||
|
|
||||||
|
**For SliverAppBar** (in CustomScrollView):
|
||||||
|
```dart
|
||||||
|
SliverAppBar(
|
||||||
|
pinned: true,
|
||||||
|
backgroundColor: AppColors.white,
|
||||||
|
foregroundColor: AppColors.grey900,
|
||||||
|
elevation: AppBarSpecs.elevation,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||||
|
onPressed: () => context.pop(),
|
||||||
|
),
|
||||||
|
title: const Text('Page Title', style: TextStyle(color: Colors.black)),
|
||||||
|
centerTitle: false,
|
||||||
|
actions: [
|
||||||
|
// Custom actions
|
||||||
|
const SizedBox(width: AppSpacing.sm),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Worker App Project Structure
|
## Worker App Project Structure
|
||||||
@@ -593,6 +673,10 @@ final otpTimerProvider = StateNotifierProvider<OTPTimerNotifier, int>
|
|||||||
- **Quick Action Grid**:
|
- **Quick Action Grid**:
|
||||||
- 4-6 icon buttons for main features
|
- 4-6 icon buttons for main features
|
||||||
- Products, Loyalty, Orders, Projects, Promotions
|
- Products, Loyalty, Orders, Projects, Promotions
|
||||||
|
- **Cart badge**: Dynamic count from `cartItemCountProvider`
|
||||||
|
- Shows cart item count when > 0
|
||||||
|
- Hidden when cart is empty
|
||||||
|
- Updates in real-time across all pages
|
||||||
|
|
||||||
- **Bottom Navigation**:
|
- **Bottom Navigation**:
|
||||||
- 5 tabs: Home, Products, Loyalty, Account, More
|
- 5 tabs: Home, Products, Loyalty, Account, More
|
||||||
@@ -657,27 +741,55 @@ final loyaltyPointsProvider = AsyncNotifierProvider<LoyaltyPointsNotifier, Loyal
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Rewards Page
|
#### Rewards Page ✅ IMPLEMENTED
|
||||||
**Page**: `rewards_page.dart`
|
**Page**: `rewards_page.dart`
|
||||||
|
|
||||||
**Features**:
|
**Features**:
|
||||||
- Grid of available rewards to redeem
|
- **Points Balance Card**: Gradient blue card showing available points and expiring points
|
||||||
- Filter by category (Vouchers/Products/Services)
|
- **Category Filter Pills**: Horizontal scrollable chips (Tất cả/Voucher/Sản phẩm/Dịch vụ/Ưu đãi đặc biệt)
|
||||||
- Reward cards showing:
|
- **Gift Grid**: 2-column grid with 6 mock gifts
|
||||||
- Image
|
- **Reward Cards** (improved UI):
|
||||||
- Title and description
|
- 120px image with CachedNetworkImage
|
||||||
- Points cost
|
- Title and description (truncated)
|
||||||
- Expiration date
|
- Points cost and button **at bottom** (using Spacer())
|
||||||
- Redeem button (disabled if insufficient points)
|
- Button states: "Đổi quà" (enabled) or "Không đủ điểm" (disabled)
|
||||||
- Redemption confirmation dialog
|
- **Redemption Flow**:
|
||||||
- Success animation and gift code display
|
- Confirmation dialog with gift details and balance after redemption
|
||||||
|
- Success SnackBar with checkmark icon
|
||||||
|
- Points deduction via `loyaltyPointsProvider`
|
||||||
|
|
||||||
**Widgets**:
|
**Widgets** (Location: `lib/features/loyalty/presentation/`):
|
||||||
- `RewardCard`: Card with image, info, and redeem button
|
- `widgets/points_balance_card.dart`: Gradient card with points display
|
||||||
- `RewardRedemptionDialog`: Confirmation dialog
|
- `widgets/reward_card.dart`: Individual gift card with bottom-aligned action
|
||||||
- `GiftCodeDisplay`: Copy-able gift code after redemption
|
- `pages/rewards_page.dart`: Main rewards screen
|
||||||
|
|
||||||
**Design Reference**: `html/loyalty-rewards.html`
|
**State Management**:
|
||||||
|
```dart
|
||||||
|
// Providers in lib/features/loyalty/presentation/providers/
|
||||||
|
@riverpod
|
||||||
|
class LoyaltyPoints extends _$LoyaltyPoints {
|
||||||
|
// Manages 9,750 available points, 1,200 expiring
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class Gifts extends _$Gifts {
|
||||||
|
// 6 mock gifts matching HTML design
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
List<GiftCatalog> filteredGifts(ref) {
|
||||||
|
// Filters by selected category
|
||||||
|
}
|
||||||
|
|
||||||
|
final selectedGiftCategoryProvider = StateNotifierProvider...
|
||||||
|
final hasEnoughPointsProvider = Provider.family<bool, int>...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Navigation**:
|
||||||
|
- Route: `/loyalty/rewards` (RouteNames in app_router.dart)
|
||||||
|
- Accessible from home quick action "Đổi quà"
|
||||||
|
|
||||||
|
**Design Reference**: `html/loyalty-rewards.html` ✅
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1275,6 +1387,46 @@ class FABSpecs {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### AppBar (Standardized across all pages)
|
||||||
|
```dart
|
||||||
|
class AppBarSpecs {
|
||||||
|
// From ui_constants.dart
|
||||||
|
static const double elevation = 0.5;
|
||||||
|
|
||||||
|
// Standard pattern for all pages
|
||||||
|
static AppBar standard({
|
||||||
|
required String title,
|
||||||
|
required VoidCallback onBack,
|
||||||
|
List<Widget>? actions,
|
||||||
|
}) {
|
||||||
|
return AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||||
|
onPressed: onBack,
|
||||||
|
),
|
||||||
|
title: Text(title, style: const TextStyle(color: Colors.black)),
|
||||||
|
elevation: elevation,
|
||||||
|
backgroundColor: AppColors.white,
|
||||||
|
foregroundColor: AppColors.grey900,
|
||||||
|
centerTitle: false,
|
||||||
|
actions: [
|
||||||
|
...?actions,
|
||||||
|
const SizedBox(width: AppSpacing.sm),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage Notes**:
|
||||||
|
- ALL pages use this standard AppBar pattern
|
||||||
|
- Back arrow is always black with explicit color
|
||||||
|
- Title is always left-aligned (`centerTitle: false`)
|
||||||
|
- Title text is always black
|
||||||
|
- Background is always white
|
||||||
|
- Actions always end with `SizedBox(width: AppSpacing.sm)` spacing
|
||||||
|
- For SliverAppBar, add `pinned: true` property
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Technical Architecture
|
## Technical Architecture
|
||||||
@@ -1761,16 +1913,32 @@ end
|
|||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
### Phase 1 - Complete ✅
|
### Phase 1 - Core Features ✅
|
||||||
- Authentication (login, OTP, register)
|
- ✅ Authentication (login, OTP, register)
|
||||||
- Home with member cards (3 tiers)
|
- ✅ Home with member cards (3 tiers)
|
||||||
- Complete loyalty system
|
- ✅ Product browsing with search and filters
|
||||||
- Product browsing and cart
|
- ✅ Shopping cart with dynamic badge
|
||||||
- Order management
|
- ✅ Favorites system with Hive persistence
|
||||||
- Project and quote management
|
- ✅ Order management
|
||||||
- Chat support
|
- ✅ Project and quote management
|
||||||
- Account management
|
- ✅ Account management
|
||||||
- Promotions and notifications
|
- ✅ Promotions with detail pages
|
||||||
|
|
||||||
|
### Phase 1.5 - Loyalty System ✅ RECENT
|
||||||
|
- ✅ **Rewards/Gift Redemption Page** (Dec 2024)
|
||||||
|
- Points balance card with gradient
|
||||||
|
- Category filtering (Voucher/Product/Service/Discount)
|
||||||
|
- 2-column gift grid with 6 mock items
|
||||||
|
- Redemption flow with confirmation dialog
|
||||||
|
- Points deduction and success feedback
|
||||||
|
- ✅ **UI Standardization** (Dec 2024)
|
||||||
|
- All AppBars now follow consistent pattern
|
||||||
|
- Dynamic cart badge across all pages
|
||||||
|
- Hive Box<dynamic> best practices documented
|
||||||
|
- 🔄 Points history page (planned)
|
||||||
|
- 🔄 Referral program page (planned)
|
||||||
|
- 🔄 My gifts page (planned)
|
||||||
|
- 🔄 Chat support (planned)
|
||||||
|
|
||||||
### Phase 2 - Backend Integration 🔄
|
### Phase 2 - Backend Integration 🔄
|
||||||
- REST API integration
|
- REST API integration
|
||||||
@@ -1803,3 +1971,130 @@ When working on this Flutter Worker app:
|
|||||||
- **Performance Issues** → performance-expert
|
- **Performance Issues** → performance-expert
|
||||||
|
|
||||||
**Think delegation first, implementation second!**
|
**Think delegation first, implementation second!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recent Updates & Implementation Notes (December 2024)
|
||||||
|
|
||||||
|
### ✅ Rewards/Gift Redemption System
|
||||||
|
**Status**: Fully implemented with mock data
|
||||||
|
|
||||||
|
**Implementation Details**:
|
||||||
|
1. **Providers** (`lib/features/loyalty/presentation/providers/`):
|
||||||
|
- `loyalty_points_provider.dart` - Manages user points (9,750 available, 1,200 expiring)
|
||||||
|
- `gifts_provider.dart` - 6 mock gifts matching HTML design
|
||||||
|
- `selectedGiftCategoryProvider` - Category filter state
|
||||||
|
- `filteredGiftsProvider` - Category-based filtering
|
||||||
|
- `hasEnoughPointsProvider.family` - Points validation per gift
|
||||||
|
|
||||||
|
2. **Widgets** (`lib/features/loyalty/presentation/widgets/`):
|
||||||
|
- `points_balance_card.dart` - Gradient blue card with points display
|
||||||
|
- `reward_card.dart` - Gift card with bottom-aligned action button
|
||||||
|
|
||||||
|
3. **Pages** (`lib/features/loyalty/presentation/pages/`):
|
||||||
|
- `rewards_page.dart` - Main screen with RefreshIndicator
|
||||||
|
|
||||||
|
4. **Route**: `/loyalty/rewards` in `app_router.dart`
|
||||||
|
|
||||||
|
**Key Features**:
|
||||||
|
- 2-column grid layout with `childAspectRatio: 0.7`
|
||||||
|
- Category filtering: Tất cả/Voucher/Sản phẩm/Dịch vụ/Ưu đãi đặc biệt
|
||||||
|
- Bottom-aligned buttons using `Expanded` + `Spacer()`
|
||||||
|
- Redemption dialog with points calculation
|
||||||
|
- Success feedback with SnackBar
|
||||||
|
|
||||||
|
### ✅ AppBar Standardization
|
||||||
|
**Status**: Completed across all pages
|
||||||
|
|
||||||
|
**Standard Pattern**:
|
||||||
|
```dart
|
||||||
|
AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||||
|
onPressed: () => context.pop(),
|
||||||
|
),
|
||||||
|
title: const Text('Title', style: TextStyle(color: Colors.black)),
|
||||||
|
elevation: AppBarSpecs.elevation,
|
||||||
|
backgroundColor: AppColors.white,
|
||||||
|
foregroundColor: AppColors.grey900,
|
||||||
|
centerTitle: false,
|
||||||
|
actions: [..., const SizedBox(width: AppSpacing.sm)],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Updated Pages**:
|
||||||
|
- `cart_page.dart` - Lines 84-103
|
||||||
|
- `favorites_page.dart` - Lines 79-115
|
||||||
|
- `rewards_page.dart` - Lines 35-48
|
||||||
|
- `promotion_detail_page.dart` - Lines 59-86 (loading/error), 130-161 (SliverAppBar)
|
||||||
|
- `product_detail_page.dart` - Already correct ✅
|
||||||
|
|
||||||
|
### ✅ Dynamic Cart Badge
|
||||||
|
**Status**: Implemented across home and products pages
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
```dart
|
||||||
|
// Added provider in cart_provider.dart
|
||||||
|
@riverpod
|
||||||
|
int cartItemCount(CartItemCountRef ref) {
|
||||||
|
final cartState = ref.watch(cartProvider);
|
||||||
|
return cartState.items.fold(0, (sum, item) => sum + item.quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used in home_page.dart and products_page.dart
|
||||||
|
final cartItemCount = ref.watch(cartItemCountProvider);
|
||||||
|
QuickAction(
|
||||||
|
badge: cartItemCount > 0 ? '$cartItemCount' : null,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Behavior**:
|
||||||
|
- Shows total quantity across all cart items
|
||||||
|
- Hidden when cart is empty
|
||||||
|
- Updates in real-time via Riverpod
|
||||||
|
- Navigation via `RouteNames.cart` constant
|
||||||
|
|
||||||
|
### ✅ Hive Box Type Management
|
||||||
|
**Status**: Best practices documented and implemented
|
||||||
|
|
||||||
|
**Problem Solved**:
|
||||||
|
```
|
||||||
|
HiveError: The box "favorite_box" is already open and of type Box<dynamic>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution Applied**:
|
||||||
|
- All data sources now use `Box<dynamic>` getters
|
||||||
|
- Type-safe queries via `.whereType<T>()`
|
||||||
|
- Applied to `favorites_local_datasource.dart`
|
||||||
|
|
||||||
|
**Pattern**:
|
||||||
|
```dart
|
||||||
|
Box<dynamic> get _box => Hive.box<dynamic>(boxName);
|
||||||
|
|
||||||
|
Future<List<FavoriteModel>> getAllFavorites() async {
|
||||||
|
return _box.values
|
||||||
|
.whereType<FavoriteModel>() // Type-safe!
|
||||||
|
.where((fav) => fav.userId == userId)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔄 Next Steps (Planned)
|
||||||
|
1. Points history page with transaction list
|
||||||
|
2. Referral program page with share functionality
|
||||||
|
3. My gifts page with tabs (Active/Used/Expired)
|
||||||
|
4. Backend API integration for loyalty endpoints
|
||||||
|
5. Real redemption flow with gift codes
|
||||||
|
|
||||||
|
### 📝 Code Quality Checklist
|
||||||
|
All recent implementations follow:
|
||||||
|
- ✅ Clean architecture (data/domain/presentation)
|
||||||
|
- ✅ Riverpod state management
|
||||||
|
- ✅ Hive for local persistence
|
||||||
|
- ✅ Material 3 design system
|
||||||
|
- ✅ Vietnamese localization
|
||||||
|
- ✅ AppBar standardization
|
||||||
|
- ✅ CachedNetworkImage for all remote images
|
||||||
|
- ✅ Proper error handling
|
||||||
|
- ✅ Loading states (CircularProgressIndicator)
|
||||||
|
- ✅ Empty states with helpful messages
|
||||||
|
|||||||
Reference in New Issue
Block a user