add price policy
This commit is contained in:
718
CLAUDE.md
718
CLAUDE.md
@@ -19,6 +19,16 @@ A Flutter-based mobile application designed for contractors, distributors, archi
|
||||
### 📁 Reference Materials:
|
||||
The `html/` folder contains UI/UX reference mockups that show the desired design and flow. These HTML files serve as design specifications for the Flutter implementation.
|
||||
|
||||
### 📝 Code Examples:
|
||||
All Dart code examples, patterns, and snippets are maintained in **CODE_EXAMPLES.md**. Refer to that document for:
|
||||
- Best practices (Hive, AppBar standardization)
|
||||
- UI/UX components (colors, typography, specs)
|
||||
- State management patterns
|
||||
- Performance optimization
|
||||
- Offline strategies
|
||||
- Localization setup
|
||||
- Deployment configurations
|
||||
|
||||
---
|
||||
|
||||
## 🤖 SUBAGENT DELEGATION SYSTEM 🤖
|
||||
@@ -89,55 +99,14 @@ You have access to these expert subagents - USE THEM PROACTIVELY:
|
||||
|
||||
### 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);
|
||||
}
|
||||
```
|
||||
When working with Hive boxes, always use `Box<dynamic>` in data sources and apply `.whereType<T>()` for type-safe queries.
|
||||
|
||||
**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`):
|
||||
**See CODE_EXAMPLES.md → Best Practices → Hive Box Type Management** for correct and incorrect patterns.
|
||||
|
||||
```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
|
||||
],
|
||||
)
|
||||
```
|
||||
### AppBar Standardization
|
||||
**ALL AppBars must follow this standard pattern** (reference: `products_page.dart`).
|
||||
|
||||
**Key Requirements**:
|
||||
- Black back arrow with explicit color
|
||||
@@ -147,25 +116,7 @@ AppBar(
|
||||
- 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),
|
||||
],
|
||||
)
|
||||
```
|
||||
**See CODE_EXAMPLES.md → Best Practices → AppBar Standardization** for standard AppBar and SliverAppBar patterns.
|
||||
|
||||
---
|
||||
|
||||
@@ -641,11 +592,7 @@ The `html/` folder contains 25+ HTML mockup files that serve as UI/UX design ref
|
||||
- Full registration form with user type selection
|
||||
- Form validation for all fields
|
||||
|
||||
**State Management**:
|
||||
```dart
|
||||
final authProvider = AsyncNotifierProvider<AuthNotifier, AuthState>
|
||||
final otpTimerProvider = StateNotifierProvider<OTPTimerNotifier, int>
|
||||
```
|
||||
**State Management**: See **CODE_EXAMPLES.md → State Management → Authentication Providers**
|
||||
|
||||
**Key Widgets**:
|
||||
- `PhoneInputField`: Vietnamese phone number format (+84)
|
||||
@@ -688,19 +635,7 @@ final otpTimerProvider = StateNotifierProvider<OTPTimerNotifier, int>
|
||||
- Positioned bottom-right
|
||||
- Accent cyan color (#35C6F4)
|
||||
|
||||
**State Management**:
|
||||
```dart
|
||||
final memberCardProvider = Provider<MemberCard>((ref) {
|
||||
final user = ref.watch(authProvider).user;
|
||||
return MemberCard(
|
||||
tier: user.memberTier,
|
||||
name: user.name,
|
||||
memberId: user.id,
|
||||
points: user.points,
|
||||
qrCode: generateQRCode(user.id),
|
||||
);
|
||||
});
|
||||
```
|
||||
**State Management**: See **CODE_EXAMPLES.md → State Management → Home Providers**
|
||||
|
||||
**Design Reference**: `html/index.html`
|
||||
|
||||
@@ -732,10 +667,7 @@ final memberCardProvider = Provider<MemberCard>((ref) {
|
||||
- `PointsBadge`: Circular points display
|
||||
- `TierBenefitsCard`: Expandable card with benefits list
|
||||
|
||||
**State Management**:
|
||||
```dart
|
||||
final loyaltyPointsProvider = AsyncNotifierProvider<LoyaltyPointsNotifier, LoyaltyPoints>
|
||||
```
|
||||
**State Management**: See **CODE_EXAMPLES.md → State Management → Loyalty Providers**
|
||||
|
||||
**Design Reference**: `html/loyalty.html`
|
||||
|
||||
@@ -763,27 +695,7 @@ final loyaltyPointsProvider = AsyncNotifierProvider<LoyaltyPointsNotifier, Loyal
|
||||
- `widgets/reward_card.dart`: Individual gift card with bottom-aligned action
|
||||
- `pages/rewards_page.dart`: Main rewards screen
|
||||
|
||||
**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>...
|
||||
```
|
||||
**State Management**: See **CODE_EXAMPLES.md → State Management → Loyalty Providers → Rewards Page Providers**
|
||||
|
||||
**Navigation**:
|
||||
- Route: `/loyalty/rewards` (RouteNames in app_router.dart)
|
||||
@@ -836,10 +748,7 @@ final hasEnoughPointsProvider = Provider.family<bool, int>...
|
||||
- `ReferralLinkShare`: Link with copy/share buttons
|
||||
- `ReferralShareSheet`: Bottom sheet with share options
|
||||
|
||||
**State Management**:
|
||||
```dart
|
||||
final referralProvider = AsyncNotifierProvider<ReferralNotifier, Referral>
|
||||
```
|
||||
**State Management**: See **CODE_EXAMPLES.md → State Management → Referral Provider**
|
||||
|
||||
**Design Reference**: `html/referral.html`
|
||||
|
||||
@@ -894,12 +803,7 @@ final referralProvider = AsyncNotifierProvider<ReferralNotifier, Referral>
|
||||
- `ProductSearchBar`: Search with clear button
|
||||
- `CategoryFilterChips`: Horizontal chip list
|
||||
|
||||
**State Management**:
|
||||
```dart
|
||||
final productsProvider = AsyncNotifierProvider<ProductsNotifier, List<Product>>
|
||||
final productSearchProvider = StateProvider<String>
|
||||
final selectedCategoryProvider = StateProvider<String?>
|
||||
```
|
||||
**State Management**: See **CODE_EXAMPLES.md → State Management → Products Providers**
|
||||
|
||||
**Design Reference**: `html/products.html`
|
||||
|
||||
@@ -938,11 +842,7 @@ final selectedCategoryProvider = StateProvider<String?>
|
||||
- Order details summary
|
||||
- Action buttons: View order, Continue shopping
|
||||
|
||||
**State Management**:
|
||||
```dart
|
||||
final cartProvider = NotifierProvider<CartNotifier, List<CartItem>>
|
||||
final cartTotalProvider = Provider<double>
|
||||
```
|
||||
**State Management**: See **CODE_EXAMPLES.md → State Management → Cart Providers**
|
||||
|
||||
**Design Reference**: `html/cart.html`, `html/checkout.html`, `html/order-success.html`
|
||||
|
||||
@@ -985,12 +885,7 @@ final cartTotalProvider = Provider<double>
|
||||
- Status (Processing/Completed)
|
||||
- Search and filter options
|
||||
|
||||
**State Management**:
|
||||
```dart
|
||||
final ordersProvider = AsyncNotifierProvider<OrdersNotifier, List<Order>>
|
||||
final orderFilterProvider = StateProvider<OrderStatus?>
|
||||
final paymentsProvider = AsyncNotifierProvider<PaymentsNotifier, List<Payment>>
|
||||
```
|
||||
**State Management**: See **CODE_EXAMPLES.md → State Management → Orders Providers**
|
||||
|
||||
**Design Reference**: `html/orders.html`, `html/payments.html`
|
||||
|
||||
@@ -1029,11 +924,7 @@ final paymentsProvider = AsyncNotifierProvider<PaymentsNotifier, List<Payment>>
|
||||
- Date pickers
|
||||
- Auto-generate project code option
|
||||
|
||||
**State Management**:
|
||||
```dart
|
||||
final projectsProvider = AsyncNotifierProvider<ProjectsNotifier, List<Project>>
|
||||
final projectFormProvider = StateNotifierProvider<ProjectFormNotifier, ProjectFormState>
|
||||
```
|
||||
**State Management**: See **CODE_EXAMPLES.md → State Management → Projects Providers**
|
||||
|
||||
**Design Reference**: `html/projects.html`, `html/project-create.html`
|
||||
|
||||
@@ -1084,12 +975,7 @@ final projectFormProvider = StateNotifierProvider<ProjectFormNotifier, ProjectFo
|
||||
- `TypingIndicator`: Animated dots
|
||||
- `ChatAppBar`: Custom app bar with agent info
|
||||
|
||||
**State Management**:
|
||||
```dart
|
||||
final chatProvider = AsyncNotifierProvider<ChatNotifier, ChatRoom>
|
||||
final messagesProvider = StreamProvider<List<Message>>
|
||||
final typingIndicatorProvider = StateProvider<bool>
|
||||
```
|
||||
**State Management**: See **CODE_EXAMPLES.md → State Management → Chat Providers**
|
||||
|
||||
**Design Reference**: `html/chat.html`
|
||||
|
||||
@@ -1240,186 +1126,30 @@ final typingIndicatorProvider = StateProvider<bool>
|
||||
## UI/UX Design System
|
||||
|
||||
### Color Palette
|
||||
```dart
|
||||
// colors.dart
|
||||
class AppColors {
|
||||
// Primary
|
||||
static const primaryBlue = Color(0xFF005B9A);
|
||||
static const lightBlue = Color(0xFF38B6FF);
|
||||
static const accentCyan = Color(0xFF35C6F4);
|
||||
|
||||
// Status
|
||||
static const success = Color(0xFF28a745);
|
||||
static const warning = Color(0xFFffc107);
|
||||
static const danger = Color(0xFFdc3545);
|
||||
static const info = Color(0xFF17a2b8);
|
||||
|
||||
// Neutrals
|
||||
static const grey50 = Color(0xFFf8f9fa);
|
||||
static const grey100 = Color(0xFFe9ecef);
|
||||
static const grey500 = Color(0xFF6c757d);
|
||||
static const grey900 = Color(0xFF343a40);
|
||||
|
||||
// Tier Gradients
|
||||
static const diamondGradient = LinearGradient(
|
||||
colors: [Color(0xFF4A00E0), Color(0xFF8E2DE2)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
);
|
||||
|
||||
static const platinumGradient = LinearGradient(
|
||||
colors: [Color(0xFF7F8C8D), Color(0xFFBDC3C7)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
);
|
||||
|
||||
static const goldGradient = LinearGradient(
|
||||
colors: [Color(0xFFf7b733), Color(0xFFfc4a1a)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
);
|
||||
}
|
||||
```
|
||||
See **CODE_EXAMPLES.md → UI/UX Components → Color Palette** for the complete color system including:
|
||||
- Primary colors (blue shades)
|
||||
- Status colors (success, warning, danger, info)
|
||||
- Neutral grays
|
||||
- Tier gradients (Diamond, Platinum, Gold)
|
||||
|
||||
### Typography
|
||||
```dart
|
||||
// typography.dart
|
||||
class AppTypography {
|
||||
static const fontFamily = 'Roboto';
|
||||
|
||||
static const displayLarge = TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: fontFamily,
|
||||
);
|
||||
|
||||
static const headlineLarge = TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: fontFamily,
|
||||
);
|
||||
|
||||
static const titleLarge = TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: fontFamily,
|
||||
);
|
||||
|
||||
static const bodyLarge = TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontFamily: fontFamily,
|
||||
);
|
||||
|
||||
static const labelSmall = TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontFamily: fontFamily,
|
||||
);
|
||||
}
|
||||
```
|
||||
See **CODE_EXAMPLES.md → UI/UX Components → Typography** for text styles:
|
||||
- Display, headline, title, body, and label styles
|
||||
- Roboto font family
|
||||
- Font sizes and weights
|
||||
|
||||
### Component Specifications
|
||||
|
||||
#### Member Card Design
|
||||
```dart
|
||||
class MemberCardSpecs {
|
||||
static const double width = double.infinity;
|
||||
static const double height = 200;
|
||||
static const double borderRadius = 16;
|
||||
static const double elevation = 8;
|
||||
static const EdgeInsets padding = EdgeInsets.all(20);
|
||||
All component specifications are documented in **CODE_EXAMPLES.md → UI/UX Components**:
|
||||
|
||||
// QR Code
|
||||
static const double qrSize = 80;
|
||||
static const double qrBackgroundSize = 90;
|
||||
- **Member Card Design**: Dimensions, padding, QR code specs, points display
|
||||
- **Status Badges**: Color mapping for order statuses
|
||||
- **Bottom Navigation**: Heights, icon sizes, colors
|
||||
- **Floating Action Button**: Size, elevation, colors, position
|
||||
- **AppBar Specifications**: Standard pattern with helper method
|
||||
|
||||
// Points Display
|
||||
static const double pointsFontSize = 28;
|
||||
static const FontWeight pointsFontWeight = FontWeight.bold;
|
||||
}
|
||||
```
|
||||
|
||||
#### Status Badges
|
||||
```dart
|
||||
class StatusBadge extends StatelessWidget {
|
||||
final String status;
|
||||
final Color color;
|
||||
|
||||
static Color getColorForStatus(OrderStatus status) {
|
||||
switch (status) {
|
||||
case OrderStatus.pending:
|
||||
return AppColors.info;
|
||||
case OrderStatus.processing:
|
||||
return AppColors.warning;
|
||||
case OrderStatus.shipping:
|
||||
return AppColors.lightBlue;
|
||||
case OrderStatus.completed:
|
||||
return AppColors.success;
|
||||
case OrderStatus.cancelled:
|
||||
return AppColors.danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Bottom Navigation
|
||||
```dart
|
||||
class BottomNavSpecs {
|
||||
static const double height = 72;
|
||||
static const double iconSize = 24;
|
||||
static const double selectedIconSize = 28;
|
||||
static const double labelFontSize = 12;
|
||||
static const Color selectedColor = AppColors.primaryBlue;
|
||||
static const Color unselectedColor = AppColors.grey500;
|
||||
}
|
||||
```
|
||||
|
||||
#### Floating Action Button
|
||||
```dart
|
||||
class FABSpecs {
|
||||
static const double size = 56;
|
||||
static const double elevation = 6;
|
||||
static const Color backgroundColor = AppColors.accentCyan;
|
||||
static const Color iconColor = Colors.white;
|
||||
static const double iconSize = 24;
|
||||
static const Offset position = Offset(16, 16); // from bottom-right
|
||||
}
|
||||
```
|
||||
|
||||
#### 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
|
||||
**AppBar Usage Notes**:
|
||||
- ALL pages use the standard AppBar pattern
|
||||
- Back arrow is always black with explicit color
|
||||
- Title is always left-aligned (`centerTitle: false`)
|
||||
- Title text is always black
|
||||
@@ -1433,38 +1163,10 @@ class AppBarSpecs {
|
||||
|
||||
### State Management (Riverpod 2.x)
|
||||
|
||||
#### Authentication State
|
||||
```dart
|
||||
@riverpod
|
||||
class Auth extends _$Auth {
|
||||
@override
|
||||
Future<AuthState> build() async {
|
||||
final token = await _getStoredToken();
|
||||
if (token != null) {
|
||||
final user = await _getUserFromToken(token);
|
||||
return AuthState.authenticated(user);
|
||||
}
|
||||
return const AuthState.unauthenticated();
|
||||
}
|
||||
|
||||
Future<void> loginWithPhone(String phone) async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref.read(authRepositoryProvider).requestOTP(phone);
|
||||
return AuthState.otpSent(phone);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> verifyOTP(String phone, String otp) async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final response = await ref.read(authRepositoryProvider).verifyOTP(phone, otp);
|
||||
await _storeToken(response.token);
|
||||
return AuthState.authenticated(response.user);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
All state management patterns and implementations are documented in **CODE_EXAMPLES.md → State Management**, including:
|
||||
- Authentication State with phone login and OTP verification
|
||||
- All feature-specific providers (Home, Loyalty, Products, Cart, Orders, Projects, Chat)
|
||||
- Provider patterns and best practices
|
||||
|
||||
|
||||
### Domain Entities & Data Models
|
||||
@@ -1597,292 +1299,39 @@ All enums are defined in `lib/core/database/models/enums.dart` with Hive type ad
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Image Caching
|
||||
```dart
|
||||
// Use cached_network_image for all remote images
|
||||
CachedNetworkImage(
|
||||
imageUrl: product.images.first,
|
||||
placeholder: (context, url) => const ShimmerPlaceholder(),
|
||||
errorWidget: (context, url, error) => const Icon(Icons.error),
|
||||
fit: BoxFit.cover,
|
||||
memCacheWidth: 400, // Optimize memory usage
|
||||
fadeInDuration: const Duration(milliseconds: 300),
|
||||
)
|
||||
```
|
||||
All performance optimization patterns are documented in **CODE_EXAMPLES.md → Performance Optimization**:
|
||||
|
||||
### List Performance
|
||||
```dart
|
||||
// Use ListView.builder with RepaintBoundary for long lists
|
||||
ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
return RepaintBoundary(
|
||||
child: ProductCard(product: items[index]),
|
||||
);
|
||||
},
|
||||
cacheExtent: 1000, // Pre-render items
|
||||
)
|
||||
|
||||
// Use AutomaticKeepAliveClientMixin for expensive widgets
|
||||
class ProductCard extends StatefulWidget {
|
||||
@override
|
||||
State<ProductCard> createState() => _ProductCardState();
|
||||
}
|
||||
|
||||
class _ProductCardState extends State<ProductCard>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Card(...);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### State Optimization
|
||||
```dart
|
||||
// Use .select() to avoid unnecessary rebuilds
|
||||
final userName = ref.watch(authProvider.select((state) => state.user?.name));
|
||||
|
||||
// Use family modifiers for parameterized providers
|
||||
@riverpod
|
||||
Future<Product> product(ProductRef ref, String id) async {
|
||||
return await ref.read(productRepositoryProvider).getProduct(id);
|
||||
}
|
||||
|
||||
// Keep providers outside build method
|
||||
final productsProvider = ...;
|
||||
|
||||
class ProductsPage extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final products = ref.watch(productsProvider);
|
||||
return ...;
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Image Caching**: Using CachedNetworkImage with proper configuration
|
||||
- **List Performance**: RepaintBoundary, AutomaticKeepAliveClientMixin, cacheExtent
|
||||
- **State Optimization**: Using .select(), family modifiers, provider best practices
|
||||
|
||||
---
|
||||
|
||||
## Offline Strategy
|
||||
|
||||
### Data Sync Flow
|
||||
```dart
|
||||
@riverpod
|
||||
class DataSync extends _$DataSync {
|
||||
@override
|
||||
Future<SyncStatus> build() async {
|
||||
// Listen to connectivity changes
|
||||
ref.listen(connectivityProvider, (previous, next) {
|
||||
if (next == ConnectivityStatus.connected) {
|
||||
syncData();
|
||||
}
|
||||
});
|
||||
All offline strategy patterns are documented in **CODE_EXAMPLES.md → Offline Strategy**:
|
||||
|
||||
return SyncStatus.idle;
|
||||
}
|
||||
|
||||
Future<void> syncData() async {
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
state = await AsyncValue.guard(() async {
|
||||
// Sync in order of dependency
|
||||
await _syncUserData();
|
||||
await _syncProducts();
|
||||
await _syncOrders();
|
||||
await _syncProjects();
|
||||
await _syncLoyaltyData();
|
||||
|
||||
await ref.read(settingsRepositoryProvider).updateLastSyncTime();
|
||||
|
||||
return SyncStatus.success;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _syncUserData() async {
|
||||
final user = await ref.read(authRepositoryProvider).getCurrentUser();
|
||||
await ref.read(authLocalDataSourceProvider).saveUser(user);
|
||||
}
|
||||
|
||||
Future<void> _syncProducts() async {
|
||||
final products = await ref.read(productRepositoryProvider).getAllProducts();
|
||||
await ref.read(productLocalDataSourceProvider).saveProducts(products);
|
||||
}
|
||||
|
||||
// ... other sync methods
|
||||
}
|
||||
```
|
||||
|
||||
### Offline Queue
|
||||
```dart
|
||||
// Queue failed requests for retry when online
|
||||
class OfflineQueue {
|
||||
final HiveInterface hive;
|
||||
late Box<Map> _queueBox;
|
||||
|
||||
Future<void> init() async {
|
||||
_queueBox = await hive.openBox('offline_queue');
|
||||
}
|
||||
|
||||
Future<void> addToQueue(ApiRequest request) async {
|
||||
await _queueBox.add({
|
||||
'endpoint': request.endpoint,
|
||||
'method': request.method,
|
||||
'body': request.body,
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> processQueue() async {
|
||||
final requests = _queueBox.values.toList();
|
||||
|
||||
for (var i = 0; i < requests.length; i++) {
|
||||
try {
|
||||
await _executeRequest(requests[i]);
|
||||
await _queueBox.deleteAt(i);
|
||||
} catch (e) {
|
||||
// Keep in queue for next retry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Data Sync Flow**: Complete sync implementation with connectivity monitoring
|
||||
- **Offline Queue**: Request queuing system for failed API calls
|
||||
|
||||
|
||||
|
||||
|
||||
## Localization (Vietnamese Primary)
|
||||
|
||||
### Setup
|
||||
```dart
|
||||
// l10n.yaml
|
||||
arb-dir: lib/l10n
|
||||
template-arb-file: app_en.arb
|
||||
output-localization-file: app_localizations.dart
|
||||
All localization setup and usage examples are documented in **CODE_EXAMPLES.md → Localization**:
|
||||
|
||||
// lib/l10n/app_vi.arb (Vietnamese)
|
||||
{
|
||||
"@@locale": "vi",
|
||||
"appTitle": "Worker App",
|
||||
"login": "Đăng nhập",
|
||||
"phone": "Số điện thoại",
|
||||
"enterPhone": "Nhập số điện thoại",
|
||||
"continue": "Tiếp tục",
|
||||
"verifyOTP": "Xác thực OTP",
|
||||
"enterOTP": "Nhập mã OTP 6 số",
|
||||
"resendOTP": "Gửi lại mã",
|
||||
"home": "Trang chủ",
|
||||
"products": "Sản phẩm",
|
||||
"loyalty": "Hội viên",
|
||||
"account": "Tài khoản",
|
||||
"points": "Điểm",
|
||||
"cart": "Giỏ hàng",
|
||||
"checkout": "Thanh toán",
|
||||
"orders": "Đơn hàng",
|
||||
"projects": "Công trình",
|
||||
"quotes": "Báo giá",
|
||||
"myGifts": "Quà của tôi",
|
||||
"referral": "Giới thiệu bạn bè",
|
||||
"pointsHistory": "Lịch sử điểm"
|
||||
}
|
||||
|
||||
// lib/l10n/app_en.arb (English)
|
||||
{
|
||||
"@@locale": "en",
|
||||
"appTitle": "Worker App",
|
||||
"login": "Login",
|
||||
"phone": "Phone Number",
|
||||
"enterPhone": "Enter phone number",
|
||||
"continue": "Continue",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Usage
|
||||
```dart
|
||||
class LoginPage extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.login),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.phone,
|
||||
hintText: l10n.enterPhone,
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {},
|
||||
child: Text(l10n.continue),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Setup**: l10n.yaml configuration, Vietnamese and English .arb files
|
||||
- **Usage**: LoginPage example showing how to use AppLocalizations in widgets
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
### Android
|
||||
```gradle
|
||||
// android/app/build.gradle
|
||||
android {
|
||||
compileSdkVersion 34
|
||||
All deployment configurations are documented in **CODE_EXAMPLES.md → Deployment**:
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.eurotile.worker"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
storeFile file(RELEASE_STORE_FILE)
|
||||
storePassword RELEASE_STORE_PASSWORD
|
||||
keyAlias RELEASE_KEY_ALIAS
|
||||
keyPassword RELEASE_KEY_PASSWORD
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### iOS
|
||||
```ruby
|
||||
# ios/Podfile
|
||||
platform :ios, '13.0'
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
- **Android**: build.gradle configuration with signing and build settings
|
||||
- **iOS**: Podfile configuration with deployment target settings
|
||||
|
||||
---
|
||||
|
||||
@@ -2006,21 +1455,7 @@ When working on this Flutter Worker app:
|
||||
### ✅ 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)],
|
||||
)
|
||||
```
|
||||
See **CODE_EXAMPLES.md → Best Practices → AppBar Standardization** for the standard pattern.
|
||||
|
||||
**Updated Pages**:
|
||||
- `cart_page.dart` - Lines 84-103
|
||||
@@ -2032,21 +1467,7 @@ AppBar(
|
||||
### ✅ 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,
|
||||
)
|
||||
```
|
||||
See **CODE_EXAMPLES.md → State Management → Cart Providers → Dynamic Cart Badge** for the implementation.
|
||||
|
||||
**Behavior**:
|
||||
- Shows total quantity across all cart items
|
||||
@@ -2057,27 +1478,14 @@ QuickAction(
|
||||
### ✅ 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>
|
||||
```
|
||||
**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();
|
||||
}
|
||||
```
|
||||
See **CODE_EXAMPLES.md → Best Practices → Hive Box Type Management** for the correct pattern.
|
||||
|
||||
### 🔄 Next Steps (Planned)
|
||||
1. Points history page with transaction list
|
||||
|
||||
Reference in New Issue
Block a user