price policy
This commit is contained in:
22
docs/price.sh
Normal file
22
docs/price.sh
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
64
pubspec.lock
64
pubspec.lock
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user