add price policy
This commit is contained in:
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user