price policy

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

View File

@@ -1,7 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:open_file/open_file.dart';
import 'package:path_provider/path_provider.dart';
import '../../../../core/constants/ui_constants.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/theme/colors.dart';
import '../../domain/entities/price_document.dart';
import '../providers/price_documents_provider.dart';
@@ -55,36 +60,55 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
),
const SizedBox(width: AppSpacing.sm),
],
bottom: TabBar(
controller: _tabController,
labelColor: AppColors.white,
unselectedLabelColor: AppColors.grey900,
indicatorSize: TabBarIndicatorSize.tab,
indicator: BoxDecoration(
color: AppColors.primaryBlue,
borderRadius: BorderRadius.circular(8),
),
labelStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
tabs: const [
Tab(text: 'Chính sách giá'),
Tab(text: 'Bảng giá'),
],
),
),
body: TabBarView(
controller: _tabController,
body: Column(
children: [
// Policy tab
_buildDocumentList(DocumentCategory.policy),
// Price list tab
_buildDocumentList(DocumentCategory.priceList),
// TabBar with padding
Padding(
padding: const EdgeInsets.all(16),
child: Container(
height: 40,
decoration: BoxDecoration(
color: AppColors.grey100,
borderRadius: BorderRadius.circular(8),
),
child: TabBar(
controller: _tabController,
labelColor: AppColors.white,
unselectedLabelColor: AppColors.grey900,
indicatorSize: TabBarIndicatorSize.tab,
indicator: BoxDecoration(
color: AppColors.primaryBlue,
borderRadius: BorderRadius.circular(8),
),
dividerColor: Colors.transparent,
labelStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
tabs: const [
Tab(text: 'Chính sách giá'),
Tab(text: 'Bảng giá'),
],
),
),
),
// TabBarView
Expanded(
child: TabBarView(
controller: _tabController,
children: [
// Policy tab
_buildDocumentList(DocumentCategory.policy),
// Price list tab
_buildDocumentList(DocumentCategory.priceList),
],
),
),
],
),
);
@@ -96,7 +120,7 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
return documentsAsync.when(
data: (documents) {
if (documents.isEmpty) {
return Center(
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -105,7 +129,7 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
size: 64,
color: AppColors.grey500,
),
const SizedBox(height: AppSpacing.md),
SizedBox(height: AppSpacing.md),
Text(
'Chưa có tài liệu',
style: TextStyle(fontSize: 16, color: AppColors.grey500),
@@ -118,8 +142,9 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
return RefreshIndicator(
onRefresh: () async {
// Refresh documents from repository
ref.invalidate(filteredPriceDocumentsProvider(category));
await Future<void>.delayed(const Duration(milliseconds: 500));
await ref
.read(filteredPriceDocumentsProvider(category).notifier)
.refresh();
},
child: ListView.separated(
padding: const EdgeInsets.all(AppSpacing.md),
@@ -130,7 +155,7 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
final document = documents[index];
return DocumentCard(
document: document,
onDownload: () => _handleDownload(document),
onDownload: () => _handleDownload(document, category),
);
},
),
@@ -150,7 +175,9 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
const SizedBox(height: AppSpacing.sm),
ElevatedButton(
onPressed: () {
ref.invalidate(filteredPriceDocumentsProvider(category));
ref
.read(filteredPriceDocumentsProvider(category).notifier)
.refresh();
},
child: const Text('Thử lại'),
),
@@ -160,22 +187,95 @@ class _PricePolicyPageState extends ConsumerState<PricePolicyPage>
);
}
void _handleDownload(PriceDocument document) {
// In real app, this would trigger actual download
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Đang tải: ${document.title}'),
duration: const Duration(seconds: 2),
backgroundColor: AppColors.primaryBlue,
behavior: SnackBarBehavior.floating,
),
);
Future<void> _handleDownload(
PriceDocument document,
DocumentCategory category,
) async {
try {
// Check if file already downloaded and exists
if (document.filePath != null) {
final file = File(document.filePath!);
if (await file.exists()) {
// File exists, just open it
final result = await OpenFile.open(document.filePath!);
if (result.type != ResultType.done && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Không thể mở file: ${result.message}'),
backgroundColor: AppColors.danger,
behavior: SnackBarBehavior.floating,
),
);
}
return;
}
}
// Simulate download
// TODO: Implement actual file download functionality
// - Use url_launcher or dio to download file
// - Show progress indicator
// - Save to device storage
// File not downloaded yet, show loading snackbar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Đang tải: ${document.title}'),
duration: const Duration(seconds: 2),
backgroundColor: AppColors.primaryBlue,
behavior: SnackBarBehavior.floating,
),
);
// Get DioClient
final dioClient = await ref.read(dioClientProvider.future);
// Get download directory
final directory = await getApplicationDocumentsDirectory();
// Extract filename from URL or use title
final uri = Uri.parse(document.fileUrl);
final filename = uri.pathSegments.isNotEmpty
? uri.pathSegments.last
: '${document.title}.${document.documentType == DocumentType.pdf ? "pdf" : "xlsx"}';
final savePath = '${directory.path}/$filename';
// Download file with authentication headers (automatically added by AuthInterceptor)
await dioClient.downloadFile(
document.fileUrl,
savePath,
onReceiveProgress: (received, total) {
// Progress tracking available here if needed: (received / total * 100)
},
);
// Update document with file path in provider
ref
.read(filteredPriceDocumentsProvider(category).notifier)
.updateDocumentFilePath(document.title, savePath);
// Show success message
if (mounted) {
// Clear any existing snackbars
ScaffoldMessenger.of(context).clearSnackBars();
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Tải thành công: ${document.title}'),
backgroundColor: AppColors.success,
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 2),
),
);
}
} catch (e) {
// Show error message
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Lỗi tải file: ${e.toString()}'),
backgroundColor: AppColors.danger,
behavior: SnackBarBehavior.floating,
),
);
}
}
}
void _showInfoDialog() {

View File

@@ -1,38 +1,54 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/price_policy/data/datasources/price_policy_local_datasource.dart';
import 'package:worker/core/network/dio_client.dart';
import 'package:worker/features/price_policy/data/datasources/price_policy_remote_datasource.dart';
import 'package:worker/features/price_policy/data/repositories/price_policy_repository_impl.dart';
import 'package:worker/features/price_policy/domain/entities/price_document.dart';
import 'package:worker/features/price_policy/domain/repositories/price_policy_repository.dart';
part 'price_documents_provider.g.dart';
/// Provider for local data source
@riverpod
PricePolicyLocalDataSource pricePolicyLocalDataSource(Ref ref) {
return PricePolicyLocalDataSource();
}
/// Provider for price policy repository
@riverpod
PricePolicyRepository pricePolicyRepository(Ref ref) {
final localDataSource = ref.watch(pricePolicyLocalDataSourceProvider);
return PricePolicyRepositoryImpl(localDataSource: localDataSource);
Future<PricePolicyRepository> pricePolicyRepository(Ref ref) async {
final dioClient = await ref.watch(dioClientProvider.future);
final remoteDataSource = PricePolicyRemoteDataSourceImpl(dioClient);
return PricePolicyRepositoryImpl(remoteDataSource: remoteDataSource);
}
/// Provider for all price policy documents
@riverpod
Future<List<PriceDocument>> priceDocuments(Ref ref) async {
final repository = ref.watch(pricePolicyRepositoryProvider);
final repository = await ref.watch(pricePolicyRepositoryProvider.future);
return repository.getAllDocuments();
}
/// Provider for filtered documents by category
/// Provider for filtered documents by category with file path management
@riverpod
Future<List<PriceDocument>> filteredPriceDocuments(
Ref ref,
DocumentCategory category,
) async {
final repository = ref.watch(pricePolicyRepositoryProvider);
return repository.getDocumentsByCategory(category);
class FilteredPriceDocuments extends _$FilteredPriceDocuments {
@override
Future<List<PriceDocument>> build(DocumentCategory category) async {
final repository = await ref.watch(pricePolicyRepositoryProvider.future);
return repository.getDocumentsByCategory(category);
}
/// Update a document's file path after download
void updateDocumentFilePath(String documentTitle, String filePath) {
state = state.whenData((documents) {
return documents.map((doc) {
if (doc.title == documentTitle) {
return doc.copyWith(filePath: filePath);
}
return doc;
}).toList();
});
}
/// Refresh documents
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final repository = await ref.read(pricePolicyRepositoryProvider.future);
return repository.getDocumentsByCategory(category);
});
}
}

View File

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

View File

@@ -120,23 +120,11 @@ class DocumentCard extends StatelessWidget {
document.formattedDateWithPrefix,
style: const TextStyle(fontSize: 13, color: AppColors.grey500),
),
if (document.fileSize != null) ...[
const SizedBox(width: 8),
const Text(
'',
style: TextStyle(fontSize: 13, color: AppColors.grey500),
),
const SizedBox(width: 8),
Text(
document.fileSize!,
style: const TextStyle(fontSize: 13, color: AppColors.grey500),
),
],
],
),
const SizedBox(height: 6),
Text(
document.description,
document.title,
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
@@ -150,19 +138,24 @@ class DocumentCard extends StatelessWidget {
}
Widget _buildDownloadButton() {
final isDownloaded = document.filePath != null;
final buttonColor = isDownloaded ? AppColors.success : AppColors.primaryBlue;
final buttonIcon = isDownloaded ? Icons.folder_open : Icons.download;
final buttonText = isDownloaded ? 'Mở file' : 'Tải về';
return ElevatedButton.icon(
onPressed: onDownload,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
backgroundColor: buttonColor,
foregroundColor: AppColors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
icon: const Icon(Icons.download, size: 18),
label: const Text(
'Tải về',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
icon: Icon(buttonIcon, size: 18),
label: Text(
buttonText,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
);
}