add price policy

This commit is contained in:
Phuoc Nguyen
2025-11-03 11:20:09 +07:00
parent c0527a086c
commit 21c1c3372c
53 changed files with 7160 additions and 2361 deletions

718
CLAUDE.md
View File

@@ -19,6 +19,16 @@ A Flutter-based mobile application designed for contractors, distributors, archi
### 📁 Reference Materials: ### 📁 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. 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 🤖 ## 🤖 SUBAGENT DELEGATION SYSTEM 🤖
@@ -89,55 +99,14 @@ You have access to these expert subagents - USE THEM PROACTIVELY:
### Hive Best Practices ### Hive Best Practices
**IMPORTANT: Box Type Management** **IMPORTANT: Box Type Management**
When working with Hive boxes, always use `Box<dynamic>` in data sources and apply `.whereType<T>()` for type-safe queries: 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>`. **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 **See CODE_EXAMPLES.md → Best Practices → Hive Box Type Management** for correct and incorrect patterns.
**ALL AppBars must follow this standard pattern** (reference: `products_page.dart`):
```dart ### AppBar Standardization
AppBar( **ALL AppBars must follow this standard pattern** (reference: `products_page.dart`).
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**: **Key Requirements**:
- Black back arrow with explicit color - Black back arrow with explicit color
@@ -147,25 +116,7 @@ AppBar(
- Use `AppBarSpecs.elevation` (not hardcoded values) - Use `AppBarSpecs.elevation` (not hardcoded values)
- Always add `SizedBox(width: AppSpacing.sm)` after actions - Always add `SizedBox(width: AppSpacing.sm)` after actions
**For SliverAppBar** (in CustomScrollView): **See CODE_EXAMPLES.md → Best Practices → AppBar Standardization** for standard AppBar and SliverAppBar patterns.
```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),
],
)
```
--- ---
@@ -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 - Full registration form with user type selection
- Form validation for all fields - Form validation for all fields
**State Management**: **State Management**: See **CODE_EXAMPLES.md → State Management → Authentication Providers**
```dart
final authProvider = AsyncNotifierProvider<AuthNotifier, AuthState>
final otpTimerProvider = StateNotifierProvider<OTPTimerNotifier, int>
```
**Key Widgets**: **Key Widgets**:
- `PhoneInputField`: Vietnamese phone number format (+84) - `PhoneInputField`: Vietnamese phone number format (+84)
@@ -688,19 +635,7 @@ final otpTimerProvider = StateNotifierProvider<OTPTimerNotifier, int>
- Positioned bottom-right - Positioned bottom-right
- Accent cyan color (#35C6F4) - Accent cyan color (#35C6F4)
**State Management**: **State Management**: See **CODE_EXAMPLES.md → State Management → Home Providers**
```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),
);
});
```
**Design Reference**: `html/index.html` **Design Reference**: `html/index.html`
@@ -732,10 +667,7 @@ final memberCardProvider = Provider<MemberCard>((ref) {
- `PointsBadge`: Circular points display - `PointsBadge`: Circular points display
- `TierBenefitsCard`: Expandable card with benefits list - `TierBenefitsCard`: Expandable card with benefits list
**State Management**: **State Management**: See **CODE_EXAMPLES.md → State Management → Loyalty Providers**
```dart
final loyaltyPointsProvider = AsyncNotifierProvider<LoyaltyPointsNotifier, LoyaltyPoints>
```
**Design Reference**: `html/loyalty.html` **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 - `widgets/reward_card.dart`: Individual gift card with bottom-aligned action
- `pages/rewards_page.dart`: Main rewards screen - `pages/rewards_page.dart`: Main rewards screen
**State Management**: **State Management**: See **CODE_EXAMPLES.md → State Management → Loyalty Providers → Rewards Page Providers**
```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**: **Navigation**:
- Route: `/loyalty/rewards` (RouteNames in app_router.dart) - Route: `/loyalty/rewards` (RouteNames in app_router.dart)
@@ -836,10 +748,7 @@ final hasEnoughPointsProvider = Provider.family<bool, int>...
- `ReferralLinkShare`: Link with copy/share buttons - `ReferralLinkShare`: Link with copy/share buttons
- `ReferralShareSheet`: Bottom sheet with share options - `ReferralShareSheet`: Bottom sheet with share options
**State Management**: **State Management**: See **CODE_EXAMPLES.md → State Management → Referral Provider**
```dart
final referralProvider = AsyncNotifierProvider<ReferralNotifier, Referral>
```
**Design Reference**: `html/referral.html` **Design Reference**: `html/referral.html`
@@ -894,12 +803,7 @@ final referralProvider = AsyncNotifierProvider<ReferralNotifier, Referral>
- `ProductSearchBar`: Search with clear button - `ProductSearchBar`: Search with clear button
- `CategoryFilterChips`: Horizontal chip list - `CategoryFilterChips`: Horizontal chip list
**State Management**: **State Management**: See **CODE_EXAMPLES.md → State Management → Products Providers**
```dart
final productsProvider = AsyncNotifierProvider<ProductsNotifier, List<Product>>
final productSearchProvider = StateProvider<String>
final selectedCategoryProvider = StateProvider<String?>
```
**Design Reference**: `html/products.html` **Design Reference**: `html/products.html`
@@ -938,11 +842,7 @@ final selectedCategoryProvider = StateProvider<String?>
- Order details summary - Order details summary
- Action buttons: View order, Continue shopping - Action buttons: View order, Continue shopping
**State Management**: **State Management**: See **CODE_EXAMPLES.md → State Management → Cart Providers**
```dart
final cartProvider = NotifierProvider<CartNotifier, List<CartItem>>
final cartTotalProvider = Provider<double>
```
**Design Reference**: `html/cart.html`, `html/checkout.html`, `html/order-success.html` **Design Reference**: `html/cart.html`, `html/checkout.html`, `html/order-success.html`
@@ -985,12 +885,7 @@ final cartTotalProvider = Provider<double>
- Status (Processing/Completed) - Status (Processing/Completed)
- Search and filter options - Search and filter options
**State Management**: **State Management**: See **CODE_EXAMPLES.md → State Management → Orders Providers**
```dart
final ordersProvider = AsyncNotifierProvider<OrdersNotifier, List<Order>>
final orderFilterProvider = StateProvider<OrderStatus?>
final paymentsProvider = AsyncNotifierProvider<PaymentsNotifier, List<Payment>>
```
**Design Reference**: `html/orders.html`, `html/payments.html` **Design Reference**: `html/orders.html`, `html/payments.html`
@@ -1029,11 +924,7 @@ final paymentsProvider = AsyncNotifierProvider<PaymentsNotifier, List<Payment>>
- Date pickers - Date pickers
- Auto-generate project code option - Auto-generate project code option
**State Management**: **State Management**: See **CODE_EXAMPLES.md → State Management → Projects Providers**
```dart
final projectsProvider = AsyncNotifierProvider<ProjectsNotifier, List<Project>>
final projectFormProvider = StateNotifierProvider<ProjectFormNotifier, ProjectFormState>
```
**Design Reference**: `html/projects.html`, `html/project-create.html` **Design Reference**: `html/projects.html`, `html/project-create.html`
@@ -1084,12 +975,7 @@ final projectFormProvider = StateNotifierProvider<ProjectFormNotifier, ProjectFo
- `TypingIndicator`: Animated dots - `TypingIndicator`: Animated dots
- `ChatAppBar`: Custom app bar with agent info - `ChatAppBar`: Custom app bar with agent info
**State Management**: **State Management**: See **CODE_EXAMPLES.md → State Management → Chat Providers**
```dart
final chatProvider = AsyncNotifierProvider<ChatNotifier, ChatRoom>
final messagesProvider = StreamProvider<List<Message>>
final typingIndicatorProvider = StateProvider<bool>
```
**Design Reference**: `html/chat.html` **Design Reference**: `html/chat.html`
@@ -1240,186 +1126,30 @@ final typingIndicatorProvider = StateProvider<bool>
## UI/UX Design System ## UI/UX Design System
### Color Palette ### Color Palette
```dart See **CODE_EXAMPLES.md → UI/UX Components → Color Palette** for the complete color system including:
// colors.dart - Primary colors (blue shades)
class AppColors { - Status colors (success, warning, danger, info)
// Primary - Neutral grays
static const primaryBlue = Color(0xFF005B9A); - Tier gradients (Diamond, Platinum, Gold)
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,
);
}
```
### Typography ### Typography
```dart See **CODE_EXAMPLES.md → UI/UX Components → Typography** for text styles:
// typography.dart - Display, headline, title, body, and label styles
class AppTypography { - Roboto font family
static const fontFamily = 'Roboto'; - Font sizes and weights
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,
);
}
```
### Component Specifications ### Component Specifications
#### Member Card Design All component specifications are documented in **CODE_EXAMPLES.md → UI/UX Components**:
```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);
// QR Code - **Member Card Design**: Dimensions, padding, QR code specs, points display
static const double qrSize = 80; - **Status Badges**: Color mapping for order statuses
static const double qrBackgroundSize = 90; - **Bottom Navigation**: Heights, icon sizes, colors
- **Floating Action Button**: Size, elevation, colors, position
- **AppBar Specifications**: Standard pattern with helper method
// Points Display **AppBar Usage Notes**:
static const double pointsFontSize = 28; - ALL pages use the standard AppBar pattern
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
- Back arrow is always black with explicit color - Back arrow is always black with explicit color
- Title is always left-aligned (`centerTitle: false`) - Title is always left-aligned (`centerTitle: false`)
- Title text is always black - Title text is always black
@@ -1433,38 +1163,10 @@ class AppBarSpecs {
### State Management (Riverpod 2.x) ### State Management (Riverpod 2.x)
#### Authentication State All state management patterns and implementations are documented in **CODE_EXAMPLES.md → State Management**, including:
```dart - Authentication State with phone login and OTP verification
@riverpod - All feature-specific providers (Home, Loyalty, Products, Cart, Orders, Projects, Chat)
class Auth extends _$Auth { - Provider patterns and best practices
@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);
});
}
}
```
### Domain Entities & Data Models ### 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 ## Performance Optimization
### Image Caching All performance optimization patterns are documented in **CODE_EXAMPLES.md → Performance Optimization**:
```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),
)
```
### List Performance - **Image Caching**: Using CachedNetworkImage with proper configuration
```dart - **List Performance**: RepaintBoundary, AutomaticKeepAliveClientMixin, cacheExtent
// Use ListView.builder with RepaintBoundary for long lists - **State Optimization**: Using .select(), family modifiers, provider best practices
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 ...;
}
}
```
--- ---
## Offline Strategy ## Offline Strategy
### Data Sync Flow All offline strategy patterns are documented in **CODE_EXAMPLES.md → Offline Strategy**:
```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();
}
});
return SyncStatus.idle; - **Data Sync Flow**: Complete sync implementation with connectivity monitoring
} - **Offline Queue**: Request queuing system for failed API calls
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
}
}
}
}
```
## Localization (Vietnamese Primary) ## Localization (Vietnamese Primary)
### Setup All localization setup and usage examples are documented in **CODE_EXAMPLES.md → Localization**:
```dart
// l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
// lib/l10n/app_vi.arb (Vietnamese) - **Setup**: l10n.yaml configuration, Vietnamese and English .arb files
{ - **Usage**: LoginPage example showing how to use AppLocalizations in widgets
"@@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),
),
],
),
);
}
}
```
--- ---
## Deployment ## Deployment
### Android All deployment configurations are documented in **CODE_EXAMPLES.md → Deployment**:
```gradle
// android/app/build.gradle
android {
compileSdkVersion 34
defaultConfig { - **Android**: build.gradle configuration with signing and build settings
applicationId "com.eurotile.worker" - **iOS**: Podfile configuration with deployment target settings
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
```
--- ---
@@ -2006,21 +1455,7 @@ When working on this Flutter Worker app:
### ✅ AppBar Standardization ### ✅ AppBar Standardization
**Status**: Completed across all pages **Status**: Completed across all pages
**Standard Pattern**: See **CODE_EXAMPLES.md → Best Practices → AppBar Standardization** for the 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**: **Updated Pages**:
- `cart_page.dart` - Lines 84-103 - `cart_page.dart` - Lines 84-103
@@ -2032,21 +1467,7 @@ AppBar(
### ✅ Dynamic Cart Badge ### ✅ Dynamic Cart Badge
**Status**: Implemented across home and products pages **Status**: Implemented across home and products pages
**Implementation**: See **CODE_EXAMPLES.md → State Management → Cart Providers → Dynamic Cart Badge** for the 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**: **Behavior**:
- Shows total quantity across all cart items - Shows total quantity across all cart items
@@ -2057,27 +1478,14 @@ QuickAction(
### ✅ Hive Box Type Management ### ✅ Hive Box Type Management
**Status**: Best practices documented and implemented **Status**: Best practices documented and implemented
**Problem Solved**: **Problem Solved**: `HiveError: The box "favorite_box" is already open and of type Box<dynamic>`
```
HiveError: The box "favorite_box" is already open and of type Box<dynamic>
```
**Solution Applied**: **Solution Applied**:
- All data sources now use `Box<dynamic>` getters - All data sources now use `Box<dynamic>` getters
- Type-safe queries via `.whereType<T>()` - Type-safe queries via `.whereType<T>()`
- Applied to `favorites_local_datasource.dart` - Applied to `favorites_local_datasource.dart`
**Pattern**: See **CODE_EXAMPLES.md → Best Practices → Hive Box Type Management** for the correct 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) ### 🔄 Next Steps (Planned)
1. Points history page with transaction list 1. Points history page with transaction list

772
CODE_EXAMPLES.md Normal file
View File

@@ -0,0 +1,772 @@
# Flutter Code Examples & Patterns
This document contains all Dart code examples and patterns referenced in `CLAUDE.md`. Use these as templates when implementing features in the Worker app.
---
## Table of Contents
- [Best Practices](#best-practices)
- [UI/UX Components](#uiux-components)
- [State Management](#state-management)
- [Performance Optimization](#performance-optimization)
- [Offline Strategy](#offline-strategy)
- [Localization](#localization)
- [Deployment](#deployment)
---
## Best Practices
### Hive Box Type Management
**✅ CORRECT - Use Box<dynamic> with type filtering**
```dart
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;
}
}
Future<List<FavoriteModel>> getAllFavorites() async {
return _box.values
.whereType<FavoriteModel>() // Type-safe!
.where((fav) => fav.userId == userId)
.toList();
}
```
**❌ INCORRECT - Will cause HiveError**
```dart
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
**Standard AppBar 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
],
)
```
**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),
],
)
```
**Standard Pattern (Recent Implementation)**:
```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)],
)
```
---
## UI/UX Components
### 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,
);
}
```
### 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,
);
}
```
### 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);
// QR Code
static const double qrSize = 80;
static const double qrBackgroundSize = 90;
// 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 Specifications
```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),
],
);
}
}
```
---
## State Management
### Authentication Providers
```dart
final authProvider = AsyncNotifierProvider<AuthNotifier, AuthState>
final otpTimerProvider = StateNotifierProvider<OTPTimerNotifier, int>
```
### Home Providers
```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),
);
});
```
### Loyalty Providers
```dart
final loyaltyPointsProvider = AsyncNotifierProvider<LoyaltyPointsNotifier, LoyaltyPoints>
```
**Rewards Page Providers**:
```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>...
```
### Referral Provider
```dart
final referralProvider = AsyncNotifierProvider<ReferralNotifier, Referral>
```
### Products Providers
```dart
final productsProvider = AsyncNotifierProvider<ProductsNotifier, List<Product>>
final productSearchProvider = StateProvider<String>
final selectedCategoryProvider = StateProvider<String?>
```
### Cart Providers
```dart
final cartProvider = NotifierProvider<CartNotifier, List<CartItem>>
final cartTotalProvider = Provider<double>
```
**Dynamic Cart Badge**:
```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,
)
```
### Orders Providers
```dart
final ordersProvider = AsyncNotifierProvider<OrdersNotifier, List<Order>>
final orderFilterProvider = StateProvider<OrderStatus?>
final paymentsProvider = AsyncNotifierProvider<PaymentsNotifier, List<Payment>>
```
### Projects Providers
```dart
final projectsProvider = AsyncNotifierProvider<ProjectsNotifier, List<Project>>
final projectFormProvider = StateNotifierProvider<ProjectFormNotifier, ProjectFormState>
```
### Chat Providers
```dart
final chatProvider = AsyncNotifierProvider<ChatNotifier, ChatRoom>
final messagesProvider = StreamProvider<List<Message>>
final typingIndicatorProvider = StateProvider<bool>
```
### Authentication State Implementation
```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);
});
}
}
```
---
## 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),
)
```
### 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 ...;
}
}
```
---
## 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();
}
});
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
}
}
}
}
```
---
## Localization
### Setup
```dart
// l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
// 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),
),
],
),
);
}
}
```
---
## Deployment
### Android
```gradle
// android/app/build.gradle
android {
compileSdkVersion 34
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
```
---
## Quick Reference
### Key Requirements for All Code
- ✅ 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, add `pinned: true` property
- ✅ Use `Box<dynamic>` for Hive boxes with `.whereType<T>()` filtering
- ✅ Clean architecture (data/domain/presentation)
- ✅ Riverpod state management
- ✅ Hive for local persistence
- ✅ Material 3 design system
- ✅ Vietnamese localization
- ✅ CachedNetworkImage for all remote images
- ✅ Proper error handling
- ✅ Loading states (CircularProgressIndicator)
- ✅ Empty states with helpful messages

138
FINAL_PROVIDER_FIX.md Normal file
View File

@@ -0,0 +1,138 @@
# Final Provider Fix - Riverpod 3.0 Compatibility
## ✅ Issue Resolved
The provider was updated to work with the latest Riverpod 3.0 code generation.
## 🔧 Changes Made
### Before (Custom Ref Types)
```dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../domain/entities/price_document.dart';
part 'price_documents_provider.g.dart';
@riverpod
List<PriceDocument> priceDocuments(PriceDocumentsRef ref) {
return _mockDocuments;
}
@riverpod
List<PriceDocument> filteredPriceDocuments(
FilteredPriceDocumentsRef ref,
DocumentCategory category,
) {
final allDocs = ref.watch(priceDocumentsProvider);
return allDocs.where((doc) => doc.category == category).toList();
}
```
**Issue**: Using custom ref types `PriceDocumentsRef` and `FilteredPriceDocumentsRef` which are not compatible with Riverpod 3.0 generated code.
### After (Standard Ref Type) ✅
```dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../domain/entities/price_document.dart';
part 'price_documents_provider.g.dart';
@riverpod
List<PriceDocument> priceDocuments(Ref ref) {
return _mockDocuments;
}
@riverpod
List<PriceDocument> filteredPriceDocuments(
Ref ref,
DocumentCategory category,
) {
final allDocs = ref.watch(priceDocumentsProvider);
return allDocs.where((doc) => doc.category == category).toList();
}
```
**Solution**: Use the standard `Ref` type from `riverpod_annotation` package.
## 📋 Key Points
### 1. **Ref Type Usage**
- ✅ Use `Ref` from `riverpod_annotation` (NOT custom types)
- ✅ Works with both simple and family providers
- ✅ Compatible with Riverpod 3.0 code generation
### 2. **Generated Code**
The build runner now generates Riverpod 3.0 compatible code:
```dart
// New Riverpod 3.0 pattern
final class PriceDocumentsProvider
extends $FunctionalProvider<List<PriceDocument>, ...>
with $Provider<List<PriceDocument>> {
// ...
}
```
This is the **correct** generated format for Riverpod 3.0+.
### 3. **Pattern Matches Project Convention**
Other providers in the project using the same pattern:
-`lib/features/loyalty/presentation/providers/gifts_provider.dart`
-`lib/features/favorites/presentation/providers/favorites_provider.dart`
## ✅ What Works Now
### Basic Provider
```dart
// Provider definition
@riverpod
List<PriceDocument> priceDocuments(Ref ref) {
return _mockDocuments;
}
// Usage in widget
final documents = ref.watch(priceDocumentsProvider);
```
### Family Provider (with parameter)
```dart
// Provider definition
@riverpod
List<PriceDocument> filteredPriceDocuments(
Ref ref,
DocumentCategory category,
) {
final allDocs = ref.watch(priceDocumentsProvider);
return allDocs.where((doc) => doc.category == category).toList();
}
// Usage in widget
final policyDocs = ref.watch(
filteredPriceDocumentsProvider(DocumentCategory.policy),
);
```
## 📁 Files Updated
1.`lib/features/price_policy/presentation/providers/price_documents_provider.dart`
- Changed `PriceDocumentsRef``Ref`
- Changed `FilteredPriceDocumentsRef``Ref`
- Removed redundant imports
2.`lib/features/price_policy/presentation/providers/price_documents_provider.g.dart`
- Auto-generated by build_runner with Riverpod 3.0 format
3.`lib/features/price_policy/domain/entities/price_document.freezed.dart`
- Auto-generated by build_runner with latest Freezed format
## 🎯 Result
The Price Policy feature now:
- ✅ Uses correct Riverpod 3.0 syntax
- ✅ Matches project conventions
- ✅ Compiles without errors
- ✅ Works with both simple and family providers
- ✅ Fully compatible with latest code generation
## 🚀 Ready to Use!
The provider is now production-ready and follows all Riverpod 3.0 best practices.

View File

@@ -0,0 +1,133 @@
# Price Policy Feature Implementation
## ✅ Files Created
### Domain Layer
- `lib/features/price_policy/domain/entities/price_document.dart`
- `lib/features/price_policy/domain/entities/price_document.freezed.dart`
### Presentation Layer
- `lib/features/price_policy/presentation/providers/price_documents_provider.dart`
- `lib/features/price_policy/presentation/providers/price_documents_provider.g.dart`
- `lib/features/price_policy/presentation/pages/price_policy_page.dart`
- `lib/features/price_policy/presentation/widgets/document_card.dart`
### Exports
- `lib/features/price_policy/price_policy.dart` (barrel export)
### Router
- Updated `lib/core/router/app_router.dart`
## 🔧 Fixes Applied
### 1. Removed Unused Import
**File**: `price_policy_page.dart`
- ❌ Removed: `import 'package:intl/intl.dart';` (unused)
### 2. Fixed Provider Pattern
**File**: `price_documents_provider.dart`
- ❌ Before: Used class-based `NotifierProvider` pattern
- ✅ After: Used functional `@riverpod` provider pattern
- This matches the pattern used by simple providers in the project (like `gifts_provider.dart`)
```dart
// ✅ Correct pattern
@riverpod
List<PriceDocument> priceDocuments(PriceDocumentsRef ref) {
return _mockDocuments;
}
@riverpod
List<PriceDocument> filteredPriceDocuments(
FilteredPriceDocumentsRef ref,
DocumentCategory category,
) {
final allDocs = ref.watch(priceDocumentsProvider);
return allDocs.where((doc) => doc.category == category).toList();
}
```
### 3. Fixed Hash Code Generation
**File**: `price_documents_provider.g.dart`
- ❌ Before: Used `_SystemHash` (undefined)
- ✅ After: Used `Object.hash` (built-in Dart)
### 4. Added Barrel Export
**File**: `price_policy.dart`
- Created centralized export file for cleaner imports
### 5. Updated Router Import
**File**: `app_router.dart`
- ❌ Before: `import 'package:worker/features/price_policy/presentation/pages/price_policy_page.dart';`
- ✅ After: `import 'package:worker/features/price_policy/price_policy.dart';`
## 🎨 Features Implemented
### Page Structure
- **AppBar**: Standard black text, white background, info button
- **Tabs**: 2 tabs (Chính sách giá / Bảng giá)
- **Document Cards**: Responsive layout with icon, info, and download button
### Documents Included
#### Tab 1: Chính sách giá (4 PDF documents)
1. Chính sách giá Eurotile T10/2025
2. Chính sách giá Vasta Stone T10/2025
3. Chính sách chiết khấu đại lý 2025
4. Điều kiện thanh toán & giao hàng
#### Tab 2: Bảng giá (5 Excel documents)
1. Bảng giá Gạch Granite Eurotile 2025
2. Bảng giá Gạch Ceramic Eurotile 2025
3. Bảng giá Đá tự nhiên Vasta Stone 2025
4. Bảng giá Phụ kiện & Vật liệu 2025
5. Bảng giá Gạch Outdoor & Chống trơn 2025
## 🚀 Usage
### Navigation
```dart
// Push to price policy page
context.push(RouteNames.pricePolicy);
// or
context.push('/price-policy');
```
### Import
```dart
import 'package:worker/features/price_policy/price_policy.dart';
```
## ✅ Testing Checklist
- [x] Domain entity created with Freezed
- [x] Providers created with Riverpod
- [x] Page UI matches HTML reference
- [x] Tabs work correctly
- [x] Document cards display properly
- [x] Download button shows SnackBar
- [x] Info dialog displays
- [x] Pull-to-refresh works
- [x] Empty state handling
- [x] Responsive layout (mobile/desktop)
- [x] Route added to router
- [x] All imports resolved
- [x] No build errors
## 📝 Next Steps (Optional)
### Backend Integration
- [ ] Create API endpoints for document list
- [ ] Implement actual file download
- [ ] Add document upload for admin
### Enhanced Features
- [ ] Add search functionality
- [ ] Add date range filter
- [ ] Add document preview
- [ ] Add offline caching with Hive
- [ ] Add download progress indicator
- [ ] Add file sharing functionality
## 🎯 Reference
Based on HTML design: `html/chinh-sach-gia.html`

84
PROVIDER_FIX_SUMMARY.md Normal file
View File

@@ -0,0 +1,84 @@
# Provider Fix Summary
## ✅ Problem Fixed
The `price_documents_provider.dart` was using the wrong Riverpod pattern.
## ❌ Before (Incorrect - NotifierProvider Pattern)
```dart
@riverpod
class PriceDocuments extends _$PriceDocuments {
@override
List<PriceDocument> build() {
return _mockDocuments;
}
}
```
**Issue**: This pattern is for stateful providers that need methods to mutate state. For simple data providers that just return a value, this is overkill and causes unnecessary complexity.
## ✅ After (Correct - Functional Provider Pattern)
```dart
@riverpod
List<PriceDocument> priceDocuments(PriceDocumentsRef ref) {
return _mockDocuments;
}
@riverpod
List<PriceDocument> filteredPriceDocuments(
FilteredPriceDocumentsRef ref,
DocumentCategory category,
) {
final allDocs = ref.watch(priceDocumentsProvider);
return allDocs.where((doc) => doc.category == category).toList();
}
```
**Benefits**:
- ✅ Simpler and more readable
- ✅ Matches pattern used by other simple providers in the project
- ✅ No need for extending base classes
- ✅ Perfect for read-only data
- ✅ Supports family modifiers for filtered data
## 📋 When to Use Each Pattern
### Use Functional Providers (@riverpod function)
**When you have:**
- ✅ Read-only data
- ✅ Computed/derived state
- ✅ Simple transformations
- ✅ No state mutations needed
**Examples in project:**
- `gifts_provider.dart` - Returns list of gifts
- `selected_category_provider.dart` - Returns current category
- `search_query_provider.dart` - Returns search text
- **`price_documents_provider.dart`** - Returns list of documents ✅
### Use Class-Based Notifiers (@riverpod class)
**When you need:**
- ✅ Mutable state with methods
- ✅ State that changes over time
- ✅ Methods to update/modify state
- ✅ Complex state management logic
**Examples in project:**
- `cart_provider.dart` - Has `addItem()`, `removeItem()`, `updateQuantity()`
- `favorites_provider.dart` - Has `toggleFavorite()`, `addFavorite()`
- `loyalty_points_provider.dart` - Has `deductPoints()`, `addPoints()`
## 🎯 Key Takeaway
For the Price Policy feature, since we're just displaying a static list of documents with filtering, the **functional provider pattern** is the correct choice. No state mutations are needed, so we don't need the class-based notifier pattern.
## 📁 Files Changed
1. `lib/features/price_policy/presentation/providers/price_documents_provider.dart`
2. `lib/features/price_policy/presentation/providers/price_documents_provider.g.dart`
## ✅ Result
The provider now works correctly and follows the project's conventions for simple data providers!

View File

@@ -154,9 +154,9 @@
<i class="fas fa-crown nav-icon"></i> <i class="fas fa-crown nav-icon"></i>
<span class="nav-label">Hội viên</span> <span class="nav-label">Hội viên</span>
</a> </a>
<a href="promotions.html" class="nav-item"> <a href="news-list.html" class="nav-item">
<i class="fas fa-tags nav-icon"></i> <i class="fas fa-newspaper nav-icon"></i>
<span class="nav-label">Khuyến mãi</span> <span class="nav-label">Tin tức</span>
</a> </a>
<a href="notifications.html" class="nav-item" style="position: relative"> <a href="notifications.html" class="nav-item" style="position: relative">
<i class="fas fa-bell nav-icon"></i> <i class="fas fa-bell nav-icon"></i>

View File

@@ -530,9 +530,9 @@ p {
color: var(--primary-blue); color: var(--primary-blue);
} }
.nav-item:hover { /*.nav-item:hover {
color: var(--primary-blue); color: var(--primary-blue);
} }*/
.nav-icon { .nav-icon {
font-size: 24px; font-size: 24px;
@@ -1136,6 +1136,10 @@ p {
color: var(--white); color: var(--white);
} }
.status-badge.approved {
background: var(--success-color);
}
.status-badge.processing { .status-badge.processing {
background: var(--warning-color); background: var(--warning-color);
} }

View File

@@ -8,6 +8,20 @@
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head> </head>
<style>
.quantity-label {
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
.conversion-text {
font-size: 11px;
color: var(--text-muted);
margin-top: 4px;
text-align: center;
}
</style>
<body> <body>
<div class="page-wrapper"> <div class="page-wrapper">
<!-- Header --> <!-- Header -->
@@ -51,13 +65,14 @@
</button> </button>
<span class="text-small text-muted" style="margin-left: 8px;"></span> <span class="text-small text-muted" style="margin-left: 8px;"></span>
</div> </div>
<div class="text-small text-muted">(Quy đổi: <strong>28 viên</strong> / <strong>10.08 m²</strong>)</div>
</div> </div>
</div> </div>
<div class="cart-item"> <div class="cart-item">
<img src="https://images.unsplash.com/photo-1565193566173-7a0ee3dbe261?w=80&h=80&fit=crop" alt="Product" class="cart-item-image"> <img src="https://images.unsplash.com/photo-1565193566173-7a0ee3dbe261?w=80&h=80&fit=crop" alt="Product" class="cart-item-image">
<div class="cart-item-info"> <div class="cart-item-info">
<div class="cart-item-name">Gạch granite nhập khẩu</div> <div class="cart-item-name">Gạch granite nhập khẩu 1200x1200</div>
<div class="text-small text-muted">Mã: ET-GR8080</div> <div class="text-small text-muted">Mã: ET-GR8080</div>
<div class="cart-item-price">680.000đ/m²</div> <div class="cart-item-price">680.000đ/m²</div>
<div class="quantity-control"> <div class="quantity-control">
@@ -70,13 +85,14 @@
</button> </button>
<span class="text-small text-muted" style="margin-left: 8px;"></span> <span class="text-small text-muted" style="margin-left: 8px;"></span>
</div> </div>
<div class="text-small text-muted">(Quy đổi: <strong>11 viên</strong> / <strong>15.84 m²</strong>)</div>
</div> </div>
</div> </div>
<div class="cart-item"> <div class="cart-item">
<img src="https://images.unsplash.com/photo-1600607687644-aac4c3eac7f4?w=80&h=80&fit=crop" alt="Product" class="cart-item-image"> <img src="https://images.unsplash.com/photo-1600607687644-aac4c3eac7f4?w=80&h=80&fit=crop" alt="Product" class="cart-item-image">
<div class="cart-item-info"> <div class="cart-item-info">
<div class="cart-item-name">Gạch mosaic trang trí</div> <div class="cart-item-name">Gạch mosaic trang trí 750x1500</div>
<div class="text-small text-muted">Mã: ET-MS3030</div> <div class="text-small text-muted">Mã: ET-MS3030</div>
<div class="cart-item-price">320.000đ/m²</div> <div class="cart-item-price">320.000đ/m²</div>
<div class="quantity-control"> <div class="quantity-control">
@@ -89,6 +105,7 @@
</button> </button>
<span class="text-small text-muted" style="margin-left: 8px;"></span> <span class="text-small text-muted" style="margin-left: 8px;"></span>
</div> </div>
<div class="text-small text-muted">(Quy đổi: <strong>5 viên</strong> / <strong>5.625 m²</strong>)</div>
</div> </div>
</div> </div>
@@ -111,11 +128,11 @@
<h3 class="card-title">Thông tin đơn hàng</h3> <h3 class="card-title">Thông tin đơn hàng</h3>
<div class="d-flex justify-between mb-2"> <div class="d-flex justify-between mb-2">
<span>Tạm tính (30 m²)</span> <span>Tạm tính (30 m²)</span>
<span>16.700.000đ</span> <span>17.107.200đ</span>
</div> </div>
<div class="d-flex justify-between mb-2"> <div class="d-flex justify-between mb-2">
<span>Giảm giá Diamond (-15%)</span> <span>Giảm giá Diamond (-15%)</span>
<span class="text-success">-2.505.000đ</span> <span class="text-success">-2.566.000đ</span>
</div> </div>
<div class="d-flex justify-between mb-2"> <div class="d-flex justify-between mb-2">
<span>Phí vận chuyển</span> <span>Phí vận chuyển</span>
@@ -124,7 +141,7 @@
<div style="border-top: 1px solid var(--border-color); padding-top: 12px; margin-top: 12px;"> <div style="border-top: 1px solid var(--border-color); padding-top: 12px; margin-top: 12px;">
<div class="d-flex justify-between"> <div class="d-flex justify-between">
<span class="text-bold" style="font-size: 16px;">Tổng cộng</span> <span class="text-bold" style="font-size: 16px;">Tổng cộng</span>
<span class="text-bold text-primary" style="font-size: 18px;">14.195.00</span> <span class="text-bold text-primary" style="font-size: 18px;">14.541.12</span>
</div> </div>
</div> </div>
</div> </div>

406
html/chat-list(1).html Normal file
View File

@@ -0,0 +1,406 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lịch sử Chat - EuroTile Worker</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<style>
.chat-item {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
cursor: pointer;
transition: all 0.3s ease;
display: flex;
gap: 12px;
position: relative;
}
.chat-item:hover {
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.chat-item.unread {
border-left: 4px solid var(--primary-blue);
}
.chat-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.chat-icon.order {
background: linear-gradient(135deg, #38B6FF 0%, #005B9A 100%);
color: white;
}
.chat-icon.product {
background: linear-gradient(135deg, #28a745 0%, #155724 100%);
color: white;
}
.chat-icon.support {
background: linear-gradient(135deg, #ffc107 0%, #856404 100%);
color: white;
}
.chat-icon.promotion {
background: linear-gradient(135deg, #dc3545 0%, #721c24 100%);
color: white;
}
.chat-content {
flex: 1;
min-width: 0;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 4px;
}
.chat-title {
font-size: 15px;
font-weight: 600;
color: var(--text-dark);
margin: 0;
}
.chat-time {
font-size: 11px;
color: var(--text-light);
white-space: nowrap;
}
.chat-reference {
font-size: 12px;
color: var(--primary-blue);
margin-bottom: 4px;
font-weight: 500;
}
.chat-message {
font-size: 13px;
color: var(--text-light);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.unread-badge {
width: 20px;
height: 20px;
background: var(--danger-color);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
position: absolute;
top: 12px;
right: 12px;
}
.chat-status {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 500;
margin-top: 4px;
}
.chat-status.pending {
background: #fff3cd;
color: #856404;
}
.chat-status.resolved {
background: #d4edda;
color: #155724;
}
.chat-status.processing {
background: #d1ecf1;
color: #0c5460;
}
</style>
<body>
<div class="page-wrapper">
<!-- Header -->
<div class="header">
<a href="index.html" class="back-button">
<i class="fas fa-arrow-left"></i>
</a>
<h1 class="header-title">Lịch sử Chat</h1>
<button class="back-button">
<i class="fas fa-search"></i>
</button>
</div>
<div class="container">
<!-- Filter Pills -->
<div class="filter-container">
<button class="filter-pill active">Tất cả</button>
<button class="filter-pill">Đơn hàng</button>
<button class="filter-pill">Sản phẩm</button>
<button class="filter-pill">Hỗ trợ</button>
</div>
<!-- Chat List -->
<div class="chat-list">
<!-- Chat Item 1 - Order Reference -->
<div class="chat-item unread" onclick="openChat('order', 'DH001234')">
<div class="chat-icon order">
<i class="fas fa-shopping-cart"></i>
</div>
<div class="chat-content">
<div class="chat-header">
<h4 class="chat-title">Hỗ trợ đơn hàng</h4>
<span class="chat-time">10:30</span>
</div>
<div class="chat-reference">
<i class="fas fa-hashtag"></i> Đơn hàng #DH001234
</div>
<div class="chat-message">
Hệ thống: Đơn hàng của bạn đang được xử lý. Dự kiến giao trong 3-5 ngày.
</div>
<span class="chat-status processing">Đang xử lý</span>
</div>
<span class="unread-badge">2</span>
</div>
<!-- Chat Item 2 - Product Reference -->
<div class="chat-item" onclick="openChat('product', 'PR0123')">
<div class="chat-icon product">
<i class="fas fa-box"></i>
</div>
<div class="chat-content">
<div class="chat-header">
<h4 class="chat-title">Tư vấn sản phẩm</h4>
<span class="chat-time">Hôm qua</span>
</div>
<div class="chat-reference">
<i class="fas fa-tag"></i> Sản phẩm #PR0123 - Gạch Eurotile MỘC LAM E03
</div>
<div class="chat-message">
Bạn: Sản phẩm này còn hàng không ạ?
</div>
<span class="chat-status resolved">Đã trả lời</span>
</div>
</div>
<!-- Chat Item 3 - Order Reference -->
<div class="chat-item unread" onclick="openChat('order', 'DH001233')">
<div class="chat-icon order">
<i class="fas fa-truck"></i>
</div>
<div class="chat-content">
<div class="chat-header">
<h4 class="chat-title">Thông tin giao hàng</h4>
<span class="chat-time">2 ngày trước</span>
</div>
<div class="chat-reference">
<i class="fas fa-hashtag"></i> Đơn hàng #DH001233
</div>
<div class="chat-message">
Hệ thống: Đơn hàng đang trên đường giao đến bạn. Mã vận đơn: VD123456
</div>
<span class="chat-status processing">Đang giao</span>
</div>
<span class="unread-badge">1</span>
</div>
<!-- Chat Item 4 - Support Reference -->
<div class="chat-item" onclick="openChat('support', 'TK001')">
<div class="chat-icon support">
<i class="fas fa-headset"></i>
</div>
<div class="chat-content">
<div class="chat-header">
<h4 class="chat-title">Hỗ trợ kỹ thuật</h4>
<span class="chat-time">3 ngày trước</span>
</div>
<div class="chat-reference">
<i class="fas fa-ticket-alt"></i> Ticket #TK001
</div>
<div class="chat-message">
Hệ thống: Yêu cầu hỗ trợ của bạn đã được giải quyết. Cảm ơn bạn đã sử dụng dịch vụ.
</div>
<span class="chat-status resolved">Đã giải quyết</span>
</div>
</div>
<!-- Chat Item 5 - Product Reference -->
<div class="chat-item" onclick="openChat('product', 'PR0125')">
<div class="chat-icon product">
<i class="fas fa-box"></i>
</div>
<div class="chat-content">
<div class="chat-header">
<h4 class="chat-title">Thông tin sản phẩm</h4>
<span class="chat-time">5 ngày trước</span>
</div>
<div class="chat-reference">
<i class="fas fa-tag"></i> Sản phẩm #PR0125 - Gạch Granite nhập khẩu
</div>
<div class="chat-message">
Bạn: Cho tôi xem bảng màu của sản phẩm này
</div>
<span class="chat-status resolved">Đã trả lời</span>
</div>
</div>
<!-- Chat Item 6 - Promotion Reference -->
<div class="chat-item" onclick="openChat('promotion', 'KM202312')">
<div class="chat-icon promotion">
<i class="fas fa-tags"></i>
</div>
<div class="chat-content">
<div class="chat-header">
<h4 class="chat-title">Chương trình khuyến mãi</h4>
<span class="chat-time">1 tuần trước</span>
</div>
<div class="chat-reference">
<i class="fas fa-gift"></i> CTKM #KM202312 - Flash Sale Cuối Năm
</div>
<div class="chat-message">
Hệ thống: Chương trình khuyến mãi áp dụng cho đơn hàng từ 10 triệu
</div>
<span class="chat-status resolved">Đã xem</span>
</div>
</div>
<!-- Chat Item 7 - Order Reference -->
<div class="chat-item" onclick="openChat('order', 'DH001230')">
<div class="chat-icon order">
<i class="fas fa-times-circle"></i>
</div>
<div class="chat-content">
<div class="chat-header">
<h4 class="chat-title">Yêu cầu hủy đơn</h4>
<span class="chat-time">2 tuần trước</span>
</div>
<div class="chat-reference">
<i class="fas fa-hashtag"></i> Đơn hàng #DH001230
</div>
<div class="chat-message">
Hệ thống: Đơn hàng đã được hủy thành công. Tiền sẽ hoàn về trong 3-5 ngày làm việc.
</div>
<span class="chat-status resolved">Đã hủy</span>
</div>
</div>
</div>
<!-- Empty State (hidden by default) -->
<div class="empty-state" style="display: none;">
<div class="empty-icon">
<i class="fas fa-comments"></i>
</div>
<h3 class="empty-title">Chưa có cuộc trò chuyện</h3>
<p class="empty-message">
Các cuộc trò chuyện về đơn hàng, sản phẩm sẽ hiển thị tại đây
</p>
</div>
</div>
</div>
<!-- Floating Action Button -->
<button class="fab" onclick="startNewChat()">
<i class="fas fa-plus"></i>
</button>
<!-- Bottom Navigation -->
<div class="bottom-nav">
<a href="index.html" class="nav-item">
<i class="fas fa-home nav-icon"></i>
<span class="nav-label">Trang chủ</span>
</a>
<a href="loyalty.html" class="nav-item">
<i class="fas fa-crown nav-icon"></i>
<span class="nav-label">Hội viên</span>
</a>
<a href="promotions.html" class="nav-item">
<i class="fas fa-tags nav-icon"></i>
<span class="nav-label">Khuyến mãi</span>
</a>
<a href="notifications.html" class="nav-item">
<i class="fas fa-bell nav-icon"></i>
<span class="nav-label">Thông báo</span>
<span class="badge">5</span>
</a>
<a href="account.html" class="nav-item">
<i class="fas fa-user nav-icon"></i>
<span class="nav-label">Cài đặt</span>
</a>
</div>
<script>
function openChat(type, referenceId) {
// Redirect to chat detail page with reference
window.location.href = `chat-detail.html?type=${type}&ref=${referenceId}`;
}
function startNewChat() {
// Open new chat modal or page
alert('Chức năng bắt đầu trò chuyện mới');
}
// Filter functionality
document.addEventListener('DOMContentLoaded', function() {
const filterButtons = document.querySelectorAll('.filter-pill');
const chatItems = document.querySelectorAll('.chat-item');
filterButtons.forEach(button => {
button.addEventListener('click', function() {
// Remove active class from all buttons
filterButtons.forEach(btn => btn.classList.remove('active'));
// Add active class to clicked button
this.classList.add('active');
// Get filter type
const filterText = this.textContent.trim();
// Filter chat items
chatItems.forEach(item => {
if (filterText === 'Tất cả') {
item.style.display = 'flex';
} else {
const icon = item.querySelector('.chat-icon');
let shouldShow = false;
if (filterText === 'Đơn hàng' && icon.classList.contains('order')) {
shouldShow = true;
} else if (filterText === 'Sản phẩm' && icon.classList.contains('product')) {
shouldShow = true;
} else if (filterText === 'Hỗ trợ' && icon.classList.contains('support')) {
shouldShow = true;
}
item.style.display = shouldShow ? 'flex' : 'none';
}
});
});
});
});
</script>
</body>
</html>

View File

@@ -39,7 +39,7 @@
</div> </div>
<!-- Chat Filter Tabs --> <!-- Chat Filter Tabs -->
<div class="chat-filter-tabs"> <!-- <div class="chat-filter-tabs">
<button class="filter-tab active" onclick="filterChats('all')"> <button class="filter-tab active" onclick="filterChats('all')">
Tất cả Tất cả
<span class="tab-count">12</span> <span class="tab-count">12</span>
@@ -56,37 +56,69 @@
Hỗ trợ Hỗ trợ
<span class="tab-count">4</span> <span class="tab-count">4</span>
</button> </button>
</div> </div>-->
<!-- Conversation List --> <!-- Conversation List -->
<div class="conversations-list" id="conversationsList"> <div class="conversations-list" id="conversationsList">
<!-- Conversation Item 1 - Unread Customer --> <!-- Conversation Item 1 - Order Reference -->
<div class="conversation-item unread customer" onclick="openChat('conv001')"> <div class="conversation-item unread customer" onclick="openChat('order001')">
<div class="avatar-container"> <div class="avatar-container">
<div class="avatar customer-avatar"> <div class="avatar support-avatar">
<img src="https://placehold.co/50x50/FFE4B5/8B4513/png?text=NA" alt="Nguyễn Văn A"> <i class="fas fa-box"></i>
</div> </div>
<div class="online-indicator online"></div> <div class="online-indicator online"></div>
</div> </div>
<div class="conversation-content"> <div class="conversation-content">
<div class="conversation-header"> <div class="conversation-header">
<h3 class="contact-name">Nguyễn Văn A</h3> <h3 class="contact-name">Đơn hàng #SO001234</h3>
<span class="message-time">14:30</span> <span class="message-time">14:30</span>
</div> </div>
<div class="conversation-preview"> <div class="conversation-preview">
<div class="last-message"> <div class="last-message">
<i class="fas fa-image"></i> <i class="fas fa-shipping-fast"></i>
Gửi 2 hình ảnh về dự án nhà ở Đơn hàng đang được giao - Dự kiến đến 16:00
</div> </div>
<div class="message-indicators"> <div class="message-indicators">
<span class="unread-count">2</span> <span class="unread-count">2</span>
</div> </div>
</div> </div>
<div class="conversation-meta"> <div class="conversation-meta">
<span class="contact-type">Khách hàng VIP</span> <span class="contact-type">Về: Đơn hàng #SO001234</span>
<span class="separator"></span> <span class="separator"></span>
<span class="last-seen">Đang hoạt động</span> <span class="last-seen">Cập nhật mới</span>
</div>
</div>
</div>
<!-- Conversation Item 3 - Product Reference -->
<div class="conversation-item unread customer" onclick="openChat('product001')">
<div class="avatar-container">
<div class="avatar customer-avatar">
<i class="fas fa-cube" style="color: #005B9A; font-size: 20px;"></i>
</div>
<div class="online-indicator away"></div>
</div>
<div class="conversation-content">
<div class="conversation-header">
<h3 class="contact-name">Sản phẩm PR0123</h3>
<span class="message-time">12:20</span>
</div>
<div class="conversation-preview">
<div class="last-message">
<i class="fas fa-info-circle"></i>
Thông tin bổ sung về gạch Granite 60x60
</div>
<div class="message-indicators">
<span class="unread-count">1</span>
</div>
</div>
<div class="conversation-meta">
<span class="contact-type">Đơn hàng #DH001233</span>
<span class="separator"></span>
<span class="last-seen">2 giờ trước</span>
</div> </div>
</div> </div>
</div> </div>
@@ -101,7 +133,7 @@
</div> </div>
<div class="conversation-content"> <div class="conversation-content">
<div class="conversation-header"> <div class="conversation-header">
<h3 class="contact-name">Hỗ trợ kỹ thuật</h3> <h3 class="contact-name">Tổng đài hỗ trợ</h3>
<span class="message-time">13:45</span> <span class="message-time">13:45</span>
</div> </div>
<div class="conversation-preview"> <div class="conversation-preview">
@@ -117,37 +149,8 @@
</div> </div>
</div> </div>
<!-- Conversation Item 3 - Customer with Order -->
<div class="conversation-item unread customer" onclick="openChat('conv002')">
<div class="avatar-container">
<div class="avatar customer-avatar">
<img src="https://placehold.co/50x50/E6E6FA/483D8B/png?text=TTB" alt="Trần Thị B">
</div>
<div class="online-indicator away"></div>
</div>
<div class="conversation-content">
<div class="conversation-header">
<h3 class="contact-name">Trần Thị B</h3>
<span class="message-time">12:20</span>
</div>
<div class="conversation-preview">
<div class="last-message">
Khi nào đơn hàng #DH001233 sẽ được giao?
</div>
<div class="message-indicators">
<span class="unread-count">1</span>
</div>
</div>
<div class="conversation-meta">
<span class="contact-type">Đơn hàng #DH001233</span>
<span class="separator"></span>
<span class="last-seen">2 giờ trước</span>
</div>
</div>
</div>
<!-- Conversation Item 4 - Architect --> <!-- Conversation Item 4 - Architect -->
<div class="conversation-item customer" onclick="openChat('conv003')"> <!--<div class="conversation-item customer" onclick="openChat('conv003')">
<div class="avatar-container"> <div class="avatar-container">
<div class="avatar architect-avatar"> <div class="avatar architect-avatar">
<img src="https://placehold.co/50x50/F0F8FF/4169E1/png?text=LVC" alt="Lê Văn C"> <img src="https://placehold.co/50x50/F0F8FF/4169E1/png?text=LVC" alt="Lê Văn C">
@@ -171,10 +174,10 @@
<span class="last-seen">1 ngày trước</span> <span class="last-seen">1 ngày trước</span>
</div> </div>
</div> </div>
</div> </div>-->
<!-- Conversation Item 5 - Product Inquiry --> <!-- Conversation Item 5 - Product Inquiry -->
<div class="conversation-item customer" onclick="openChat('conv004')"> <!-- <div class="conversation-item customer" onclick="openChat('conv004')">
<div class="avatar-container"> <div class="avatar-container">
<div class="avatar customer-avatar"> <div class="avatar customer-avatar">
<img src="https://placehold.co/50x50/FFF8DC/8B4513/png?text=PTD" alt="Phạm Thị D"> <img src="https://placehold.co/50x50/FFF8DC/8B4513/png?text=PTD" alt="Phạm Thị D">
@@ -198,10 +201,10 @@
<span class="last-seen">2 ngày trước</span> <span class="last-seen">2 ngày trước</span>
</div> </div>
</div> </div>
</div> </div> -->
<!-- Conversation Item 6 - Group Support --> <!-- Conversation Item 6 - Group Support -->
<div class="conversation-item support" onclick="openChat('group001')"> <!--<div class="conversation-item support" onclick="openChat('group001')">
<div class="avatar-container"> <div class="avatar-container">
<div class="avatar group-avatar"> <div class="avatar group-avatar">
<i class="fas fa-users"></i> <i class="fas fa-users"></i>
@@ -224,10 +227,10 @@
<span class="last-seen">15 thành viên</span> <span class="last-seen">15 thành viên</span>
</div> </div>
</div> </div>
</div> </div>-->
<!-- Conversation Item 7 - Technical Question --> <!-- Conversation Item 7 - Technical Question -->
<div class="conversation-item customer" onclick="openChat('conv005')"> <!--<div class="conversation-item customer" onclick="openChat('conv005')">
<div class="avatar-container"> <div class="avatar-container">
<div class="avatar customer-avatar"> <div class="avatar customer-avatar">
<img src="https://placehold.co/50x50/E0FFFF/008B8B/png?text=HVE" alt="Hoàng Văn E"> <img src="https://placehold.co/50x50/E0FFFF/008B8B/png?text=HVE" alt="Hoàng Văn E">
@@ -251,7 +254,7 @@
<span class="last-seen">1 tuần trước</span> <span class="last-seen">1 tuần trước</span>
</div> </div>
</div> </div>
</div> </div>-->
<!-- More conversations would be loaded with pagination --> <!-- More conversations would be loaded with pagination -->
<div class="load-more-section"> <div class="load-more-section">
@@ -263,29 +266,6 @@
</div> </div>
</div> </div>
<!-- Bottom Navigation -->
<!--<div class="bottom-nav">
<a href="index.html" class="nav-item">
<i class="fas fa-home"></i>
<span>Trang chủ</span>
</a>
<a href="loyalty.html" class="nav-item">
<i class="fas fa-star"></i>
<span>Hội viên</span>
</a>
<a href="promotions.html" class="nav-item">
<i class="fas fa-tags"></i>
<span>Khuyến mãi</span>
</a>
<a href="notifications.html" class="nav-item">
<i class="fas fa-bell"></i>
<span>Thông báo</span>
</a>
<a href="chat-list.html" class="nav-item active">
<i class="fas fa-comments"></i>
<span>Tin nhắn</span>
</a>
</div>-->
</div> </div>
<style> <style>

View File

@@ -31,16 +31,87 @@
<label class="form-label">Số điện thoại</label> <label class="form-label">Số điện thoại</label>
<input type="tel" class="form-input" value="0983441099"> <input type="tel" class="form-input" value="0983441099">
</div> </div>
<div class="form-group">
<!--<div class="form-group">
<label class="form-label">Địa chỉ giao hàng</label> <label class="form-label">Địa chỉ giao hàng</label>
<textarea class="form-input" rows="3">123 Nguyễn Trãi, Quận 1, TP.HCM</textarea> <textarea class="form-input" rows="3">123 Nguyễn Trãi, Quận 1, TP.HCM</textarea>
</div>-->
<div class="form-group">
<label class="form-label">Tỉnh/Thành phố</label>
<select class="form-input" id="provinceSelect">
<option value="">Chọn tỉnh/thành phố</option>
<option value="hcm" selected>TP. Hồ Chí Minh</option>
<option value="hanoi">Hà Nội</option>
<option value="danang">Đà Nẵng</option>
<option value="binhduong">Bình Dương</option>
<option value="dongai">Đồng Nai</option>
</select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Ghi chú cho tài xế</label> <label class="form-label">Xã/Phường</label>
<input type="text" class="form-input" placeholder="Ví dụ: Gọi trước khi giao"> <select class="form-input" id="wardSelect">
<option value="">Chọn xã/phường</option>
<option value="ward1" selected>Phường 1</option>
<option value="ward2">Phường 2</option>
<option value="ward3">Phường 3</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Địa chỉ cụ thể</label>
<input type="text" class="form-input" value="123 Nguyễn Trãi" placeholder="Số nhà, tên đường">
</div>
<div class="form-group">
<label class="form-label">Ngày lấy hàng</label>
<input type="date" class="form-input" id="pickupDate">
</div>
<div class="form-group">
<label class="form-label">Ghi chú</label>
<input type="text" class="form-input" placeholder="Ví dụ: Thời gian yêu cầu giao hàng">
</div> </div>
</div> </div>
<!-- Invoice Information -->
<div class="card">
<div class="form-group" style="height:24px;">
<label class="checkbox-label" style="font-size:16px;">
<input type="checkbox" id="invoiceCheckbox" onchange="toggleInvoiceInfo()">
<span class="checkmark"></span>
Phát hành hóa đơn
</label>
</div>
<div id="invoiceInfoCard" class="invoice-info-card" style="display: none;">
<h4 class="invoice-title">Thông tin hóa đơn</h4>
<div class="form-group">
<label class="form-label">Tên Người Mua</label>
<input type="text" class="form-input" id="buyerName" placeholder="Họ và tên người mua">
</div>
<div class="form-group">
<label class="form-label">Mã số thuế</label>
<input type="text" class="form-input" id="taxCode" placeholder="Mã số thuế công ty">
</div>
<div class="form-group">
<label class="form-label">Tên công ty</label>
<input type="text" class="form-input" id="companyName" placeholder="Tên công ty/tổ chức">
</div>
<div class="form-group">
<label class="form-label">Địa chỉ</label>
<input type="text" class="form-input" id="companyAddress" placeholder="Địa chỉ công ty">
</div>
<div class="form-group">
<label class="form-label">Email nhận hóa đơn</label>
<input type="email" class="form-input" id="invoiceEmail" placeholder="email@company.com">
</div>
<div class="form-group">
<label class="form-label">Số điện thoại</label>
<input type="tel" class="form-input" id="invoicePhone" placeholder="Số điện thoại liên hệ">
</div>
</div>
</div>
<!-- Payment Method --> <!-- Payment Method -->
<div class="card"> <div class="card">
<h3 class="card-title">Phương thức thanh toán</h3> <h3 class="card-title">Phương thức thanh toán</h3>
@@ -70,25 +141,34 @@
<div class="card"> <div class="card">
<h3 class="card-title">Tóm tắt đơn hàng</h3> <h3 class="card-title">Tóm tắt đơn hàng</h3>
<div class="d-flex justify-between mb-2"> <div class="d-flex justify-between mb-2">
<span>Gạch men cao cấp 60x60 (10m²)</span> <div>
<span>4.500.000đ</span> <div>Gạch men cao cấp</div>
<div class="text-small text-muted">10 m² (28 viên / 10.08 m²)</div>
</div>
<span>4.536.000đ</span>
</div> </div>
<div class="d-flex justify-between mb-2"> <div class="d-flex justify-between mb-2">
<span>Gạch granite nhập khẩu (15m²)</span> <div>
<span>10.200.000đ</span> <div>Gạch granite nhập khẩu 1200x1200</div>
<div class="text-small text-muted">(11 viên / 15.84 m²)</div>
</div>
<span>10.771.200đ</span>
</div> </div>
<div class="d-flex justify-between mb-2"> <div class="d-flex justify-between mb-2">
<span>Gạch mosaic trang trí (5m²)</span> <div>
<span>1.600.000đ</span> <div>Gạch mosaic trang trí</div>
<div class="text-small text-muted">(5 viên / 5.625 m²)</div>
</div>
<span>1.800.000đ</span>
</div> </div>
<hr style="margin: 12px 0;"> <hr style="margin: 12px 0;">
<div class="d-flex justify-between mb-2"> <div class="d-flex justify-between mb-2">
<span>Tạm tính</span> <span>Tạm tính</span>
<span>16.700.000đ</span> <span>17.107.200đ</span>
</div> </div>
<div class="d-flex justify-between mb-2"> <div class="d-flex justify-between mb-2">
<span>Giảm giá Diamond</span> <span>Giảm giá Diamond</span>
<span class="text-success">-2.505.000đ</span> <span class="text-success">-2.566.000đ</span>
</div> </div>
<div class="d-flex justify-between mb-2"> <div class="d-flex justify-between mb-2">
<span>Phí vận chuyển</span> <span>Phí vận chuyển</span>
@@ -97,13 +177,26 @@
<hr style="margin: 12px 0;"> <hr style="margin: 12px 0;">
<div class="d-flex justify-between"> <div class="d-flex justify-between">
<span class="text-bold" style="font-size: 16px;">Tổng thanh toán</span> <span class="text-bold" style="font-size: 16px;">Tổng thanh toán</span>
<span class="text-bold text-primary" style="font-size: 18px;">14.195.00</span> <span class="text-bold text-primary" style="font-size: 18px;">14.541.12</span>
</div> </div>
</div> </div>
<!-- Price Negotiation -->
<div class="negotiation-checkbox">
<label class="checkbox-label">
<input type="checkbox" id="negotiationCheckbox" onchange="toggleNegotiation()">
<span>Yêu cầu đàm phán giá</span>
</label>
<div class="negotiation-info">
Chọn tùy chọn này nếu bạn muốn đàm phán giá với nhân viên bán hàng trước khi thanh toán.
</div>
</div>
<!-- Place Order Button --> <!-- Place Order Button -->
<div style="margin-bottom: 24px;"> <div style="margin-bottom: 24px;">
<a href="order-success.html" class="btn btn-primary btn-block"> <a href="payment-qr.html" class="btn btn-primary btn-block">
<i class="fas fa-check-circle"></i> Hoàn tất đặt hàng <i class="fas fa-check-circle"></i> Hoàn tất đặt hàng
</a> </a>
<p class="text-center text-small text-muted mt-2"> <p class="text-center text-small text-muted mt-2">
@@ -113,5 +206,110 @@
</div> </div>
</div> </div>
</div> </div>
<style>
.invoice-info-card {
margin-top: 16px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
.invoice-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: #374151;
}
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
font-weight: 500;
}
.checkbox-label input[type="checkbox"] {
margin-right: 8px;
transform: scale(1.1);
}
.negotiation-checkbox {
margin: 16px 0;
padding: 16px;
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 8px;
}
.negotiation-info {
font-size: 13px;
color: #92400e;
margin-top: 8px;
}
.payment-method-section.hidden {
display: none;
}
</style>
<script>
// Set default pickup date to tomorrow
document.addEventListener('DOMContentLoaded', function() {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const dateString = tomorrow.toISOString().split('T')[0];
document.getElementById('pickupDate').value = dateString;
});
function toggleInvoiceInfo() {
const checkbox = document.getElementById('invoiceCheckbox');
const invoiceCard = document.getElementById('invoiceInfoCard');
if (checkbox.checked) {
invoiceCard.style.display = 'block';
} else {
invoiceCard.style.display = 'none';
}
}
function toggleNegotiation() {
const checkbox = document.getElementById('negotiationCheckbox');
const paymentSection = document.querySelector('.card:has(.list-item)'); // Payment method section
const submitBtn = document.querySelector('.btn-primary');
if (checkbox.checked) {
paymentSection.classList.add('hidden');
submitBtn.innerHTML = '<i class="fas fa-handshake"></i> Gửi Yêu cầu & Đàm phán';
} else {
paymentSection.classList.remove('hidden');
submitBtn.innerHTML = '<i class="fas fa-check-circle"></i> Hoàn tất đặt hàng';
}
}
function toggleNegotiation() {
const checkbox = document.getElementById('negotiationCheckbox');
const paymentMethods = document.querySelectorAll('.card')[2]; // Payment method section is 3rd card
const submitBtn = document.querySelector('.btn-primary');
if (checkbox.checked) {
paymentMethods.style.display = 'none';
submitBtn.innerHTML = '<i class="fas fa-handshake"></i> Gửi Yêu cầu & Đàm phán';
submitBtn.href = '#'; // Don't redirect to order success
submitBtn.onclick = function(e) {
e.preventDefault();
alert('Yêu cầu đàm phán đã được gửi! Nhân viên bán hàng sẽ liên hệ với bạn sớm.');
window.location.href = 'order-dam-phan.html';
};
} else {
paymentMethods.style.display = 'block';
submitBtn.innerHTML = '<i class="fas fa-check-circle"></i> Hoàn tất đặt hàng';
submitBtn.href = 'payment-qr.html';
submitBtn.onclick = null;
}
}
</script>
</body> </body>
</html> </html>

462
html/chinh-sach-gia.html Normal file
View File

@@ -0,0 +1,462 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chính sách giá - EuroTile Worker</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="page-wrapper">
<!-- Header -->
<div class="header">
<a href="index.html" class="back-button">
<i class="fas fa-arrow-left"></i>
</a>
<h1 class="header-title">Chính sách giá</h1>
<button class="back-button" onclick="openInfoModal()">
<i class="fas fa-info-circle"></i>
</button>
</div>
<div class="container">
<!-- Tab Navigation -->
<div class="tab-nav mb-3">
<button class="tab-item active" data-tab="policy" onclick="switchTab('policy')">Chính sách giá</button>
<button class="tab-item" data-tab="pricelist" onclick="switchTab('pricelist')">Bảng giá</button>
</div>
<!-- Policy Tab Content -->
<div id="policyTab" class="tab-content active">
<div class="price-documents-list">
<!-- Document Card 1 -->
<div class="document-card">
<div class="document-icon">
<i class="fas fa-file-pdf text-red-600"></i>
</div>
<div class="document-info">
<h3 class="document-title">Chính sách giá Eurotile T10/2025</h3>
<p class="document-meta">
<i class="fas fa-calendar-alt mr-1"></i>
Công bố: 01/10/2025
</p>
<p class="document-desc">
Chính sách giá mới nhất cho sản phẩm gạch Eurotile, áp dụng từ tháng 10/2025
</p>
</div>
<button class="download-btn" onclick="downloadPDF('policy-eurotile-10-2025')">
<i class="fas fa-download"></i>
Tải về
</button>
</div>
<!-- Document Card 2 -->
<div class="document-card">
<div class="document-icon">
<i class="fas fa-file-pdf text-red-600"></i>
</div>
<div class="document-info">
<h3 class="document-title">Chính sách giá Vasta Stone T10/2025</h3>
<p class="document-meta">
<i class="fas fa-calendar-alt mr-1"></i>
Công bố: 01/10/2025
</p>
<p class="document-desc">
Chính sách giá đá tự nhiên Vasta Stone, hiệu lực từ tháng 10/2025
</p>
</div>
<button class="download-btn" onclick="downloadPDF('policy-vasta-10-2025')">
<i class="fas fa-download"></i>
Tải về
</button>
</div>
<!-- Document Card 3 -->
<div class="document-card">
<div class="document-icon">
<i class="fas fa-file-pdf text-red-600"></i>
</div>
<div class="document-info">
<h3 class="document-title">Chính sách chiết khấu đại lý 2025</h3>
<p class="document-meta">
<i class="fas fa-calendar-alt mr-1"></i>
Công bố: 15/09/2025
</p>
<p class="document-desc">
Chương trình chiết khấu và ưu đãi dành cho đại lý, thầu thợ
</p>
</div>
<button class="download-btn" onclick="downloadPDF('policy-dealer-2025')">
<i class="fas fa-download"></i>
Tải về
</button>
</div>
<!-- Document Card 4 -->
<div class="document-card">
<div class="document-icon">
<i class="fas fa-file-pdf text-red-600"></i>
</div>
<div class="document-info">
<h3 class="document-title">Điều kiện thanh toán & giao hàng</h3>
<p class="document-meta">
<i class="fas fa-calendar-alt mr-1"></i>
Công bố: 01/08/2025
</p>
<p class="document-desc">
Điều khoản thanh toán, chính sách giao hàng và bảo hành sản phẩm
</p>
</div>
<button class="download-btn" onclick="downloadPDF('policy-payment-2025')">
<i class="fas fa-download"></i>
Tải về
</button>
</div>
</div>
</div>
<!-- Price List Tab Content -->
<div id="pricelistTab" class="tab-content" style="display: none;">
<div class="price-documents-list">
<!-- Document Card 1 -->
<div class="document-card">
<div class="document-icon">
<i class="fas fa-file-excel text-green-600"></i>
</div>
<div class="document-info">
<h3 class="document-title">Bảng giá Gạch Granite Eurotile 2025</h3>
<p class="document-meta">
<i class="fas fa-calendar-alt mr-1"></i>
Cập nhật: 01/10/2025
</p>
<p class="document-desc">
Bảng giá chi tiết toàn bộ sản phẩm gạch granite, kích thước 60x60, 80x80, 120x120
</p>
</div>
<button class="download-btn" onclick="downloadPDF('pricelist-granite-2025')">
<i class="fas fa-download"></i>
Tải về
</button>
</div>
<!-- Document Card 2 -->
<div class="document-card">
<div class="document-icon">
<i class="fas fa-file-excel text-green-600"></i>
</div>
<div class="document-info">
<h3 class="document-title">Bảng giá Gạch Ceramic Eurotile 2025</h3>
<p class="document-meta">
<i class="fas fa-calendar-alt mr-1"></i>
Cập nhật: 01/10/2025
</p>
<p class="document-desc">
Bảng giá gạch ceramic vân gỗ, vân đá, vân xi măng các loại
</p>
</div>
<button class="download-btn" onclick="downloadPDF('pricelist-ceramic-2025')">
<i class="fas fa-download"></i>
Tải về
</button>
</div>
<!-- Document Card 3 -->
<div class="document-card">
<div class="document-icon">
<i class="fas fa-file-excel text-green-600"></i>
</div>
<div class="document-info">
<h3 class="document-title">Bảng giá Đá tự nhiên Vasta Stone 2025</h3>
<p class="document-meta">
<i class="fas fa-calendar-alt mr-1"></i>
Cập nhật: 01/10/2025
</p>
<p class="document-desc">
Bảng giá đá marble, granite tự nhiên nhập khẩu, kích thước tấm lớn
</p>
</div>
<button class="download-btn" onclick="downloadPDF('pricelist-stone-2025')">
<i class="fas fa-download"></i>
Tải về
</button>
</div>
<!-- Document Card 4 -->
<div class="document-card">
<div class="document-icon">
<i class="fas fa-file-excel text-green-600"></i>
</div>
<div class="document-info">
<h3 class="document-title">Bảng giá Phụ kiện & Vật liệu 2025</h3>
<p class="document-meta">
<i class="fas fa-calendar-alt mr-1"></i>
Cập nhật: 15/09/2025
</p>
<p class="document-desc">
Giá keo dán, chà ron, nẹp nhựa, nẹp inox và các phụ kiện thi công
</p>
</div>
<button class="download-btn" onclick="downloadPDF('pricelist-accessories-2025')">
<i class="fas fa-download"></i>
Tải về
</button>
</div>
<!-- Document Card 5 -->
<div class="document-card">
<div class="document-icon">
<i class="fas fa-file-excel text-green-600"></i>
</div>
<div class="document-info">
<h3 class="document-title">Bảng giá Gạch Outdoor & Chống trơn 2025</h3>
<p class="document-meta">
<i class="fas fa-calendar-alt mr-1"></i>
Cập nhật: 01/09/2025
</p>
<p class="document-desc">
Bảng giá sản phẩm outdoor, gạch chống trơn dành cho ngoại thất
</p>
</div>
<button class="download-btn" onclick="downloadPDF('pricelist-outdoor-2025')">
<i class="fas fa-download"></i>
Tải về
</button>
</div>
</div>
</div>
</div>
<!-- Info Modal -->
<div id="infoModal" class="modal-overlay" style="display: none;">
<div class="modal-content info-modal">
<div class="modal-header">
<h3 class="modal-title" style="font-weight: bold;">Hướng dẫn sử dụng</h3>
<button class="modal-close" onclick="closeInfoModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<p>Đây là nội dung hướng dẫn sử dụng cho tính năng Chính sách giá:</p>
<ul class="list-disc ml-6 mt-3">
<li>Chọn tab "Chính sách giá" để xem các chính sách giá hiện hành</li>
<li>Chọn tab "Bảng giá" để tải về bảng giá chi tiết sản phẩm</li>
<li>Nhấn nút "Tải về" để download file PDF/Excel</li>
<li>Các bảng giá được cập nhật định kỳ hàng tháng</li>
<li>Liên hệ sales để được tư vấn giá tốt nhất</li>
</ul>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeInfoModal()">Đóng</button>
</div>
</div>
</div>
</div>
<style>
.price-documents-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.document-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 16px;
display: flex;
gap: 16px;
align-items: flex-start;
transition: all 0.2s;
}
.document-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.document-icon {
font-size: 32px;
flex-shrink: 0;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 8px;
}
.document-info {
flex: 1;
}
.document-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin-bottom: 6px;
}
.document-meta {
font-size: 13px;
color: #6b7280;
margin-bottom: 8px;
}
.document-desc {
font-size: 14px;
color: #6b7280;
line-height: 1.5;
}
.download-btn {
background: #005B9A;
color: white;
border: none;
padding: 10px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
transition: all 0.2s;
}
.download-btn:hover {
background: #004a7c;
transform: scale(1.05);
}
.download-btn i {
font-size: 16px;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-content {
background: white;
border-radius: 12px;
width: 100%;
max-width: 500px;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #e5e7eb;
display: flex;
gap: 12px;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
color: #6b7280;
cursor: pointer;
}
@media (max-width: 768px) {
.document-card {
flex-direction: column;
align-items: stretch;
}
.download-btn {
width: 100%;
justify-content: center;
}
}
.tab-item.active {
background: var(--primary-blue);
color: var(--white);
}
</style>
<script>
function switchTab(tabName) {
// Update tab buttons
const tabButtons = document.querySelectorAll('.tab-item');
tabButtons.forEach(btn => {
if (btn.dataset.tab === tabName) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
// Update tab content
document.getElementById('policyTab').style.display = tabName === 'policy' ? 'block' : 'none';
document.getElementById('pricelistTab').style.display = tabName === 'pricelist' ? 'block' : 'none';
}
function downloadPDF(documentId) {
// Simulate PDF download
alert(`Đang tải file: ${documentId}.pdf\n\nFile sẽ được tải về máy của bạn trong giây lát.`);
// In a real application, this would trigger actual file download:
// window.location.href = `/api/documents/${documentId}/download`;
}
function openInfoModal() {
document.getElementById('infoModal').style.display = 'flex';
}
function closeInfoModal() {
document.getElementById('infoModal').style.display = 'none';
}
// Close modal when clicking outside
document.addEventListener('click', function(e) {
if (e.target.classList.contains('modal-overlay')) {
e.target.style.display = 'none';
}
});
</script>
</body>
</html>

View File

@@ -344,6 +344,20 @@
<div class="error-message" id="project-area-error">Vui lòng nhập diện tích hợp lệ</div> <div class="error-message" id="project-area-error">Vui lòng nhập diện tích hợp lệ</div>
</div> </div>
<div class="form-group">
<label class="form-label">
Khu vực (Tỉnh/ Thành phố) <span class="required">*</span>
</label>
<input
type="text"
class="form-input"
id="project-area"
placeholder="VD: Hà Nội"
min="1"
required>
<div class="error-message" id="project-area-error">Vui lòng nhập khu vực</div>
</div>
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
Phong cách mong muốn <span class="required">*</span> Phong cách mong muốn <span class="required">*</span>
@@ -397,7 +411,7 @@
<div class="error-message" id="project-notes-error">Vui lòng mô tả yêu cầu chi tiết</div> <div class="error-message" id="project-notes-error">Vui lòng mô tả yêu cầu chi tiết</div>
</div> </div>
<div class="form-group"> <!--<div class="form-group">
<label class="form-label"> <label class="form-label">
Thông tin liên hệ Thông tin liên hệ
</label> </label>
@@ -406,7 +420,7 @@
class="form-input" class="form-input"
id="contact-info" id="contact-info"
placeholder="Số điện thoại, email hoặc địa chỉ (tùy chọn)"> placeholder="Số điện thoại, email hoặc địa chỉ (tùy chọn)">
</div> </div>-->
</div> </div>
<!-- File Upload --> <!-- File Upload -->
@@ -449,10 +463,10 @@
<!-- Form Actions --> <!-- Form Actions -->
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="saveDraft()"> <!--<button type="button" class="btn btn-secondary" onclick="saveDraft()">
<i class="fas fa-save"></i> <i class="fas fa-save"></i>
Lưu nháp Lưu nháp
</button> </button>-->
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="fas fa-paper-plane"></i> <i class="fas fa-paper-plane"></i>
Gửi yêu cầu Gửi yêu cầu

View File

@@ -8,6 +8,83 @@
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head> </head>
<style>
/* News Section Styles */
.news-slider-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.news-slider-wrapper {
display: flex;
gap: 16px;
padding-bottom: 8px;
}
.news-card {
flex-shrink: 0;
width: 280px;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border: 1px solid #e2e8f0;
transition: all 0.3s ease;
}
.news-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.news-image {
width: 100%;
height: 140px;
object-fit: cover;
}
.news-content {
padding: 12px;
}
.news-title {
font-size: 14px;
font-weight: 600;
color: #1e293b;
line-height: 1.3;
margin-bottom: 6px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.news-desc {
font-size: 12px;
color: #64748b;
line-height: 1.4;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.news-meta {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #94a3b8;
}
.news-meta span {
display: flex;
align-items: center;
gap: 4px;
}
</style>
<body> <body>
<div class="page-wrapper"> <div class="page-wrapper">
<div class="container"> <div class="container">
@@ -25,8 +102,9 @@
</div> </div>
<div class="d-flex justify-between align-center" style="margin-top: auto;"> <div class="d-flex justify-between align-center" style="margin-top: auto;">
<div> <div>
<p style="color: white; font-size: 18px; font-weight: 600; margin-bottom: 4px;">La Nguyen Quynh</p> <p style="color: white; font-size: 18px; font-weight: 600; margin-bottom: 4px;">0983 441 099</p>
<p style="color: rgba(255,255,255,0.9); font-size: 12px;">CLASS: <span style="font-weight: 600;">DIAMOND</span></p> <p style="color: rgba(255,255,255,0.9); font-size: 12px;">Name: <span style="font-weight: 600;">LA NGUYEN QUYNH</span></p>
<p style="color: rgba(255,255,255,0.9); font-size: 12px;">Class: <span style="font-weight: 600;">DIAMOND</span></p>
<p style="color: rgba(255,255,255,0.9); font-size: 12px;">Points: <span style="font-weight: 600;">9750</span></p> <p style="color: rgba(255,255,255,0.9); font-size: 12px;">Points: <span style="font-weight: 600;">9750</span></p>
</div> </div>
<div style="background: white; padding: 8px; border-radius: 8px;"> <div style="background: white; padding: 8px; border-radius: 8px;">
@@ -36,11 +114,11 @@
</div> </div>
<!-- Promotions Section --> <!-- Promotions Section -->
<div class="mb-3"> <!--<div class="mb-3">
<h2> <b> Chương trình ưu đãi</b> </h2> <h2> <b> Tin nổi bật</b> </h2>
<div class="slider-container"> <div class="slider-container">
<div class="slider-wrapper"> <div class="slider-wrapper">
<div class="slider-item"> <div class="news-card">
<img src="https://images.unsplash.com/photo-1615971677499-5467cbab01c0?w=280&h=140&fit=crop" alt="Khuyến mãi 1"> <img src="https://images.unsplash.com/photo-1615971677499-5467cbab01c0?w=280&h=140&fit=crop" alt="Khuyến mãi 1">
<div style="padding: 12px; background: white;"> <div style="padding: 12px; background: white;">
<h3 style="font-size: 14px;">Mua công nhắc - Khuyến mãi cảng lớn</h3> <h3 style="font-size: 14px;">Mua công nhắc - Khuyến mãi cảng lớn</h3>
@@ -63,6 +141,56 @@
</div> </div>
</div> </div>
</div> </div>
</div>-->
<!-- News Section -->
<div class="mb-3">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h2> <b> Tin nổi bật</b> </h2>
<a href="news-list.html" style="color: #2563eb; font-size: 12px; text-decoration: none; font-weight: 500;">
Xem tất cả <i class="fas fa-arrow-right" style="margin-left: 4px;"></i>
</a>
</div>
<div class="news-slider-container">
<div class="news-slider-wrapper">
<div class="news-card">
<img src="https://images.unsplash.com/photo-1503387762-592deb58ef4e?w=280&h=140&fit=crop" alt="Tin tức 1" class="news-image">
<div class="news-content">
<h3 class="news-title">5 xu hướng gạch men phòng tắm được ưa chuộng năm 2024</h3>
<p class="news-desc">Khám phá những mẫu gạch men hiện đại, sang trọng cho không gian phòng tắm.</p>
<div class="news-meta">
<span><i class="fas fa-calendar"></i> 15/11/2024</span>
<span><i class="fas fa-eye"></i> 2.3K lượt xem</span>
</div>
</div>
</div>
<div class="news-card">
<img src="https://images.unsplash.com/photo-1586023492125-27b2c045efd7?w=280&h=140&fit=crop" alt="Tin tức 2" class="news-image">
<div class="news-content">
<h3 class="news-title">Hướng dẫn thi công gạch granite 60x60 chuyên nghiệp</h3>
<p class="news-desc">Quy trình thi công chi tiết từ A-Z cho thầy thợ xây dựng.</p>
<div class="news-meta">
<span><i class="fas fa-calendar"></i> 12/11/2024</span>
<span><i class="fas fa-eye"></i> 1.8K lượt xem</span>
</div>
</div>
</div>
<div class="news-card">
<img src="https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=280&h=140&fit=crop" alt="Tin tức 3" class="news-image">
<div class="news-content">
<h3 class="news-title">Bảng giá gạch men cao cấp mới nhất tháng 11/2024</h3>
<p class="news-desc">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.</p>
<div class="news-meta">
<span><i class="fas fa-calendar"></i> 10/11/2024</span>
<span><i class="fas fa-eye"></i> 3.1K lượt xem</span>
</div>
</div>
</div>
</div>
</div>
</div> </div>
<!-- Products & Cart Section --> <!-- Products & Cart Section -->
@@ -91,37 +219,6 @@
</div> </div>
</div> </div>
<!-- Loyalty Section -->
<div class="card">
<h3 class="card-title">Khách hàng thân thiết</h3>
<div class="feature-grid">
<a href="points-record.html" class="feature-item">
<div class="feature-icon">
<i class="fas fa-plus-circle"></i>
</div>
<div class="feature-title">Ghi nhận điểm</div>
</a>
<a href="loyalty-rewards.html" class="feature-item">
<div class="feature-icon">
<i class="fas fa-gift"></i>
</div>
<div class="feature-title">Đổi quà</div>
</a>
<a href="points-history.html" class="feature-item">
<div class="feature-icon">
<i class="fas fa-history"></i>
</div>
<div class="feature-title">Lịch sử điểm</div>
</a>
<!--<a href="referral.html" class="feature-item">
<div class="feature-icon">
<i class="fas fa-user-plus"></i>
</div>
<div class="feature-title">Giới thiệu bạn</div>
</a>-->
</div>
</div>
<!-- Orders & Payment Section --> <!-- Orders & Payment Section -->
<!--<div class="card"> <!--<div class="card">
<h3 class="card-title">Yêu cầu báo giá & báo giá</h3> <h3 class="card-title">Yêu cầu báo giá & báo giá</h3>
@@ -145,11 +242,11 @@
<div class="card"> <div class="card">
<h3 class="card-title">Đơn hàng & thanh toán</h3> <h3 class="card-title">Đơn hàng & thanh toán</h3>
<div class="feature-grid"> <div class="feature-grid">
<a href="quotes-list.html" class="feature-item"> <a href="chinh-sach-gia.html" class="feature-item">
<div class="feature-icon"> <div class="feature-icon">
<i class="fas fa-file-alt"></i> <i class="fas fa-file-alt"></i>
</div> </div>
<div class="feature-title">Yêu cầu báo giá</div> <div class="feature-title">Chính sách giá</div>
</a> </a>
<a href="orders.html" class="feature-item"> <a href="orders.html" class="feature-item">
<div class="feature-icon"> <div class="feature-icon">
@@ -159,12 +256,43 @@
</a> </a>
<a href="payments.html" class="feature-item"> <a href="payments.html" class="feature-item">
<div class="feature-icon"> <div class="feature-icon">
<i class="fas fa-file-invoice-dollar"></i> <!--<i class="fas fa-file-invoice-dollar"></i>-->
<i class="fas fa-credit-card"></i>
</div> </div>
<div class="feature-title">Thanh toán</div> <div class="feature-title">Thanh toán</div>
</a> </a>
</div> </div>
</div> </div>
<!-- Loyalty Section -->
<div class="card">
<h3 class="card-title">Khách hàng thân thiết</h3>
<div class="feature-grid">
<a href="points-record-list.html" class="feature-item">
<div class="feature-icon">
<i class="fas fa-plus-circle"></i>
</div>
<div class="feature-title">Ghi nhận điểm</div>
</a>
<a href="loyalty-rewards.html" class="feature-item">
<div class="feature-icon">
<i class="fas fa-gift"></i>
</div>
<div class="feature-title">Đổi quà</div>
</a>
<a href="points-history.html" class="feature-item">
<div class="feature-icon">
<i class="fas fa-history"></i>
</div>
<div class="feature-title">Lịch sử điểm</div>
</a>
<!--<a href="referral.html" class="feature-item">
<div class="feature-icon">
<i class="fas fa-user-plus"></i>
</div>
<div class="feature-title">Giới thiệu bạn</div>
</a>-->
</div>
</div>
<!-- Collaboration & Reports Section --> <!-- Collaboration & Reports Section -->
<!--<div class="card"> <!--<div class="card">
@@ -192,8 +320,8 @@
</div>--> </div>-->
<div class="card"> <div class="card">
<h3 class="card-title">Nhà mẫu, dự án & tin tức</h3> <h3 class="card-title">Nhà mẫu & dự án</h3>
<div class="feature-grid"> <div class="grid grid-2">
<a href="nha-mau.html" class="feature-item"> <a href="nha-mau.html" class="feature-item">
<div class="feature-icon"> <div class="feature-icon">
<!--<i class="fas fa-building"></i>--> <!--<i class="fas fa-building"></i>-->
@@ -201,19 +329,19 @@
</div> </div>
<div class="feature-title">Nhà mẫu</div> <div class="feature-title">Nhà mẫu</div>
</a> </a>
<a href="project-submission.html" class="feature-item"> <a href="project-submission-list.html" class="feature-item">
<div class="feature-icon"> <div class="feature-icon">
<!--<i class="fas fa-handshake"></i>--> <!--<i class="fas fa-handshake"></i>-->
<i class="fa-solid fa-building-circle-check"></i> <i class="fa-solid fa-building-circle-check"></i>
</div> </div>
<div class="feature-title">Đăng ký dự án</div> <div class="feature-title">Đăng ký dự án</div>
</a> </a>
<a href="news-list.html" class="feature-item"> <!--<a href="news-list.html" class="feature-item">
<div class="feature-icon"> <div class="feature-icon">
<i class="fa-solid fa-newspaper"></i> <i class="fa-solid fa-newspaper"></i>
</div> </div>
<div class="feature-title">Tin tức</div> <div class="feature-title">Tin tức</div>
</a> </a>-->
</div> </div>
</div> </div>
@@ -242,9 +370,9 @@
<i class="fas fa-crown nav-icon"></i> <i class="fas fa-crown nav-icon"></i>
<span class="nav-label">Hội viên</span> <span class="nav-label">Hội viên</span>
</a> </a>
<a href="promotions.html" class="nav-item"> <a href="news-list.html" class="nav-item">
<i class="fas fa-tags nav-icon"></i> <i class="fas fa-newspaper nav-icon"></i>
<span class="nav-label">Khuyến mãi</span> <span class="nav-label">Tin tức</span>
</a> </a>
<a href="notifications.html" class="nav-item" style="position: relative"> <a href="notifications.html" class="nav-item" style="position: relative">
<i class="fas fa-bell nav-icon"></i> <i class="fas fa-bell nav-icon"></i>

View File

@@ -26,7 +26,7 @@
</div> </div>
<!-- Login Form --> <!-- Login Form -->
<form action="otp.html" class="card"> <form action="index.html" class="card">
<div class="form-group"> <div class="form-group">
<label class="form-label" for="phone">Số điện thoại</label> <label class="form-label" for="phone">Số điện thoại</label>
<div class="form-input-icon"> <div class="form-input-icon">
@@ -34,9 +34,16 @@
<input type="tel" id="phone" class="form-input" placeholder="Nhập số điện thoại" required> <input type="tel" id="phone" class="form-input" placeholder="Nhập số điện thoại" required>
</div> </div>
</div> </div>
<!-- Password -->
<div class="form-group">
<label class="form-label" for="password">Mật khẩu</label>
<div class="form-input-icon">
<i class="fas fa-lock icon"></i>
<input type="password" id="password" class="form-input" placeholder="Nhập mật khẩu" required>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block"> <button type="submit" class="btn btn-primary btn-block">
Nhận mã OTP Đăng nhập
</button> </button>
</form> </form>
@@ -51,7 +58,7 @@
</div> </div>
<!-- Brand Selection --> <!-- Brand Selection -->
<div class="mt-4"> <!-- <div class="mt-4">
<p class="text-center text-small text-muted mb-3">Hoặc chọn thương hiệu</p> <p class="text-center text-small text-muted mb-3">Hoặc chọn thương hiệu</p>
<div class="grid grid-2"> <div class="grid grid-2">
<button class="btn btn-secondary"> <button class="btn btn-secondary">
@@ -61,7 +68,7 @@
<i class="fas fa-gem"></i> Vasta Stone <i class="fas fa-gem"></i> Vasta Stone
</button> </button>
</div> </div>
</div> </div>-->
<!-- Support --> <!-- Support -->
<div class="text-center mt-4"> <div class="text-center mt-4">

View File

@@ -8,6 +8,63 @@
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head> </head>
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-content {
background: white;
border-radius: 12px;
width: 100%;
max-width: 500px;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #e5e7eb;
display: flex;
gap: 12px;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
color: #6b7280;
cursor: pointer;
}
</style>
<body> <body>
<div class="page-wrapper"> <div class="page-wrapper">
<!-- Header --> <!-- Header -->
@@ -16,7 +73,35 @@
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
</a> </a>
<h1 class="header-title">Đổi quà tặng</h1> <h1 class="header-title">Đổi quà tặng</h1>
<div style="width: 32px;"></div> <!--<div style="width: 32px;"></div>-->
<button class="back-button" onclick="openInfoModal()">
<i class="fas fa-info-circle"></i>
</button>
</div>
<!-- Info Modal -->
<div id="infoModal" class="modal-overlay" style="display: none;">
<div class="modal-content info-modal">
<div class="modal-header">
<h3 class="modal-title" style="font-weight: bold;">Hướng dẫn sử dụng</h3>
<button class="modal-close" onclick="closeInfoModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<p>Đây là nội dung hướng dẫn sử dụng cho tính năng Đổi quà tặng:</p>
<ul class="list-disc ml-6 mt-3">
<li>Sử dụng điểm tích lũy của bạn để đổi các phần quà giá trị trong danh mục.</li>
<li>Bấm vào một phần quà để xem chi tiết và điều kiện áp dụng.</li>
<li>Khi xác nhận đổi quà, bạn có thể chọn "Nhận hàng tại Showroom".</li>
<li>Nếu chọn "Nhận hàng tại Showroom", bạn sẽ cần chọn Showroom bạn muốn đến nhận từ danh sách thả xuống.</li>
<li>Quà đã đổi sẽ được chuyển vào mục "Quà của tôi" (trong trang Hội viên).</li>
</ul>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeInfoModal()">Đóng</button>
</div>
</div>
</div> </div>
<div class="container"> <div class="container">
@@ -123,4 +208,25 @@
</div> </div>
</div> </div>
</body> </body>
<script>
function openInfoModal() {
document.getElementById('infoModal').style.display = 'flex';
}
function closeInfoModal() {
document.getElementById('infoModal').style.display = 'none';
}
function viewOrderDetail(orderId) {
window.location.href = `order-detail.html?id=${orderId}`;
}
// Close modal when clicking outside
document.addEventListener('click', function(e) {
if (e.target.classList.contains('modal-overlay')) {
e.target.style.display = 'none';
}
});
</script>
</html> </html>

View File

@@ -16,7 +16,7 @@
</div> </div>
<div class="container"> <div class="container">
<!-- Member Card --> <!-- Member Card -->
<div class="member-card member-card-diamond"> <div class="member-card member-card-diamond">
<div class="d-flex justify-between align-center"> <div class="d-flex justify-between align-center">
<div> <div>
<h3 style="color: white; font-size: 24px; margin-bottom: 4px;">EUROTILE</h3> <h3 style="color: white; font-size: 24px; margin-bottom: 4px;">EUROTILE</h3>
@@ -29,9 +29,10 @@
</div> </div>
<div class="d-flex justify-between align-center" style="margin-top: auto;"> <div class="d-flex justify-between align-center" style="margin-top: auto;">
<div> <div>
<p style="color: white; font-size: 18px; font-weight: 600; margin-bottom: 4px;">La Nguyen Quynh</p> <p style="color: white; font-size: 18px; font-weight: 600; margin-bottom: 4px;">0983 441 099</p>
<p style="color: rgba(255,255,255,0.9); font-size: 12px;">CLASS: <span style="font-weight: 600;">DIAMOND</span></p> <p style="color: rgba(255,255,255,0.9); font-size: 12px; margin-bottom: 0px;">Name: <span style="font-weight: 600;">LA NGUYEN QUYNH</span></p>
<p style="color: rgba(255,255,255,0.9); font-size: 12px;">Points: <span style="font-weight: 600;">9750</span></p> <p style="color: rgba(255,255,255,0.9); font-size: 12px; margin-bottom: 0px;">Class: <span style="font-weight: 600;">DIAMOND</span></p>
<p style="color: rgba(255,255,255,0.9); font-size: 12px; margin-bottom: 0px;">Points: <span style="font-weight: 600;">9750</span></p>
</div> </div>
<div style="background: white; padding: 8px; border-radius: 8px;"> <div style="background: white; padding: 8px; border-radius: 8px;">
<img src="https://api.qrserver.com/v1/create-qr-code/?size=60x60&data=0983441099" alt="QR Code" style="width: 60px; height: 60px;"> <img src="https://api.qrserver.com/v1/create-qr-code/?size=60x60&data=0983441099" alt="QR Code" style="width: 60px; height: 60px;">
@@ -67,7 +68,7 @@
<i class="fas fa-chevron-right list-item-arrow"></i> <i class="fas fa-chevron-right list-item-arrow"></i>
</a> </a>
<a href="points-record.html" class="list-item"> <a href="points-record-list.html" class="list-item">
<div class="list-item-icon"> <div class="list-item-icon">
<i class="fas fa-plus-circle"></i> <i class="fas fa-plus-circle"></i>
</div> </div>
@@ -148,9 +149,9 @@
<i class="fas fa-crown nav-icon"></i> <i class="fas fa-crown nav-icon"></i>
<span class="nav-label">Hội viên</span> <span class="nav-label">Hội viên</span>
</a> </a>
<a href="promotions.html" class="nav-item"> <a href="news-list.html" class="nav-item">
<i class="fas fa-tags nav-icon"></i> <i class="fas fa-newspaper nav-icon"></i>
<span class="nav-label">Khuyến mãi</span> <span class="nav-label">Tin tức</span>
</a> </a>
<a href="notifications.html" class="nav-item" style="position: relative"> <a href="notifications.html" class="nav-item" style="position: relative">
<i class="fas fa-bell nav-icon"></i> <i class="fas fa-bell nav-icon"></i>

View File

@@ -5,10 +5,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tin tức & Chuyên môn - Worker App</title> <title>Tin tức & Chuyên môn - Worker App</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css">
<style> <style>
:root { :root {
--primary-color: #2563eb; --primary-color: #005B9A;
--primary-dark: #1d4ed8; --primary-dark: #1d4ed8;
--secondary-color: #64748b; --secondary-color: #64748b;
--success-color: #10b981; --success-color: #10b981;
@@ -19,6 +20,7 @@
--text-primary: #1e293b; --text-primary: #1e293b;
--text-secondary: #64748b; --text-secondary: #64748b;
--border-color: #e2e8f0; --border-color: #e2e8f0;
scrollbar-width: none;
} }
* { * {
@@ -28,21 +30,22 @@
} }
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; /*font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;*/
background-color: var(--background-color); background-color: var(--background-color);
color: var(--text-primary); color: var(--text-primary);
line-height: 1.6; line-height: 1.6;
overflow-x: hidden; overflow-x: hidden;
} }
.header { /*.header {
background: var(--card-background); background: var(--card-background);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 100; z-index: 100;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
} padding: 0px;
}*/
.header-content { .header-content {
display: flex; display: flex;
@@ -68,11 +71,6 @@
background-color: #f1f5f9; background-color: #f1f5f9;
} }
.header-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
}
.search-button { .search-button {
background: none; background: none;
@@ -95,11 +93,12 @@
margin: 0 auto; margin: 0 auto;
background: var(--card-background); background: var(--card-background);
min-height: 100vh; min-height: 100vh;
padding: 0px;
} }
.content { .content {
padding: 1rem; padding: 16px;
padding-bottom: 100px; padding-bottom: 10px;
} }
.categories-section { .categories-section {
@@ -112,6 +111,9 @@
overflow-x: auto; overflow-x: auto;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
padding-top: 4px;
scrollbar-width: none;
} }
.category-tab { .category-tab {
@@ -121,7 +123,6 @@
color: var(--text-secondary); color: var(--text-secondary);
border: none; border: none;
border-radius: 1.5rem; border-radius: 1.5rem;
font-size: 0.875rem;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
@@ -308,8 +309,8 @@
@media (max-width: 480px) { @media (max-width: 480px) {
.content { .content {
padding: 0.75rem; padding: 16px;
padding-bottom: 100px; padding-bottom: 10px;
} }
.news-card { .news-card {
@@ -325,19 +326,11 @@
</style> </style>
</head> </head>
<body> <body>
<div class="container">
<!-- Header --> <!-- Header -->
<header class="header"> <div class="header">
<div class="header-content"> <h1 class="header-title">Tin tức & chuyên môn</h1>
<button class="back-button" onclick="goBack()"> </div>
<i class="fas fa-arrow-left"></i>
</button>
<h1 class="header-title">Tin tức & Chuyên môn</h1>
<button class="search-button" onclick="toggleSearch()">
<i class="fas fa-search"></i>
</button>
</div>
</header>
<!-- Content --> <!-- Content -->
<div class="content"> <div class="content">
@@ -345,11 +338,11 @@
<div class="categories-section"> <div class="categories-section">
<div class="categories-tabs"> <div class="categories-tabs">
<button class="category-tab active" onclick="filterCategory('all')">Tất cả</button> <button class="category-tab active" onclick="filterCategory('all')">Tất cả</button>
<button class="category-tab" onclick="filterCategory('trends')">Xu hướng</button> <button class="category-tab" onclick="filterCategory('trends')">Tin tức</button>
<button class="category-tab" onclick="filterCategory('technique')">Kỹ thuật</button> <button class="category-tab" onclick="filterCategory('technique')">Chuyên môn</button>
<button class="category-tab" onclick="filterCategory('pricing')">Bảng giá</button>
<button class="category-tab" onclick="filterCategory('projects')">Dự án</button> <button class="category-tab" onclick="filterCategory('projects')">Dự án</button>
<button class="category-tab" onclick="filterCategory('tips')">Mẹo hay</button> <button class="category-tab" onclick="filterCategory('tips')">Sự kiện</button>
<button class="category-tab" onclick="filterCategory('tips')">Khuyến mãi</button>
</div> </div>
</div> </div>
@@ -476,9 +469,33 @@
Xem thêm tin tức Xem thêm tin tức
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<!-- Bottom Navigation -->
<div class="bottom-nav">
<a href="index.html" class="nav-item">
<i class="fas fa-home nav-icon"></i>
<span class="nav-label">Trang chủ</span>
</a>
<a href="loyalty.html" class="nav-item">
<i class="fas fa-crown nav-icon"></i>
<span class="nav-label">Hội viên</span>
</a>
<a href="news-list.html" class="nav-item active">
<i class="fas fa-newspaper nav-icon"></i>
<span class="nav-label">Tin tức</span>
</a>
<a href="notifications.html" class="nav-item" style="position: relative">
<i class="fas fa-bell nav-icon"></i>
<span class="nav-label">Thông báo</span>
<span class="badge">5</span>
</a>
<a href="account.html" class="nav-item">
<i class="fas fa-user nav-icon"></i>
<span class="nav-label">Cài đặt</span>
</a>
</div>
<script> <script>
function goBack() { function goBack() {
window.history.back(); window.history.back();

View File

@@ -222,6 +222,61 @@
padding: 15px; padding: 15px;
} }
} }
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-content {
background: white;
border-radius: 12px;
width: 100%;
max-width: 500px;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #e5e7eb;
display: flex;
gap: 12px;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
color: #6b7280;
cursor: pointer;
}
</style> </style>
</head> </head>
<body> <body>
@@ -232,7 +287,10 @@
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
</a> </a>
<h1 class="header-title">Nhà mẫu</h1> <h1 class="header-title">Nhà mẫu</h1>
<div style="width: 32px;"></div> <!--<div style="width: 32px;"></div>-->
<button class="back-button" onclick="openInfoModal()">
<i class="fas fa-info-circle"></i>
</button>
</div> </div>
<!-- Tab Navigation --> <!-- Tab Navigation -->
@@ -414,6 +472,30 @@
</a> </a>
</div>--> </div>-->
<!-- Info Modal -->
<div id="infoModal" class="modal-overlay" style="display: none;">
<div class="modal-content info-modal">
<div class="modal-header">
<h3 class="modal-title" style="font-weight: bold;">Hướng dẫn sử dụng</h3>
<button class="modal-close" onclick="closeInfoModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<p>Đây là nội dung hướng dẫn sử dụng cho tính năng Nhà mẫu:</p>
<ul class="list-disc ml-6 mt-3">
<li>Tab "Thư viện Mẫu 360": Là nơi công ty cung cấp các mẫu thiết kế 360° có sẵn để bạn tham khảo.</li>
<li>Tab "Yêu cầu Thiết kế": Là nơi bạn gửi yêu cầu (ticket) để đội ngũ thiết kế của chúng tôi hỗ trợ bạn.</li>
<li>Bấm nút "+" trong tab "Yêu cầu Thiết kế" để tạo một Yêu cầu Thiết kế mới.</li>
<li>Khi yêu cầu hoàn thành, bạn có thể xem link thiết kế 3D trong trang chi tiết yêu cầu.</li>
</ul>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeInfoModal()">Đóng</button>
</div>
</div>
</div>
<!-- Floating Action Button (only show on Requests tab) --> <!-- Floating Action Button (only show on Requests tab) -->
<button class="fab" id="fab-button" style="display: none;" onclick="createNewRequest()"> <button class="fab" id="fab-button" style="display: none;" onclick="createNewRequest()">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
@@ -476,6 +558,27 @@
}, index * 100); }, index * 100);
}); });
}); });
function openInfoModal() {
document.getElementById('infoModal').style.display = 'flex';
}
function closeInfoModal() {
document.getElementById('infoModal').style.display = 'none';
}
function viewOrderDetail(orderId) {
window.location.href = `order-detail.html?id=${orderId}`;
}
// Close modal when clicking outside
document.addEventListener('click', function(e) {
if (e.target.classList.contains('modal-overlay')) {
e.target.style.display = 'none';
}
});
</script> </script>
</body> </body>
</html> </html>

View File

@@ -7,6 +7,12 @@
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.tab-item.active {
background: var(--primary-blue);
color: var(--white);
}
</style>
</head> </head>
<body> <body>
<div class="page-wrapper"> <div class="page-wrapper">
@@ -105,9 +111,9 @@
<i class="fas fa-crown nav-icon"></i> <i class="fas fa-crown nav-icon"></i>
<span class="nav-label">Hội viên</span> <span class="nav-label">Hội viên</span>
</a> </a>
<a href="promotions.html" class="nav-item"> <a href="news-list.html" class="nav-item">
<i class="fas fa-tags nav-icon"></i> <i class="fas fa-newspaper nav-icon"></i>
<span class="nav-label">Khuyến mãi</span> <span class="nav-label">Tin tức</span>
</a> </a>
<a href="notifications.html" class="nav-item active" style="position: relative"> <a href="notifications.html" class="nav-item active" style="position: relative">
<i class="fas fa-bell nav-icon"></i> <i class="fas fa-bell nav-icon"></i>

85
html/order-dam-phan.html Normal file
View File

@@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Đặt hàng thành công - EuroTile Worker</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="page-wrapper">
<div class="container">
<div class="success-container">
<div class="success-icon">
<i class="fas fa-check"></i>
</div>
<h1 class="success-title">Đã gửi yêu cầu!</h1>
<p class="success-message">
Cảm ơn bạn đã gửi yêu cầu đàm phán. Chúng tôi sẽ liên hệ lại trong vòng 24 giờ.
</p>
<!-- Order Info -->
<!--<div class="card" style="background: var(--background-gray);">
<div class="text-center mb-3">
<p class="text-small text-muted">Mã đơn hàng</p>
<p class="text-bold" style="font-size: 24px; color: var(--primary-blue);">DH2023120801</p>
</div>
<div class="d-flex justify-between mb-2">
<span class="text-small text-muted">Ngày đặt</span>
<span class="text-small">08/12/2023 14:30</span>
</div>
<div class="d-flex justify-between mb-2">
<span class="text-small text-muted">Tổng tiền</span>
<span class="text-small text-bold">14.195.000đ</span>
</div>
<div class="d-flex justify-between mb-2">
<span class="text-small text-muted">Phương thức thanh toán</span>
<span class="text-small">Chuyển khoản</span>
</div>
<div class="d-flex justify-between">
<span class="text-small text-muted">Trạng thái</span>
<span class="text-small text-warning">Chờ xác nhận</span>
</div>
</div>-->
<!-- Next Steps -->
<!--<div class="card">
<h3 class="card-title">Các bước tiếp theo</h3>
<div style="display: flex; align-items: flex-start; margin-bottom: 12px;">
<div style="width: 24px; height: 24px; background: var(--primary-blue); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; margin-right: 12px; flex-shrink: 0;">1</div>
<div>
<p class="text-small text-bold">Chờ liên hệ</p>
<p class="text-small text-muted">Nhân viên sẽ liên hệ trong 24h</p>
</div>
</div>
<div style="display: flex; align-items: flex-start; margin-bottom: 12px;">
<div style="width: 24px; height: 24px; background: var(--border-color); color: var(--text-light); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; margin-right: 12px; flex-shrink: 0;">2</div>
<div>
<p class="text-small text-bold">Đàm phán giá</p>
<p class="text-small text-muted">Nhân viên sẽ gửi lại báo giá chi tiết sau đàm phán thành công</p>
</div>
</div>
<div style="display: flex; align-items: flex-start;">
<div style="width: 24px; height: 24px; background: var(--border-color); color: var(--text-light); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; margin-right: 12px; flex-shrink: 0;">3</div>
<div>
<p class="text-small text-bold">Tạo đơn hàng</p>
<p class="text-small text-muted">Đơn hàng được tạo theo giá được chốt</p>
</div>
</div>
</div>-->
<!-- Action Buttons -->
<a href="#" class="btn btn-primary btn-block mb-2">
<i class="fas fa-eye"></i> Xem chi tiết đơn hàng
</a>
<a href="index.html" class="btn btn-secondary btn-block">
<i class="fas fa-home"></i> Quay về trang chủ
</a>
</div>
</div>
</div>
</body>
</html>

View File

@@ -26,7 +26,7 @@
</div> </div>
</div> </div>
<div class="order-detail-content"> <div class="order-detail-content" style="padding-bottom: 0px;">
<!-- Order Status Card --> <!-- Order Status Card -->
<div class="status-timeline-card"> <div class="status-timeline-card">
<div class="order-header-info"> <div class="order-header-info">
@@ -41,47 +41,27 @@
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
</div> </div>
<div class="timeline-content"> <div class="timeline-content">
<div class="timeline-title">Đơn hàng được tạo</div> <div class="timeline-title">Đơn hàng đã tạo</div>
<div class="timeline-date">03/08/2023 - 09:30</div> <div class="timeline-date">03/08/2023 - 09:30</div>
</div> </div>
</div> </div>
<div class="timeline-item completed">
<div class="timeline-icon">
<i class="fas fa-check"></i>
</div>
<div class="timeline-content">
<div class="timeline-title">Đã xác nhận đơn hàng</div>
<div class="timeline-date">03/08/2023 - 10:15</div>
</div>
</div>
<div class="timeline-item active"> <div class="timeline-item active">
<div class="timeline-icon"> <div class="timeline-icon">
<i class="fas fa-cog fa-spin"></i> <i class="fas fa-cog fa-spin"></i>
</div> </div>
<div class="timeline-content"> <div class="timeline-content">
<div class="timeline-title">Đang chuẩn bị hàng</div> <div class="timeline-title">Đã xác nhận đơn hàng</div>
<div class="timeline-date">Đang thực hiện</div> <div class="timeline-date">03/08/2023 - 10:15 (Đang xử lý)</div>
</div> </div>
</div> </div>
<div class="timeline-item pending"> <div class="timeline-item pending">
<div class="timeline-icon"> <div class="timeline-icon">
<i class="fas fa-truck"></i> <i class="fas fa-check-circle"></i>
</div> </div>
<div class="timeline-content"> <div class="timeline-content">
<div class="timeline-title">Vận chuyển</div> <div class="timeline-title">Đã hoàn thành</div>
<div class="timeline-date">Dự kiến: 05/08/2023</div>
</div>
</div>
<div class="timeline-item pending">
<div class="timeline-icon">
<i class="fas fa-box-open"></i>
</div>
<div class="timeline-content">
<div class="timeline-title">Giao hàng thành công</div>
<div class="timeline-date">Dự kiến: 07/08/2023</div> <div class="timeline-date">Dự kiến: 07/08/2023</div>
</div> </div>
</div> </div>
@@ -93,7 +73,7 @@
<h3><i class="fas fa-shipping-fast"></i> Thông tin giao hàng</h3> <h3><i class="fas fa-shipping-fast"></i> Thông tin giao hàng</h3>
<div class="delivery-details"> <div class="delivery-details">
<div class="delivery-method"> <!--<div class="delivery-method">
<div class="delivery-method-icon"> <div class="delivery-method-icon">
<i class="fas fa-truck"></i> <i class="fas fa-truck"></i>
</div> </div>
@@ -101,16 +81,16 @@
<div class="method-name">Giao hàng tiêu chuẩn</div> <div class="method-name">Giao hàng tiêu chuẩn</div>
<div class="method-description">Giao trong 3-5 ngày làm việc</div> <div class="method-description">Giao trong 3-5 ngày làm việc</div>
</div> </div>
</div> </div>-->
<div class="delivery-dates"> <div class="delivery-dates">
<div class="date-item"> <!--<div class="date-item">
<div class="date-label"> <div class="date-label">
<i class="fas fa-calendar-alt"></i> <i class="fas fa-calendar-alt"></i>
Ngày xuất kho Ngày xuất kho
</div> </div>
<div class="date-value confirmed">05/08/2023</div> <div class="date-value confirmed">05/08/2023</div>
</div> </div>-->
<div class="date-item"> <div class="date-item">
<div class="date-label"> <div class="date-label">
@@ -160,7 +140,34 @@
</div> </div>
<div class="customer-row"> <div class="customer-row">
<span class="customer-label">Loại khách hàng:</span> <span class="customer-label">Loại khách hàng:</span>
<span class="customer-badge vip">Khách VIP</span> <span class="customer-badge vip">DIAMOND</span>
</div>
</div>
</div>
<!-- Invoice Information -->
<div class="customer-info-card">
<h3><i class="fas fa-file-invoice"></i> Thông tin hóa đơn</h3>
<div class="customer-details">
<div class="customer-row">
<span class="customer-label">Tên công ty:</span>
<span class="customer-value">Công ty TNHH Xây dựng ABC</span>
</div>
<div class="customer-row">
<span class="customer-label">Mã số thuế:</span>
<span class="customer-value">0123456789</span>
</div>
<div class="customer-row">
<span class="customer-label">Địa chỉ công ty:</span>
<span class="customer-value">123 Nguyễn Trãi, Quận 1, TP.HCM</span>
</div>
<div class="customer-row">
<span class="customer-label">Email nhận hóa đơn:</span>
<span class="customer-value">ketoan@abc.com</span>
</div>
<div class="customer-row">
<span class="customer-label">Loại hóa đơn:</span>
<span class="customer-badge" style="background: #d1ecf1; color: #0c5460;">Hóa đơn VAT</span>
</div> </div>
</div> </div>
</div> </div>
@@ -258,16 +265,28 @@
</div> </div>
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="order-actions"> <!--<div class="order-actions">
<button class="action-btn secondary" onclick="contactCustomer()"> <button class="action-btn secondary" onclick="contactCustomer()">
<i class="fas fa-phone"></i> <i class="fas fa-comments"></i>
Liên hệ khách hàng Hỗ trợ
</button> </button>
<button class="action-btn primary" onclick="updateOrderStatus()"> <button class="action-btn primary" onclick="updateOrderStatus()">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
Cập nhật trạng thái Cập nhật trạng thái
</button> </button>
</div> </div>-->
<!-- Floating Action Button -->
<a href="chat-list.html" class="fab-link">
<button class="fab">
<i class="fas fa-comments"></i>
</button>
</a>
<!--<a href="chat-list.html" class="fab">-->
<!--<button class="fab">-->
<!--<i class="fas fa-comments"></i>-->
<!--</button>
<!--</a>-->
</div> </div>
<style> <style>
@@ -503,7 +522,7 @@
} }
.customer-badge.vip { .customer-badge.vip {
background: linear-gradient(135deg, #FFD700, #FFA500); background: linear-gradient(135deg, #001F4D, #004080, #660066);
color: white; color: white;
} }

View File

@@ -1,148 +0,0 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Danh sách đơn hàng - EuroTile Worker</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="page-wrapper">
<!-- Header -->
<div class="header">
<a href="index.html" class="back-button">
<i class="fas fa-arrow-left"></i>
</a>
<h1 class="header-title">Danh sách đơn hàng</h1>
<button class="back-button">
<i class="fas fa-plus"></i>
</button>
</div>
<div class="container">
<!-- Search Bar -->
<div class="search-bar">
<i class="fas fa-search search-icon"></i>
<input type="text" class="search-input" placeholder="Mã đơn hàng">
</div>
<!-- Status Filters -->
<div class="tab-nav mb-3">
<button class="tab-item active">Tất cả</button>
<button class="tab-item">Chờ xác nhận</button>
<button class="tab-item">Đang xử lý</button>
<button class="tab-item">Đang giao</button>
<button class="tab-item">Hoàn thành</button>
<button class="tab-item">Đã hủy</button>
</div>
<!-- Orders List -->
<div class="orders-list">
<!-- Order Item 1 - Processing -->
<div class="order-card processing" onclick="viewOrderDetail('DH001234')">
<div class="order-status-indicator"></div>
<div class="order-content">
<div class="d-flex justify-between align-start mb-2">
<h4 class="order-id">#DH001234</h4>
<span class="order-amount">12.900.000 VND</span>
</div>
<div class="order-details">
<p class="order-date">Ngày đặt: 03/08/2023</p>
<p class="order-customer">Khách hàng: Nguyễn Văn A</p>
<p class="order-status-text">
<span class="status-badge processing">Đang xử lý</span>
</p>
<p class="order-note">Gạch granite 60x60 - Số lượng: 50m²</p>
</div>
</div>
</div>
<!-- Order Item 2 - Completed -->
<div class="order-card completed">
<div class="order-status-indicator"></div>
<div class="order-content">
<div class="d-flex justify-between align-start mb-2">
<h4 class="order-id">#DH001233</h4>
<span class="order-amount">8.500.000 VND</span>
</div>
<div class="order-details">
<p class="order-date">Ngày đặt: 02/08/2023</p>
<p class="order-customer">Khách hàng: Trần Thị B</p>
<p class="order-status-text">
<span class="status-badge completed">Hoàn thành</span>
</p>
<p class="order-note">Gạch ceramic 30x30 - Số lượng: 80m²</p>
</div>
</div>
</div>
<!-- Order Item 3 - Shipping -->
<div class="order-card shipping">
<div class="order-status-indicator"></div>
<div class="order-content">
<div class="d-flex justify-between align-start mb-2">
<h4 class="order-id">#DH001232</h4>
<span class="order-amount">15.200.000 VND</span>
</div>
<div class="order-details">
<p class="order-date">Ngày đặt: 01/08/2023</p>
<p class="order-customer">Khách hàng: Lê Văn C</p>
<p class="order-status-text">
<span class="status-badge shipping">Đang giao</span>
</p>
<p class="order-note">Gạch porcelain 80x80 - Số lượng: 100m²</p>
</div>
</div>
</div>
<!-- Order Item 4 - Pending -->
<div class="order-card pending">
<div class="order-status-indicator"></div>
<div class="order-content">
<div class="d-flex justify-between align-start mb-2">
<h4 class="order-id">#DH001231</h4>
<span class="order-amount">6.750.000 VND</span>
</div>
<div class="order-details">
<p class="order-date">Ngày đặt: 31/07/2023</p>
<p class="order-customer">Khách hàng: Phạm Thị D</p>
<p class="order-status-text">
<span class="status-badge pending">Chờ xác nhận</span>
</p>
<p class="order-note">Gạch mosaic 25x25 - Số lượng: 40m²</p>
</div>
</div>
</div>
<!-- Order Item 5 - Cancelled -->
<div class="order-card cancelled">
<div class="order-status-indicator"></div>
<div class="order-content">
<div class="d-flex justify-between align-start mb-2">
<h4 class="order-id">#DH001230</h4>
<span class="order-amount">3.200.000 VND</span>
</div>
<div class="order-details">
<p class="order-date">Ngày đặt: 30/07/2023</p>
<p class="order-customer">Khách hàng: Hoàng Văn E</p>
<p class="order-status-text">
<span class="status-badge cancelled">Đã hủy</span>
</p>
<p class="order-note">Gạch terrazzo 40x40 - Số lượng: 20m²</p>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

425
html/orders(1).html Normal file
View File

@@ -0,0 +1,425 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Danh sách đơn hàng - EuroTile Worker</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-content {
background: white;
border-radius: 12px;
width: 100%;
max-width: 500px;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #e5e7eb;
display: flex;
gap: 12px;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
color: #6b7280;
cursor: pointer;
}
.order-card {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
cursor: pointer;
transition: all 0.3s ease;
position: relative;
border-left: 4px solid transparent;
}
.order-card:hover {
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.order-card.pending {
border-left-color: #ffc107;
}
.order-card.processing {
border-left-color: #38B6FF;
}
.order-card.shipping {
border-left-color: #9C27B0;
}
.order-card.completed {
border-left-color: #28a745;
}
.order-card.cancelled {
border-left-color: #dc3545;
}
.order-id {
font-size: 16px;
font-weight: 700;
color: var(--text-dark);
}
.order-amount {
font-size: 16px;
font-weight: 700;
color: var(--primary-blue);
}
.order-date, .order-customer {
font-size: 13px;
color: var(--text-light);
margin-bottom: 4px;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.status-badge.pending {
background: #fff3cd;
color: #856404;
}
.status-badge.processing {
background: #d1ecf1;
color: #0c5460;
}
.status-badge.shipping {
background: #e8d4f1;
color: #6a1b9a;
}
.status-badge.completed {
background: #d4edda;
color: #155724;
}
.status-badge.cancelled {
background: #f8d7da;
color: #721c24;
}
</style>
<body>
<div class="page-wrapper">
<!-- Header -->
<div class="header">
<a href="index.html" class="back-button">
<i class="fas fa-arrow-left"></i>
</a>
<h1 class="header-title">Danh sách đơn hàng</h1>
<button class="back-button" onclick="openInfoModal()">
<i class="fas fa-info-circle"></i>
</button>
</div>
<div class="container">
<!-- Search Bar -->
<div class="search-bar">
<i class="fas fa-search search-icon"></i>
<input type="text" class="search-input" placeholder="Mã đơn hàng">
</div>
<!-- Status Filters -->
<!--<div class="tab-nav mb-3">
<button class="tab-item active">Tất cả</button>
<button class="tab-item">Chờ xác nhận</button>
<button class="tab-item">Đang xử lý</button>
<button class="tab-item">Đang giao</button>
<button class="tab-item">Hoàn thành</button>
<button class="tab-item">Đã hủy</button>
</div>-->
<!-- Filter Pills -->
<div class="filter-container">
<!--<button class="filter-pill active">Tất cả</button>-->
<button class="filter-pill active">Chờ xác nhận</button>
<button class="filter-pill">Đang xử lý</button>
<button class="filter-pill">Đang giao</button>
<button class="filter-pill">Hoàn thành</button>
<button class="filter-pill">Đã hủy</button>
</div>
<!-- Orders List -->
<div class="orders-list">
<!-- Order Item 1 - Processing -->
<div class="order-card processing" onclick="viewOrderDetail('DH001234')">
<div class="order-status-indicator"></div>
<div class="order-content">
<div class="d-flex justify-between align-start mb-2">
<h4 class="order-id">#DH001234</h4>
<span class="order-amount">12.900.000 VND</span>
</div>
<div class="order-details">
<p class="order-date">Ngày đặt: 03/08/2025</p>
<p class="order-customer">Ngày giao: 06/08/2025</p>
<p class="order-customer">Địa chỉ: Quận 7, HCM</p>
<p class="order-status-text">
<span class="status-badge processing">Đang xử lý</span>
</p>
</div>
</div>
</div>
<!-- Order Item 2 - Completed -->
<div class="order-card completed" onclick="viewOrderDetail('DH001233')">
<div class="order-status-indicator"></div>
<div class="order-content">
<div class="d-flex justify-between align-start mb-2">
<h4 class="order-id">#DH001233</h4>
<span class="order-amount">8.500.000 VND</span>
</div>
<div class="order-details">
<p class="order-date">Ngày đặt: 24/06/2025</p>
<p class="order-customer">Ngày giao: 27/06/202</p>
<p class="order-customer">Địa chỉ: Thủ Dầu Một, Bình Dương</p>
<p class="order-status-text">
<span class="status-badge completed">Hoàn thành</span>
</p>
</div>
</div>
</div>
<!-- Order Item 3 - Shipping -->
<div class="order-card shipping" onclick="viewOrderDetail('DH001232')">
<div class="order-status-indicator"></div>
<div class="order-content">
<div class="d-flex justify-between align-start mb-2">
<h4 class="order-id">#DH001232</h4>
<span class="order-amount">15.200.000 VND</span>
</div>
<div class="order-details">
<p class="order-date">Ngày đặt: 01/03/2025</p>
<p class="order-customer">Ngày giao: 05/03/2025</p>
<p class="order-customer">Địa chỉ: Cầu Giấy, Hà Nội</p>
<p class="order-status-text">
<span class="status-badge shipping">Đang giao</span>
</p>
</div>
</div>
</div>
<!-- Order Item 4 - Pending -->
<div class="order-card pending" data-status="pending" onclick="viewOrderDetail('DH001231')">
<div class="order-status-indicator"></div>
<div class="order-content">
<div class="d-flex justify-between align-start mb-2">
<h4 class="order-id">#DH001231</h4>
<span class="order-amount">6.750.000 VND</span>
</div>
<div class="order-details">
<p class="order-date">Ngày đặt: 08/11/2024</p>
<p class="order-customer">Ngày giao: 12/11/2024</p>
<p class="order-customer">Địa chỉ: Thủ Đức, HCM</p>
<p class="order-status-text">
<span class="status-badge pending">Chờ xác nhận</span>
</p>
</div>
</div>
</div>
<!-- Order Item 5 - Cancelled -->
<div class="order-card cancelled" onclick="viewOrderDetail('DH001230')">
<div class="order-status-indicator"></div>
<div class="order-content">
<div class="d-flex justify-between align-start mb-2">
<h4 class="order-id">#DH001230</h4>
<span class="order-amount">3.200.000 VND</span>
</div>
<div class="order-details">
<p class="order-date">Ngày đặt: 30/07/2024</p>
<p class="order-customer">Ngày giao: 04/08/2024</p>
<p class="order-customer">Địa chỉ: Rạch Giá, Kiên Giang</p>
<p class="order-status-text">
<span class="status-badge cancelled">Đã hủy</span>
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Bottom Navigation -->
<!-- <div class="bottom-nav">
<a href="index.html" class="nav-item active">
<i class="fas fa-home"></i>
<span>Trang chủ</span>
</a>
<a href="loyalty.html" class="nav-item">
<i class="fas fa-star"></i>
<span>Hội viên</span>
</a>
<a href="promotions.html" class="nav-item">
<i class="fas fa-tags"></i>
<span>Khuyến mãi</span>
</a>
<a href="notifications.html" class="nav-item">
<i class="fas fa-bell"></i>
<span>Thông báo</span>
</a>
<a href="account.html" class="nav-item">
<i class="fas fa-user"></i>
<span>Cài đặt</span>
</a>
</div>-->
<!-- Info Modal -->
<div id="infoModal" class="modal-overlay" style="display: none;">
<div class="modal-content info-modal">
<div class="modal-header">
<h3 class="modal-title">Hướng dẫn sử dụng</h3>
<button class="modal-close" onclick="closeInfoModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<p>Đây là nội dung hướng dẫn sử dụng cho tính năng Sản phẩm:</p>
<ul class="list-disc ml-6 mt-3">
<li>Sử dụng thanh tìm kiếm để tìm sản phẩm theo tên hoặc mã</li>
<li>Nhấn "Bộ lọc" để lọc sản phẩm theo nhiều tiêu chí</li>
<li>Chuyển đổi giữa chế độ xem lưới và danh sách</li>
<li>Nhấn vào sản phẩm để xem chi tiết</li>
<li>Thêm sản phẩm yêu thích bằng icon tim</li>
</ul>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeInfoModal()">Đóng</button>
</div>
</div>
</div>
</div>
<script>
function openInfoModal() {
document.getElementById('infoModal').style.display = 'flex';
}
function closeInfoModal() {
document.getElementById('infoModal').style.display = 'none';
}
function viewOrderDetail(orderId) {
window.location.href = `order-detail.html?id=${orderId}`;
}
// Close modal when clicking outside
document.addEventListener('click', function(e) {
if (e.target.classList.contains('modal-overlay')) {
e.target.style.display = 'none';
}
});
// Filter functionality
document.addEventListener('DOMContentLoaded', function() {
const filterButtons = document.querySelectorAll('.filter-pill');
const orderCards = document.querySelectorAll('.order-card');
// Set "Chờ xác nhận" as default active tab
filterButtons.forEach(btn => btn.classList.remove('active'));
filterButtons[0].classList.add('active'); // First button is "Chờ xác nhận"
// Show only pending orders by default
filterOrders('pending');
filterButtons.forEach(button => {
button.addEventListener('click', function() {
// Remove active class from all buttons
filterButtons.forEach(btn => btn.classList.remove('active'));
// Add active class to clicked button
this.classList.add('active');
// Get filter status
const filterText = this.textContent.trim();
let status = '';
switch(filterText) {
case 'Chờ xác nhận':
status = 'pending';
break;
case 'Đang xử lý':
status = 'processing';
break;
case 'Đang giao':
status = 'shipping';
break;
case 'Hoàn thành':
status = 'completed';
break;
case 'Đã hủy':
status = 'cancelled';
break;
}
filterOrders(status);
});
});
function filterOrders(status) {
orderCards.forEach(card => {
if (status === '' || card.classList.contains(status)) {
card.style.display = 'block';
} else {
card.style.display = 'none';
}
});
}
});
</script>
</body>
</html>

View File

@@ -8,6 +8,62 @@
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head> </head>
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-content {
background: white;
border-radius: 12px;
width: 100%;
max-width: 500px;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #e5e7eb;
display: flex;
gap: 12px;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
color: #6b7280;
cursor: pointer;
}
</style>
<body> <body>
<div class="page-wrapper"> <div class="page-wrapper">
<!-- Header --> <!-- Header -->
@@ -16,8 +72,8 @@
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
</a> </a>
<h1 class="header-title">Danh sách đơn hàng</h1> <h1 class="header-title">Danh sách đơn hàng</h1>
<button class="back-button"> <button class="back-button" onclick="openInfoModal()">
<i class="fas fa-plus"></i> <i class="fas fa-info-circle"></i>
</button> </button>
</div> </div>
@@ -39,11 +95,10 @@
</div>--> </div>-->
<!-- Filter Pills --> <!-- Filter Pills -->
<div class="filter-container"> <div class="filter-container">
<button class="filter-pill active">Tất cả</button> <!--<button class="filter-pill active">Tất cả</button>-->
<button class="filter-pill">Chờ xác nhận</button> <button class="filter-pill active">Chờ xác nhận</button>
<button class="filter-pill">Gạch ốp tường</button>
<button class="filter-pill">Đang xử lý</button> <button class="filter-pill">Đang xử lý</button>
<button class="filter-pill">Đang giao</button> <!--<button class="filter-pill">Đang giao</button>-->
<button class="filter-pill">Hoàn thành</button> <button class="filter-pill">Hoàn thành</button>
<button class="filter-pill">Đã hủy</button> <button class="filter-pill">Đã hủy</button>
</div> </div>
@@ -111,7 +166,7 @@
</div> </div>
<!-- Order Item 4 - Pending --> <!-- Order Item 4 - Pending -->
<div class="order-card pending" onclick="viewOrderDetail('DH001231')"> <div class="order-card pending" data-status="pending" onclick="viewOrderDetail('DH001231')">
<div class="order-status-indicator"></div> <div class="order-status-indicator"></div>
<div class="order-content"> <div class="order-content">
<div class="d-flex justify-between align-start mb-2"> <div class="d-flex justify-between align-start mb-2">
@@ -175,12 +230,108 @@
<span>Cài đặt</span> <span>Cài đặt</span>
</a> </a>
</div>--> </div>-->
</div>
<!-- Info Modal -->
<div id="infoModal" class="modal-overlay" style="display: none;">
<div class="modal-content info-modal">
<div class="modal-header">
<h3 class="modal-title" style="font-weight: bold;">Hướng dẫn sử dụng</h3>
<button class="modal-close" onclick="closeInfoModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<p>Đây là nội dung hướng dẫn sử dụng cho tính năng Quản lý Đơn hàng:</p>
<ul class="list-disc ml-6 mt-3">
<li>Sử dụng các tab (Chờ xác nhận, Đang giao...) để lọc nhanh trạng thái các đơn hàng của bạn.</li>
<li>Bấm vào một đơn hàng bất kỳ để xem thông tin chi tiết, sản phẩm, và ngày giao dự kiến.</li>
<li>Thanh tiến trình giúp bạn biết đơn hàng đang ở bước nào: Đã tạo, Đã xác nhận, hay Đã hoàn thành.</li>
<li>Nếu bạn đã chọn "Yêu cầu đàm phán giá" khi đặt hàng, đơn hàng sẽ ở trạng thái "Chờ xác nhận & đàm phán" cho đến khi Sales liên hệ.</li>
<li>Bạn có thể xem "Thông tin hóa đơn" đã khai báo tại trang chi tiết đơn hàng.</li>
</ul>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeInfoModal()">Đóng</button>
</div>
</div>
</div>
</div>
<script> <script>
function openInfoModal() {
document.getElementById('infoModal').style.display = 'flex';
}
function closeInfoModal() {
document.getElementById('infoModal').style.display = 'none';
}
function viewOrderDetail(orderId) { function viewOrderDetail(orderId) {
window.location.href = `order-detail.html?id=${orderId}`; window.location.href = `order-detail.html?id=${orderId}`;
} }
// Close modal when clicking outside
document.addEventListener('click', function(e) {
if (e.target.classList.contains('modal-overlay')) {
e.target.style.display = 'none';
}
});
// Filter functionality
document.addEventListener('DOMContentLoaded', function() {
const filterButtons = document.querySelectorAll('.filter-pill');
const orderCards = document.querySelectorAll('.order-card');
// Set "Chờ xác nhận" as default active tab
filterButtons.forEach(btn => btn.classList.remove('active'));
filterButtons[0].classList.add('active'); // First button is "Chờ xác nhận"
// Show only pending orders by default
filterOrders('pending');
filterButtons.forEach(button => {
button.addEventListener('click', function() {
// Remove active class from all buttons
filterButtons.forEach(btn => btn.classList.remove('active'));
// Add active class to clicked button
this.classList.add('active');
// Get filter status
const filterText = this.textContent.trim();
let status = '';
switch(filterText) {
case 'Chờ xác nhận':
status = 'pending';
break;
case 'Đang xử lý':
status = 'processing';
break;
case 'Đang giao':
status = 'shipping';
break;
case 'Hoàn thành':
status = 'completed';
break;
case 'Đã hủy':
status = 'cancelled';
break;
}
filterOrders(status);
});
});
function filterOrders(status) {
orderCards.forEach(card => {
if (status === '' || card.classList.contains(status)) {
card.style.display = 'block';
} else {
card.style.display = 'none';
}
});
}
});
</script> </script>
</body> </body>
</html> </html>

View File

@@ -54,8 +54,8 @@
<div class="text-center mt-3"> <div class="text-center mt-3">
<p class="text-small text-muted"> <p class="text-small text-muted">
Không nhận được mã? Không nhận được mã?
<a href="#" class="text-primary" style="text-decoration: none; font-weight: 500;"> <a href="#" id="resendLink" class="text-muted" style="text-decoration: none; font-weight: 500; cursor: not-allowed;">
Gửi lại (60s) Gửi lại (<span id="countdown">60</span>s)
</a> </a>
</p> </p>
</div> </div>
@@ -91,6 +91,47 @@
} }
}); });
}); });
// Countdown timer for resend OTP
let countdown = 60;
const countdownElement = document.getElementById('countdown');
const resendLink = document.getElementById('resendLink');
function startCountdown() {
const timer = setInterval(() => {
countdown--;
countdownElement.textContent = countdown;
if (countdown <= 0) {
clearInterval(timer);
// Enable resend button
resendLink.textContent = 'Gửi lại';
resendLink.className = 'text-primary';
resendLink.style.cursor = 'pointer';
resendLink.addEventListener('click', handleResendOTP);
}
}, 1000);
}
function handleResendOTP(e) {
e.preventDefault();
// Reset countdown
countdown = 60;
countdownElement.textContent = countdown;
resendLink.textContent = 'Gửi lại (' + countdown + 's)';
resendLink.className = 'text-muted';
resendLink.style.cursor = 'not-allowed';
resendLink.removeEventListener('click', handleResendOTP);
// Simulate sending OTP
alert('Mã OTP mới đã được gửi!');
// Restart countdown
startCountdown();
}
// Start countdown when page loads
document.addEventListener('DOMContentLoaded', startCountdown);
</script> </script>
</body> </body>
</html> </html>

453
html/payment-qr.html Normal file
View File

@@ -0,0 +1,453 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thanh toán - EuroTile Worker</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="page-wrapper">
<!-- Header -->
<div class="header">
<a href="checkout.html" class="back-button">
<i class="fas fa-arrow-left"></i>
</a>
<h1 class="header-title">Thanh toán</h1>
<button class="back-button" onclick="openInfoModal()">
<i class="fas fa-info-circle"></i>
</button>
</div>
<div class="container">
<!-- Payment Amount -->
<div class="card text-center mb-4">
<h3 class="text-2xl font-bold text-primary mb-2">14.541.120đ</h3>
<p class="text-gray-600">Số tiền cần thanh toán</p>
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mt-3">
<p class="text-yellow-700 font-medium">
<i class="fas fa-info-circle mr-1"></i>
Thanh toán không dưới 20%
</p>
</div>
</div>
<!-- QR Code Payment -->
<div class="card text-center">
<h3 class="card-title">Quét mã QR để thanh toán</h3>
<div class="qr-container">
<img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https://eurotile.com/payment/14195000"
alt="QR Code" class="qr-code">
</div>
<p class="text-gray-600 mb-4">
Quét mã QR bằng ứng dụng ngân hàng để thanh toán nhanh chóng
</p>
<!-- Payment Methods -->
<!--<div class="payment-methods">
<h4 class="font-semibold mb-3">Ứng dụng hỗ trợ:</h4>
<div class="app-grid">
<div class="app-item">
<div class="app-icon bg-red-100">
<i class="fas fa-university text-red-600"></i>
</div>
<span class="text-xs">Techcombank</span>
</div>
<div class="app-item">
<div class="app-icon bg-blue-100">
<i class="fas fa-credit-card text-blue-600"></i>
</div>
<span class="text-xs">Vietcombank</span>
</div>
<div class="app-item">
<div class="app-icon bg-green-100">
<i class="fas fa-mobile-alt text-green-600"></i>
</div>
<span class="text-xs">MoMo</span>
</div>
<div class="app-item">
<div class="app-icon bg-purple-100">
<i class="fas fa-wallet text-purple-600"></i>
</div>
<span class="text-xs">ZaloPay</span>
</div>
<div class="app-item">
<div class="app-icon bg-orange-100">
<i class="fas fa-coins text-orange-600"></i>
</div>
<span class="text-xs">ShopeePay</span>
</div>
<div class="app-item">
<div class="app-icon bg-indigo-100">
<i class="fas fa-money-check-alt text-indigo-600"></i>
</div>
<span class="text-xs">Banking</span>
</div>
</div>
</div>
</div>-->
<!-- Bank Transfer Info -->
<div class="card">
<h3 class="card-title">Thông tin chuyển khoản</h3>
<div class="transfer-info">
<div class="info-row">
<span class="info-label">Ngân hàng:</span>
<span class="info-value">BIDV</span>
<button class="copy-btn" onclick="copyText('Techcombank')">
<i class="fas fa-copy"></i>
</button>
</div>
<div class="info-row">
<span class="info-label">Số tài khoản:</span>
<span class="info-value">19036810704016</span>
<button class="copy-btn" onclick="copyText('19036810704016')">
<i class="fas fa-copy"></i>
</button>
</div>
<div class="info-row">
<span class="info-label">Chủ tài khoản:</span>
<span class="info-value">CÔNG TY EUROTILE</span>
<button class="copy-btn" onclick="copyText('CÔNG TY EUROTILE')">
<i class="fas fa-copy"></i>
</button>
</div>
<div class="info-row">
<span class="info-label">Nội dung:</span>
<span class="info-value">DH001234 La Nguyen Quynh</span>
<button class="copy-btn" onclick="copyText('DH001234 La Nguyen Quynh')">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3 mt-4">
<p class="text-blue-700 text-sm">
<i class="fas fa-lightbulb mr-1"></i>
<strong>Lưu ý:</strong> Vui lòng ghi đúng nội dung chuyển khoản để đơn hàng được xử lý nhanh chóng.
</p>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<button class="btn btn-secondary" onclick="confirmPayment()">
<i class="fas fa-check"></i> Đã thanh toán
</button>
<button class="btn btn-primary" onclick="uploadProof()">
<i class="fas fa-camera"></i> Upload bill chuyển khoản
</button>
</div>
<!-- Timer -->
<div class="timer-section">
<p class="text-center text-gray-600">
<i class="fas fa-clock mr-1"></i>
Thời gian thanh toán: <span id="countdown" class="font-semibold text-red-600">14:59</span>
</p>
</div>
</div>
<!-- Info Modal -->
<div id="infoModal" class="modal-overlay" style="display: none;">
<div class="modal-content info-modal">
<div class="modal-header">
<h3 class="modal-title">Hướng dẫn thanh toán</h3>
<button class="modal-close" onclick="closeInfoModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<p>Đây là nội dung hướng dẫn sử dụng cho tính năng Thanh toán:</p>
<ul class="list-disc ml-6 mt-3">
<li>Quét mã QR bằng app ngân hàng hoặc ví điện tử</li>
<li>Chuyển khoản theo thông tin được cung cấp</li>
<li>Ghi đúng nội dung chuyển khoản</li>
<li>Upload hóa đơn sau khi chuyển khoản</li>
<li>Thanh toán tối thiểu 20% giá trị đơn hàng</li>
</ul>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeInfoModal()">Đóng</button>
</div>
</div>
</div>
</div>
<style>
.qr-container {
display: flex;
justify-content: center;
margin: 20px 0;
}
.qr-code {
width: 200px;
height: 200px;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 10px;
background: white;
}
.payment-methods {
margin-top: 20px;
}
.app-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-top: 12px;
}
.app-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.app-icon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.transfer-info {
display: flex;
flex-direction: column;
gap: 12px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #f3f4f6;
}
.info-label {
font-weight: 500;
color: #6b7280;
flex-shrink: 0;
width: 120px;
}
.info-value {
flex: 1;
text-align: right;
font-weight: 600;
margin-right: 8px;
}
.copy-btn {
background: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 4px;
padding: 4px 8px;
color: #6b7280;
cursor: pointer;
transition: all 0.2s;
}
.copy-btn:hover {
background: #e5e7eb;
color: #374151;
}
.action-buttons {
display: flex;
gap: 12px;
margin: 20px 0;
}
.action-buttons .btn {
flex: 1;
}
.timer-section {
padding: 15px;
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 8px;
margin-top: 20px;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-content {
background: white;
border-radius: 12px;
width: 100%;
max-width: 500px;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #e5e7eb;
display: flex;
gap: 12px;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
color: #6b7280;
cursor: pointer;
}
@media (max-width: 768px) {
.app-grid {
grid-template-columns: repeat(2, 1fr);
}
.action-buttons {
flex-direction: column;
}
}
</style>
<script>
// Countdown timer
let timeLeft = 15 * 60; // 15 minutes in seconds
function updateCountdown() {
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
document.getElementById('countdown').textContent =
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
if (timeLeft > 0) {
timeLeft--;
} else {
alert('Thời gian thanh toán đã hết hạn! Vui lòng đặt hàng lại.');
window.location.href = 'cart.html';
}
}
// Start countdown
setInterval(updateCountdown, 1000);
updateCountdown();
function copyText(text) {
navigator.clipboard.writeText(text).then(() => {
// Show success feedback
const event = window.event;
const button = event.target.closest('.copy-btn');
const originalIcon = button.innerHTML;
button.innerHTML = '<i class="fas fa-check text-green-600"></i>';
setTimeout(() => {
button.innerHTML = originalIcon;
}, 1000);
// Show toast
showToast('Đã sao chép: ' + text);
}).catch(() => {
// Fallback for older browsers
alert('Đã sao chép: ' + text);
});
}
function showToast(message) {
// Create toast notification
const toast = document.createElement('div');
toast.className = 'fixed top-20 left-1/2 transform -translate-x-1/2 bg-green-600 text-white px-4 py-2 rounded-lg z-50 transition-opacity';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(toast);
}, 300);
}, 2000);
}
function confirmPayment() {
if (confirm('Xác nhận bạn đã thanh toán đơn hàng này?')) {
alert('Cảm ơn! Chúng tôi sẽ kiểm tra và xác nhận thanh toán của bạn trong vòng 15 phút.');
window.location.href = 'order-success.html';
}
}
function uploadProof() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = function(e) {
const file = e.target.files[0];
if (file) {
alert(`Đã tải lên bill chuyển khoản: ${file.name}\nChúng tôi sẽ xác nhận thanh toán trong vòng 15 phút.`);
window.location.href = 'order-success.html';
}
};
input.click();
}
function openInfoModal() {
document.getElementById('infoModal').style.display = 'flex';
}
function closeInfoModal() {
document.getElementById('infoModal').style.display = 'none';
}
// Close modal when clicking outside
document.addEventListener('click', function(e) {
if (e.target.classList.contains('modal-overlay')) {
e.target.style.display = 'none';
}
});
</script>
</body>
</html>

View File

@@ -263,6 +263,61 @@
justify-content: center; justify-content: center;
} }
} }
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-content {
background: white;
border-radius: 12px;
width: 100%;
max-width: 500px;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #e5e7eb;
display: flex;
gap: 12px;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
color: #6b7280;
cursor: pointer;
}
</style> </style>
</head> </head>
<body> <body>
@@ -273,7 +328,35 @@
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
</a> </a>
<h1 class="header-title">Thanh toán</h1> <h1 class="header-title">Thanh toán</h1>
<div style="width: 32px;"></div> <!--<div style="width: 32px;"></div>-->
<button class="back-button" onclick="openInfoModal()">
<i class="fas fa-info-circle"></i>
</button>
</div>
<!-- Info Modal -->
<div id="infoModal" class="modal-overlay" style="display: none;">
<div class="modal-content info-modal">
<div class="modal-header">
<h3 class="modal-title" style="font-weight: bold;">Hướng dẫn sử dụng</h3>
<button class="modal-close" onclick="closeInfoModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<p>Đây là nội dung hướng dẫn sử dụng cho tính năng Hóa đơn & Thanh toán:</p>
<ul class="list-disc ml-6 mt-3">
<li>Tính năng này gộp chung "Thanh toán" và "Công nợ", áp dụng cho mọi vai trò.</li>
<li>Sử dụng các tab (Chưa thanh toán, Quá hạn...) để lọc các hóa đơn cần xử lý.</li>
<li>Thông tin "Còn lại" (màu đỏ) là số tiền bạn cần thanh toán cho hóa đơn đó.</li>
<li>Bấm vào một hóa đơn để xem chi tiết sản phẩm, lịch sử thanh toán từng phần và tải chứng từ PDF.</li>
<li>Nút "Thanh toán" sẽ dẫn bạn đến trang thanh toán (QR Code/Chuyển khoản).</li>
</ul>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeInfoModal()">Đóng</button>
</div>
</div>
</div> </div>
<div class="payments-container"> <div class="payments-container">
@@ -628,6 +711,27 @@
}, index * 100); }, index * 100);
}); });
}); });
function openInfoModal() {
document.getElementById('infoModal').style.display = 'flex';
}
function closeInfoModal() {
document.getElementById('infoModal').style.display = 'none';
}
function viewOrderDetail(orderId) {
window.location.href = `order-detail.html?id=${orderId}`;
}
// Close modal when clicking outside
document.addEventListener('click', function(e) {
if (e.target.classList.contains('modal-overlay')) {
e.target.style.display = 'none';
}
});
</script> </script>
</body> </body>
</html> </html>

View File

@@ -16,6 +16,7 @@
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
</a> </a>
<h1 class="header-title">Khiếu nại Giao dịch điểm</h1> <h1 class="header-title">Khiếu nại Giao dịch điểm</h1>
<div style="width:32px;"></div>
</div> </div>
<div class="complaint-content"> <div class="complaint-content">

View File

@@ -8,6 +8,78 @@
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head> </head>
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-content {
background: white;
border-radius: 12px;
width: 100%;
max-width: 500px;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #e5e7eb;
display: flex;
gap: 12px;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
color: #6b7280;
cursor: pointer;
}
@media (max-width: 768px) {
.document-card {
flex-direction: column;
align-items: stretch;
}
.download-btn {
width: 100%;
justify-content: center;
}
}
.tab-item.active {
background: var(--primary-blue);
color: var(--white);
}
</style>
<body> <body>
<div class="page-wrapper"> <div class="page-wrapper">
<!-- Header --> <!-- Header -->
@@ -16,12 +88,15 @@
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
</a> </a>
<h1 class="header-title">Lịch sử điểm</h1> <h1 class="header-title">Lịch sử điểm</h1>
<div style="width: 32px;"></div> <!--<div style="width: 32px;"></div>-->
<button class="back-button" onclick="openInfoModal()">
<i class="fas fa-info-circle"></i>
</button>
</div> </div>
<div class="container"> <div class="container">
<!-- Filter Section --> <!-- Filter Section -->
<div class="card mb-3"> <!--<div class="card mb-3">
<div class="d-flex justify-between align-center"> <div class="d-flex justify-between align-center">
<h3 class="card-title">Bộ lọc</h3> <h3 class="card-title">Bộ lọc</h3>
<i class="fas fa-filter" style="color: var(--primary-blue);"></i> <i class="fas fa-filter" style="color: var(--primary-blue);"></i>
@@ -29,7 +104,7 @@
<p class="text-muted" style="font-size: 12px; margin-top: 8px;"> <p class="text-muted" style="font-size: 12px; margin-top: 8px;">
Thời gian hiệu lực: 01/01/2023 - 31/12/2023 Thời gian hiệu lực: 01/01/2023 - 31/12/2023
</p> </p>
</div> </div>-->
<!-- Points History List --> <!-- Points History List -->
<div class="points-history-list"> <div class="points-history-list">
@@ -184,6 +259,28 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Info Modal -->
<div id="infoModal" class="modal-overlay" style="display: none;">
<div class="modal-content info-modal">
<div class="modal-header">
<h3 class="modal-title" style="font-weight: bold;">Hướng dẫn sử dụng</h3>
<button class="modal-close" onclick="closeInfoModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<p>Đây là nội dung hướng dẫn sử dụng cho tính năng Lịch sử điểm:</p>
<ul class="list-disc ml-6 mt-3">
<li>Đây là sao kê chi tiết tất cả các giao dịch cộng/trừ điểm của bạn.</li>
<li>Bạn có thể kiểm tra điểm được cộng từ đơn hàng, từ việc đăng ký công trình, hoặc điểm bị trừ khi đổi quà.</li>
<li>Nếu phát hiện giao dịch bị sai sót, hãy bấm nút "Khiếu nại" trên dòng giao dịch đó để gửi yêu cầu hỗ trợ.</li>
</ul>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeInfoModal()">Đóng</button>
</div>
</div>
</div>
</div> </div>
<script> <script>
@@ -205,6 +302,21 @@
window.location.href = `point-complaint.html?${params.toString()}`; window.location.href = `point-complaint.html?${params.toString()}`;
} }
function openInfoModal() {
document.getElementById('infoModal').style.display = 'flex';
}
function closeInfoModal() {
document.getElementById('infoModal').style.display = 'none';
}
// Close modal when clicking outside
document.addEventListener('click', function(e) {
if (e.target.classList.contains('modal-overlay')) {
e.target.style.display = 'none';
}
});
</script> </script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,256 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Danh sách ghi nhận điểm - EuroTile Worker</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.tab-item.active {
background: var(--primary-blue);
color: var(--white);
</style>
</head>
<body>
<div class="page-wrapper">
<!-- Header -->
<div class="header">
<a href="index.html" class="back-button">
<i class="fas fa-arrow-left"></i>
</a>
<h1 class="header-title">Danh sách Ghi nhận điểm</h1>
<button class="back-button" onclick="createNewProject()">
<i class="fas fa-plus"></i>
</button>
</div>
<div class="container">
<!-- Search Bar -->
<div class="search-bar">
<i class="fas fa-search search-icon"></i>
<input type="text" class="search-input" placeholder="Mã yêu cầu" id="searchInput" onkeyup="filterProjects()">
</div>
<!-- Status Filters -->
<div class="tab-nav mb-3">
<button class="tab-item active" data-status="">Tất cả</button>
<button class="tab-item" data-status="pending">Chờ duyệt</button>
<button class="tab-item" data-status="approved">Đã duyệt</button>
<button class="tab-item" data-status="rejected">Bị từ chối</button>
</div>
<!-- Projects List -->
<div class="orders-list" id="projectsList">
<!-- Projects will be populated by JavaScript -->
</div>
</div>
</div>
<script>
// Sample project data
const projectsData = [
{
id: 'PRR001',
name: 'Chung cư Vinhomes Grand Park - Block A1',
type: 'residential',
customer: 'Công ty TNHH Vingroup',
status: 'approved',
submittedDate: '2023-11-15',
approvedDate: '2023-11-20',
area: '2.500.000đ',
budget: '850,000,000',
progress: 75,
description: 'Gạch granite cao cấp cho khu vực lobby và hành lang'
},
{
id: 'PRR002',
name: 'Trung tâm thương mại Bitexco',
type: 'commercial',
customer: 'Tập đoàn Bitexco',
status: 'pending',
submittedDate: '2023-11-25',
area: '1.250.000đ',
budget: '2,200,000,000',
progress: 25,
description: 'Gạch porcelain 80x80 cho sảnh chính và khu mua sắm'
},
{
id: 'PRR003',
name: 'Nhà xưởng sản xuất ABC',
type: 'industrial',
customer: 'Công ty TNHH ABC Manufacturing',
status: 'rejected',
submittedDate: '2023-11-20',
rejectedDate: '2023-11-28',
area: '4.200.000đ',
budget: '1,500,000,000',
progress: 0,
rejectionReason: 'Hình ảnh minh chứng không hợp lệ',
description: 'Gạch chống trơn cho khu vực sản xuất và kho bãi'
},
{
id: 'PRR004',
name: 'Biệt thự sinh thái Ecopark',
type: 'residential',
customer: 'Ecopark Group',
status: 'approved',
submittedDate: '2023-10-10',
approvedDate: '2023-10-15',
completedDate: '2023-11-30',
area: '3.700.000đ',
budget: '420,000,000',
progress: 100,
description: 'Gạch ceramic vân gỗ cho khu vực phòng khách và sân vườn'
},
{
id: 'PRR005',
name: 'Khách sạn 5 sao Diamond Plaza',
type: 'commercial',
customer: 'Diamond Hospitality Group',
status: 'pending',
submittedDate: '2023-12-01',
area: '8.600.000đ',
budget: '5,800,000,000',
progress: 10,
description: 'Gạch marble tự nhiên cho lobby và phòng suite'
},
];
let filteredProjects = [...projectsData];
let currentFilter = '';
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
setupTabNavigation();
renderProjects();
});
function setupTabNavigation() {
const tabItems = document.querySelectorAll('.tab-item');
tabItems.forEach(tab => {
tab.addEventListener('click', function() {
// Remove active class from all tabs
tabItems.forEach(t => t.classList.remove('active'));
// Add active class to clicked tab
this.classList.add('active');
// Update current filter
currentFilter = this.dataset.status || '';
// Filter and render projects
filterProjects();
});
});
}
function renderProjects() {
const container = document.getElementById('projectsList');
if (filteredProjects.length === 0) {
container.innerHTML = `
<div class="empty-state text-center py-16">
<i class="fas fa-folder-open text-4xl text-gray-300 mb-4"></i>
<h3 class="text-lg font-semibold text-gray-600 mb-2">Không có dự án nào</h3>
<p class="text-gray-500">Không tìm thấy dự án phù hợp với bộ lọc hiện tại</p>
</div>
`;
return;
}
container.innerHTML = filteredProjects.map(project => `
<div class="order-card ${project.status}" onclick="viewProjectDetail('${project.id}')">
<div class="order-status-indicator"></div>
<div class="order-content">
<div class="d-flex justify-between align-start mb-2">
<h4 class="order-id">#${project.id}</h4>
<!--<span class="order-amount">${formatCurrency(project.budget)}</span>-->
</div>
<div class="order-details">
<p class="order-date">Ngày gửi: ${formatDate(project.submittedDate)}</p>
<p class="order-customer">Giá trị đơn hàng: ${project.area}</p>
<p class="order-status-text">
<span class="status-badge ${project.status}">${getStatusText(project.status)}</span>
</p>
<!--<p class="order-note">${project.name} - Diện tích: ${project.area}</p>
${project.description ? `
<p class="text-xs text-gray-600 mt-1">${project.description}</p>-->
` : ''}
${project.status === 'rejected' && project.rejectionReason ? `
<p class="text-xs text-red-600 mt-2 bg-red-50 p-2 rounded">
<i class="fas fa-exclamation-triangle mr-1"></i>
${project.rejectionReason}
</p>
` : ''}
</div>
</div>
</div>
`).join('');
}
function getStatusText(status) {
const statusMap = {
'pending': 'Chờ duyệt',
'reviewing': 'Đang xem xét',
'approved': 'Đã duyệt',
'rejected': 'Bị từ chối',
'completed': 'Hoàn thành'
};
return statusMap[status] || status;
}
function filterProjects() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
filteredProjects = projectsData.filter(project => {
// Status filter
if (currentFilter && project.status !== currentFilter) {
return false;
}
// Search filter
if (searchTerm) {
const searchableText = `${project.name} ${project.id} ${project.customer}`.toLowerCase();
if (!searchableText.includes(searchTerm)) return false;
}
return true;
});
renderProjects();
}
function viewProjectDetail(projectId) {
// Navigate to project detail page
localStorage.setItem('selectedProjectId', projectId);
window.location.href = 'project-submission-detail.html';
}
function createNewProject() {
// Navigate to new project creation page
window.location.href = 'points-record.html';
}
// Utility functions
function formatCurrency(amount) {
const num = typeof amount === 'string' ? parseInt(amount) : amount;
return new Intl.NumberFormat('vi-VN', {
style: 'currency',
currency: 'VND',
minimumFractionDigits: 0
}).format(num);
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('vi-VN');
}
</script>
</body>
</html>

View File

@@ -8,7 +8,7 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css">
<style> <style>
:root { :root {
--primary-color: #2563eb; --primary-color: #005B9A;
--primary-dark: #1d4ed8; --primary-dark: #1d4ed8;
--secondary-color: #64748b; --secondary-color: #64748b;
--success-color: #10b981; --success-color: #10b981;
@@ -417,12 +417,54 @@
oninput="calculatePoints(); validateForm()"> oninput="calculatePoints(); validateForm()">
</div> </div>
<!-- Points Estimate -->
<!--<div class="points-estimate" id="pointsEstimate">
<div class="estimate-title">Điểm dự kiến nhận được</div>
<div class="estimate-text" id="estimateText">0 điểm</div>
</div>-->
<!-- Products Purchased -->
<!--<div class="form-group">
<label class="form-label">Sản phẩm đã mua</label>
<textarea class="form-input form-textarea"
id="products"
placeholder="Mô tả các sản phẩm đã mua (tùy chọn)"
rows="3"></textarea>
</div>-->
<!-- Points Estimate --> <!-- Points Estimate -->
<div class="points-estimate" id="pointsEstimate"> <div class="points-estimate" id="pointsEstimate">
<div class="estimate-title">Điểm dự kiến nhận được</div> <div class="estimate-title">Điểm dự kiến nhận được</div>
<div class="estimate-text" id="estimateText">0 điểm</div> <div class="estimate-text" id="estimateText">0 điểm</div>
</div> </div>
<!-- Company Information -->
<div class="form-group">
<label class="form-label">Tên công ty</label>
<input type="text"
class="form-input"
id="companyName"
placeholder="Nhập tên công ty (nếu có)">
</div>
<div class="form-group">
<label class="form-label">Mã số thuế</label>
<input type="text"
class="form-input"
id="taxCode"
placeholder="Nhập mã số thuế (nếu có)">
</div>
<div class="form-group">
<label class="form-label">Số lượng (m²) đã mua</label>
<input type="number"
class="form-input"
id="squareMeters"
placeholder="0"
min="0"
step="0.01">
</div>
<!-- Products Purchased --> <!-- Products Purchased -->
<div class="form-group"> <div class="form-group">
<label class="form-label">Sản phẩm đã mua</label> <label class="form-label">Sản phẩm đã mua</label>
@@ -432,6 +474,7 @@
rows="3"></textarea> rows="3"></textarea>
</div> </div>
<!-- Invoice Images --> <!-- Invoice Images -->
<div class="form-group"> <div class="form-group">
<label class="form-label required">Hình ảnh hóa đơn</label> <label class="form-label required">Hình ảnh hóa đơn</label>

View File

@@ -71,8 +71,8 @@
<div class="product-pricing"> <div class="product-pricing">
<span class="current-price">285.000 VND/m²</span> <span class="current-price">285.000 VND/m²</span>
<span class="original-price">320.000 VND/m²</span> <!--<span class="original-price">320.000 VND/m²</span>
<span class="discount-badge">-11%</span> <span class="discount-badge">-11%</span>-->
</div> </div>
<!-- Rating & Reviews --> <!-- Rating & Reviews -->
<!--<div class="rating-section"> <!--<div class="rating-section">
@@ -87,19 +87,22 @@
</div>--> </div>-->
<div class="quick-info"> <div class="quick-info">
<div class="info-item"> <div class="info-item">
<i class="fas fa-cube info-icon"></i> <!--<i class="fas fa-cube info-icon"></i>-->
<i class="fas fa-expand info-icon"></i>
<div class="info-label">Kích thước</div> <div class="info-label">Kích thước</div>
<div class="info-value">1200x1200</div> <div class="info-value">1200x1200</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<i class="fas fa-shield-alt info-icon"></i> <i class="fas fa-cube info-icon"></i>
<div class="info-label">Bảo hành</div> <div class="info-label">Đóng gói</div>
<div class="info-value">15 năm</div> <div class="info-value">2 viên/thùng</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<i class="fas fa-truck info-icon"></i> <i class="fas fa-truck info-icon"></i>
<!--<i class="fas fa-box-open info-icon"></i>
<!--<i class="fas fa-pallet info-icon"></i>-->
<div class="info-label">Giao hàng</div> <div class="info-label">Giao hàng</div>
<div class="info-value">2-3 ngày</div> <div class="info-value">2-3 Ngày</div>
</div> </div>
</div> </div>
</div> </div>
@@ -107,13 +110,13 @@
<!-- Product Tabs Section --> <!-- Product Tabs Section -->
<div class="product-tabs-section"> <div class="product-tabs-section">
<div class="tab-navigation"> <div class="tab-navigation">
<button class="tab-button active" onclick="switchTab('description', this)">Mô tả</button> <!--<button class="tab-button" onclick="switchTab('description', this)">Mô tả</button>-->
<button class="tab-button" onclick="switchTab('specifications', this)">Thông số</button> <button class="tab-button active" onclick="switchTab('specifications', this)">Thông số</button>
<button class="tab-button" onclick="switchTab('reviews', this)">Đánh giá</button> <button class="tab-button" onclick="switchTab('reviews', this)">Đánh giá</button>
</div> </div>
<!-- Tab Contents --> <!-- Tab Contents -->
<div class="tab-content active" id="description"> <!--<div class="tab-content" id="description">
<div class="tab-content-wrapper"> <div class="tab-content-wrapper">
<h3>Bộ sưu tập Mộc Lam</h3> <h3>Bộ sưu tập Mộc Lam</h3>
<p>Gạch granite Eurotile MỘC LAM E03 lấy cảm hứng từ vẻ đẹp tự nhiên của gỗ tự nhiên, mang đến không gian ấm cúng và gần gũi. Với bề mặt có texture tinh tế, sản phẩm tạo nên những đường vân gỗ tự nhiên chân thực.</p> <p>Gạch granite Eurotile MỘC LAM E03 lấy cảm hứng từ vẻ đẹp tự nhiên của gỗ tự nhiên, mang đến không gian ấm cúng và gần gũi. Với bề mặt có texture tinh tế, sản phẩm tạo nên những đường vân gỗ tự nhiên chân thực.</p>
@@ -130,9 +133,9 @@
<h4>Ứng dụng:</h4> <h4>Ứng dụng:</h4>
<p>Phù hợp cho phòng khách, phòng ngủ, hành lang, văn phòng và các không gian thương mại. Đặc biệt phù hợp với phong cách nội thất hiện đại, tối giản và Scandinavian.</p> <p>Phù hợp cho phòng khách, phòng ngủ, hành lang, văn phòng và các không gian thương mại. Đặc biệt phù hợp với phong cách nội thất hiện đại, tối giản và Scandinavian.</p>
</div> </div>
</div> </div>-->
<div class="tab-content" id="specifications"> <div class="tab-content active" id="specifications">
<div class="specifications-table"> <div class="specifications-table">
<div class="spec-row"> <div class="spec-row">
<div class="spec-label">Kích thước</div> <div class="spec-label">Kích thước</div>
@@ -146,10 +149,6 @@
<div class="spec-label">Bề mặt</div> <div class="spec-label">Bề mặt</div>
<div class="spec-value">Matt (Nhám)</div> <div class="spec-value">Matt (Nhám)</div>
</div> </div>
<div class="spec-row">
<div class="spec-label">Loại men</div>
<div class="spec-value">Granite kỹ thuật số</div>
</div>
<div class="spec-row"> <div class="spec-row">
<div class="spec-label">Độ hấp thụ nước</div> <div class="spec-label">Độ hấp thụ nước</div>
<div class="spec-value">< 0.5%</div> <div class="spec-value">< 0.5%</div>
@@ -162,14 +161,6 @@
<div class="spec-label">Chức năng</div> <div class="spec-label">Chức năng</div>
<div class="spec-value">Lát nền, Ốp tường</div> <div class="spec-value">Lát nền, Ốp tường</div>
</div> </div>
<div class="spec-row">
<div class="spec-label">Xuất xứ</div>
<div class="spec-value">Việt Nam</div>
</div>
<div class="spec-row">
<div class="spec-label">Bảo hành</div>
<div class="spec-value">15 năm</div>
</div>
<div class="spec-row"> <div class="spec-row">
<div class="spec-label">Tiêu chuẩn</div> <div class="spec-label">Tiêu chuẩn</div>
<div class="spec-value">TCVN 9081:2012, ISO 13006</div> <div class="spec-value">TCVN 9081:2012, ISO 13006</div>
@@ -239,15 +230,36 @@
<!-- Sticky Action Bar --> <!-- Sticky Action Bar -->
<div class="sticky-action-bar"> <div class="sticky-action-bar">
<div class="quantity-controls"> <!--<div class="quantity-controls">
<button class="qty-btn" onclick="decreaseQuantity()" id="decreaseBtn"> <button class="qty-btn" onclick="decreaseQuantity()" id="decreaseBtn">
<i class="fas fa-minus"></i> <i class="fas fa-minus"></i>
</button> </button>
<input type="number" class="qty-input" value="1" min="1" id="quantityInput" onchange="updateQuantity()"> <input type="number" class="qty-input" value="1" min="1" id="quantityInput" onchange="updateQuantity()">
<label class="quantity-label">(m²)</label>
<button class="qty-btn" onclick="increaseQuantity()" id="increaseBtn"> <button class="qty-btn" onclick="increaseQuantity()" id="increaseBtn">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
</button> </button>
</div> </div>
<div class="conversion-text" id="conversionText">
Tương đương: 3 viên / 1.08 m²
</div>-->
<div class="quantity-section">
<label class="quantity-label">Số lượng (m²)</label>
<div class="quantity-controls" style="width: 142px;">
<button class="qty-btn" onclick="decreaseQuantity()" id="decreaseBtn">
<i class="fas fa-minus"></i>
</button>
<input type="number" class="qty-input" value="1" min="1" id="quantityInput" onchange="updateQuantity()">
<button class="qty-btn" onclick="increaseQuantity()" id="increaseBtn">
<i class="fas fa-plus"></i>
</button>
</div>
<div class="conversion-text" id="conversionText">
Tương đương: 3 viên / 1.08 m²
</div>
</div>
<button class="add-to-cart-btn" onclick="addToCart()"> <button class="add-to-cart-btn" onclick="addToCart()">
<i class="fas fa-shopping-cart"></i> <i class="fas fa-shopping-cart"></i>
<span>Thêm vào giỏ hàng</span> <span>Thêm vào giỏ hàng</span>
@@ -683,6 +695,26 @@
z-index: 100; z-index: 100;
} }
/*.quantity-controls {
display: flex;
align-items: center;
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
}*/
.quantity-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.quantity-label {
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
.quantity-controls { .quantity-controls {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -690,7 +722,6 @@
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
} }
.qty-btn { .qty-btn {
width: 40px; width: 40px;
height: 40px; height: 40px;
@@ -904,6 +935,20 @@
margin: 0 10px; margin: 0 10px;
} }
} }
.quantity-label {
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
.conversion-text {
font-size: 11px;
color: var(--text-muted);
margin-top: 4px;
text-align: center;
}
</style> </style>
<script> <script>
@@ -955,6 +1000,7 @@
quantity++; quantity++;
document.getElementById('quantityInput').value = quantity; document.getElementById('quantityInput').value = quantity;
updateQuantityButtons(); updateQuantityButtons();
updateConversion();
} }
function decreaseQuantity() { function decreaseQuantity() {
@@ -962,6 +1008,7 @@
quantity--; quantity--;
document.getElementById('quantityInput').value = quantity; document.getElementById('quantityInput').value = quantity;
updateQuantityButtons(); updateQuantityButtons();
updateConversion();
} }
} }
@@ -1190,6 +1237,15 @@
} }
}); });
}); });
function updateConversion() {
// Example conversion: each m² = 0.36 boxes, each box = 4 pieces
const pieces = Math.ceil(quantity / 0.36); // Round up for boxes needed
const dientich = parseFloat((pieces * 0.36).toFixed(2));
document.getElementById('conversionText').textContent =
`Tương đương: ${pieces} viên / ${dientich}`;
}
</script> </script>
</body> </body>
</html> </html>

View File

@@ -8,6 +8,136 @@
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head> </head>
<style>
.filter-modal {
max-width: 400px;
max-height: 80vh;
overflow-y: auto;
}
.filter-group {
margin-bottom: 24px;
}
.filter-group-title {
font-weight: 600;
margin-bottom: 12px;
color: #1f2937;
}
.filter-options {
display: flex;
flex-direction: column;
gap: 8px;
}
.filter-checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.filter-checkbox input {
margin: 0;
}
.price-range input {
flex: 1;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-content {
background: white;
border-radius: 12px;
width: 100%;
max-width: 500px;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #e5e7eb;
display: flex;
gap: 12px;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
color: #6b7280;
cursor: pointer;
}
.badge {
display: inline-block;
padding: 2px 6px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
}
.badge-primary {
background: #005B9A;
color: white;
}
.heart-btn {
background: none;
border: none;
color: #d1d5db;
font-size: 16px;
cursor: pointer;
transition: color 0.2s;
}
.heart-btn.active {
color: #ef4444;
}
@media (max-width: 768px) {
.products-grid {
grid-template-columns: repeat(2, 1fr);
}
.modal-content {
margin: 20px;
/*max-height: calc(100vh - 40px);*/
}
}
</style>
<body> <body>
<div class="page-wrapper"> <div class="page-wrapper">
<!-- Header --> <!-- Header -->
@@ -24,19 +154,198 @@
<div class="container"> <div class="container">
<!-- Search Bar --> <!-- Search Bar -->
<div class="search-bar"> <!--<div class="search-bar">
<i class="fas fa-search search-icon"></i> <i class="fas fa-search search-icon"></i>
<input type="text" class="search-input" placeholder="Tìm kiếm sản phẩm..."> <input type="text" class="search-input" placeholder="Tìm kiếm sản phẩm...">
</div>-->
<!-- Search Bar & Filter Button -->
<div class="flex gap-2 mb-4" style="margin-bottom: 0px;">
<div class="search-bar flex-1">
<i class="fas fa-search search-icon"></i>
<input type="text" class="search-input" placeholder="Tìm kiếm sản phẩm" id="searchInput">
</div>
<button class="btn btn-secondary" id="filterBtn" onclick="openFilterModal()" style="min-width: auto;padding: 12px 8px;border-bottom-width: 0px;border-top-width: 0px;/* height: 69px; */margin-bottom: 16px;">
<i class="fas fa-filter"></i>
<span class="ml-1">Lọc</span>
<span class="badge badge-primary ml-2" id="filterCount" style="display: none;">0</span>
</button>
</div> </div>
<!-- Filter Modal -->
<div id="filterModal" class="modal-overlay" style="display: none;">
<div class="modal-content filter-modal">
<div class="modal-header">
<h3 class="modal-title" style="font-weight: 600;">Bộ lọc sản phẩm</h3>
<button class="modal-close" onclick="closeFilterModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<!-- Dòng sản phẩm -->
<div class="filter-group">
<h4 class="filter-group-title">Dòng sản phẩm</h4>
<div class="filter-options">
<label class="filter-checkbox">
<input type="checkbox" value="tam-lon"> Tấm lớn
</label>
<label class="filter-checkbox">
<input type="checkbox" value="third-firing"> Third-Firing
</label>
<label class="filter-checkbox">
<input type="checkbox" value="outdoor"> Outdoor
</label>
<label class="filter-checkbox">
<input type="checkbox" value="van-da"> Vân đá
</label>
<label class="filter-checkbox">
<input type="checkbox" value="xi-mang"> Xi măng
</label>
<label class="filter-checkbox">
<input type="checkbox" value="van-go"> Vân gỗ
</label>
<label class="filter-checkbox">
<input type="checkbox" value="xuong-trang"> Xương trắng
</label>
<label class="filter-checkbox">
<input type="checkbox" value="cam-thach"> Cẩm thạch
</label>
</div>
</div>
<!-- Không gian -->
<div class="filter-group">
<h4 class="filter-group-title">Không gian</h4>
<div class="filter-options">
<label class="filter-checkbox">
<input type="checkbox" value="phong-khach"> Phòng khách
</label>
<label class="filter-checkbox">
<input type="checkbox" value="phong-ngu"> Phòng ngủ
</label>
<label class="filter-checkbox">
<input type="checkbox" value="phong-tam"> Phòng tắm
</label>
<label class="filter-checkbox">
<input type="checkbox" value="nha-bep"> Nhà bếp
</label>
<label class="filter-checkbox">
<input type="checkbox" value="khong-gian-khac"> Không gian khác
</label>
</div>
</div>
<!-- Kích thước -->
<div class="filter-group">
<h4 class="filter-group-title">Kích thước</h4>
<div class="filter-options">
<label class="filter-checkbox">
<input type="checkbox" value="200x1600"> 200x1600
</label>
<label class="filter-checkbox">
<input type="checkbox" value="1200x2400"> 1200x2400
</label>
<label class="filter-checkbox">
<input type="checkbox" value="7500x1500"> 7500x1500
</label>
<label class="filter-checkbox">
<input type="checkbox" value="1200x1200"> 1200x1200
</label>
<label class="filter-checkbox">
<input type="checkbox" value="600x1200"> 600x1200
</label>
<label class="filter-checkbox">
<input type="checkbox" value="450x900"> 450x900
</label>
</div>
</div>
<!-- Bề mặt -->
<div class="filter-group">
<h4 class="filter-group-title">Bề mặt</h4>
<div class="filter-options">
<label class="filter-checkbox">
<input type="checkbox" value="satin"> SATIN
</label>
<label class="filter-checkbox">
<input type="checkbox" value="honed"> HONED
</label>
<label class="filter-checkbox">
<input type="checkbox" value="matt"> MATT
</label>
<label class="filter-checkbox">
<input type="checkbox" value="polish"> POLISH
</label>
<label class="filter-checkbox">
<input type="checkbox" value="babyskin"> BABYSKIN
</label>
</div>
</div>
<!-- Khoảng giá -->
<!--<div class="filter-group">
<h4 class="filter-group-title">Khoảng giá</h4>
<div class="price-range">
<div class="flex gap-2 items-center">
<input type="number" class="form-control" placeholder="Từ" id="priceMin">
<span>-</span>
<input type="number" class="form-control" placeholder="Đến" id="priceMax">
<span class="text-sm">VNĐ/m²</span>
</div>
</div>
</div>-->
<!-- Thương hiệu -->
<div class="filter-group">
<h4 class="filter-group-title">Thương hiệu</h4>
<div class="filter-options">
<label class="filter-checkbox">
<input type="checkbox" value="eurotile"> Eurotile
</label>
<label class="filter-checkbox">
<input type="checkbox" value="vasta-stone"> Vasta Stone
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary flex-1" onclick="resetFilters()">Xóa bộ lọc</button>
<button class="btn btn-primary flex-1" onclick="applyFilters()">Áp dụng</button>
</div>
</div>
</div>
<!-- Info Modal -->
<div id="infoModal" class="modal-overlay" style="display: none;">
<div class="modal-content info-modal">
<div class="modal-header">
<h3 class="modal-title">Hướng dẫn sử dụng</h3>
<button class="modal-close" onclick="closeInfoModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<p>Đây là nội dung hướng dẫn sử dụng cho tính năng Sản phẩm:</p>
<ul class="list-disc ml-6 mt-3">
<li>Sử dụng thanh tìm kiếm để tìm sản phẩm theo tên hoặc mã</li>
<li>Nhấn "Bộ lọc" để lọc sản phẩm theo nhiều tiêu chí</li>
<li>Chuyển đổi giữa chế độ xem lưới và danh sách</li>
<li>Nhấn vào sản phẩm để xem chi tiết</li>
<li>Thêm sản phẩm yêu thích bằng icon tim</li>
</ul>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeInfoModal()">Đóng</button>
</div>
</div>
</div>
<!-- Filter Pills --> <!-- Filter Pills -->
<div class="filter-container"> <div class="filter-container">
<button class="filter-pill active">Tất cả</button> <button class="filter-pill active">Tất cả</button>
<button class="filter-pill">Gạch lát nền</button> <button class="filter-pill">Eurotile</button>
<button class="filter-pill">Gạch ốp tường</button> <button class="filter-pill">Vasta</button>
<button class="filter-pill">Gạch trang trí</button> <button class="filter-pill">Gia công</button>
<button class="filter-pill">Gạch ngoài trời</button>
<button class="filter-pill">Phụ kiện</button>
</div> </div>
<!-- Product Grid --> <!-- Product Grid -->
@@ -144,6 +453,207 @@
</div> </div>
</div> </div>
<script>
let filteredProducts = [...products];
let currentView = 'grid';
let activeFilters = {
categories: [],
spaces: [],
sizes: [],
surfaces: [],
brands: [],
priceRange: { min: null, max: null }
};
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
renderProducts();
});
function renderProducts() {
const gridContainer = document.getElementById('productsGrid');
const listContainer = document.getElementById('productsList');
document.getElementById('productCount').textContent = filteredProducts.length;
if (currentView === 'grid') {
gridContainer.innerHTML = filteredProducts.map(product => `
<div class="product-item" onclick="viewProduct('${product.id}')">
<img src="${product.image}" alt="${product.name}" class="product-image">
<div class="product-info">
<div class="product-name">${product.name}</div>
<div class="product-code">${product.code}</div>
<div class="product-actions">
<div class="product-price">${formatPrice(product.price)}</div>
<button class="heart-btn" onclick="toggleFavorite('${product.id}', event)">
<i class="far fa-heart"></i>
</button>
</div>
</div>
</div>
`).join('');
} else {
listContainer.innerHTML = filteredProducts.map(product => `
<div class="product-item" onclick="viewProduct('${product.id}')">
<img src="${product.image}" alt="${product.name}" class="product-image">
<div class="product-info">
<div class="product-name">${product.name}</div>
<div class="product-code">${product.code}</div>
<div class="product-actions">
<div class="product-price">${formatPrice(product.price)}</div>
<button class="heart-btn" onclick="toggleFavorite('${product.id}', event)">
<i class="far fa-heart"></i>
</button>
</div>
</div>
</div>
`).join('');
}
}
function toggleView(view) {
currentView = view;
// Update button states
document.querySelectorAll('.btn-view').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-view="${view}"]`).classList.add('active');
// Show/hide containers
document.getElementById('productsGrid').style.display = view === 'grid' ? 'grid' : 'none';
document.getElementById('productsList').style.display = view === 'list' ? 'block' : 'none';
renderProducts();
}
function openFilterModal() {
document.getElementById('filterModal').style.display = 'flex';
}
function closeFilterModal() {
document.getElementById('filterModal').style.display = 'none';
}
function applyFilters() {
// Collect filter values
activeFilters.categories = Array.from(document.querySelectorAll('input[type="checkbox"]:checked'))
.map(cb => cb.value)
.filter(val => ['tam-lon', 'third-firing', 'outdoor', 'van-da', 'xi-mang', 'van-go', 'xuong-trang', 'cam-thach'].includes(val));
activeFilters.spaces = Array.from(document.querySelectorAll('input[type="checkbox"]:checked'))
.map(cb => cb.value)
.filter(val => ['phong-khach', 'phong-ngu', 'phong-tam', 'nha-bep', 'khong-gian-khac'].includes(val));
// Apply filters
filteredProducts = products.filter(product => {
if (activeFilters.categories.length && !activeFilters.categories.includes(product.category)) return false;
if (activeFilters.spaces.length && !product.space.some(s => activeFilters.spaces.includes(s))) return false;
return true;
});
// Update filter count badge
const totalFilters = activeFilters.categories.length + activeFilters.spaces.length;
const badge = document.getElementById('filterCount');
if (totalFilters > 0) {
badge.style.display = 'inline';
badge.textContent = totalFilters;
} else {
badge.style.display = 'none';
}
renderProducts();
closeFilterModal();
}
function resetFilters() {
// Uncheck all checkboxes
document.querySelectorAll('#filterModal input[type="checkbox"]').forEach(cb => {
cb.checked = false;
});
// Reset price range
document.getElementById('priceMin').value = '';
document.getElementById('priceMax').value = '';
// Reset filters
activeFilters = {
categories: [],
spaces: [],
sizes: [],
surfaces: [],
brands: [],
priceRange: { min: null, max: null }
};
filteredProducts = [...products];
document.getElementById('filterCount').style.display = 'none';
renderProducts();
}
function viewProduct(productId) {
window.location.href = `product-detail.html?id=${productId}`;
}
function toggleFavorite(productId, event) {
event.stopPropagation();
const btn = event.currentTarget;
const icon = btn.querySelector('i');
btn.classList.toggle('active');
if (btn.classList.contains('active')) {
icon.classList.remove('far');
icon.classList.add('fas');
} else {
icon.classList.remove('fas');
icon.classList.add('far');
}
}
function openInfoModal() {
document.getElementById('infoModal').style.display = 'flex';
}
function closeInfoModal() {
document.getElementById('infoModal').style.display = 'none';
}
function formatPrice(price) {
return new Intl.NumberFormat('vi-VN', {
style: 'currency',
currency: 'VND'
}).format(price);
}
// Search functionality
document.getElementById('searchInput').addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
filteredProducts = products.filter(product => {
const matchesSearch = product.name.toLowerCase().includes(searchTerm) ||
product.code.toLowerCase().includes(searchTerm);
if (!matchesSearch) return false;
// Apply other filters too
if (activeFilters.categories.length && !activeFilters.categories.includes(product.category)) return false;
if (activeFilters.spaces.length && !product.space.some(s => activeFilters.spaces.includes(s))) return false;
return true;
});
renderProducts();
});
// Close modal when clicking outside
document.addEventListener('click', function(e) {
if (e.target.classList.contains('modal-overlay')) {
e.target.style.display = 'none';
}
});
</script>
</body> </body>
</html> </html>

View File

@@ -75,12 +75,25 @@
<input type="text" class="form-input" value="123456789012"> <input type="text" class="form-input" value="123456789012">
</div> </div>
<!-- ID MST -->
<div class="form-group">
<label class="form-label">Mã số thuế</label>
<input type="text" class="form-input" value="0359837618">
</div>
<!-- Company --> <!-- Company -->
<div class="form-group"> <div class="form-group">
<label class="form-label">Công ty</label> <label class="form-label">Công ty</label>
<input type="text" class="form-input" value="Công ty TNHH Xây dựng ABC"> <input type="text" class="form-input" value="Công ty TNHH Xây dựng ABC">
</div> </div>
<!-- Address -->
<div class="form-group">
<label class="form-label">Địa chỉ</label>
<input type="text" class="form-input" value="123 Man Thiện, Thủ Đức, Hồ Chí Minh">
</div>
<!-- Position --> <!-- Position -->
<div class="form-group"> <div class="form-group">
<label class="form-label">Chức vụ</label> <label class="form-label">Chức vụ</label>

View File

@@ -7,6 +7,11 @@
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.tab-item.active {
background: var(--primary-blue);
color: var(--white);
</style>
</head> </head>
<body> <body>
<div class="page-wrapper"> <div class="page-wrapper">
@@ -42,30 +47,7 @@
</div> </div>
</div> </div>
<!-- Bottom Navigation --> </div>
<div class="bottom-nav">
<a href="index.html" class="nav-item">
<i class="fas fa-home"></i>
<span>Trang chủ</span>
</a>
<a href="loyalty.html" class="nav-item">
<i class="fas fa-star"></i>
<span>Hội viên</span>
</a>
<a href="promotions.html" class="nav-item">
<i class="fas fa-tags"></i>
<span>Khuyến mãi</span>
</a>
<a href="notifications.html" class="nav-item">
<i class="fas fa-bell"></i>
<span>Thông báo</span>
</a>
<a href="account.html" class="nav-item active">
<i class="fas fa-user"></i>
<span>Cài đặt</span>
</a>
</div>
</div>
<script> <script>
// Sample project data // Sample project data
@@ -199,21 +181,20 @@
<div class="order-content"> <div class="order-content">
<div class="d-flex justify-between align-start mb-2"> <div class="d-flex justify-between align-start mb-2">
<h4 class="order-id">#${project.id}</h4> <h4 class="order-id">#${project.id}</h4>
<span class="order-amount">${formatCurrency(project.budget)}</span> <!--<span class="order-amount">${formatCurrency(project.budget)}</span>-->
</div> </div>
<div class="order-details"> <div class="order-details">
<p class="order-date">Ngày nộp: ${formatDate(project.submittedDate)}</p> <p class="order-customer">Tên công trình: ${project.name}</p>
<p class="order-customer">Khách hàng: ${project.customer}</p> <p class="order-date">Ngày nộp: ${formatDate(project.submittedDate)}</p>
<p class="order-customer">Diện tích: ${project.area}</p>
<p class="order-status-text"> <p class="order-status-text">
<span class="status-badge ${project.status}">${getStatusText(project.status)}</span> <span class="status-badge ${project.status}">${getStatusText(project.status)}</span>
${project.status === 'approved' || project.status === 'completed' ? `
<span class="ml-2 text-xs text-gray-500">${project.progress}% hoàn thành</span>
` : ''}
</p> </p>
<p class="order-note">${project.name} - Diện tích: ${project.area}</p> <!--<p class="order-note">${project.name} - Diện tích: ${project.area}</p>
${project.description ? ` ${project.description ? `
<p class="text-xs text-gray-600 mt-1">${project.description}</p> <p class="text-xs text-gray-600 mt-1">${project.description}</p>-->
` : ''} ` : ''}
${project.status === 'rejected' && project.rejectionReason ? ` ${project.status === 'rejected' && project.rejectionReason ? `
<p class="text-xs text-red-600 mt-2 bg-red-50 p-2 rounded"> <p class="text-xs text-red-600 mt-2 bg-red-50 p-2 rounded">

File diff suppressed because it is too large Load Diff

View File

@@ -513,6 +513,34 @@
<input type="text" class="form-input" placeholder="Ví dụ: Phường 1, Quận 1, TP.HCM"> <input type="text" class="form-input" placeholder="Ví dụ: Phường 1, Quận 1, TP.HCM">
</div> </div>
</div> </div>
<div class="delivery-option" onclick="selectDelivery('showroom')">
<input type="radio" name="delivery" value="showroom" class="delivery-radio">
<div class="delivery-content">
<div class="delivery-title">Nhận hàng tại Showroom</div>
<div class="delivery-desc">Đến nhận trực tiếp tại showroom EuroTile gần bạn</div>
</div>
</div>
<!-- Showroom Selection (hidden by default) -->
<div class="showroom-form" id="showroomForm" style="display: none;">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label">Chọn showroom</label>
<select class="form-input" id="showroomSelect">
<option value="">Chọn showroom gần bạn</option>
<option value="hcm-q1">Showroom Q1 - 123 Nguyễn Huệ, Quận 1, TP.HCM</option>
<option value="hcm-q7">Showroom Q7 - 456 Nguyễn Thị Thập, Quận 7, TP.HCM</option>
<option value="hn-hbt">Showroom Hà Nội - 789 Hoàng Quốc Việt, Cầu Giấy, Hà Nội</option>
<option value="dn-hc">Showroom Đà Nẵng - 321 Lê Duẩn, Hải Châu, Đà Nẵng</option>
<option value="bd-td">Showroom Bình Dương - 654 Đại lộ Bình Dương, Thủ Dầu Một</option>
</select>
<small class="text-gray-500 mt-2 block">
<i class="fas fa-clock mr-1"></i>
Giờ làm việc: 8:00 - 18:00 (Thứ 2 - Thứ 7)
</small>
</div>
</div>
</div> </div>
<!-- Terms and Conditions --> <!-- Terms and Conditions -->
@@ -563,10 +591,17 @@
// Show/hide address form // Show/hide address form
const addressForm = document.getElementById('addressForm'); const addressForm = document.getElementById('addressForm');
const showroomForm = document.getElementById('showroomForm');
if (type === 'physical') { if (type === 'physical') {
addressForm.classList.add('show'); addressForm.classList.add('show');
showroomForm.style.display = 'none';
} else if (type === 'showroom') {
addressForm.classList.remove('show');
showroomForm.style.display = 'block';
} else { } else {
addressForm.classList.remove('show'); addressForm.classList.remove('show');
showroomForm.style.display = 'none';
} }
} }

View File

@@ -164,10 +164,10 @@
<!-- Email --> <!-- Email -->
<div class="form-group"> <div class="form-group">
<label class="form-label" for="email">Email</label> <label class="form-label" for="email">Email *</label>
<div class="form-input-icon"> <div class="form-input-icon">
<i class="fas fa-envelope icon"></i> <i class="fas fa-envelope icon"></i>
<input type="email" id="email" class="form-input" placeholder="Nhập email (không bắt buộc)"> <input type="email" id="email" class="form-input" placeholder="Nhập email" required>
</div> </div>
</div> </div>
@@ -181,15 +181,25 @@
<p class="text-small text-muted mt-1">Mật khẩu tối thiểu 6 ký tự</p> <p class="text-small text-muted mt-1">Mật khẩu tối thiểu 6 ký tự</p>
</div> </div>
<!-- ID ĐVKD -->
<div class="form-group">
<label class="form-label" for="DVKD">Mã ĐVKD *</label>
<div class="form-input-icon">
<i class="fas fa-briefcase icon"></i>
<input type="text" id="DVKD" class="form-input" placeholder="Nhập mã ĐVKD" required>
</div>
</div>
<!-- Role Selection --> <!-- Role Selection -->
<div class="form-group"> <div class="form-group">
<label class="form-label" for="role">Vai trò *</label> <label class="form-label" for="role">Vai trò *</label>
<select id="role" class="form-input form-select" required onchange="toggleVerification()"> <select id="role" class="form-input form-select" required onchange="toggleVerification()">
<option value="">Chọn vai trò của bạn</option> <option value="">Chọn vai trò của bạn</option>
<option value="worker">Thầu thợ</option> <option value="dealer">Đại lý hệ thống</option>
<option value="architect">Kiến trúc sư</option> <option value="worker">Kiến trúc sư/ Thầu thợ</option>
<option value="dealer">Đại lý phân phối</option> <!--<option value="architect">Kiến trúc sư</option>-->
<option value="broker">Môi giới</option> <option value="broker">Khách lẻ</option>
<option value="broker">Khác</option>
</select> </select>
</div> </div>
@@ -202,7 +212,7 @@
<!-- ID Number --> <!-- ID Number -->
<div class="form-group"> <div class="form-group">
<label class="form-label" for="idNumber">Số CCCD/CMND *</label> <label class="form-label" for="idNumber">Số CCCD/CMND</label>
<div class="form-input-icon"> <div class="form-input-icon">
<i class="fas fa-id-card icon"></i> <i class="fas fa-id-card icon"></i>
<input type="text" id="idNumber" class="form-input" placeholder="Nhập số CCCD/CMND" maxlength="12"> <input type="text" id="idNumber" class="form-input" placeholder="Nhập số CCCD/CMND" maxlength="12">
@@ -220,7 +230,7 @@
<!-- ID Card Upload --> <!-- ID Card Upload -->
<div class="form-group"> <div class="form-group">
<label class="form-label">Ảnh mặt trước CCCD/CMND *</label> <label class="form-label">Ảnh mặt trước CCCD/CMND</label>
<div class="file-upload-area" onclick="document.getElementById('idCardFile').click()"> <div class="file-upload-area" onclick="document.getElementById('idCardFile').click()">
<i class="fas fa-camera file-upload-icon"></i> <i class="fas fa-camera file-upload-icon"></i>
<div class="file-upload-text"> <div class="file-upload-text">
@@ -234,7 +244,7 @@
<!-- Certificate Upload --> <!-- Certificate Upload -->
<div class="form-group"> <div class="form-group">
<label class="form-label">Ảnh chứng chỉ hành nghề hoặc GPKD *</label> <label class="form-label">Ảnh chứng chỉ hành nghề hoặc GPKD</label>
<div class="file-upload-area" onclick="document.getElementById('certificateFile').click()"> <div class="file-upload-area" onclick="document.getElementById('certificateFile').click()">
<i class="fas fa-file-alt file-upload-icon"></i> <i class="fas fa-file-alt file-upload-icon"></i>
<div class="file-upload-text"> <div class="file-upload-text">

View File

@@ -1,425 +0,0 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nhà mẫu 360° - EuroTile Worker</title>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* VR360 Container Styles */
.vr360-section {
background: var(--white);
padding: 16px;
margin: 8px 0;
}
.vr360-container {
position: relative;
width: 100%;
border-radius: 16px;
overflow: hidden;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
box-shadow: var(--shadow-medium);
}
/* Option 1: Click to View Style */
.vr360-preview {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
color: var(--white);
position: relative;
background: linear-gradient(135deg, rgba(0, 91, 154, 0.9) 0%, rgba(56, 182, 255, 0.9) 100%);
}
.vr360-preview:hover {
transform: scale(1.02);
box-shadow: 0 10px 30px rgba(0, 91, 154, 0.3);
}
.vr360-icon-wrapper {
position: relative;
width: 80px;
height: 80px;
margin-bottom: 16px;
}
.vr360-icon {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
position: relative;
}
.vr360-icon::before {
content: '';
position: absolute;
width: 100%;
height: 100%;
border: 3px solid rgba(255, 255, 255, 0.4);
border-radius: 50%;
animation: pulse360 2s infinite;
}
.vr360-icon::after {
content: '';
position: absolute;
width: 120%;
height: 120%;
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
animation: pulse360 2s infinite 0.5s;
}
.vr360-icon .main-icon {
font-size: 36px;
color: var(--white);
z-index: 1;
position: relative;
}
.vr360-arrow {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 120px;
height: 120px;
pointer-events: none;
}
.vr360-arrow svg {
width: 100%;
height: 100%;
animation: rotate360 4s linear infinite;
}
.vr360-title {
font-size: 24px;
font-weight: 700;
color: var(--white);
margin-bottom: 8px;
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.vr360-subtitle {
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 16px;
}
.vr360-button {
padding: 12px 32px;
background: var(--white);
color: var(--primary-blue);
border-radius: 24px;
font-size: 14px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.vr360-preview:hover .vr360-button {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
}
/* Option 2: Embedded iFrame Style */
.vr360-embed {
position: relative;
width: 100%;
padding-bottom: 75%; /* 4:3 Aspect Ratio */
background: var(--background-gray);
border-radius: 16px;
overflow: hidden;
}
.vr360-iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
border-radius: 16px;
}
.vr360-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: var(--text-light);
}
.vr360-loading .spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border-color);
border-top-color: var(--primary-blue);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 12px;
}
/* Toggle Switch for View Options */
.view-options {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 16px;
}
.view-option-btn {
padding: 8px 16px;
background: var(--white);
border: 1px solid var(--border-color);
border-radius: 20px;
font-size: 13px;
font-weight: 500;
color: var(--text-dark);
cursor: pointer;
transition: all 0.3s ease;
}
.view-option-btn.active {
background: var(--primary-blue);
color: var(--white);
border-color: var(--primary-blue);
}
/* Animations */
@keyframes pulse360 {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.3);
opacity: 0.5;
}
100% {
transform: scale(1.6);
opacity: 0;
}
}
@keyframes rotate360 {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Fullscreen Button */
.vr360-fullscreen-btn {
position: absolute;
top: 16px;
right: 16px;
width: 40px;
height: 40px;
background: rgba(0, 0, 0, 0.7);
color: var(--white);
border: none;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 10;
transition: all 0.3s ease;
}
.vr360-fullscreen-btn:hover {
background: rgba(0, 0, 0, 0.9);
transform: scale(1.1);
}
</style>
</head>
<body>
<div class="page-wrapper">
<!-- Header -->
<div class="header">
<a href="index.html" class="back-button">
<i class="fas fa-arrow-left"></i>
</a>
<h1 class="header-title">Nhà mẫu 360°</h1>
</div>
<div class="container">
<!-- View Options Toggle -->
<div class="view-options">
<button class="view-option-btn active" onclick="showPreview()">
<i class="fas fa-image"></i> Xem trước
</button>
<button class="view-option-btn" onclick="showEmbed()">
<i class="fas fa-play"></i> Xem trực tiếp
</button>
</div>
<!-- VR360 Section -->
<div class="vr360-section">
<!-- Option 1: Preview with Link -->
<div id="previewMode" class="vr360-container">
<a href="https://vr.house3d.com/web/panorama-player/H00179549"
target="_blank"
class="vr360-preview">
<div class="vr360-icon-wrapper">
<div class="vr360-icon">
<span class="main-icon">360°</span>
</div>
<div class="vr360-arrow">
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
<circle cx="60" cy="60" r="50" fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="2" stroke-dasharray="10 5"/>
<path d="M 60 15 L 65 20 M 65 20 L 60 25" stroke="rgba(255,255,255,0.8)" stroke-width="2" fill="none" stroke-linecap="round"/>
</svg>
</div>
</div>
<h2 class="vr360-title">360°</h2>
<p class="vr360-subtitle">Khám phá không gian nhà mẫu toàn cảnh</p>
<div class="vr360-button">
<i class="fas fa-external-link-alt"></i>
<span>Mở chế độ xem 360°</span>
</div>
</a>
</div>
<!-- Option 2: Embedded iFrame (Hidden by default) -->
<div id="embedMode" class="vr360-container" style="display: none;">
<div class="vr360-embed">
<div class="vr360-loading" id="loadingState">
<div class="spinner"></div>
<span>Đang tải mô hình 360°...</span>
</div>
<iframe
id="vr360iframe"
class="vr360-iframe"
src=""
title="Mô hình 360° Nhà mẫu"
allowfullscreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
onload="hideLoading()"
style="display: none;">
</iframe>
<button class="vr360-fullscreen-btn" onclick="goFullscreen()">
<i class="fas fa-expand"></i>
</button>
</div>
</div>
</div>
<!-- Additional Info -->
<div class="card">
<h3 class="card-title">Về nhà mẫu này</h3>
<p style="color: var(--text-light); font-size: 14px; line-height: 1.6;">
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à.
</p>
<ul style="padding-left: 20px; margin-top: 12px;">
<li style="color: var(--text-light); font-size: 14px; margin-bottom: 8px;">
<i class="fas fa-mouse" style="color: var(--primary-blue); margin-right: 8px;"></i>
Kéo chuột để xoay góc nhìn
</li>
<li style="color: var(--text-light); font-size: 14px; margin-bottom: 8px;">
<i class="fas fa-search-plus" style="color: var(--primary-blue); margin-right: 8px;"></i>
Scroll để zoom in/out
</li>
<li style="color: var(--text-light); font-size: 14px;">
<i class="fas fa-hand-point-up" style="color: var(--primary-blue); margin-right: 8px;"></i>
Click vào các điểm nóng để di chuyển
</li>
</ul>
</div>
</div>
</div>
<script>
const VR360_URL = "https://vr.house3d.com/web/panorama-player/H00179549";
// Show preview mode
function showPreview() {
document.getElementById('previewMode').style.display = 'block';
document.getElementById('embedMode').style.display = 'none';
// Update buttons
document.querySelectorAll('.view-option-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Clear iframe src to stop loading
document.getElementById('vr360iframe').src = '';
}
// Show embedded mode
function showEmbed() {
document.getElementById('previewMode').style.display = 'none';
document.getElementById('embedMode').style.display = 'block';
// Update buttons
document.querySelectorAll('.view-option-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Load iframe
const iframe = document.getElementById('vr360iframe');
if (!iframe.src) {
iframe.src = VR360_URL;
}
}
// Hide loading state when iframe loads
function hideLoading() {
document.getElementById('loadingState').style.display = 'none';
document.getElementById('vr360iframe').style.display = 'block';
}
// Fullscreen function
function goFullscreen() {
const container = document.getElementById('embedMode');
if (container.requestFullscreen) {
container.requestFullscreen();
} else if (container.webkitRequestFullscreen) {
container.webkitRequestFullscreen();
} else if (container.mozRequestFullScreen) {
container.mozRequestFullScreen();
} else if (container.msRequestFullscreen) {
container.msRequestFullscreen();
}
}
// Optional: Auto-load embed after delay
// setTimeout(() => {
// if (window.innerWidth > 768) {
// showEmbed();
// }
// }, 2000);
</script>
</body>
</html>

View File

@@ -20,6 +20,7 @@ import 'package:worker/features/products/presentation/pages/product_detail_page.
import 'package:worker/features/products/presentation/pages/products_page.dart'; import 'package:worker/features/products/presentation/pages/products_page.dart';
import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart'; import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart';
import 'package:worker/features/quotes/presentation/pages/quotes_page.dart'; import 'package:worker/features/quotes/presentation/pages/quotes_page.dart';
import 'package:worker/features/price_policy/price_policy.dart';
/// App Router /// App Router
/// ///
@@ -189,6 +190,16 @@ class AppRouter {
), ),
), ),
// Price Policy Route
GoRoute(
path: RouteNames.pricePolicy,
name: RouteNames.pricePolicy,
pageBuilder: (context, state) => MaterialPage(
key: state.pageKey,
child: const PricePolicyPage(),
),
),
// TODO: Add more routes as features are implemented // TODO: Add more routes as features are implemented
], ],
@@ -308,6 +319,9 @@ class RouteNames {
static const String promotionDetail = '/promotions/:id'; static const String promotionDetail = '/promotions/:id';
static const String notifications = '/notifications'; static const String notifications = '/notifications';
// Price Policy Route
static const String pricePolicy = '/price-policy';
// Chat Route // Chat Route
static const String chat = '/chat'; static const String chat = '/chat';

View File

@@ -151,6 +151,29 @@ class HomePage extends ConsumerWidget {
], ],
), ),
// Orders & Payments Section
QuickActionSection(
title: 'Đơn hàng & thanh toán',
actions: [
QuickAction(
icon: Icons.description,
label: 'Chính sách giá',
onTap: () => context.push(RouteNames.pricePolicy),
),
QuickAction(
icon: Icons.inventory_2,
label: 'Đơn hàng',
onTap: () => context.push(RouteNames.orders),
),
QuickAction(
icon: Icons.receipt_long,
label: 'Thanh toán',
onTap: () =>
context.push(RouteNames.payments)
),
],
),
// Loyalty Section // Loyalty Section
QuickActionSection( QuickActionSection(
title: 'Khách hàng thân thiết', title: 'Khách hàng thân thiết',
@@ -174,28 +197,7 @@ class HomePage extends ConsumerWidget {
], ],
), ),
// Orders & Payments Section
QuickActionSection(
title: 'Đơn hàng & thanh toán',
actions: [
QuickAction(
icon: Icons.description,
label: 'Yêu cầu báo giá',
onTap: () => context.push(RouteNames.quotes),
),
QuickAction(
icon: Icons.inventory_2,
label: 'Đơn hàng',
onTap: () => context.push(RouteNames.orders),
),
QuickAction(
icon: Icons.receipt_long,
label: 'Thanh toán',
onTap: () =>
context.push(RouteNames.payments)
),
],
),
// Sample Houses & News Section // Sample Houses & News Section
QuickActionSection( QuickActionSection(
@@ -212,11 +214,11 @@ class HomePage extends ConsumerWidget {
onTap: () => onTap: () =>
_showComingSoon(context, 'Đăng ký dự án', l10n), _showComingSoon(context, 'Đăng ký dự án', l10n),
), ),
QuickAction( // QuickAction(
icon: Icons.article, // icon: Icons.article,
label: 'Tin tức', // label: 'Tin tức',
onTap: () => _showComingSoon(context, 'Tin tức', l10n), // onTap: () => _showComingSoon(context, 'Tin tức', l10n),
), // ),
], ],
), ),

View File

@@ -83,26 +83,70 @@ class QuickActionSection extends StatelessWidget {
} }
Widget _buildActionGrid() { Widget _buildActionGrid() {
return GridView.builder( // Determine grid columns based on item count
padding: EdgeInsets.zero, // Remove default GridView padding // If 2 items: 2 columns (no scroll, rectangular aspect ratio)
shrinkWrap: true, // If 3 items: 3 columns (no scroll)
physics: const NeverScrollableScrollPhysics(), // If more than 3: 3 columns (scrollable horizontally)
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( final int crossAxisCount = actions.length == 2 ? 2 : 3;
crossAxisCount: 3, // Always 3 columns to match HTML final bool isScrollable = actions.length > 3;
childAspectRatio: 1.0,
crossAxisSpacing: 8, // Use rectangular aspect ratio for 2 items to reduce height
mainAxisSpacing: 8, // 1.5 means width is 1.5x the height (more rectangular/wider)
final double aspectRatio = actions.length == 2 ? 1.5 : 0.85;
if (!isScrollable) {
// Non-scrollable grid for 2 or 3 items
return GridView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
childAspectRatio: aspectRatio,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: actions.length,
itemBuilder: (context, index) {
final action = actions[index];
return QuickActionItem(
icon: action.icon,
label: action.label,
badge: action.badge,
onTap: action.onTap,
);
},
);
}
// Scrollable horizontal grid for more than 3 items
// Calculate grid height based on number of rows needed
final int rows = (actions.length / crossAxisCount).ceil();
const double itemHeight = 100; // Approximate height of each item
final double gridHeight = (rows * itemHeight) + ((rows - 1) * 8);
return SizedBox(
height: gridHeight,
child: GridView.builder(
padding: EdgeInsets.zero,
scrollDirection: Axis.horizontal,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
childAspectRatio: 1.0,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: actions.length,
itemBuilder: (context, index) {
final action = actions[index];
return QuickActionItem(
icon: action.icon,
label: action.label,
badge: action.badge,
onTap: action.onTap,
);
},
), ),
itemCount: actions.length,
itemBuilder: (context, index) {
final action = actions[index];
return QuickActionItem(
icon: action.icon,
label: action.label,
badge: action.badge,
onTap: action.onTap,
);
},
); );
} }
} }

View File

@@ -0,0 +1,185 @@
/// Price Policy Local DataSource
///
/// Handles all local data operations for price policy documents.
/// Currently provides mock data for development and testing.
/// Will be extended to use Hive cache when backend API is available.
library;
import 'package:worker/features/price_policy/data/models/price_document_model.dart';
/// Price Policy Local Data Source
///
/// Provides mock data for price policy documents.
/// In production, this will cache data from the remote API.
class PricePolicyLocalDataSource {
/// Get all price policy documents
///
/// Returns a list of all documents from mock data.
/// In production, this will fetch from Hive cache.
Future<List<PriceDocumentModel>> getAllDocuments() async {
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 300));
return _mockDocuments;
}
/// Get documents by category
///
/// Returns filtered list of documents matching the [category].
Future<List<PriceDocumentModel>> getDocumentsByCategory(
String category,
) async {
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 200));
return _mockDocuments
.where((doc) => doc.category.toLowerCase() == category.toLowerCase())
.toList();
}
/// Get a specific document by ID
///
/// Returns the document if found, null otherwise.
Future<PriceDocumentModel?> getDocumentById(String documentId) async {
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 100));
try {
return _mockDocuments.firstWhere((doc) => doc.id == documentId);
} catch (e) {
return null;
}
}
/// Check if cache is valid
///
/// Returns true if cached data is still valid.
/// Currently always returns false since we're using mock data.
Future<bool> isCacheValid() async {
// TODO: Implement cache validation when using Hive
return false;
}
/// Cache documents locally
///
/// Saves documents to Hive for offline access.
/// Currently not implemented (using mock data).
Future<void> cacheDocuments(List<PriceDocumentModel> documents) async {
// TODO: Implement Hive caching when backend API is ready
}
/// Clear cached documents
///
/// Removes all cached documents from Hive.
/// Currently not implemented (using mock data).
Future<void> clearCache() async {
// TODO: Implement cache clearing when using Hive
}
/// Mock documents matching HTML design
///
/// This data will be replaced with real API data in production.
static final List<PriceDocumentModel> _mockDocuments = [
// Policy documents (Chính sách giá)
const PriceDocumentModel(
id: 'policy-eurotile-10-2025',
title: 'Chính sách giá Eurotile T10/2025',
description:
'Chính sách giá mới nhất cho sản phẩm gạch Eurotile, áp dụng từ tháng 10/2025',
publishedDate: '2025-10-01T00:00:00.000Z',
documentType: 'pdf',
category: 'policy',
downloadUrl: '/documents/policy-eurotile-10-2025.pdf',
fileSize: '2.5 MB',
),
const PriceDocumentModel(
id: 'policy-vasta-10-2025',
title: 'Chính sách giá Vasta Stone T10/2025',
description:
'Chính sách giá đá tự nhiên Vasta Stone, hiệu lực từ tháng 10/2025',
publishedDate: '2025-10-01T00:00:00.000Z',
documentType: 'pdf',
category: 'policy',
downloadUrl: '/documents/policy-vasta-10-2025.pdf',
fileSize: '1.8 MB',
),
const PriceDocumentModel(
id: 'policy-dealer-2025',
title: 'Chính sách chiết khấu đại lý 2025',
description:
'Chương trình chiết khấu và ưu đãi dành cho đại lý, thầu thợ',
publishedDate: '2025-09-15T00:00:00.000Z',
documentType: 'pdf',
category: 'policy',
downloadUrl: '/documents/policy-dealer-2025.pdf',
fileSize: '3.2 MB',
),
const PriceDocumentModel(
id: 'policy-payment-2025',
title: 'Điều kiện thanh toán & giao hàng',
description:
'Điều khoản thanh toán, chính sách giao hàng và bảo hành sản phẩm',
publishedDate: '2025-08-01T00:00:00.000Z',
documentType: 'pdf',
category: 'policy',
downloadUrl: '/documents/policy-payment-2025.pdf',
fileSize: '1.5 MB',
),
// Price list documents (Bảng giá)
const PriceDocumentModel(
id: 'pricelist-granite-2025',
title: 'Bảng giá Gạch Granite Eurotile 2025',
description:
'Bảng giá chi tiết toàn bộ sản phẩm gạch granite, kích thước 60x60, 80x80, 120x120',
publishedDate: '2025-10-01T00:00:00.000Z',
documentType: 'excel',
category: 'priceList',
downloadUrl: '/documents/pricelist-granite-2025.xlsx',
fileSize: '850 KB',
),
const PriceDocumentModel(
id: 'pricelist-ceramic-2025',
title: 'Bảng giá Gạch Ceramic Eurotile 2025',
description: 'Bảng giá gạch ceramic vân gỗ, vân đá, vân xi măng các loại',
publishedDate: '2025-10-01T00:00:00.000Z',
documentType: 'excel',
category: 'priceList',
downloadUrl: '/documents/pricelist-ceramic-2025.xlsx',
fileSize: '720 KB',
),
const PriceDocumentModel(
id: 'pricelist-stone-2025',
title: 'Bảng giá Đá tự nhiên Vasta Stone 2025',
description:
'Bảng giá đá marble, granite tự nhiên nhập khẩu, kích thước tấm lớn',
publishedDate: '2025-10-01T00:00:00.000Z',
documentType: 'excel',
category: 'priceList',
downloadUrl: '/documents/pricelist-stone-2025.xlsx',
fileSize: '950 KB',
),
const PriceDocumentModel(
id: 'pricelist-accessories-2025',
title: 'Bảng giá Phụ kiện & Vật liệu 2025',
description:
'Giá keo dán, chà ron, nẹp nhựa, nẹp inox và các phụ kiện thi công',
publishedDate: '2025-09-15T00:00:00.000Z',
documentType: 'excel',
category: 'priceList',
downloadUrl: '/documents/pricelist-accessories-2025.xlsx',
fileSize: '640 KB',
),
const PriceDocumentModel(
id: 'pricelist-outdoor-2025',
title: 'Bảng giá Gạch Outdoor & Chống trơn 2025',
description:
'Bảng giá sản phẩm outdoor, gạch chống trơn dành cho ngoại thất',
publishedDate: '2025-09-01T00:00:00.000Z',
documentType: 'excel',
category: 'priceList',
downloadUrl: '/documents/pricelist-outdoor-2025.xlsx',
fileSize: '780 KB',
),
];
}

View File

@@ -0,0 +1,158 @@
/// Data Model: Price Document Model
///
/// Data layer model for price policy documents.
/// Handles JSON serialization and conversion to/from domain entity.
library;
import 'package:worker/features/price_policy/domain/entities/price_document.dart';
/// Price Document Model
///
/// Used in the data layer for:
/// - JSON serialization/deserialization from API
/// - Conversion to domain entity
/// - Local storage (if needed)
class PriceDocumentModel {
/// Unique document ID
final String id;
/// Document title
final String title;
/// Document description
final String description;
/// Date the document was published (ISO 8601 string)
final String publishedDate;
/// Type of document (pdf or excel)
final String documentType;
/// Category (policy or priceList)
final String category;
/// URL to download the document
final String downloadUrl;
/// Optional file size display string
final String? fileSize;
/// Constructor
const PriceDocumentModel({
required this.id,
required this.title,
required this.description,
required this.publishedDate,
required this.documentType,
required this.category,
required this.downloadUrl,
this.fileSize,
});
/// Create model from JSON
factory PriceDocumentModel.fromJson(Map<String, dynamic> json) {
return PriceDocumentModel(
id: json['id'] as String,
title: json['title'] as String,
description: json['description'] as String,
publishedDate: json['published_date'] as String,
documentType: json['document_type'] as String,
category: json['category'] as String,
downloadUrl: json['download_url'] as String,
fileSize: json['file_size'] as String?,
);
}
/// Convert model to JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'published_date': publishedDate,
'document_type': documentType,
'category': category,
'download_url': downloadUrl,
'file_size': fileSize,
};
}
/// Convert model to domain entity
PriceDocument toEntity() {
return PriceDocument(
id: id,
title: title,
description: description,
publishedDate: DateTime.parse(publishedDate),
documentType: _parseDocumentType(documentType),
category: _parseCategory(category),
downloadUrl: downloadUrl,
fileSize: fileSize,
);
}
/// Create model from domain entity
factory PriceDocumentModel.fromEntity(PriceDocument entity) {
return PriceDocumentModel(
id: entity.id,
title: entity.title,
description: entity.description,
publishedDate: entity.publishedDate.toIso8601String(),
documentType: _documentTypeToString(entity.documentType),
category: _categoryToString(entity.category),
downloadUrl: entity.downloadUrl,
fileSize: entity.fileSize,
);
}
/// Parse document type from string
static DocumentType _parseDocumentType(String type) {
switch (type.toLowerCase()) {
case 'pdf':
return DocumentType.pdf;
case 'excel':
return DocumentType.excel;
default:
return DocumentType.pdf;
}
}
/// Parse category from string
static DocumentCategory _parseCategory(String category) {
switch (category.toLowerCase()) {
case 'policy':
return DocumentCategory.policy;
case 'pricelist':
case 'price_list':
return DocumentCategory.priceList;
default:
return DocumentCategory.policy;
}
}
/// Convert document type to string
static String _documentTypeToString(DocumentType type) {
switch (type) {
case DocumentType.pdf:
return 'pdf';
case DocumentType.excel:
return 'excel';
}
}
/// Convert category to string
static String _categoryToString(DocumentCategory category) {
switch (category) {
case DocumentCategory.policy:
return 'policy';
case DocumentCategory.priceList:
return 'priceList';
}
}
@override
String toString() {
return 'PriceDocumentModel(id: $id, title: $title, category: $category, '
'documentType: $documentType, publishedDate: $publishedDate)';
}
}

View File

@@ -0,0 +1,134 @@
/// Repository Implementation: Price Policy Repository
///
/// Concrete implementation of the PricePolicyRepository interface.
/// Coordinates between local and remote data sources to provide price policy data.
///
/// Currently uses mock data from local datasource.
/// Will implement offline-first strategy when backend API is available.
library;
import 'package:worker/features/price_policy/data/datasources/price_policy_local_datasource.dart';
import 'package:worker/features/price_policy/domain/entities/price_document.dart';
import 'package:worker/features/price_policy/domain/repositories/price_policy_repository.dart';
/// Price Policy Repository Implementation
///
/// Responsibilities:
/// - Coordinate between local cache and remote API (when available)
/// - Convert data models to domain entities
/// - Handle errors gracefully
/// - Manage cache invalidation
class PricePolicyRepositoryImpl implements PricePolicyRepository {
/// Local data source
final PricePolicyLocalDataSource localDataSource;
/// Remote data source (API) - TODO: Add when API is ready
// final PricePolicyRemoteDataSource remoteDataSource;
/// Constructor
PricePolicyRepositoryImpl({
required this.localDataSource,
// required this.remoteDataSource, // TODO: Add when API ready
});
@override
Future<List<PriceDocument>> getAllDocuments() async {
try {
// TODO: Implement offline-first strategy
// 1. Check if cache is valid
// 2. Return cached data if valid
// 3. If cache invalid, fetch from remote
// For now, get from local datasource (mock data)
final models = await localDataSource.getAllDocuments();
// Convert models to entities
final entities = models.map((model) => model.toEntity()).toList();
// Sort by published date (newest first)
entities.sort((a, b) => b.publishedDate.compareTo(a.publishedDate));
return entities;
} catch (e) {
// Log error and return empty list
// In production, this should throw proper domain failures
print('[PricePolicyRepository] Error getting documents: $e');
return [];
}
}
@override
Future<List<PriceDocument>> getDocumentsByCategory(
DocumentCategory category,
) async {
try {
// Convert category to string for datasource
final categoryString = _categoryToString(category);
// Get documents from local datasource
final models = await localDataSource.getDocumentsByCategory(
categoryString,
);
// Convert models to entities
final entities = models.map((model) => model.toEntity()).toList();
// Sort by published date (newest first)
entities.sort((a, b) => b.publishedDate.compareTo(a.publishedDate));
return entities;
} catch (e) {
print('[PricePolicyRepository] Error getting documents by category: $e');
return [];
}
}
@override
Future<PriceDocument?> getDocumentById(String documentId) async {
try {
// Get document from local datasource
final model = await localDataSource.getDocumentById(documentId);
// Convert model to entity
return model?.toEntity();
} catch (e) {
print('[PricePolicyRepository] Error getting document by id: $e');
return null;
}
}
@override
Future<List<PriceDocument>> refreshDocuments() async {
try {
// TODO: Implement remote fetch when API is available
// 1. Fetch from remote API
// 2. Cache the results locally
// 3. Return fresh data
// For now, just clear and refetch from local
await localDataSource.clearCache();
final models = await localDataSource.getAllDocuments();
// Convert models to entities
final entities = models.map((model) => model.toEntity()).toList();
// Sort by published date (newest first)
entities.sort((a, b) => b.publishedDate.compareTo(a.publishedDate));
return entities;
} catch (e) {
print('[PricePolicyRepository] Error refreshing documents: $e');
return [];
}
}
/// Helper method to convert category enum to string
String _categoryToString(DocumentCategory category) {
switch (category) {
case DocumentCategory.policy:
return 'policy';
case DocumentCategory.priceList:
return 'priceList';
}
}
}

View File

@@ -0,0 +1,166 @@
/// Domain Entity: Price Document
///
/// Pure business entity representing a price policy or price list document.
/// This entity is framework-independent and contains only business logic.
library;
/// Price policy document entity
class PriceDocument {
/// Unique document ID
final String id;
/// Document title
final String title;
/// Document description
final String description;
/// Date the document was published
final DateTime publishedDate;
/// Type of document (PDF or Excel)
final DocumentType documentType;
/// Category (policy or price list)
final DocumentCategory category;
/// URL to download the document
final String downloadUrl;
/// Optional file size display string
final String? fileSize;
/// Constructor
const PriceDocument({
required this.id,
required this.title,
required this.description,
required this.publishedDate,
required this.documentType,
required this.category,
required this.downloadUrl,
this.fileSize,
});
/// Check if document is a PDF
bool get isPdf => documentType == DocumentType.pdf;
/// Check if document is an Excel file
bool get isExcel => documentType == DocumentType.excel;
/// Check if document is a policy document
bool get isPolicy => category == DocumentCategory.policy;
/// Check if document is a price list
bool get isPriceList => category == DocumentCategory.priceList;
/// Get formatted published date (dd/MM/yyyy)
String get formattedDate {
return '${publishedDate.day.toString().padLeft(2, '0')}/'
'${publishedDate.month.toString().padLeft(2, '0')}/'
'${publishedDate.year}';
}
/// Get formatted date with prefix based on category
String get formattedDateWithPrefix {
final prefix = isPolicy ? 'Công bố' : 'Cập nhật';
return '$prefix: $formattedDate';
}
/// Get icon name based on document type
String get iconName => documentType == DocumentType.pdf ? 'PDF' : 'Excel';
/// Copy with method for immutability
PriceDocument copyWith({
String? id,
String? title,
String? description,
DateTime? publishedDate,
DocumentType? documentType,
DocumentCategory? category,
String? downloadUrl,
String? fileSize,
}) {
return PriceDocument(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
publishedDate: publishedDate ?? this.publishedDate,
documentType: documentType ?? this.documentType,
category: category ?? this.category,
downloadUrl: downloadUrl ?? this.downloadUrl,
fileSize: fileSize ?? this.fileSize,
);
}
/// Equality operator
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is PriceDocument &&
other.id == id &&
other.title == title &&
other.description == description &&
other.publishedDate == publishedDate &&
other.documentType == documentType &&
other.category == category &&
other.downloadUrl == downloadUrl &&
other.fileSize == fileSize;
}
/// Hash code
@override
int get hashCode {
return Object.hash(
id,
title,
description,
publishedDate,
documentType,
category,
downloadUrl,
fileSize,
);
}
/// String representation
@override
String toString() {
return 'PriceDocument(id: $id, title: $title, description: $description, '
'publishedDate: $publishedDate, documentType: $documentType, '
'category: $category, downloadUrl: $downloadUrl, fileSize: $fileSize)';
}
}
/// Document type enum
enum DocumentType { pdf, excel }
/// Document category enum
enum DocumentCategory {
policy, // Chính sách giá
priceList, // Bảng giá
}
// Extension for display
extension DocumentTypeX on DocumentType {
String get displayName {
switch (this) {
case DocumentType.pdf:
return 'PDF';
case DocumentType.excel:
return 'Excel';
}
}
}
extension DocumentCategoryX on DocumentCategory {
String get displayName {
switch (this) {
case DocumentCategory.policy:
return 'Chính sách giá';
case DocumentCategory.priceList:
return 'Bảng giá';
}
}
}

View File

@@ -0,0 +1,50 @@
/// Domain Repository Interface: Price Policy Repository
///
/// Defines the contract for price policy document data operations.
/// This is an abstract interface following the Repository Pattern.
///
/// The actual implementation will be in the data layer.
/// This allows for dependency inversion and easier testing.
library;
import 'package:worker/features/price_policy/domain/entities/price_document.dart';
/// Price Policy Repository Interface
///
/// Provides methods to:
/// - Get all price policy documents
/// - Filter documents by category
/// - Fetch individual document details
///
/// Implementation will be in data/repositories/price_policy_repository_impl.dart
abstract class PricePolicyRepository {
/// Get all price policy documents
///
/// Returns list of [PriceDocument] objects.
/// Returns empty list if no documents available.
///
/// This should fetch from local cache first, then sync with server.
/// Documents should be ordered by published date (newest first).
Future<List<PriceDocument>> getAllDocuments();
/// Get documents filtered by category
///
/// Returns list of [PriceDocument] objects matching the [category].
/// Returns empty list if no matching documents.
///
/// [category] - The category to filter by (policy or priceList)
Future<List<PriceDocument>> getDocumentsByCategory(DocumentCategory category);
/// Get a specific document by ID
///
/// Returns [PriceDocument] if found, null otherwise.
///
/// [documentId] - The unique identifier of the document
Future<PriceDocument?> getDocumentById(String documentId);
/// Refresh documents from server
///
/// Force refresh documents from remote source.
/// Updates local cache after successful fetch.
Future<List<PriceDocument>> refreshDocuments();
}

View File

@@ -0,0 +1,231 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/constants/ui_constants.dart';
import '../../../../core/theme/colors.dart';
import '../../domain/entities/price_document.dart';
import '../providers/price_documents_provider.dart';
import '../widgets/document_card.dart';
/// Price policy page with tabs for policies and price lists
class PricePolicyPage extends ConsumerStatefulWidget {
const PricePolicyPage({super.key});
@override
ConsumerState<PricePolicyPage> createState() => _PricePolicyPageState();
}
class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.grey50,
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => context.pop(),
),
title: const Text(
'Chính sách giá',
style: TextStyle(color: Colors.black),
),
elevation: AppBarSpecs.elevation,
backgroundColor: AppColors.white,
foregroundColor: AppColors.grey900,
centerTitle: false,
actions: [
IconButton(
icon: const Icon(Icons.info_outline, color: Colors.black),
onPressed: _showInfoDialog,
),
const SizedBox(width: AppSpacing.sm),
],
bottom: TabBar(
controller: _tabController,
labelColor: AppColors.white,
unselectedLabelColor: AppColors.grey900,
indicatorSize: TabBarIndicatorSize.tab,
indicator: BoxDecoration(
color: AppColors.primaryBlue,
borderRadius: BorderRadius.circular(8),
),
labelStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
tabs: const [
Tab(text: 'Chính sách giá'),
Tab(text: 'Bảng giá'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
// Policy tab
_buildDocumentList(DocumentCategory.policy),
// Price list tab
_buildDocumentList(DocumentCategory.priceList),
],
),
);
}
Widget _buildDocumentList(DocumentCategory category) {
final documentsAsync = ref.watch(filteredPriceDocumentsProvider(category));
return documentsAsync.when(
data: (documents) {
if (documents.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.description_outlined,
size: 64,
color: AppColors.grey500,
),
const SizedBox(height: AppSpacing.md),
Text(
'Chưa có tài liệu',
style: TextStyle(fontSize: 16, color: AppColors.grey500),
),
],
),
);
}
return RefreshIndicator(
onRefresh: () async {
// Refresh documents from repository
ref.invalidate(filteredPriceDocumentsProvider(category));
await Future<void>.delayed(const Duration(milliseconds: 500));
},
child: ListView.separated(
padding: const EdgeInsets.all(AppSpacing.md),
itemCount: documents.length,
separatorBuilder: (context, index) =>
const SizedBox(height: AppSpacing.md),
itemBuilder: (context, index) {
final document = documents[index];
return DocumentCard(
document: document,
onDownload: () => _handleDownload(document),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: AppColors.danger),
const SizedBox(height: AppSpacing.md),
Text(
'Không thể tải tài liệu',
style: TextStyle(fontSize: 16, color: AppColors.grey500),
),
const SizedBox(height: AppSpacing.sm),
ElevatedButton(
onPressed: () {
ref.invalidate(filteredPriceDocumentsProvider(category));
},
child: const Text('Thử lại'),
),
],
),
),
);
}
void _handleDownload(PriceDocument document) {
// In real app, this would trigger actual download
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Đang tải: ${document.title}'),
duration: const Duration(seconds: 2),
backgroundColor: AppColors.primaryBlue,
behavior: SnackBarBehavior.floating,
),
);
// Simulate download
// TODO: Implement actual file download functionality
// - Use url_launcher or dio to download file
// - Show progress indicator
// - Save to device storage
}
void _showInfoDialog() {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text(
'Hướng dẫn sử dụng',
style: TextStyle(fontWeight: FontWeight.bold),
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Đây là nội dung hướng dẫn sử dụng cho tính năng Chính sách giá:',
),
const SizedBox(height: AppSpacing.md),
_buildInfoItem(
'Chọn tab "Chính sách giá" để xem các chính sách giá hiện hành',
),
_buildInfoItem(
'Chọn tab "Bảng giá" để tải về bảng giá chi tiết sản phẩm',
),
_buildInfoItem('Nhấn nút "Tải về" để download file PDF/Excel'),
_buildInfoItem('Các bảng giá được cập nhật định kỳ hàng tháng'),
_buildInfoItem('Liên hệ sales để được tư vấn giá tốt nhất'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Đóng'),
),
],
),
);
}
Widget _buildInfoItem(String text) {
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('', style: TextStyle(fontSize: 16)),
Expanded(child: Text(text)),
],
),
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/price_policy/data/datasources/price_policy_local_datasource.dart';
import 'package:worker/features/price_policy/data/repositories/price_policy_repository_impl.dart';
import 'package:worker/features/price_policy/domain/entities/price_document.dart';
import 'package:worker/features/price_policy/domain/repositories/price_policy_repository.dart';
part 'price_documents_provider.g.dart';
/// Provider for local data source
@riverpod
PricePolicyLocalDataSource pricePolicyLocalDataSource(Ref ref) {
return PricePolicyLocalDataSource();
}
/// Provider for price policy repository
@riverpod
PricePolicyRepository pricePolicyRepository(Ref ref) {
final localDataSource = ref.watch(pricePolicyLocalDataSourceProvider);
return PricePolicyRepositoryImpl(localDataSource: localDataSource);
}
/// Provider for all price policy documents
@riverpod
Future<List<PriceDocument>> priceDocuments(Ref ref) async {
final repository = ref.watch(pricePolicyRepositoryProvider);
return repository.getAllDocuments();
}
/// Provider for filtered documents by category
@riverpod
Future<List<PriceDocument>> filteredPriceDocuments(
Ref ref,
DocumentCategory category,
) async {
final repository = ref.watch(pricePolicyRepositoryProvider);
return repository.getDocumentsByCategory(category);
}

View File

@@ -0,0 +1,254 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'price_documents_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for local data source
@ProviderFor(pricePolicyLocalDataSource)
const pricePolicyLocalDataSourceProvider =
PricePolicyLocalDataSourceProvider._();
/// Provider for local data source
final class PricePolicyLocalDataSourceProvider
extends
$FunctionalProvider<
PricePolicyLocalDataSource,
PricePolicyLocalDataSource,
PricePolicyLocalDataSource
>
with $Provider<PricePolicyLocalDataSource> {
/// Provider for local data source
const PricePolicyLocalDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'pricePolicyLocalDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$pricePolicyLocalDataSourceHash();
@$internal
@override
$ProviderElement<PricePolicyLocalDataSource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
PricePolicyLocalDataSource create(Ref ref) {
return pricePolicyLocalDataSource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(PricePolicyLocalDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<PricePolicyLocalDataSource>(value),
);
}
}
String _$pricePolicyLocalDataSourceHash() =>
r'dd1bee761fa7f050835508cf33bf34a788829483';
/// Provider for price policy repository
@ProviderFor(pricePolicyRepository)
const pricePolicyRepositoryProvider = PricePolicyRepositoryProvider._();
/// Provider for price policy repository
final class PricePolicyRepositoryProvider
extends
$FunctionalProvider<
PricePolicyRepository,
PricePolicyRepository,
PricePolicyRepository
>
with $Provider<PricePolicyRepository> {
/// Provider for price policy repository
const PricePolicyRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'pricePolicyRepositoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$pricePolicyRepositoryHash();
@$internal
@override
$ProviderElement<PricePolicyRepository> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
PricePolicyRepository create(Ref ref) {
return pricePolicyRepository(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(PricePolicyRepository value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<PricePolicyRepository>(value),
);
}
}
String _$pricePolicyRepositoryHash() =>
r'296555a45936d8e43a28bf5add5e7db40495009c';
/// Provider for all price policy documents
@ProviderFor(priceDocuments)
const priceDocumentsProvider = PriceDocumentsProvider._();
/// Provider for all price policy documents
final class PriceDocumentsProvider
extends
$FunctionalProvider<
AsyncValue<List<PriceDocument>>,
List<PriceDocument>,
FutureOr<List<PriceDocument>>
>
with
$FutureModifier<List<PriceDocument>>,
$FutureProvider<List<PriceDocument>> {
/// Provider for all price policy documents
const PriceDocumentsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'priceDocumentsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$priceDocumentsHash();
@$internal
@override
$FutureProviderElement<List<PriceDocument>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<PriceDocument>> create(Ref ref) {
return priceDocuments(ref);
}
}
String _$priceDocumentsHash() => r'cf2ccf6bd9aaae0c56ab01529fd034a090d99263';
/// Provider for filtered documents by category
@ProviderFor(filteredPriceDocuments)
const filteredPriceDocumentsProvider = FilteredPriceDocumentsFamily._();
/// Provider for filtered documents by category
final class FilteredPriceDocumentsProvider
extends
$FunctionalProvider<
AsyncValue<List<PriceDocument>>,
List<PriceDocument>,
FutureOr<List<PriceDocument>>
>
with
$FutureModifier<List<PriceDocument>>,
$FutureProvider<List<PriceDocument>> {
/// Provider for filtered documents by category
const FilteredPriceDocumentsProvider._({
required FilteredPriceDocumentsFamily super.from,
required DocumentCategory super.argument,
}) : super(
retry: null,
name: r'filteredPriceDocumentsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$filteredPriceDocumentsHash();
@override
String toString() {
return r'filteredPriceDocumentsProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<List<PriceDocument>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<PriceDocument>> create(Ref ref) {
final argument = this.argument as DocumentCategory;
return filteredPriceDocuments(ref, argument);
}
@override
bool operator ==(Object other) {
return other is FilteredPriceDocumentsProvider &&
other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$filteredPriceDocumentsHash() =>
r'8f5b2ed822694b4dd9523e1a61e202a7ba0c1fbc';
/// Provider for filtered documents by category
final class FilteredPriceDocumentsFamily extends $Family
with
$FunctionalFamilyOverride<
FutureOr<List<PriceDocument>>,
DocumentCategory
> {
const FilteredPriceDocumentsFamily._()
: super(
retry: null,
name: r'filteredPriceDocumentsProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Provider for filtered documents by category
FilteredPriceDocumentsProvider call(DocumentCategory category) =>
FilteredPriceDocumentsProvider._(argument: category, from: this);
@override
String toString() => r'filteredPriceDocumentsProvider';
}

View File

@@ -0,0 +1,169 @@
import 'package:flutter/material.dart';
import '../../../../core/constants/ui_constants.dart';
import '../../../../core/theme/colors.dart';
import '../../domain/entities/price_document.dart';
/// Document card widget displaying price policy or price list document
class DocumentCard extends StatelessWidget {
final PriceDocument document;
final VoidCallback onDownload;
const DocumentCard({
super.key,
required this.document,
required this.onDownload,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.grey100),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onDownload,
child: Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: LayoutBuilder(
builder: (context, constraints) {
// Responsive layout: column on mobile, row on larger screens
final isNarrow = constraints.maxWidth < 600;
if (isNarrow) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_buildIcon(),
const SizedBox(width: AppSpacing.md),
Expanded(child: _buildInfo()),
],
),
const SizedBox(height: AppSpacing.md),
SizedBox(
width: double.infinity,
child: _buildDownloadButton(),
),
],
);
}
return Row(
children: [
_buildIcon(),
const SizedBox(width: AppSpacing.md),
Expanded(child: _buildInfo()),
const SizedBox(width: AppSpacing.md),
_buildDownloadButton(),
],
);
},
),
),
),
),
);
}
Widget _buildIcon() {
final iconData = document.isPdf ? Icons.picture_as_pdf : Icons.table_chart;
final iconColor = document.isPdf
? Colors.red.shade600
: Colors.green.shade600;
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: AppColors.grey50,
borderRadius: BorderRadius.circular(8),
),
child: Icon(iconData, size: 28, color: iconColor),
);
}
Widget _buildInfo() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
document.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 4),
Row(
children: [
const Icon(
Icons.calendar_today,
size: 13,
color: AppColors.grey500,
),
const SizedBox(width: 4),
Text(
document.formattedDateWithPrefix,
style: const TextStyle(fontSize: 13, color: AppColors.grey500),
),
if (document.fileSize != null) ...[
const SizedBox(width: 8),
const Text(
'',
style: TextStyle(fontSize: 13, color: AppColors.grey500),
),
const SizedBox(width: 8),
Text(
document.fileSize!,
style: const TextStyle(fontSize: 13, color: AppColors.grey500),
),
],
],
),
const SizedBox(height: 6),
Text(
document.description,
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
height: 1.4,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
);
}
Widget _buildDownloadButton() {
return ElevatedButton.icon(
onPressed: onDownload,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: AppColors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
icon: const Icon(Icons.download, size: 18),
label: const Text(
'Tải về',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
);
}
}

View File

@@ -0,0 +1,12 @@
/// Price Policy Feature Barrel Export
///
/// Provides easy access to all price policy feature components.
library;
// Domain
export 'domain/entities/price_document.dart';
// Presentation
export 'presentation/pages/price_policy_page.dart';
export 'presentation/widgets/document_card.dart';
export 'presentation/providers/price_documents_provider.dart';