add price policy
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
/// Price Policy Local DataSource
|
||||
///
|
||||
/// Handles all local data operations for price policy documents.
|
||||
/// Currently provides mock data for development and testing.
|
||||
/// Will be extended to use Hive cache when backend API is available.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/price_policy/data/models/price_document_model.dart';
|
||||
|
||||
/// Price Policy Local Data Source
|
||||
///
|
||||
/// Provides mock data for price policy documents.
|
||||
/// In production, this will cache data from the remote API.
|
||||
class PricePolicyLocalDataSource {
|
||||
/// Get all price policy documents
|
||||
///
|
||||
/// Returns a list of all documents from mock data.
|
||||
/// In production, this will fetch from Hive cache.
|
||||
Future<List<PriceDocumentModel>> getAllDocuments() async {
|
||||
// Simulate network delay
|
||||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
return _mockDocuments;
|
||||
}
|
||||
|
||||
/// Get documents by category
|
||||
///
|
||||
/// Returns filtered list of documents matching the [category].
|
||||
Future<List<PriceDocumentModel>> getDocumentsByCategory(
|
||||
String category,
|
||||
) async {
|
||||
// Simulate network delay
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
return _mockDocuments
|
||||
.where((doc) => doc.category.toLowerCase() == category.toLowerCase())
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Get a specific document by ID
|
||||
///
|
||||
/// Returns the document if found, null otherwise.
|
||||
Future<PriceDocumentModel?> getDocumentById(String documentId) async {
|
||||
// Simulate network delay
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
try {
|
||||
return _mockDocuments.firstWhere((doc) => doc.id == documentId);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if cache is valid
|
||||
///
|
||||
/// Returns true if cached data is still valid.
|
||||
/// Currently always returns false since we're using mock data.
|
||||
Future<bool> isCacheValid() async {
|
||||
// TODO: Implement cache validation when using Hive
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Cache documents locally
|
||||
///
|
||||
/// Saves documents to Hive for offline access.
|
||||
/// Currently not implemented (using mock data).
|
||||
Future<void> cacheDocuments(List<PriceDocumentModel> documents) async {
|
||||
// TODO: Implement Hive caching when backend API is ready
|
||||
}
|
||||
|
||||
/// Clear cached documents
|
||||
///
|
||||
/// Removes all cached documents from Hive.
|
||||
/// Currently not implemented (using mock data).
|
||||
Future<void> clearCache() async {
|
||||
// TODO: Implement cache clearing when using Hive
|
||||
}
|
||||
|
||||
/// Mock documents matching HTML design
|
||||
///
|
||||
/// This data will be replaced with real API data in production.
|
||||
static final List<PriceDocumentModel> _mockDocuments = [
|
||||
// Policy documents (Chính sách giá)
|
||||
const PriceDocumentModel(
|
||||
id: 'policy-eurotile-10-2025',
|
||||
title: 'Chính sách giá Eurotile T10/2025',
|
||||
description:
|
||||
'Chính sách giá mới nhất cho sản phẩm gạch Eurotile, áp dụng từ tháng 10/2025',
|
||||
publishedDate: '2025-10-01T00:00:00.000Z',
|
||||
documentType: 'pdf',
|
||||
category: 'policy',
|
||||
downloadUrl: '/documents/policy-eurotile-10-2025.pdf',
|
||||
fileSize: '2.5 MB',
|
||||
),
|
||||
const PriceDocumentModel(
|
||||
id: 'policy-vasta-10-2025',
|
||||
title: 'Chính sách giá Vasta Stone T10/2025',
|
||||
description:
|
||||
'Chính sách giá đá tự nhiên Vasta Stone, hiệu lực từ tháng 10/2025',
|
||||
publishedDate: '2025-10-01T00:00:00.000Z',
|
||||
documentType: 'pdf',
|
||||
category: 'policy',
|
||||
downloadUrl: '/documents/policy-vasta-10-2025.pdf',
|
||||
fileSize: '1.8 MB',
|
||||
),
|
||||
const PriceDocumentModel(
|
||||
id: 'policy-dealer-2025',
|
||||
title: 'Chính sách chiết khấu đại lý 2025',
|
||||
description:
|
||||
'Chương trình chiết khấu và ưu đãi dành cho đại lý, thầu thợ',
|
||||
publishedDate: '2025-09-15T00:00:00.000Z',
|
||||
documentType: 'pdf',
|
||||
category: 'policy',
|
||||
downloadUrl: '/documents/policy-dealer-2025.pdf',
|
||||
fileSize: '3.2 MB',
|
||||
),
|
||||
const PriceDocumentModel(
|
||||
id: 'policy-payment-2025',
|
||||
title: 'Điều kiện thanh toán & giao hàng',
|
||||
description:
|
||||
'Điều khoản thanh toán, chính sách giao hàng và bảo hành sản phẩm',
|
||||
publishedDate: '2025-08-01T00:00:00.000Z',
|
||||
documentType: 'pdf',
|
||||
category: 'policy',
|
||||
downloadUrl: '/documents/policy-payment-2025.pdf',
|
||||
fileSize: '1.5 MB',
|
||||
),
|
||||
|
||||
// Price list documents (Bảng giá)
|
||||
const PriceDocumentModel(
|
||||
id: 'pricelist-granite-2025',
|
||||
title: 'Bảng giá Gạch Granite Eurotile 2025',
|
||||
description:
|
||||
'Bảng giá chi tiết toàn bộ sản phẩm gạch granite, kích thước 60x60, 80x80, 120x120',
|
||||
publishedDate: '2025-10-01T00:00:00.000Z',
|
||||
documentType: 'excel',
|
||||
category: 'priceList',
|
||||
downloadUrl: '/documents/pricelist-granite-2025.xlsx',
|
||||
fileSize: '850 KB',
|
||||
),
|
||||
const PriceDocumentModel(
|
||||
id: 'pricelist-ceramic-2025',
|
||||
title: 'Bảng giá Gạch Ceramic Eurotile 2025',
|
||||
description: 'Bảng giá gạch ceramic vân gỗ, vân đá, vân xi măng các loại',
|
||||
publishedDate: '2025-10-01T00:00:00.000Z',
|
||||
documentType: 'excel',
|
||||
category: 'priceList',
|
||||
downloadUrl: '/documents/pricelist-ceramic-2025.xlsx',
|
||||
fileSize: '720 KB',
|
||||
),
|
||||
const PriceDocumentModel(
|
||||
id: 'pricelist-stone-2025',
|
||||
title: 'Bảng giá Đá tự nhiên Vasta Stone 2025',
|
||||
description:
|
||||
'Bảng giá đá marble, granite tự nhiên nhập khẩu, kích thước tấm lớn',
|
||||
publishedDate: '2025-10-01T00:00:00.000Z',
|
||||
documentType: 'excel',
|
||||
category: 'priceList',
|
||||
downloadUrl: '/documents/pricelist-stone-2025.xlsx',
|
||||
fileSize: '950 KB',
|
||||
),
|
||||
const PriceDocumentModel(
|
||||
id: 'pricelist-accessories-2025',
|
||||
title: 'Bảng giá Phụ kiện & Vật liệu 2025',
|
||||
description:
|
||||
'Giá keo dán, chà ron, nẹp nhựa, nẹp inox và các phụ kiện thi công',
|
||||
publishedDate: '2025-09-15T00:00:00.000Z',
|
||||
documentType: 'excel',
|
||||
category: 'priceList',
|
||||
downloadUrl: '/documents/pricelist-accessories-2025.xlsx',
|
||||
fileSize: '640 KB',
|
||||
),
|
||||
const PriceDocumentModel(
|
||||
id: 'pricelist-outdoor-2025',
|
||||
title: 'Bảng giá Gạch Outdoor & Chống trơn 2025',
|
||||
description:
|
||||
'Bảng giá sản phẩm outdoor, gạch chống trơn dành cho ngoại thất',
|
||||
publishedDate: '2025-09-01T00:00:00.000Z',
|
||||
documentType: 'excel',
|
||||
category: 'priceList',
|
||||
downloadUrl: '/documents/pricelist-outdoor-2025.xlsx',
|
||||
fileSize: '780 KB',
|
||||
),
|
||||
];
|
||||
}
|
||||
158
lib/features/price_policy/data/models/price_document_model.dart
Normal file
158
lib/features/price_policy/data/models/price_document_model.dart
Normal file
@@ -0,0 +1,158 @@
|
||||
/// Data Model: Price Document Model
|
||||
///
|
||||
/// Data layer model for price policy documents.
|
||||
/// Handles JSON serialization and conversion to/from domain entity.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/price_policy/domain/entities/price_document.dart';
|
||||
|
||||
/// Price Document Model
|
||||
///
|
||||
/// Used in the data layer for:
|
||||
/// - JSON serialization/deserialization from API
|
||||
/// - Conversion to domain entity
|
||||
/// - Local storage (if needed)
|
||||
class PriceDocumentModel {
|
||||
/// Unique document ID
|
||||
final String id;
|
||||
|
||||
/// Document title
|
||||
final String title;
|
||||
|
||||
/// Document description
|
||||
final String description;
|
||||
|
||||
/// Date the document was published (ISO 8601 string)
|
||||
final String publishedDate;
|
||||
|
||||
/// Type of document (pdf or excel)
|
||||
final String documentType;
|
||||
|
||||
/// Category (policy or priceList)
|
||||
final String category;
|
||||
|
||||
/// URL to download the document
|
||||
final String downloadUrl;
|
||||
|
||||
/// Optional file size display string
|
||||
final String? fileSize;
|
||||
|
||||
/// Constructor
|
||||
const PriceDocumentModel({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.publishedDate,
|
||||
required this.documentType,
|
||||
required this.category,
|
||||
required this.downloadUrl,
|
||||
this.fileSize,
|
||||
});
|
||||
|
||||
/// Create model from JSON
|
||||
factory PriceDocumentModel.fromJson(Map<String, dynamic> json) {
|
||||
return PriceDocumentModel(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
publishedDate: json['published_date'] as String,
|
||||
documentType: json['document_type'] as String,
|
||||
category: json['category'] as String,
|
||||
downloadUrl: json['download_url'] as String,
|
||||
fileSize: json['file_size'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert model to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'published_date': publishedDate,
|
||||
'document_type': documentType,
|
||||
'category': category,
|
||||
'download_url': downloadUrl,
|
||||
'file_size': fileSize,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert model to domain entity
|
||||
PriceDocument toEntity() {
|
||||
return PriceDocument(
|
||||
id: id,
|
||||
title: title,
|
||||
description: description,
|
||||
publishedDate: DateTime.parse(publishedDate),
|
||||
documentType: _parseDocumentType(documentType),
|
||||
category: _parseCategory(category),
|
||||
downloadUrl: downloadUrl,
|
||||
fileSize: fileSize,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create model from domain entity
|
||||
factory PriceDocumentModel.fromEntity(PriceDocument entity) {
|
||||
return PriceDocumentModel(
|
||||
id: entity.id,
|
||||
title: entity.title,
|
||||
description: entity.description,
|
||||
publishedDate: entity.publishedDate.toIso8601String(),
|
||||
documentType: _documentTypeToString(entity.documentType),
|
||||
category: _categoryToString(entity.category),
|
||||
downloadUrl: entity.downloadUrl,
|
||||
fileSize: entity.fileSize,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse document type from string
|
||||
static DocumentType _parseDocumentType(String type) {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'pdf':
|
||||
return DocumentType.pdf;
|
||||
case 'excel':
|
||||
return DocumentType.excel;
|
||||
default:
|
||||
return DocumentType.pdf;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse category from string
|
||||
static DocumentCategory _parseCategory(String category) {
|
||||
switch (category.toLowerCase()) {
|
||||
case 'policy':
|
||||
return DocumentCategory.policy;
|
||||
case 'pricelist':
|
||||
case 'price_list':
|
||||
return DocumentCategory.priceList;
|
||||
default:
|
||||
return DocumentCategory.policy;
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert document type to string
|
||||
static String _documentTypeToString(DocumentType type) {
|
||||
switch (type) {
|
||||
case DocumentType.pdf:
|
||||
return 'pdf';
|
||||
case DocumentType.excel:
|
||||
return 'excel';
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert category to string
|
||||
static String _categoryToString(DocumentCategory category) {
|
||||
switch (category) {
|
||||
case DocumentCategory.policy:
|
||||
return 'policy';
|
||||
case DocumentCategory.priceList:
|
||||
return 'priceList';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PriceDocumentModel(id: $id, title: $title, category: $category, '
|
||||
'documentType: $documentType, publishedDate: $publishedDate)';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/// Repository Implementation: Price Policy Repository
|
||||
///
|
||||
/// Concrete implementation of the PricePolicyRepository interface.
|
||||
/// Coordinates between local and remote data sources to provide price policy data.
|
||||
///
|
||||
/// Currently uses mock data from local datasource.
|
||||
/// Will implement offline-first strategy when backend API is available.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/price_policy/data/datasources/price_policy_local_datasource.dart';
|
||||
import 'package:worker/features/price_policy/domain/entities/price_document.dart';
|
||||
import 'package:worker/features/price_policy/domain/repositories/price_policy_repository.dart';
|
||||
|
||||
/// Price Policy Repository Implementation
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - Coordinate between local cache and remote API (when available)
|
||||
/// - Convert data models to domain entities
|
||||
/// - Handle errors gracefully
|
||||
/// - Manage cache invalidation
|
||||
class PricePolicyRepositoryImpl implements PricePolicyRepository {
|
||||
/// Local data source
|
||||
final PricePolicyLocalDataSource localDataSource;
|
||||
|
||||
/// Remote data source (API) - TODO: Add when API is ready
|
||||
// final PricePolicyRemoteDataSource remoteDataSource;
|
||||
|
||||
/// Constructor
|
||||
PricePolicyRepositoryImpl({
|
||||
required this.localDataSource,
|
||||
// required this.remoteDataSource, // TODO: Add when API ready
|
||||
});
|
||||
|
||||
@override
|
||||
Future<List<PriceDocument>> getAllDocuments() async {
|
||||
try {
|
||||
// TODO: Implement offline-first strategy
|
||||
// 1. Check if cache is valid
|
||||
// 2. Return cached data if valid
|
||||
// 3. If cache invalid, fetch from remote
|
||||
|
||||
// For now, get from local datasource (mock data)
|
||||
final models = await localDataSource.getAllDocuments();
|
||||
|
||||
// Convert models to entities
|
||||
final entities = models.map((model) => model.toEntity()).toList();
|
||||
|
||||
// Sort by published date (newest first)
|
||||
entities.sort((a, b) => b.publishedDate.compareTo(a.publishedDate));
|
||||
|
||||
return entities;
|
||||
} catch (e) {
|
||||
// Log error and return empty list
|
||||
// In production, this should throw proper domain failures
|
||||
print('[PricePolicyRepository] Error getting documents: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<PriceDocument>> getDocumentsByCategory(
|
||||
DocumentCategory category,
|
||||
) async {
|
||||
try {
|
||||
// Convert category to string for datasource
|
||||
final categoryString = _categoryToString(category);
|
||||
|
||||
// Get documents from local datasource
|
||||
final models = await localDataSource.getDocumentsByCategory(
|
||||
categoryString,
|
||||
);
|
||||
|
||||
// Convert models to entities
|
||||
final entities = models.map((model) => model.toEntity()).toList();
|
||||
|
||||
// Sort by published date (newest first)
|
||||
entities.sort((a, b) => b.publishedDate.compareTo(a.publishedDate));
|
||||
|
||||
return entities;
|
||||
} catch (e) {
|
||||
print('[PricePolicyRepository] Error getting documents by category: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PriceDocument?> getDocumentById(String documentId) async {
|
||||
try {
|
||||
// Get document from local datasource
|
||||
final model = await localDataSource.getDocumentById(documentId);
|
||||
|
||||
// Convert model to entity
|
||||
return model?.toEntity();
|
||||
} catch (e) {
|
||||
print('[PricePolicyRepository] Error getting document by id: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<PriceDocument>> refreshDocuments() async {
|
||||
try {
|
||||
// TODO: Implement remote fetch when API is available
|
||||
// 1. Fetch from remote API
|
||||
// 2. Cache the results locally
|
||||
// 3. Return fresh data
|
||||
|
||||
// For now, just clear and refetch from local
|
||||
await localDataSource.clearCache();
|
||||
final models = await localDataSource.getAllDocuments();
|
||||
|
||||
// Convert models to entities
|
||||
final entities = models.map((model) => model.toEntity()).toList();
|
||||
|
||||
// Sort by published date (newest first)
|
||||
entities.sort((a, b) => b.publishedDate.compareTo(a.publishedDate));
|
||||
|
||||
return entities;
|
||||
} catch (e) {
|
||||
print('[PricePolicyRepository] Error refreshing documents: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper method to convert category enum to string
|
||||
String _categoryToString(DocumentCategory category) {
|
||||
switch (category) {
|
||||
case DocumentCategory.policy:
|
||||
return 'policy';
|
||||
case DocumentCategory.priceList:
|
||||
return 'priceList';
|
||||
}
|
||||
}
|
||||
}
|
||||
166
lib/features/price_policy/domain/entities/price_document.dart
Normal file
166
lib/features/price_policy/domain/entities/price_document.dart
Normal file
@@ -0,0 +1,166 @@
|
||||
/// Domain Entity: Price Document
|
||||
///
|
||||
/// Pure business entity representing a price policy or price list document.
|
||||
/// This entity is framework-independent and contains only business logic.
|
||||
library;
|
||||
|
||||
/// Price policy document entity
|
||||
class PriceDocument {
|
||||
/// Unique document ID
|
||||
final String id;
|
||||
|
||||
/// Document title
|
||||
final String title;
|
||||
|
||||
/// Document description
|
||||
final String description;
|
||||
|
||||
/// Date the document was published
|
||||
final DateTime publishedDate;
|
||||
|
||||
/// Type of document (PDF or Excel)
|
||||
final DocumentType documentType;
|
||||
|
||||
/// Category (policy or price list)
|
||||
final DocumentCategory category;
|
||||
|
||||
/// URL to download the document
|
||||
final String downloadUrl;
|
||||
|
||||
/// Optional file size display string
|
||||
final String? fileSize;
|
||||
|
||||
/// Constructor
|
||||
const PriceDocument({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.publishedDate,
|
||||
required this.documentType,
|
||||
required this.category,
|
||||
required this.downloadUrl,
|
||||
this.fileSize,
|
||||
});
|
||||
|
||||
/// Check if document is a PDF
|
||||
bool get isPdf => documentType == DocumentType.pdf;
|
||||
|
||||
/// Check if document is an Excel file
|
||||
bool get isExcel => documentType == DocumentType.excel;
|
||||
|
||||
/// Check if document is a policy document
|
||||
bool get isPolicy => category == DocumentCategory.policy;
|
||||
|
||||
/// Check if document is a price list
|
||||
bool get isPriceList => category == DocumentCategory.priceList;
|
||||
|
||||
/// Get formatted published date (dd/MM/yyyy)
|
||||
String get formattedDate {
|
||||
return '${publishedDate.day.toString().padLeft(2, '0')}/'
|
||||
'${publishedDate.month.toString().padLeft(2, '0')}/'
|
||||
'${publishedDate.year}';
|
||||
}
|
||||
|
||||
/// Get formatted date with prefix based on category
|
||||
String get formattedDateWithPrefix {
|
||||
final prefix = isPolicy ? 'Công bố' : 'Cập nhật';
|
||||
return '$prefix: $formattedDate';
|
||||
}
|
||||
|
||||
/// Get icon name based on document type
|
||||
String get iconName => documentType == DocumentType.pdf ? 'PDF' : 'Excel';
|
||||
|
||||
/// Copy with method for immutability
|
||||
PriceDocument copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? description,
|
||||
DateTime? publishedDate,
|
||||
DocumentType? documentType,
|
||||
DocumentCategory? category,
|
||||
String? downloadUrl,
|
||||
String? fileSize,
|
||||
}) {
|
||||
return PriceDocument(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
publishedDate: publishedDate ?? this.publishedDate,
|
||||
documentType: documentType ?? this.documentType,
|
||||
category: category ?? this.category,
|
||||
downloadUrl: downloadUrl ?? this.downloadUrl,
|
||||
fileSize: fileSize ?? this.fileSize,
|
||||
);
|
||||
}
|
||||
|
||||
/// Equality operator
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is PriceDocument &&
|
||||
other.id == id &&
|
||||
other.title == title &&
|
||||
other.description == description &&
|
||||
other.publishedDate == publishedDate &&
|
||||
other.documentType == documentType &&
|
||||
other.category == category &&
|
||||
other.downloadUrl == downloadUrl &&
|
||||
other.fileSize == fileSize;
|
||||
}
|
||||
|
||||
/// Hash code
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
publishedDate,
|
||||
documentType,
|
||||
category,
|
||||
downloadUrl,
|
||||
fileSize,
|
||||
);
|
||||
}
|
||||
|
||||
/// String representation
|
||||
@override
|
||||
String toString() {
|
||||
return 'PriceDocument(id: $id, title: $title, description: $description, '
|
||||
'publishedDate: $publishedDate, documentType: $documentType, '
|
||||
'category: $category, downloadUrl: $downloadUrl, fileSize: $fileSize)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Document type enum
|
||||
enum DocumentType { pdf, excel }
|
||||
|
||||
/// Document category enum
|
||||
enum DocumentCategory {
|
||||
policy, // Chính sách giá
|
||||
priceList, // Bảng giá
|
||||
}
|
||||
|
||||
// Extension for display
|
||||
extension DocumentTypeX on DocumentType {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case DocumentType.pdf:
|
||||
return 'PDF';
|
||||
case DocumentType.excel:
|
||||
return 'Excel';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DocumentCategoryX on DocumentCategory {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case DocumentCategory.policy:
|
||||
return 'Chính sách giá';
|
||||
case DocumentCategory.priceList:
|
||||
return 'Bảng giá';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/// Domain Repository Interface: Price Policy Repository
|
||||
///
|
||||
/// Defines the contract for price policy document data operations.
|
||||
/// This is an abstract interface following the Repository Pattern.
|
||||
///
|
||||
/// The actual implementation will be in the data layer.
|
||||
/// This allows for dependency inversion and easier testing.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/price_policy/domain/entities/price_document.dart';
|
||||
|
||||
/// Price Policy Repository Interface
|
||||
///
|
||||
/// Provides methods to:
|
||||
/// - Get all price policy documents
|
||||
/// - Filter documents by category
|
||||
/// - Fetch individual document details
|
||||
///
|
||||
/// Implementation will be in data/repositories/price_policy_repository_impl.dart
|
||||
abstract class PricePolicyRepository {
|
||||
/// Get all price policy documents
|
||||
///
|
||||
/// Returns list of [PriceDocument] objects.
|
||||
/// Returns empty list if no documents available.
|
||||
///
|
||||
/// This should fetch from local cache first, then sync with server.
|
||||
/// Documents should be ordered by published date (newest first).
|
||||
Future<List<PriceDocument>> getAllDocuments();
|
||||
|
||||
/// Get documents filtered by category
|
||||
///
|
||||
/// Returns list of [PriceDocument] objects matching the [category].
|
||||
/// Returns empty list if no matching documents.
|
||||
///
|
||||
/// [category] - The category to filter by (policy or priceList)
|
||||
Future<List<PriceDocument>> getDocumentsByCategory(DocumentCategory category);
|
||||
|
||||
/// Get a specific document by ID
|
||||
///
|
||||
/// Returns [PriceDocument] if found, null otherwise.
|
||||
///
|
||||
/// [documentId] - The unique identifier of the document
|
||||
Future<PriceDocument?> getDocumentById(String documentId);
|
||||
|
||||
/// Refresh documents from server
|
||||
///
|
||||
/// Force refresh documents from remote source.
|
||||
/// Updates local cache after successful fetch.
|
||||
Future<List<PriceDocument>> refreshDocuments();
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/constants/ui_constants.dart';
|
||||
import '../../../../core/theme/colors.dart';
|
||||
import '../../domain/entities/price_document.dart';
|
||||
import '../providers/price_documents_provider.dart';
|
||||
import '../widgets/document_card.dart';
|
||||
|
||||
/// Price policy page with tabs for policies and price lists
|
||||
class PricePolicyPage extends ConsumerStatefulWidget {
|
||||
const PricePolicyPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<PricePolicyPage> createState() => _PricePolicyPageState();
|
||||
}
|
||||
|
||||
class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.grey50,
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
title: const Text(
|
||||
'Chính sách giá',
|
||||
style: TextStyle(color: Colors.black),
|
||||
),
|
||||
elevation: AppBarSpecs.elevation,
|
||||
backgroundColor: AppColors.white,
|
||||
foregroundColor: AppColors.grey900,
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.info_outline, color: Colors.black),
|
||||
onPressed: _showInfoDialog,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: AppColors.white,
|
||||
unselectedLabelColor: AppColors.grey900,
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicator: BoxDecoration(
|
||||
color: AppColors.primaryBlue,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
labelStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
tabs: const [
|
||||
Tab(text: 'Chính sách giá'),
|
||||
Tab(text: 'Bảng giá'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
// Policy tab
|
||||
_buildDocumentList(DocumentCategory.policy),
|
||||
// Price list tab
|
||||
_buildDocumentList(DocumentCategory.priceList),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDocumentList(DocumentCategory category) {
|
||||
final documentsAsync = ref.watch(filteredPriceDocumentsProvider(category));
|
||||
|
||||
return documentsAsync.when(
|
||||
data: (documents) {
|
||||
if (documents.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.description_outlined,
|
||||
size: 64,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
'Chưa có tài liệu',
|
||||
style: TextStyle(fontSize: 16, color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
// Refresh documents from repository
|
||||
ref.invalidate(filteredPriceDocumentsProvider(category));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||
},
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
itemCount: documents.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
itemBuilder: (context, index) {
|
||||
final document = documents[index];
|
||||
return DocumentCard(
|
||||
document: document,
|
||||
onDownload: () => _handleDownload(document),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 64, color: AppColors.danger),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
'Không thể tải tài liệu',
|
||||
style: TextStyle(fontSize: 16, color: AppColors.grey500),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
ref.invalidate(filteredPriceDocumentsProvider(category));
|
||||
},
|
||||
child: const Text('Thử lại'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleDownload(PriceDocument document) {
|
||||
// In real app, this would trigger actual download
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Đang tải: ${document.title}'),
|
||||
duration: const Duration(seconds: 2),
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
|
||||
// Simulate download
|
||||
// TODO: Implement actual file download functionality
|
||||
// - Use url_launcher or dio to download file
|
||||
// - Show progress indicator
|
||||
// - Save to device storage
|
||||
}
|
||||
|
||||
void _showInfoDialog() {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Text(
|
||||
'Hướng dẫn sử dụng',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Đây là nội dung hướng dẫn sử dụng cho tính năng Chính sách giá:',
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildInfoItem(
|
||||
'Chọn tab "Chính sách giá" để xem các chính sách giá hiện hành',
|
||||
),
|
||||
_buildInfoItem(
|
||||
'Chọn tab "Bảng giá" để tải về bảng giá chi tiết sản phẩm',
|
||||
),
|
||||
_buildInfoItem('Nhấn nút "Tải về" để download file PDF/Excel'),
|
||||
_buildInfoItem('Các bảng giá được cập nhật định kỳ hàng tháng'),
|
||||
_buildInfoItem('Liên hệ sales để được tư vấn giá tốt nhất'),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Đóng'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoItem(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('• ', style: TextStyle(fontSize: 16)),
|
||||
Expanded(child: Text(text)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/features/price_policy/data/datasources/price_policy_local_datasource.dart';
|
||||
import 'package:worker/features/price_policy/data/repositories/price_policy_repository_impl.dart';
|
||||
import 'package:worker/features/price_policy/domain/entities/price_document.dart';
|
||||
import 'package:worker/features/price_policy/domain/repositories/price_policy_repository.dart';
|
||||
|
||||
part 'price_documents_provider.g.dart';
|
||||
|
||||
/// Provider for local data source
|
||||
@riverpod
|
||||
PricePolicyLocalDataSource pricePolicyLocalDataSource(Ref ref) {
|
||||
return PricePolicyLocalDataSource();
|
||||
}
|
||||
|
||||
/// Provider for price policy repository
|
||||
@riverpod
|
||||
PricePolicyRepository pricePolicyRepository(Ref ref) {
|
||||
final localDataSource = ref.watch(pricePolicyLocalDataSourceProvider);
|
||||
|
||||
return PricePolicyRepositoryImpl(localDataSource: localDataSource);
|
||||
}
|
||||
|
||||
/// Provider for all price policy documents
|
||||
@riverpod
|
||||
Future<List<PriceDocument>> priceDocuments(Ref ref) async {
|
||||
final repository = ref.watch(pricePolicyRepositoryProvider);
|
||||
return repository.getAllDocuments();
|
||||
}
|
||||
|
||||
/// Provider for filtered documents by category
|
||||
@riverpod
|
||||
Future<List<PriceDocument>> filteredPriceDocuments(
|
||||
Ref ref,
|
||||
DocumentCategory category,
|
||||
) async {
|
||||
final repository = ref.watch(pricePolicyRepositoryProvider);
|
||||
return repository.getDocumentsByCategory(category);
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'price_documents_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider for local data source
|
||||
|
||||
@ProviderFor(pricePolicyLocalDataSource)
|
||||
const pricePolicyLocalDataSourceProvider =
|
||||
PricePolicyLocalDataSourceProvider._();
|
||||
|
||||
/// Provider for local data source
|
||||
|
||||
final class PricePolicyLocalDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
PricePolicyLocalDataSource,
|
||||
PricePolicyLocalDataSource,
|
||||
PricePolicyLocalDataSource
|
||||
>
|
||||
with $Provider<PricePolicyLocalDataSource> {
|
||||
/// Provider for local data source
|
||||
const PricePolicyLocalDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'pricePolicyLocalDataSourceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$pricePolicyLocalDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<PricePolicyLocalDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
PricePolicyLocalDataSource create(Ref ref) {
|
||||
return pricePolicyLocalDataSource(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(PricePolicyLocalDataSource value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<PricePolicyLocalDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$pricePolicyLocalDataSourceHash() =>
|
||||
r'dd1bee761fa7f050835508cf33bf34a788829483';
|
||||
|
||||
/// Provider for price policy repository
|
||||
|
||||
@ProviderFor(pricePolicyRepository)
|
||||
const pricePolicyRepositoryProvider = PricePolicyRepositoryProvider._();
|
||||
|
||||
/// Provider for price policy repository
|
||||
|
||||
final class PricePolicyRepositoryProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
PricePolicyRepository,
|
||||
PricePolicyRepository,
|
||||
PricePolicyRepository
|
||||
>
|
||||
with $Provider<PricePolicyRepository> {
|
||||
/// Provider for price policy repository
|
||||
const PricePolicyRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'pricePolicyRepositoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$pricePolicyRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<PricePolicyRepository> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
PricePolicyRepository create(Ref ref) {
|
||||
return pricePolicyRepository(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(PricePolicyRepository value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<PricePolicyRepository>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$pricePolicyRepositoryHash() =>
|
||||
r'296555a45936d8e43a28bf5add5e7db40495009c';
|
||||
|
||||
/// Provider for all price policy documents
|
||||
|
||||
@ProviderFor(priceDocuments)
|
||||
const priceDocumentsProvider = PriceDocumentsProvider._();
|
||||
|
||||
/// Provider for all price policy documents
|
||||
|
||||
final class PriceDocumentsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<PriceDocument>>,
|
||||
List<PriceDocument>,
|
||||
FutureOr<List<PriceDocument>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<PriceDocument>>,
|
||||
$FutureProvider<List<PriceDocument>> {
|
||||
/// Provider for all price policy documents
|
||||
const PriceDocumentsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'priceDocumentsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$priceDocumentsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<PriceDocument>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<PriceDocument>> create(Ref ref) {
|
||||
return priceDocuments(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$priceDocumentsHash() => r'cf2ccf6bd9aaae0c56ab01529fd034a090d99263';
|
||||
|
||||
/// Provider for filtered documents by category
|
||||
|
||||
@ProviderFor(filteredPriceDocuments)
|
||||
const filteredPriceDocumentsProvider = FilteredPriceDocumentsFamily._();
|
||||
|
||||
/// Provider for filtered documents by category
|
||||
|
||||
final class FilteredPriceDocumentsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<PriceDocument>>,
|
||||
List<PriceDocument>,
|
||||
FutureOr<List<PriceDocument>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<PriceDocument>>,
|
||||
$FutureProvider<List<PriceDocument>> {
|
||||
/// Provider for filtered documents by category
|
||||
const FilteredPriceDocumentsProvider._({
|
||||
required FilteredPriceDocumentsFamily super.from,
|
||||
required DocumentCategory super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'filteredPriceDocumentsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$filteredPriceDocumentsHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'filteredPriceDocumentsProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<PriceDocument>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<PriceDocument>> create(Ref ref) {
|
||||
final argument = this.argument as DocumentCategory;
|
||||
return filteredPriceDocuments(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is FilteredPriceDocumentsProvider &&
|
||||
other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$filteredPriceDocumentsHash() =>
|
||||
r'8f5b2ed822694b4dd9523e1a61e202a7ba0c1fbc';
|
||||
|
||||
/// Provider for filtered documents by category
|
||||
|
||||
final class FilteredPriceDocumentsFamily extends $Family
|
||||
with
|
||||
$FunctionalFamilyOverride<
|
||||
FutureOr<List<PriceDocument>>,
|
||||
DocumentCategory
|
||||
> {
|
||||
const FilteredPriceDocumentsFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'filteredPriceDocumentsProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Provider for filtered documents by category
|
||||
|
||||
FilteredPriceDocumentsProvider call(DocumentCategory category) =>
|
||||
FilteredPriceDocumentsProvider._(argument: category, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'filteredPriceDocumentsProvider';
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/constants/ui_constants.dart';
|
||||
import '../../../../core/theme/colors.dart';
|
||||
import '../../domain/entities/price_document.dart';
|
||||
|
||||
/// Document card widget displaying price policy or price list document
|
||||
class DocumentCard extends StatelessWidget {
|
||||
final PriceDocument document;
|
||||
final VoidCallback onDownload;
|
||||
|
||||
const DocumentCard({
|
||||
super.key,
|
||||
required this.document,
|
||||
required this.onDownload,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.grey100),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: onDownload,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// Responsive layout: column on mobile, row on larger screens
|
||||
final isNarrow = constraints.maxWidth < 600;
|
||||
|
||||
if (isNarrow) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_buildIcon(),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(child: _buildInfo()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: _buildDownloadButton(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
_buildIcon(),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(child: _buildInfo()),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
_buildDownloadButton(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIcon() {
|
||||
final iconData = document.isPdf ? Icons.picture_as_pdf : Icons.table_chart;
|
||||
final iconColor = document.isPdf
|
||||
? Colors.red.shade600
|
||||
: Colors.green.shade600;
|
||||
|
||||
return Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grey50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(iconData, size: 28, color: iconColor),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfo() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
document.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.calendar_today,
|
||||
size: 13,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
document.formattedDateWithPrefix,
|
||||
style: const TextStyle(fontSize: 13, color: AppColors.grey500),
|
||||
),
|
||||
if (document.fileSize != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'•',
|
||||
style: TextStyle(fontSize: 13, color: AppColors.grey500),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
document.fileSize!,
|
||||
style: const TextStyle(fontSize: 13, color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
document.description,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
height: 1.4,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDownloadButton() {
|
||||
return ElevatedButton.icon(
|
||||
onPressed: onDownload,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
icon: const Icon(Icons.download, size: 18),
|
||||
label: const Text(
|
||||
'Tải về',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
12
lib/features/price_policy/price_policy.dart
Normal file
12
lib/features/price_policy/price_policy.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
/// Price Policy Feature Barrel Export
|
||||
///
|
||||
/// Provides easy access to all price policy feature components.
|
||||
library;
|
||||
|
||||
// Domain
|
||||
export 'domain/entities/price_document.dart';
|
||||
|
||||
// Presentation
|
||||
export 'presentation/pages/price_policy_page.dart';
|
||||
export 'presentation/widgets/document_card.dart';
|
||||
export 'presentation/providers/price_documents_provider.dart';
|
||||
Reference in New Issue
Block a user