price policy

This commit is contained in:
Phuoc Nguyen
2025-11-26 14:44:17 +07:00
parent a07f165f0c
commit 88ac2f2f07
14 changed files with 588 additions and 654 deletions

22
docs/price.sh Normal file
View File

@@ -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"
}
]
}

View File

@@ -94,6 +94,8 @@ PODS:
- nanopb/encode (= 2.30910.0) - nanopb/encode (= 2.30910.0)
- nanopb/decode (2.30910.0) - nanopb/decode (2.30910.0)
- nanopb/encode (2.30910.0) - nanopb/encode (2.30910.0)
- open_file_ios (0.0.1):
- Flutter
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@@ -121,6 +123,7 @@ DEPENDENCIES:
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/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`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
@@ -161,6 +164,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/integration_test/ios" :path: ".symlinks/plugins/integration_test/ios"
mobile_scanner: mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/ios" :path: ".symlinks/plugins/mobile_scanner/ios"
open_file_ios:
:path: ".symlinks/plugins/open_file_ios/ios"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
share_plus: share_plus:
@@ -193,6 +198,7 @@ SPEC CHECKSUMS:
MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1 MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1
mobile_scanner: 96e91f2e1fb396bb7df8da40429ba8dfad664740 mobile_scanner: 96e91f2e1fb396bb7df8da40429ba8dfad664740
nanopb: 438bc412db1928dac798aa6fd75726007be04262 nanopb: 438bc412db1928dac798aa6fd75726007be04262
open_file_ios: 461db5853723763573e140de3193656f91990d9e
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a

View File

@@ -570,7 +570,7 @@ class RouteNames {
// Main Routes // Main Routes
static const String home = '/'; static const String home = '/';
static const String products = '/products'; 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 writeReview = 'write-review';
static const String cart = '/cart'; static const String cart = '/cart';
static const String favorites = '/favorites'; static const String favorites = '/favorites';
@@ -579,40 +579,40 @@ class RouteNames {
// Loyalty Routes // Loyalty Routes
static const String loyalty = '/loyalty'; static const String loyalty = '/loyalty';
static const String rewards = '/loyalty/rewards'; static const String rewards = '$loyalty/rewards';
static const String pointsHistory = '/loyalty/points-history'; static const String pointsHistory = '$loyalty/points-history';
static const String pointsRecords = '/$loyalty/points-records'; static const String pointsRecords = '$loyalty/points-records';
static const String myGifts = '/loyalty/gifts'; static const String myGifts = '$loyalty/gifts';
static const String referral = '/loyalty/referral'; static const String referral = '$loyalty/referral';
// Orders & Payments Routes // Orders & Payments Routes
static const String orders = '/orders'; 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 payments = '/payments';
static const String paymentDetail = '/payments/:id'; static const String paymentDetail = '$payments/:id';
static const String paymentQr = '/payment-qr'; static const String paymentQr = '/payment-qr';
// Projects & Quotes Routes // Projects & Quotes Routes
static const String projects = '/projects'; static const String projects = '/projects';
static const String projectDetail = '/projects/:id'; static const String projectDetail = '$projects/:id';
static const String projectCreate = '/projects/create'; static const String projectCreate = '$projects/create';
static const String submissions = '/submissions'; 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 quotes = '/quotes';
static const String quoteDetail = '/quotes/:id'; static const String quoteDetail = '$quotes/:id';
static const String quoteCreate = '/quotes/create'; static const String quoteCreate = '$quotes/create';
// Account Routes // Account Routes
static const String account = '/account'; static const String account = '/account';
static const String profile = '/account/profile'; static const String profile = '$account/profile';
static const String addresses = '/account/addresses'; static const String addresses = '$account/addresses';
static const String addressForm = '/account/addresses/form'; static const String addressForm = '$addresses/form';
static const String changePassword = '/account/change-password'; static const String changePassword = '$account/change-password';
static const String settings = '/account/settings'; static const String settings = '$account/settings';
// Promotions & Notifications Routes // Promotions & Notifications Routes
static const String promotions = '/promotions'; static const String promotions = '/promotions';
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 // Price Policy Route
@@ -620,17 +620,16 @@ class RouteNames {
// News Route // News Route
static const String news = '/news'; static const String news = '/news';
static const String newsDetail = '/news/:id'; static const String newsDetail = '$news/:id';
// Chat Route // Chat Route
static const String chat = '/chat'; static const String chat = '/chat';
// Model Houses & Design Requests Routes // Model Houses & Design Requests Routes
static const String modelHouses = '/model-houses'; static const String modelHouses = '/model-houses';
static const String modelHouseDetail = '/model-houses/:id'; static const String modelHouseDetail = '$modelHouses/:id';
static const String designRequestCreate = static const String designRequestCreate = '$modelHouses/design-request/create';
'/model-houses/design-request/create'; static const String designRequestDetail = '$modelHouses/design-request/:id';
static const String designRequestDetail = '/model-houses/design-request/:id';
// Authentication Routes // Authentication Routes
static const String splash = '/splash'; static const String splash = '/splash';

View File

@@ -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<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,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<List<PriceDocumentModel>> getDocumentsByType(String pricingType);
/// Get all documents (both pricing rule and price list)
Future<List<PriceDocumentModel>> getAllDocuments();
}
/// Price Policy Remote Data Source Implementation
class PricePolicyRemoteDataSourceImpl implements PricePolicyRemoteDataSource {
const PricePolicyRemoteDataSourceImpl(this._dioClient);
final DioClient _dioClient;
@override
Future<List<PriceDocumentModel>> getDocumentsByType(String pricingType) async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'/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<List<PriceDocumentModel>> 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');
}
}
}

View File

@@ -1,158 +1,115 @@
/// Data Model: Price Document Model /// Data Model: Price Document
/// ///
/// Data layer model for price policy documents. /// Data model for price policy documents from API.
/// Handles JSON serialization and conversion to/from domain entity.
library; library;
import 'package:worker/features/price_policy/domain/entities/price_document.dart'; import 'package:worker/features/price_policy/domain/entities/price_document.dart';
/// Price Document Model /// Price document data model
///
/// Used in the data layer for:
/// - JSON serialization/deserialization from API
/// - Conversion to domain entity
/// - Local storage (if needed)
class PriceDocumentModel { class PriceDocumentModel {
/// Unique document ID
final String id;
/// Document title /// Document title
final String 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 /// URL to download the document
final String downloadUrl; final String fileUrl;
/// Optional file size display string /// Last updated timestamp
final String? fileSize; final String updatedAt;
/// Constructor /// Constructor
const PriceDocumentModel({ const PriceDocumentModel({
required this.id,
required this.title, required this.title,
required this.description, required this.fileUrl,
required this.publishedDate, required this.updatedAt,
required this.documentType,
required this.category,
required this.downloadUrl,
this.fileSize,
}); });
/// Create model from JSON /// Create from JSON
factory PriceDocumentModel.fromJson(Map<String, dynamic> json) { factory PriceDocumentModel.fromJson(Map<String, dynamic> json) {
return PriceDocumentModel( return PriceDocumentModel(
id: json['id'] as String, title: json['title'] as String? ?? '',
title: json['title'] as String, fileUrl: json['file_url'] as String? ?? '',
description: json['description'] as String, updatedAt: json['updated_at'] 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 /// Convert to JSON
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'id': id,
'title': title, 'title': title,
'description': description, 'file_url': fileUrl,
'published_date': publishedDate, 'updated_at': updatedAt,
'document_type': documentType,
'category': category,
'download_url': downloadUrl,
'file_size': fileSize,
}; };
} }
/// Convert model to domain entity /// Convert to domain entity
PriceDocument toEntity() { PriceDocument toEntity(DocumentCategory category) {
return PriceDocument( return PriceDocument(
id: id,
title: title, title: title,
description: description, fileUrl: fileUrl,
publishedDate: DateTime.parse(publishedDate), updatedAt: _parseDateTime(updatedAt),
documentType: _parseDocumentType(documentType), category: category,
category: _parseCategory(category),
downloadUrl: downloadUrl,
fileSize: fileSize,
); );
} }
/// 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) { factory PriceDocumentModel.fromEntity(PriceDocument entity) {
return PriceDocumentModel( return PriceDocumentModel(
id: entity.id,
title: entity.title, title: entity.title,
description: entity.description, fileUrl: entity.fileUrl,
publishedDate: entity.publishedDate.toIso8601String(), updatedAt: _formatDateTime(entity.updatedAt),
documentType: _documentTypeToString(entity.documentType),
category: _categoryToString(entity.category),
downloadUrl: entity.downloadUrl,
fileSize: entity.fileSize,
); );
} }
/// Parse document type from string /// Format datetime to API format
static DocumentType _parseDocumentType(String type) { static String _formatDateTime(DateTime dateTime) {
switch (type.toLowerCase()) { return '${dateTime.year}-'
case 'pdf': '${dateTime.month.toString().padLeft(2, '0')}-'
return DocumentType.pdf; '${dateTime.day.toString().padLeft(2, '0')} '
case 'excel': '${dateTime.hour.toString().padLeft(2, '0')}:'
return DocumentType.excel; '${dateTime.minute.toString().padLeft(2, '0')}:'
default: '${dateTime.second.toString().padLeft(2, '0')}';
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 @override
String toString() { String toString() {
return 'PriceDocumentModel(id: $id, title: $title, category: $category, ' return 'PriceDocumentModel(title: $title, fileUrl: $fileUrl, updatedAt: $updatedAt)';
'documentType: $documentType, publishedDate: $publishedDate)'; }
}
/// API Response wrapper
class PricingApiResponse {
/// List of price documents
final List<PriceDocumentModel> message;
const PricingApiResponse({required this.message});
/// Create from JSON
factory PricingApiResponse.fromJson(Map<String, dynamic> json) {
final messageList = json['message'] as List<dynamic>? ?? [];
return PricingApiResponse(
message: messageList
.map((item) => PriceDocumentModel.fromJson(item as Map<String, dynamic>))
.toList(),
);
}
/// Convert to JSON
Map<String, dynamic> toJson() {
return {
'message': message.map((doc) => doc.toJson()).toList(),
};
} }
} }

View File

@@ -1,59 +1,44 @@
/// Repository Implementation: Price Policy Repository /// Repository Implementation: Price Policy Repository
/// ///
/// Concrete implementation of the PricePolicyRepository interface. /// Concrete implementation of the PricePolicyRepository interface.
/// Coordinates between local and remote data sources to provide price policy data. /// Fetches price policy documents from remote API.
///
/// Currently uses mock data from local datasource.
/// Will implement offline-first strategy when backend API is available.
library; 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/entities/price_document.dart';
import 'package:worker/features/price_policy/domain/repositories/price_policy_repository.dart'; import 'package:worker/features/price_policy/domain/repositories/price_policy_repository.dart';
/// Price Policy Repository Implementation /// 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 { class PricePolicyRepositoryImpl implements PricePolicyRepository {
/// Local data source /// Remote data source
final PricePolicyLocalDataSource localDataSource; final PricePolicyRemoteDataSource remoteDataSource;
/// Remote data source (API) - TODO: Add when API is ready
// final PricePolicyRemoteDataSource remoteDataSource;
/// Constructor /// Constructor
PricePolicyRepositoryImpl({ const PricePolicyRepositoryImpl({
required this.localDataSource, required this.remoteDataSource,
// required this.remoteDataSource, // TODO: Add when API ready
}); });
@override @override
Future<List<PriceDocument>> getAllDocuments() async { Future<List<PriceDocument>> getAllDocuments() async {
try { try {
// TODO: Implement offline-first strategy // Fetch documents separately to maintain category info
// 1. Check if cache is valid final pricingRuleModels =
// 2. Return cached data if valid await remoteDataSource.getDocumentsByType('PRICING_RULE');
// 3. If cache invalid, fetch from remote final priceListModels =
await remoteDataSource.getDocumentsByType('PRICE_LIST');
// For now, get from local datasource (mock data) final entities = <PriceDocument>[
final models = await localDataSource.getAllDocuments(); ...pricingRuleModels.map((model) => model.toEntity(DocumentCategory.policy)),
...priceListModels.map((model) => model.toEntity(DocumentCategory.priceList)),
];
// Convert models to entities // Sort by update date (newest first)
final entities = models.map((model) => model.toEntity()).toList(); entities.sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
// Sort by published date (newest first)
entities.sort((a, b) => b.publishedDate.compareTo(a.publishedDate));
return entities; return entities;
} catch (e) { } catch (e) {
// Log error and return empty list
// In production, this should throw proper domain failures // In production, this should throw proper domain failures
print('[PricePolicyRepository] Error getting documents: $e'); rethrow;
return [];
} }
} }
@@ -62,37 +47,35 @@ class PricePolicyRepositoryImpl implements PricePolicyRepository {
DocumentCategory category, DocumentCategory category,
) async { ) async {
try { try {
// Convert category to string for datasource // Convert category to API parameter
final categoryString = _categoryToString(category); final pricingType = category.apiValue;
// Get documents from local datasource // Fetch documents by type from API
final models = await localDataSource.getDocumentsByCategory( final models = await remoteDataSource.getDocumentsByType(pricingType);
categoryString,
);
// Convert models to entities // Convert models to entities with the correct category
final entities = models.map((model) => model.toEntity()).toList(); final entities = models.map((model) => model.toEntity(category)).toList();
// Sort by published date (newest first) // Sort by update date (newest first)
entities.sort((a, b) => b.publishedDate.compareTo(a.publishedDate)); entities.sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
return entities; return entities;
} catch (e) { } catch (e) {
print('[PricePolicyRepository] Error getting documents by category: $e'); rethrow;
return [];
} }
} }
@override @override
Future<PriceDocument?> getDocumentById(String documentId) async { Future<PriceDocument?> getDocumentById(String documentId) async {
try { try {
// Get document from local datasource // Since API doesn't have a get-by-id endpoint,
final model = await localDataSource.getDocumentById(documentId); // we fetch all and find the matching one
final allDocuments = await getAllDocuments();
// Convert model to entity return allDocuments.firstWhere(
return model?.toEntity(); (doc) => doc.title == documentId,
orElse: () => throw Exception('Document not found'),
);
} catch (e) { } catch (e) {
print('[PricePolicyRepository] Error getting document by id: $e');
return null; return null;
} }
} }
@@ -100,35 +83,10 @@ class PricePolicyRepositoryImpl implements PricePolicyRepository {
@override @override
Future<List<PriceDocument>> refreshDocuments() async { Future<List<PriceDocument>> refreshDocuments() async {
try { try {
// TODO: Implement remote fetch when API is available // Refresh is same as getAllDocuments since we're fetching from API
// 1. Fetch from remote API return await getAllDocuments();
// 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) { } catch (e) {
print('[PricePolicyRepository] Error refreshing documents: $e'); rethrow;
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

@@ -4,44 +4,47 @@
/// This entity is framework-independent and contains only business logic. /// This entity is framework-independent and contains only business logic.
library; library;
/// Price policy document entity import 'package:equatable/equatable.dart';
class PriceDocument {
/// Unique document ID
final String id;
/// Price policy document entity
class PriceDocument extends Equatable {
/// Document title /// Document title
final String title; final String title;
/// Document description /// URL to download the document
final String description; final String fileUrl;
/// Date the document was published /// Date the document was last updated
final DateTime publishedDate; final DateTime updatedAt;
/// Type of document (PDF or Excel)
final DocumentType documentType;
/// Category (policy or price list) /// Category (policy or price list)
final DocumentCategory category; final DocumentCategory category;
/// URL to download the document /// Local file path after download (in-memory cache for current session)
final String downloadUrl; final String? filePath;
/// Optional file size display string
final String? fileSize;
/// Constructor /// Constructor
const PriceDocument({ const PriceDocument({
required this.id,
required this.title, required this.title,
required this.description, required this.fileUrl,
required this.publishedDate, required this.updatedAt,
required this.documentType,
required this.category, required this.category,
required this.downloadUrl, this.filePath,
this.fileSize,
}); });
/// 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 /// Check if document is a PDF
bool get isPdf => documentType == DocumentType.pdf; bool get isPdf => documentType == DocumentType.pdf;
@@ -56,9 +59,15 @@ class PriceDocument {
/// Get formatted published date (dd/MM/yyyy) /// Get formatted published date (dd/MM/yyyy)
String get formattedDate { String get formattedDate {
return '${publishedDate.day.toString().padLeft(2, '0')}/' return '${updatedAt.day.toString().padLeft(2, '0')}/'
'${publishedDate.month.toString().padLeft(2, '0')}/' '${updatedAt.month.toString().padLeft(2, '0')}/'
'${publishedDate.year}'; '${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 /// Get formatted date with prefix based on category
@@ -72,64 +81,30 @@ class PriceDocument {
/// Copy with method for immutability /// Copy with method for immutability
PriceDocument copyWith({ PriceDocument copyWith({
String? id,
String? title, String? title,
String? description, String? fileUrl,
DateTime? publishedDate, DateTime? updatedAt,
DocumentType? documentType,
DocumentCategory? category, DocumentCategory? category,
String? downloadUrl, String? filePath,
String? fileSize,
}) { }) {
return PriceDocument( return PriceDocument(
id: id ?? this.id,
title: title ?? this.title, title: title ?? this.title,
description: description ?? this.description, fileUrl: fileUrl ?? this.fileUrl,
publishedDate: publishedDate ?? this.publishedDate, updatedAt: updatedAt ?? this.updatedAt,
documentType: documentType ?? this.documentType,
category: category ?? this.category, category: category ?? this.category,
downloadUrl: downloadUrl ?? this.downloadUrl, filePath: filePath ?? this.filePath,
fileSize: fileSize ?? this.fileSize,
); );
} }
/// Equality operator /// Equatable props for equality comparison
@override @override
bool operator ==(Object other) { List<Object?> get props => [title, fileUrl, updatedAt, category, filePath];
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 /// String representation
@override @override
String toString() { String toString() {
return 'PriceDocument(id: $id, title: $title, description: $description, ' return 'PriceDocument(title: $title, fileUrl: $fileUrl, '
'publishedDate: $publishedDate, documentType: $documentType, ' 'updatedAt: $updatedAt, category: $category, filePath: $filePath)';
'category: $category, downloadUrl: $downloadUrl, fileSize: $fileSize)';
} }
} }
@@ -138,8 +113,8 @@ enum DocumentType { pdf, excel }
/// Document category enum /// Document category enum
enum DocumentCategory { enum DocumentCategory {
policy, // Chính sách giá policy, // Chính sách giá (PRICING_RULE)
priceList, // Bảng giá priceList, // Bảng giá (PRICE_LIST)
} }
// Extension for display // Extension for display
@@ -163,4 +138,14 @@ extension DocumentCategoryX on DocumentCategory {
return 'Bảng giá'; 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';
}
}
} }

View File

@@ -1,7 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.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/constants/ui_constants.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/theme/colors.dart'; import '../../../../core/theme/colors.dart';
import '../../domain/entities/price_document.dart'; import '../../domain/entities/price_document.dart';
import '../providers/price_documents_provider.dart'; import '../providers/price_documents_provider.dart';
@@ -55,7 +60,19 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
), ),
const SizedBox(width: AppSpacing.sm), const SizedBox(width: AppSpacing.sm),
], ],
bottom: TabBar( ),
body: Column(
children: [
// 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, controller: _tabController,
labelColor: AppColors.white, labelColor: AppColors.white,
unselectedLabelColor: AppColors.grey900, unselectedLabelColor: AppColors.grey900,
@@ -64,6 +81,7 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
color: AppColors.primaryBlue, color: AppColors.primaryBlue,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
dividerColor: Colors.transparent,
labelStyle: const TextStyle( labelStyle: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -78,7 +96,10 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
], ],
), ),
), ),
body: TabBarView( ),
// TabBarView
Expanded(
child: TabBarView(
controller: _tabController, controller: _tabController,
children: [ children: [
// Policy tab // Policy tab
@@ -87,6 +108,9 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
_buildDocumentList(DocumentCategory.priceList), _buildDocumentList(DocumentCategory.priceList),
], ],
), ),
),
],
),
); );
} }
@@ -96,7 +120,7 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
return documentsAsync.when( return documentsAsync.when(
data: (documents) { data: (documents) {
if (documents.isEmpty) { if (documents.isEmpty) {
return Center( return const Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -105,7 +129,7 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
size: 64, size: 64,
color: AppColors.grey500, color: AppColors.grey500,
), ),
const SizedBox(height: AppSpacing.md), SizedBox(height: AppSpacing.md),
Text( Text(
'Chưa có tài liệu', 'Chưa có tài liệu',
style: TextStyle(fontSize: 16, color: AppColors.grey500), style: TextStyle(fontSize: 16, color: AppColors.grey500),
@@ -118,8 +142,9 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
// Refresh documents from repository // Refresh documents from repository
ref.invalidate(filteredPriceDocumentsProvider(category)); await ref
await Future<void>.delayed(const Duration(milliseconds: 500)); .read(filteredPriceDocumentsProvider(category).notifier)
.refresh();
}, },
child: ListView.separated( child: ListView.separated(
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
@@ -130,7 +155,7 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
final document = documents[index]; final document = documents[index];
return DocumentCard( return DocumentCard(
document: document, document: document,
onDownload: () => _handleDownload(document), onDownload: () => _handleDownload(document, category),
); );
}, },
), ),
@@ -150,7 +175,9 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
const SizedBox(height: AppSpacing.sm), const SizedBox(height: AppSpacing.sm),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
ref.invalidate(filteredPriceDocumentsProvider(category)); ref
.read(filteredPriceDocumentsProvider(category).notifier)
.refresh();
}, },
child: const Text('Thử lại'), child: const Text('Thử lại'),
), ),
@@ -160,8 +187,31 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
); );
} }
void _handleDownload(PriceDocument document) { Future<void> _handleDownload(
// In real app, this would trigger actual download 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;
}
}
// File not downloaded yet, show loading snackbar
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Đang tải: ${document.title}'), content: Text('Đang tải: ${document.title}'),
@@ -171,11 +221,61 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
), ),
); );
// Simulate download // Get DioClient
// TODO: Implement actual file download functionality final dioClient = await ref.read(dioClientProvider.future);
// - Use url_launcher or dio to download file
// - Show progress indicator // Get download directory
// - Save to device storage 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() { void _showInfoDialog() {

View File

@@ -1,38 +1,54 @@
import 'package:riverpod_annotation/riverpod_annotation.dart'; 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/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/entities/price_document.dart';
import 'package:worker/features/price_policy/domain/repositories/price_policy_repository.dart'; import 'package:worker/features/price_policy/domain/repositories/price_policy_repository.dart';
part 'price_documents_provider.g.dart'; part 'price_documents_provider.g.dart';
/// Provider for local data source
@riverpod
PricePolicyLocalDataSource pricePolicyLocalDataSource(Ref ref) {
return PricePolicyLocalDataSource();
}
/// Provider for price policy repository /// Provider for price policy repository
@riverpod @riverpod
PricePolicyRepository pricePolicyRepository(Ref ref) { Future<PricePolicyRepository> pricePolicyRepository(Ref ref) async {
final localDataSource = ref.watch(pricePolicyLocalDataSourceProvider); final dioClient = await ref.watch(dioClientProvider.future);
final remoteDataSource = PricePolicyRemoteDataSourceImpl(dioClient);
return PricePolicyRepositoryImpl(localDataSource: localDataSource); return PricePolicyRepositoryImpl(remoteDataSource: remoteDataSource);
} }
/// Provider for all price policy documents /// Provider for all price policy documents
@riverpod @riverpod
Future<List<PriceDocument>> priceDocuments(Ref ref) async { Future<List<PriceDocument>> priceDocuments(Ref ref) async {
final repository = ref.watch(pricePolicyRepositoryProvider); final repository = await ref.watch(pricePolicyRepositoryProvider.future);
return repository.getAllDocuments(); return repository.getAllDocuments();
} }
/// Provider for filtered documents by category /// Provider for filtered documents by category with file path management
@riverpod @riverpod
Future<List<PriceDocument>> filteredPriceDocuments( class FilteredPriceDocuments extends _$FilteredPriceDocuments {
Ref ref, @override
DocumentCategory category, Future<List<PriceDocument>> build(DocumentCategory category) async {
) async { final repository = await ref.watch(pricePolicyRepositoryProvider.future);
final repository = ref.watch(pricePolicyRepositoryProvider);
return repository.getDocumentsByCategory(category); 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<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final repository = await ref.read(pricePolicyRepositoryProvider.future);
return repository.getDocumentsByCategory(category);
});
}
} }

View File

@@ -8,60 +8,6 @@ part of 'price_documents_provider.dart';
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning // 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 /// Provider for price policy repository
@ProviderFor(pricePolicyRepository) @ProviderFor(pricePolicyRepository)
@@ -72,11 +18,13 @@ const pricePolicyRepositoryProvider = PricePolicyRepositoryProvider._();
final class PricePolicyRepositoryProvider final class PricePolicyRepositoryProvider
extends extends
$FunctionalProvider< $FunctionalProvider<
AsyncValue<PricePolicyRepository>,
PricePolicyRepository, PricePolicyRepository,
PricePolicyRepository, FutureOr<PricePolicyRepository>
PricePolicyRepository
> >
with $Provider<PricePolicyRepository> { with
$FutureModifier<PricePolicyRepository>,
$FutureProvider<PricePolicyRepository> {
/// Provider for price policy repository /// Provider for price policy repository
const PricePolicyRepositoryProvider._() const PricePolicyRepositoryProvider._()
: super( : super(
@@ -94,26 +42,18 @@ final class PricePolicyRepositoryProvider
@$internal @$internal
@override @override
$ProviderElement<PricePolicyRepository> $createElement( $FutureProviderElement<PricePolicyRepository> $createElement(
$ProviderPointer pointer, $ProviderPointer pointer,
) => $ProviderElement(pointer); ) => $FutureProviderElement(pointer);
@override @override
PricePolicyRepository create(Ref ref) { FutureOr<PricePolicyRepository> create(Ref ref) {
return pricePolicyRepository(ref); return pricePolicyRepository(ref);
} }
/// {@macro riverpod.override_with_value}
Override overrideWithValue(PricePolicyRepository value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<PricePolicyRepository>(value),
);
}
} }
String _$pricePolicyRepositoryHash() => String _$pricePolicyRepositoryHash() =>
r'296555a45936d8e43a28bf5add5e7db40495009c'; r'35aa21067e77bbb6b91dd29c4772b1c6707be116';
/// Provider for all price policy documents /// 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._(); const filteredPriceDocumentsProvider = FilteredPriceDocumentsFamily._();
/// Provider for filtered documents by category /// Provider for filtered documents by category with file path management
final class FilteredPriceDocumentsProvider final class FilteredPriceDocumentsProvider
extends extends
$FunctionalProvider< $AsyncNotifierProvider<FilteredPriceDocuments, List<PriceDocument>> {
AsyncValue<List<PriceDocument>>, /// Provider for filtered documents by category with file path management
List<PriceDocument>,
FutureOr<List<PriceDocument>>
>
with
$FutureModifier<List<PriceDocument>>,
$FutureProvider<List<PriceDocument>> {
/// Provider for filtered documents by category
const FilteredPriceDocumentsProvider._({ const FilteredPriceDocumentsProvider._({
required FilteredPriceDocumentsFamily super.from, required FilteredPriceDocumentsFamily super.from,
required DocumentCategory super.argument, required DocumentCategory super.argument,
@@ -202,15 +134,7 @@ final class FilteredPriceDocumentsProvider
@$internal @$internal
@override @override
$FutureProviderElement<List<PriceDocument>> $createElement( FilteredPriceDocuments create() => FilteredPriceDocuments();
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<PriceDocument>> create(Ref ref) {
final argument = this.argument as DocumentCategory;
return filteredPriceDocuments(ref, argument);
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
@@ -225,13 +149,16 @@ final class FilteredPriceDocumentsProvider
} }
String _$filteredPriceDocumentsHash() => 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 final class FilteredPriceDocumentsFamily extends $Family
with with
$FunctionalFamilyOverride< $ClassFamilyOverride<
FilteredPriceDocuments,
AsyncValue<List<PriceDocument>>,
List<PriceDocument>,
FutureOr<List<PriceDocument>>, FutureOr<List<PriceDocument>>,
DocumentCategory DocumentCategory
> { > {
@@ -244,7 +171,7 @@ final class FilteredPriceDocumentsFamily extends $Family
isAutoDispose: true, isAutoDispose: true,
); );
/// Provider for filtered documents by category /// Provider for filtered documents by category with file path management
FilteredPriceDocumentsProvider call(DocumentCategory category) => FilteredPriceDocumentsProvider call(DocumentCategory category) =>
FilteredPriceDocumentsProvider._(argument: category, from: this); FilteredPriceDocumentsProvider._(argument: category, from: this);
@@ -252,3 +179,29 @@ final class FilteredPriceDocumentsFamily extends $Family
@override @override
String toString() => r'filteredPriceDocumentsProvider'; String toString() => r'filteredPriceDocumentsProvider';
} }
/// Provider for filtered documents by category with file path management
abstract class _$FilteredPriceDocuments
extends $AsyncNotifier<List<PriceDocument>> {
late final _$args = ref.$arg as DocumentCategory;
DocumentCategory get category => _$args;
FutureOr<List<PriceDocument>> build(DocumentCategory category);
@$mustCallSuper
@override
void runBuild() {
final created = build(_$args);
final ref =
this.ref as $Ref<AsyncValue<List<PriceDocument>>, List<PriceDocument>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<PriceDocument>>, List<PriceDocument>>,
AsyncValue<List<PriceDocument>>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -120,23 +120,11 @@ class DocumentCard extends StatelessWidget {
document.formattedDateWithPrefix, document.formattedDateWithPrefix,
style: const TextStyle(fontSize: 13, color: AppColors.grey500), 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), const SizedBox(height: 6),
Text( Text(
document.description, document.title,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
color: AppColors.grey500, color: AppColors.grey500,
@@ -150,19 +138,24 @@ class DocumentCard extends StatelessWidget {
} }
Widget _buildDownloadButton() { 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( return ElevatedButton.icon(
onPressed: onDownload, onPressed: onDownload,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue, backgroundColor: buttonColor,
foregroundColor: AppColors.white, foregroundColor: AppColors.white,
elevation: 0, elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
), ),
icon: const Icon(Icons.download, size: 18), icon: Icon(buttonIcon, size: 18),
label: const Text( label: Text(
'Tải về', buttonText,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
), ),
); );
} }

View File

@@ -964,6 +964,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" 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: package_config:
dependency: transitive dependency: transitive
description: description:

View File

@@ -74,6 +74,7 @@ dependencies:
file_picker: ^8.0.0 file_picker: ^8.0.0
url_launcher: ^6.3.0 url_launcher: ^6.3.0
path_provider: ^2.1.3 path_provider: ^2.1.3
open_file: ^3.5.10
shared_preferences: ^2.2.3 shared_preferences: ^2.2.3
flutter_secure_storage: ^9.2.4 flutter_secure_storage: ^9.2.4