diff --git a/docs/price.sh b/docs/price.sh new file mode 100644 index 0000000..b9e3685 --- /dev/null +++ b/docs/price.sh @@ -0,0 +1,22 @@ +#get price list +curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.pricing.get_pricing_info' \ +--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \ +--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \ +--header 'Content-Type: application/json' \ +--data '{ + "pricing_type" : "PRICE_LIST", + "limit_page_length" : 0, + "limit_start" : 0 +}' +//note: PRICING_RULE = Chính sách giá,PRICE_LIST= bảng giá + +#response +{ + "message": [ + { + "title": "EUROTILE", + "file_url": "https://land.dbiz.com/private/files/City.xlsx", + "updated_at": "2025-11-26 11:36:43" + } + ] +} \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b32f963..d465686 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -94,6 +94,8 @@ PODS: - nanopb/encode (= 2.30910.0) - nanopb/decode (2.30910.0) - nanopb/encode (2.30910.0) + - open_file_ios (0.0.1): + - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS @@ -121,6 +123,7 @@ DEPENDENCIES: - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) + - open_file_ios (from `.symlinks/plugins/open_file_ios/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) @@ -161,6 +164,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/integration_test/ios" mobile_scanner: :path: ".symlinks/plugins/mobile_scanner/ios" + open_file_ios: + :path: ".symlinks/plugins/open_file_ios/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" share_plus: @@ -193,6 +198,7 @@ SPEC CHECKSUMS: MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1 mobile_scanner: 96e91f2e1fb396bb7df8da40429ba8dfad664740 nanopb: 438bc412db1928dac798aa6fd75726007be04262 + open_file_ios: 461db5853723763573e140de3193656f91990d9e path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 422514a..f6f8d2c 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -570,7 +570,7 @@ class RouteNames { // Main Routes static const String home = '/'; static const String products = '/products'; - static const String productDetail = '/products/:id'; + static const String productDetail = '$products/:id'; static const String writeReview = 'write-review'; static const String cart = '/cart'; static const String favorites = '/favorites'; @@ -579,40 +579,40 @@ class RouteNames { // Loyalty Routes static const String loyalty = '/loyalty'; - static const String rewards = '/loyalty/rewards'; - static const String pointsHistory = '/loyalty/points-history'; - static const String pointsRecords = '/$loyalty/points-records'; - static const String myGifts = '/loyalty/gifts'; - static const String referral = '/loyalty/referral'; + static const String rewards = '$loyalty/rewards'; + static const String pointsHistory = '$loyalty/points-history'; + static const String pointsRecords = '$loyalty/points-records'; + static const String myGifts = '$loyalty/gifts'; + static const String referral = '$loyalty/referral'; // Orders & Payments Routes static const String orders = '/orders'; - static const String orderDetail = '/orders/:id'; + static const String orderDetail = '$orders/:id'; static const String payments = '/payments'; - static const String paymentDetail = '/payments/:id'; + static const String paymentDetail = '$payments/:id'; static const String paymentQr = '/payment-qr'; // Projects & Quotes Routes static const String projects = '/projects'; - static const String projectDetail = '/projects/:id'; - static const String projectCreate = '/projects/create'; + static const String projectDetail = '$projects/:id'; + static const String projectCreate = '$projects/create'; static const String submissions = '/submissions'; - static const String submissionCreate = '/submissions/create'; + static const String submissionCreate = '$submissions/create'; static const String quotes = '/quotes'; - static const String quoteDetail = '/quotes/:id'; - static const String quoteCreate = '/quotes/create'; + static const String quoteDetail = '$quotes/:id'; + static const String quoteCreate = '$quotes/create'; // Account Routes static const String account = '/account'; - static const String profile = '/account/profile'; - static const String addresses = '/account/addresses'; - static const String addressForm = '/account/addresses/form'; - static const String changePassword = '/account/change-password'; - static const String settings = '/account/settings'; + static const String profile = '$account/profile'; + static const String addresses = '$account/addresses'; + static const String addressForm = '$addresses/form'; + static const String changePassword = '$account/change-password'; + static const String settings = '$account/settings'; // Promotions & Notifications Routes static const String promotions = '/promotions'; - static const String promotionDetail = '/promotions/:id'; + static const String promotionDetail = '$promotions/:id'; static const String notifications = '/notifications'; // Price Policy Route @@ -620,17 +620,16 @@ class RouteNames { // News Route static const String news = '/news'; - static const String newsDetail = '/news/:id'; + static const String newsDetail = '$news/:id'; // Chat Route static const String chat = '/chat'; // Model Houses & Design Requests Routes static const String modelHouses = '/model-houses'; - static const String modelHouseDetail = '/model-houses/:id'; - static const String designRequestCreate = - '/model-houses/design-request/create'; - static const String designRequestDetail = '/model-houses/design-request/:id'; + static const String modelHouseDetail = '$modelHouses/:id'; + static const String designRequestCreate = '$modelHouses/design-request/create'; + static const String designRequestDetail = '$modelHouses/design-request/:id'; // Authentication Routes static const String splash = '/splash'; diff --git a/lib/features/price_policy/data/datasources/price_policy_local_datasource.dart b/lib/features/price_policy/data/datasources/price_policy_local_datasource.dart deleted file mode 100644 index 33815aa..0000000 --- a/lib/features/price_policy/data/datasources/price_policy_local_datasource.dart +++ /dev/null @@ -1,185 +0,0 @@ -/// 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> getAllDocuments() async { - // Simulate network delay - await Future.delayed(const Duration(milliseconds: 300)); - - return _mockDocuments; - } - - /// Get documents by category - /// - /// Returns filtered list of documents matching the [category]. - Future> getDocumentsByCategory( - String category, - ) async { - // Simulate network delay - await Future.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 getDocumentById(String documentId) async { - // Simulate network delay - await Future.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 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 cacheDocuments(List 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 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 _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', - ), - ]; -} diff --git a/lib/features/price_policy/data/datasources/price_policy_remote_datasource.dart b/lib/features/price_policy/data/datasources/price_policy_remote_datasource.dart new file mode 100644 index 0000000..dc95ebe --- /dev/null +++ b/lib/features/price_policy/data/datasources/price_policy_remote_datasource.dart @@ -0,0 +1,65 @@ +/// Remote Data Source: Price Policy +/// +/// Handles API communication for price policy documents. +library; + +import 'package:dio/dio.dart'; +import 'package:worker/core/network/dio_client.dart'; +import 'package:worker/features/price_policy/data/models/price_document_model.dart'; + +/// Price Policy Remote Data Source Interface +abstract class PricePolicyRemoteDataSource { + /// Get documents by pricing type + Future> getDocumentsByType(String pricingType); + + /// Get all documents (both pricing rule and price list) + Future> getAllDocuments(); +} + +/// Price Policy Remote Data Source Implementation +class PricePolicyRemoteDataSourceImpl implements PricePolicyRemoteDataSource { + + const PricePolicyRemoteDataSourceImpl(this._dioClient); + final DioClient _dioClient; + + @override + Future> getDocumentsByType(String pricingType) async { + try { + final response = await _dioClient.post>( + '/api/method/building_material.building_material.api.pricing.get_pricing_info', + data: { + 'pricing_type': pricingType, + 'limit_page_length': 0, + 'limit_start': 0, + }, + ); + + if (response.data == null) { + return []; + } + + final apiResponse = PricingApiResponse.fromJson(response.data!); + return apiResponse.message; + } on DioException catch (e) { + throw Exception('Failed to fetch pricing documents: ${e.message}'); + } catch (e) { + throw Exception('Failed to parse pricing documents: $e'); + } + } + + @override + Future> getAllDocuments() async { + try { + // Fetch both pricing rule and price list in parallel + final results = await Future.wait([ + getDocumentsByType('PRICING_RULE'), + getDocumentsByType('PRICE_LIST'), + ]); + + // Combine results + return [...results[0], ...results[1]]; + } catch (e) { + throw Exception('Failed to fetch all documents: $e'); + } + } +} diff --git a/lib/features/price_policy/data/models/price_document_model.dart b/lib/features/price_policy/data/models/price_document_model.dart index b013fae..13af4aa 100644 --- a/lib/features/price_policy/data/models/price_document_model.dart +++ b/lib/features/price_policy/data/models/price_document_model.dart @@ -1,158 +1,115 @@ -/// Data Model: Price Document Model +/// Data Model: Price Document /// -/// Data layer model for price policy documents. -/// Handles JSON serialization and conversion to/from domain entity. +/// Data model for price policy documents from API. 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) +/// Price document data model 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; + final String fileUrl; - /// Optional file size display string - final String? fileSize; + /// Last updated timestamp + final String updatedAt; /// 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, + required this.fileUrl, + required this.updatedAt, }); - /// Create model from JSON + /// Create from JSON factory PriceDocumentModel.fromJson(Map 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?, + title: json['title'] as String? ?? '', + fileUrl: json['file_url'] as String? ?? '', + updatedAt: json['updated_at'] as String? ?? '', ); } - /// Convert model to JSON + /// Convert to JSON Map toJson() { return { - 'id': id, 'title': title, - 'description': description, - 'published_date': publishedDate, - 'document_type': documentType, - 'category': category, - 'download_url': downloadUrl, - 'file_size': fileSize, + 'file_url': fileUrl, + 'updated_at': updatedAt, }; } - /// Convert model to domain entity - PriceDocument toEntity() { + /// Convert to domain entity + PriceDocument toEntity(DocumentCategory category) { return PriceDocument( - id: id, title: title, - description: description, - publishedDate: DateTime.parse(publishedDate), - documentType: _parseDocumentType(documentType), - category: _parseCategory(category), - downloadUrl: downloadUrl, - fileSize: fileSize, + fileUrl: fileUrl, + updatedAt: _parseDateTime(updatedAt), + category: category, ); } - /// Create model from domain entity + /// Parse datetime string from API format + /// Format: "2025-11-26 11:36:43" + DateTime _parseDateTime(String dateTimeStr) { + try { + // Replace space with 'T' for ISO 8601 format + final isoFormat = dateTimeStr.trim().replaceFirst(' ', 'T'); + return DateTime.parse(isoFormat); + } catch (e) { + // If parsing fails, return current datetime + return DateTime.now(); + } + } + + /// Create 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, + fileUrl: entity.fileUrl, + updatedAt: _formatDateTime(entity.updatedAt), ); } - /// 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'; - } + /// Format datetime to API format + static String _formatDateTime(DateTime dateTime) { + return '${dateTime.year}-' + '${dateTime.month.toString().padLeft(2, '0')}-' + '${dateTime.day.toString().padLeft(2, '0')} ' + '${dateTime.hour.toString().padLeft(2, '0')}:' + '${dateTime.minute.toString().padLeft(2, '0')}:' + '${dateTime.second.toString().padLeft(2, '0')}'; } @override String toString() { - return 'PriceDocumentModel(id: $id, title: $title, category: $category, ' - 'documentType: $documentType, publishedDate: $publishedDate)'; + return 'PriceDocumentModel(title: $title, fileUrl: $fileUrl, updatedAt: $updatedAt)'; + } +} + +/// API Response wrapper +class PricingApiResponse { + /// List of price documents + final List message; + + const PricingApiResponse({required this.message}); + + /// Create from JSON + factory PricingApiResponse.fromJson(Map json) { + final messageList = json['message'] as List? ?? []; + return PricingApiResponse( + message: messageList + .map((item) => PriceDocumentModel.fromJson(item as Map)) + .toList(), + ); + } + + /// Convert to JSON + Map toJson() { + return { + 'message': message.map((doc) => doc.toJson()).toList(), + }; } } diff --git a/lib/features/price_policy/data/repositories/price_policy_repository_impl.dart b/lib/features/price_policy/data/repositories/price_policy_repository_impl.dart index 36d729e..23fe099 100644 --- a/lib/features/price_policy/data/repositories/price_policy_repository_impl.dart +++ b/lib/features/price_policy/data/repositories/price_policy_repository_impl.dart @@ -1,59 +1,44 @@ /// 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. +/// Fetches price policy documents from remote API. library; -import 'package:worker/features/price_policy/data/datasources/price_policy_local_datasource.dart'; +import 'package:worker/features/price_policy/data/datasources/price_policy_remote_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; + /// Remote data source + final PricePolicyRemoteDataSource remoteDataSource; /// Constructor - PricePolicyRepositoryImpl({ - required this.localDataSource, - // required this.remoteDataSource, // TODO: Add when API ready + const PricePolicyRepositoryImpl({ + required this.remoteDataSource, }); @override Future> 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 + // Fetch documents separately to maintain category info + final pricingRuleModels = + await remoteDataSource.getDocumentsByType('PRICING_RULE'); + final priceListModels = + await remoteDataSource.getDocumentsByType('PRICE_LIST'); - // For now, get from local datasource (mock data) - final models = await localDataSource.getAllDocuments(); + final entities = [ + ...pricingRuleModels.map((model) => model.toEntity(DocumentCategory.policy)), + ...priceListModels.map((model) => model.toEntity(DocumentCategory.priceList)), + ]; - // 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)); + // Sort by update date (newest first) + entities.sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); 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 []; + rethrow; } } @@ -62,37 +47,35 @@ class PricePolicyRepositoryImpl implements PricePolicyRepository { DocumentCategory category, ) async { try { - // Convert category to string for datasource - final categoryString = _categoryToString(category); + // Convert category to API parameter + final pricingType = category.apiValue; - // Get documents from local datasource - final models = await localDataSource.getDocumentsByCategory( - categoryString, - ); + // Fetch documents by type from API + final models = await remoteDataSource.getDocumentsByType(pricingType); - // Convert models to entities - final entities = models.map((model) => model.toEntity()).toList(); + // Convert models to entities with the correct category + final entities = models.map((model) => model.toEntity(category)).toList(); - // Sort by published date (newest first) - entities.sort((a, b) => b.publishedDate.compareTo(a.publishedDate)); + // Sort by update date (newest first) + entities.sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); return entities; } catch (e) { - print('[PricePolicyRepository] Error getting documents by category: $e'); - return []; + rethrow; } } @override Future getDocumentById(String documentId) async { try { - // Get document from local datasource - final model = await localDataSource.getDocumentById(documentId); - - // Convert model to entity - return model?.toEntity(); + // Since API doesn't have a get-by-id endpoint, + // we fetch all and find the matching one + final allDocuments = await getAllDocuments(); + return allDocuments.firstWhere( + (doc) => doc.title == documentId, + orElse: () => throw Exception('Document not found'), + ); } catch (e) { - print('[PricePolicyRepository] Error getting document by id: $e'); return null; } } @@ -100,35 +83,10 @@ class PricePolicyRepositoryImpl implements PricePolicyRepository { @override Future> 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; + // Refresh is same as getAllDocuments since we're fetching from API + return await getAllDocuments(); } 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'; + rethrow; } } } diff --git a/lib/features/price_policy/domain/entities/price_document.dart b/lib/features/price_policy/domain/entities/price_document.dart index 3687ca3..4fedbb2 100644 --- a/lib/features/price_policy/domain/entities/price_document.dart +++ b/lib/features/price_policy/domain/entities/price_document.dart @@ -4,44 +4,47 @@ /// This entity is framework-independent and contains only business logic. library; -/// Price policy document entity -class PriceDocument { - /// Unique document ID - final String id; +import 'package:equatable/equatable.dart'; +/// Price policy document entity +class PriceDocument extends Equatable { /// Document title final String title; - /// Document description - final String description; + /// URL to download the document + final String fileUrl; - /// Date the document was published - final DateTime publishedDate; - - /// Type of document (PDF or Excel) - final DocumentType documentType; + /// Date the document was last updated + final DateTime updatedAt; /// Category (policy or price list) final DocumentCategory category; - /// URL to download the document - final String downloadUrl; - - /// Optional file size display string - final String? fileSize; + /// Local file path after download (in-memory cache for current session) + final String? filePath; /// Constructor const PriceDocument({ - required this.id, required this.title, - required this.description, - required this.publishedDate, - required this.documentType, + required this.fileUrl, + required this.updatedAt, required this.category, - required this.downloadUrl, - this.fileSize, + this.filePath, }); + /// Get document type based on file extension + DocumentType get documentType { + final lowerUrl = fileUrl.toLowerCase(); + if (lowerUrl.endsWith('.pdf')) { + return DocumentType.pdf; + } else if (lowerUrl.endsWith('.xlsx') || + lowerUrl.endsWith('.xls') || + lowerUrl.endsWith('.csv')) { + return DocumentType.excel; + } + return DocumentType.excel; // Default to excel + } + /// Check if document is a PDF bool get isPdf => documentType == DocumentType.pdf; @@ -56,9 +59,15 @@ class PriceDocument { /// 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}'; + return '${updatedAt.day.toString().padLeft(2, '0')}/' + '${updatedAt.month.toString().padLeft(2, '0')}/' + '${updatedAt.year}'; + } + + /// Get formatted date with time (dd/MM/yyyy HH:mm) + String get formattedDateTime { + return '$formattedDate ${updatedAt.hour.toString().padLeft(2, '0')}:' + '${updatedAt.minute.toString().padLeft(2, '0')}'; } /// Get formatted date with prefix based on category @@ -72,64 +81,30 @@ class PriceDocument { /// Copy with method for immutability PriceDocument copyWith({ - String? id, String? title, - String? description, - DateTime? publishedDate, - DocumentType? documentType, + String? fileUrl, + DateTime? updatedAt, DocumentCategory? category, - String? downloadUrl, - String? fileSize, + String? filePath, }) { return PriceDocument( - id: id ?? this.id, title: title ?? this.title, - description: description ?? this.description, - publishedDate: publishedDate ?? this.publishedDate, - documentType: documentType ?? this.documentType, + fileUrl: fileUrl ?? this.fileUrl, + updatedAt: updatedAt ?? this.updatedAt, category: category ?? this.category, - downloadUrl: downloadUrl ?? this.downloadUrl, - fileSize: fileSize ?? this.fileSize, + filePath: filePath ?? this.filePath, ); } - /// Equality operator + /// Equatable props for equality comparison @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, - ); - } + List get props => [title, fileUrl, updatedAt, category, filePath]; /// String representation @override String toString() { - return 'PriceDocument(id: $id, title: $title, description: $description, ' - 'publishedDate: $publishedDate, documentType: $documentType, ' - 'category: $category, downloadUrl: $downloadUrl, fileSize: $fileSize)'; + return 'PriceDocument(title: $title, fileUrl: $fileUrl, ' + 'updatedAt: $updatedAt, category: $category, filePath: $filePath)'; } } @@ -138,8 +113,8 @@ enum DocumentType { pdf, excel } /// Document category enum enum DocumentCategory { - policy, // Chính sách giá - priceList, // Bảng giá + policy, // Chính sách giá (PRICING_RULE) + priceList, // Bảng giá (PRICE_LIST) } // Extension for display @@ -163,4 +138,14 @@ extension DocumentCategoryX on DocumentCategory { return 'Bảng giá'; } } + + /// Get API parameter value for this category + String get apiValue { + switch (this) { + case DocumentCategory.policy: + return 'PRICING_RULE'; + case DocumentCategory.priceList: + return 'PRICE_LIST'; + } + } } diff --git a/lib/features/price_policy/presentation/pages/price_policy_page.dart b/lib/features/price_policy/presentation/pages/price_policy_page.dart index 5342d20..8517afb 100644 --- a/lib/features/price_policy/presentation/pages/price_policy_page.dart +++ b/lib/features/price_policy/presentation/pages/price_policy_page.dart @@ -1,7 +1,12 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:open_file/open_file.dart'; +import 'package:path_provider/path_provider.dart'; import '../../../../core/constants/ui_constants.dart'; +import '../../../../core/network/dio_client.dart'; import '../../../../core/theme/colors.dart'; import '../../domain/entities/price_document.dart'; import '../providers/price_documents_provider.dart'; @@ -55,36 +60,55 @@ class _PricePolicyPageState extends ConsumerState ), 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, + body: Column( children: [ - // Policy tab - _buildDocumentList(DocumentCategory.policy), - // Price list tab - _buildDocumentList(DocumentCategory.priceList), + // TabBar with padding + Padding( + padding: const EdgeInsets.all(16), + child: Container( + height: 40, + decoration: BoxDecoration( + color: AppColors.grey100, + borderRadius: BorderRadius.circular(8), + ), + child: TabBar( + controller: _tabController, + labelColor: AppColors.white, + unselectedLabelColor: AppColors.grey900, + indicatorSize: TabBarIndicatorSize.tab, + indicator: BoxDecoration( + color: AppColors.primaryBlue, + borderRadius: BorderRadius.circular(8), + ), + dividerColor: Colors.transparent, + 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á'), + ], + ), + ), + ), + // TabBarView + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + // Policy tab + _buildDocumentList(DocumentCategory.policy), + // Price list tab + _buildDocumentList(DocumentCategory.priceList), + ], + ), + ), ], ), ); @@ -96,7 +120,7 @@ class _PricePolicyPageState extends ConsumerState return documentsAsync.when( data: (documents) { if (documents.isEmpty) { - return Center( + return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -105,7 +129,7 @@ class _PricePolicyPageState extends ConsumerState size: 64, color: AppColors.grey500, ), - const SizedBox(height: AppSpacing.md), + SizedBox(height: AppSpacing.md), Text( 'Chưa có tài liệu', style: TextStyle(fontSize: 16, color: AppColors.grey500), @@ -118,8 +142,9 @@ class _PricePolicyPageState extends ConsumerState return RefreshIndicator( onRefresh: () async { // Refresh documents from repository - ref.invalidate(filteredPriceDocumentsProvider(category)); - await Future.delayed(const Duration(milliseconds: 500)); + await ref + .read(filteredPriceDocumentsProvider(category).notifier) + .refresh(); }, child: ListView.separated( padding: const EdgeInsets.all(AppSpacing.md), @@ -130,7 +155,7 @@ class _PricePolicyPageState extends ConsumerState final document = documents[index]; return DocumentCard( document: document, - onDownload: () => _handleDownload(document), + onDownload: () => _handleDownload(document, category), ); }, ), @@ -150,7 +175,9 @@ class _PricePolicyPageState extends ConsumerState const SizedBox(height: AppSpacing.sm), ElevatedButton( onPressed: () { - ref.invalidate(filteredPriceDocumentsProvider(category)); + ref + .read(filteredPriceDocumentsProvider(category).notifier) + .refresh(); }, child: const Text('Thử lại'), ), @@ -160,22 +187,95 @@ class _PricePolicyPageState extends ConsumerState ); } - 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, - ), - ); + Future _handleDownload( + PriceDocument document, + DocumentCategory category, + ) async { + try { + // Check if file already downloaded and exists + if (document.filePath != null) { + final file = File(document.filePath!); + if (await file.exists()) { + // File exists, just open it + final result = await OpenFile.open(document.filePath!); + if (result.type != ResultType.done && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Không thể mở file: ${result.message}'), + backgroundColor: AppColors.danger, + behavior: SnackBarBehavior.floating, + ), + ); + } + return; + } + } - // Simulate download - // TODO: Implement actual file download functionality - // - Use url_launcher or dio to download file - // - Show progress indicator - // - Save to device storage + // File not downloaded yet, show loading snackbar + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Đang tải: ${document.title}'), + duration: const Duration(seconds: 2), + backgroundColor: AppColors.primaryBlue, + behavior: SnackBarBehavior.floating, + ), + ); + + // Get DioClient + final dioClient = await ref.read(dioClientProvider.future); + + // Get download directory + final directory = await getApplicationDocumentsDirectory(); + + // Extract filename from URL or use title + final uri = Uri.parse(document.fileUrl); + final filename = uri.pathSegments.isNotEmpty + ? uri.pathSegments.last + : '${document.title}.${document.documentType == DocumentType.pdf ? "pdf" : "xlsx"}'; + + final savePath = '${directory.path}/$filename'; + + // Download file with authentication headers (automatically added by AuthInterceptor) + await dioClient.downloadFile( + document.fileUrl, + savePath, + onReceiveProgress: (received, total) { + // Progress tracking available here if needed: (received / total * 100) + }, + ); + + // Update document with file path in provider + ref + .read(filteredPriceDocumentsProvider(category).notifier) + .updateDocumentFilePath(document.title, savePath); + + // Show success message + if (mounted) { + // Clear any existing snackbars + ScaffoldMessenger.of(context).clearSnackBars(); + + // Show success message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Tải thành công: ${document.title}'), + backgroundColor: AppColors.success, + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 2), + ), + ); + } + } catch (e) { + // Show error message + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Lỗi tải file: ${e.toString()}'), + backgroundColor: AppColors.danger, + behavior: SnackBarBehavior.floating, + ), + ); + } + } } void _showInfoDialog() { diff --git a/lib/features/price_policy/presentation/providers/price_documents_provider.dart b/lib/features/price_policy/presentation/providers/price_documents_provider.dart index 78aa284..c3bf836 100644 --- a/lib/features/price_policy/presentation/providers/price_documents_provider.dart +++ b/lib/features/price_policy/presentation/providers/price_documents_provider.dart @@ -1,38 +1,54 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:worker/features/price_policy/data/datasources/price_policy_local_datasource.dart'; +import 'package:worker/core/network/dio_client.dart'; +import 'package:worker/features/price_policy/data/datasources/price_policy_remote_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); +Future pricePolicyRepository(Ref ref) async { + final dioClient = await ref.watch(dioClientProvider.future); + final remoteDataSource = PricePolicyRemoteDataSourceImpl(dioClient); + return PricePolicyRepositoryImpl(remoteDataSource: remoteDataSource); } /// Provider for all price policy documents @riverpod Future> priceDocuments(Ref ref) async { - final repository = ref.watch(pricePolicyRepositoryProvider); + final repository = await ref.watch(pricePolicyRepositoryProvider.future); return repository.getAllDocuments(); } -/// Provider for filtered documents by category +/// Provider for filtered documents by category with file path management @riverpod -Future> filteredPriceDocuments( - Ref ref, - DocumentCategory category, -) async { - final repository = ref.watch(pricePolicyRepositoryProvider); - return repository.getDocumentsByCategory(category); +class FilteredPriceDocuments extends _$FilteredPriceDocuments { + @override + Future> build(DocumentCategory category) async { + final repository = await ref.watch(pricePolicyRepositoryProvider.future); + return repository.getDocumentsByCategory(category); + } + + /// Update a document's file path after download + void updateDocumentFilePath(String documentTitle, String filePath) { + state = state.whenData((documents) { + return documents.map((doc) { + if (doc.title == documentTitle) { + return doc.copyWith(filePath: filePath); + } + return doc; + }).toList(); + }); + } + + /// Refresh documents + Future refresh() async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + final repository = await ref.read(pricePolicyRepositoryProvider.future); + return repository.getDocumentsByCategory(category); + }); + } } diff --git a/lib/features/price_policy/presentation/providers/price_documents_provider.g.dart b/lib/features/price_policy/presentation/providers/price_documents_provider.g.dart index e7c4aa0..4f6683e 100644 --- a/lib/features/price_policy/presentation/providers/price_documents_provider.g.dart +++ b/lib/features/price_policy/presentation/providers/price_documents_provider.g.dart @@ -8,60 +8,6 @@ part of 'price_documents_provider.dart'; // 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 { - /// 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 $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(value), - ); - } -} - -String _$pricePolicyLocalDataSourceHash() => - r'dd1bee761fa7f050835508cf33bf34a788829483'; - /// Provider for price policy repository @ProviderFor(pricePolicyRepository) @@ -72,11 +18,13 @@ const pricePolicyRepositoryProvider = PricePolicyRepositoryProvider._(); final class PricePolicyRepositoryProvider extends $FunctionalProvider< + AsyncValue, PricePolicyRepository, - PricePolicyRepository, - PricePolicyRepository + FutureOr > - with $Provider { + with + $FutureModifier, + $FutureProvider { /// Provider for price policy repository const PricePolicyRepositoryProvider._() : super( @@ -94,26 +42,18 @@ final class PricePolicyRepositoryProvider @$internal @override - $ProviderElement $createElement( + $FutureProviderElement $createElement( $ProviderPointer pointer, - ) => $ProviderElement(pointer); + ) => $FutureProviderElement(pointer); @override - PricePolicyRepository create(Ref ref) { + FutureOr create(Ref ref) { return pricePolicyRepository(ref); } - - /// {@macro riverpod.override_with_value} - Override overrideWithValue(PricePolicyRepository value) { - return $ProviderOverride( - origin: this, - providerOverride: $SyncValueProvider(value), - ); - } } String _$pricePolicyRepositoryHash() => - r'296555a45936d8e43a28bf5add5e7db40495009c'; + r'35aa21067e77bbb6b91dd29c4772b1c6707be116'; /// Provider for all price policy documents @@ -159,26 +99,18 @@ final class PriceDocumentsProvider } } -String _$priceDocumentsHash() => r'cf2ccf6bd9aaae0c56ab01529fd034a090d99263'; +String _$priceDocumentsHash() => r'dffe292742776681c22d0ccdb3e091491290057d'; -/// Provider for filtered documents by category +/// Provider for filtered documents by category with file path management -@ProviderFor(filteredPriceDocuments) +@ProviderFor(FilteredPriceDocuments) const filteredPriceDocumentsProvider = FilteredPriceDocumentsFamily._(); -/// Provider for filtered documents by category - +/// Provider for filtered documents by category with file path management final class FilteredPriceDocumentsProvider extends - $FunctionalProvider< - AsyncValue>, - List, - FutureOr> - > - with - $FutureModifier>, - $FutureProvider> { - /// Provider for filtered documents by category + $AsyncNotifierProvider> { + /// Provider for filtered documents by category with file path management const FilteredPriceDocumentsProvider._({ required FilteredPriceDocumentsFamily super.from, required DocumentCategory super.argument, @@ -202,15 +134,7 @@ final class FilteredPriceDocumentsProvider @$internal @override - $FutureProviderElement> $createElement( - $ProviderPointer pointer, - ) => $FutureProviderElement(pointer); - - @override - FutureOr> create(Ref ref) { - final argument = this.argument as DocumentCategory; - return filteredPriceDocuments(ref, argument); - } + FilteredPriceDocuments create() => FilteredPriceDocuments(); @override bool operator ==(Object other) { @@ -225,13 +149,16 @@ final class FilteredPriceDocumentsProvider } String _$filteredPriceDocumentsHash() => - r'8f5b2ed822694b4dd9523e1a61e202a7ba0c1fbc'; + r'c06d858ed1027d6408c4b70c29f47a4c4c9eb21c'; -/// Provider for filtered documents by category +/// Provider for filtered documents by category with file path management final class FilteredPriceDocumentsFamily extends $Family with - $FunctionalFamilyOverride< + $ClassFamilyOverride< + FilteredPriceDocuments, + AsyncValue>, + List, FutureOr>, DocumentCategory > { @@ -244,7 +171,7 @@ final class FilteredPriceDocumentsFamily extends $Family isAutoDispose: true, ); - /// Provider for filtered documents by category + /// Provider for filtered documents by category with file path management FilteredPriceDocumentsProvider call(DocumentCategory category) => FilteredPriceDocumentsProvider._(argument: category, from: this); @@ -252,3 +179,29 @@ final class FilteredPriceDocumentsFamily extends $Family @override String toString() => r'filteredPriceDocumentsProvider'; } + +/// Provider for filtered documents by category with file path management + +abstract class _$FilteredPriceDocuments + extends $AsyncNotifier> { + late final _$args = ref.$arg as DocumentCategory; + DocumentCategory get category => _$args; + + FutureOr> build(DocumentCategory category); + @$mustCallSuper + @override + void runBuild() { + final created = build(_$args); + final ref = + this.ref as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier>, List>, + AsyncValue>, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/lib/features/price_policy/presentation/widgets/document_card.dart b/lib/features/price_policy/presentation/widgets/document_card.dart index 4c480ea..63b1507 100644 --- a/lib/features/price_policy/presentation/widgets/document_card.dart +++ b/lib/features/price_policy/presentation/widgets/document_card.dart @@ -120,23 +120,11 @@ class DocumentCard extends StatelessWidget { 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, + document.title, style: const TextStyle( fontSize: 14, color: AppColors.grey500, @@ -150,19 +138,24 @@ class DocumentCard extends StatelessWidget { } Widget _buildDownloadButton() { + final isDownloaded = document.filePath != null; + final buttonColor = isDownloaded ? AppColors.success : AppColors.primaryBlue; + final buttonIcon = isDownloaded ? Icons.folder_open : Icons.download; + final buttonText = isDownloaded ? 'Mở file' : 'Tải về'; + return ElevatedButton.icon( onPressed: onDownload, style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryBlue, + backgroundColor: buttonColor, 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), + icon: Icon(buttonIcon, size: 18), + label: Text( + buttonText, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), ), ); } diff --git a/pubspec.lock b/pubspec.lock index 726ffef..8205081 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -964,6 +964,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + open_file: + dependency: "direct main" + description: + name: open_file + sha256: d17e2bddf5b278cb2ae18393d0496aa4f162142ba97d1a9e0c30d476adf99c0e + url: "https://pub.dev" + source: hosted + version: "3.5.10" + open_file_android: + dependency: transitive + description: + name: open_file_android + sha256: "58141fcaece2f453a9684509a7275f231ac0e3d6ceb9a5e6de310a7dff9084aa" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + open_file_ios: + dependency: transitive + description: + name: open_file_ios + sha256: "02996f01e5f6863832068e97f8f3a5ef9b613516db6897f373b43b79849e4d07" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + open_file_linux: + dependency: transitive + description: + name: open_file_linux + sha256: d189f799eecbb139c97f8bc7d303f9e720954fa4e0fa1b0b7294767e5f2d7550 + url: "https://pub.dev" + source: hosted + version: "0.0.5" + open_file_mac: + dependency: transitive + description: + name: open_file_mac + sha256: "1440b1e37ceb0642208cfeb2c659c6cda27b25187a90635c9d1acb7d0584d324" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + open_file_platform_interface: + dependency: transitive + description: + name: open_file_platform_interface + sha256: "101b424ca359632699a7e1213e83d025722ab668b9fd1412338221bf9b0e5757" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + open_file_web: + dependency: transitive + description: + name: open_file_web + sha256: e3dbc9584856283dcb30aef5720558b90f88036360bd078e494ab80a80130c4f + url: "https://pub.dev" + source: hosted + version: "0.0.4" + open_file_windows: + dependency: transitive + description: + name: open_file_windows + sha256: d26c31ddf935a94a1a3aa43a23f4fff8a5ff4eea395fe7a8cb819cf55431c875 + url: "https://pub.dev" + source: hosted + version: "0.0.3" package_config: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3dfa6d1..414a7cc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,6 +74,7 @@ dependencies: file_picker: ^8.0.0 url_launcher: ^6.3.0 path_provider: ^2.1.3 + open_file: ^3.5.10 shared_preferences: ^2.2.3 flutter_secure_storage: ^9.2.4