|
|
@@ -1,4 +1,4 @@
|
|
|
-import 'package:flutter/material.dart';
|
|
|
+import 'package:flutter/material.dart';
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
import 'package:go_router/go_router.dart';
|
|
|
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
|
|
@@ -14,6 +14,7 @@ import '../../core/i18n/app_localizations.dart';
|
|
|
import 'expense_apply_list_controller.dart';
|
|
|
import 'expense_apply_model.dart';
|
|
|
import 'expense_apply_approval_api.dart';
|
|
|
+import '../../core/utils/amount_utils.dart';
|
|
|
class ExpenseApplyListPage extends ConsumerStatefulWidget {
|
|
|
const ExpenseApplyListPage({super.key});
|
|
|
@override
|
|
|
@@ -21,7 +22,7 @@ class ExpenseApplyListPage extends ConsumerStatefulWidget {
|
|
|
}
|
|
|
|
|
|
class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
|
|
|
- final _keywordCtrl = TextEditingController();
|
|
|
+ String _keyword = '';
|
|
|
final _startDateCtrl = TextEditingController();
|
|
|
final _endDateCtrl = TextEditingController();
|
|
|
late final EasyRefreshController _refreshCtrl;
|
|
|
@@ -46,7 +47,6 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
|
|
|
|
|
|
@override
|
|
|
void dispose() {
|
|
|
- _keywordCtrl.dispose();
|
|
|
_startDateCtrl.dispose();
|
|
|
_endDateCtrl.dispose();
|
|
|
_refreshCtrl.dispose();
|
|
|
@@ -76,7 +76,7 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
|
|
|
}
|
|
|
|
|
|
Future<void> _doRefresh() async {
|
|
|
- ref.read(expenseApplyKeywordProvider.notifier).state = _keywordCtrl.text.trim();
|
|
|
+ ref.read(expenseApplyKeywordProvider.notifier).state = _keyword;
|
|
|
ref.read(expenseApplyDateStartProvider.notifier).state = _startDateCtrl.text.isNotEmpty ? DateTime.tryParse(_startDateCtrl.text) : null;
|
|
|
ref.read(expenseApplyDateEndProvider.notifier).state = _endDateCtrl.text.isNotEmpty ? DateTime.tryParse(_endDateCtrl.text) : null;
|
|
|
ref.read(expenseApplyRefreshProvider.notifier).state++;
|
|
|
@@ -121,24 +121,26 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
|
|
|
border: Border(bottom: BorderSide(color: tdTheme.componentStrokeColor)),
|
|
|
),
|
|
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
|
|
- Padding(padding: const EdgeInsets.fromLTRB(12, 8, 12, 0), child: Row(children: [
|
|
|
+ Padding(padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: Row(children: [
|
|
|
Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, onTap: () => _pickDate(_startDateCtrl), child: _dateChip(_startDateCtrl, l10n.get('filterStartDate'), tdTheme, colors))),
|
|
|
const SizedBox(width: 8), Text('—', style: TextStyle(fontSize: 14, color: colors.textSecondary)), const SizedBox(width: 8),
|
|
|
Expanded(child: GestureDetector(behavior: HitTestBehavior.opaque, onTap: () => _pickDate(_endDateCtrl), child: _dateChip(_endDateCtrl, l10n.get('filterEndDate'), tdTheme, colors))),
|
|
|
])),
|
|
|
const SizedBox(height: 8),
|
|
|
- Padding(padding: const EdgeInsets.fromLTRB(12, 0, 12, 8), child: Row(children: [
|
|
|
+ Padding(padding: const EdgeInsets.fromLTRB(0, 0, 12, 8), child: Row(children: [
|
|
|
Expanded(
|
|
|
- child: Container(
|
|
|
- height: 40, padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
|
- decoration: BoxDecoration(color: colors.bgSecondaryContainer, borderRadius: BorderRadius.circular(20), border: Border.all(color: tdTheme.componentStrokeColor)),
|
|
|
- child: Row(children: [
|
|
|
- Expanded(child: TextField(controller: _keywordCtrl, style: TextStyle(fontSize: 14, color: colors.textPrimary), decoration: InputDecoration(hintText: l10n.get('searchExpenseApply'), hintStyle: TextStyle(fontSize: 14, color: colors.textSecondary), border: InputBorder.none, isCollapsed: true), onChanged: (_) => setState(() {}))),
|
|
|
- if (_keywordCtrl.text.isNotEmpty) GestureDetector(onTap: () { _keywordCtrl.clear(); setState(() {}); _applyFilter(); }, child: Icon(Icons.close, size: 18, color: colors.textSecondary)),
|
|
|
- ]),
|
|
|
+ child: TDSearchBar(
|
|
|
+ placeHolder: l10n.get('searchExpenseApply'),
|
|
|
+ style: TDSearchStyle.round,
|
|
|
+ onTextChanged: (String text) {
|
|
|
+ setState(() {
|
|
|
+ _keyword = text;
|
|
|
+ });
|
|
|
+ if (text.isEmpty) _applyFilter();
|
|
|
+ },
|
|
|
+ onSubmitted: (_) => _applyFilter(),
|
|
|
),
|
|
|
),
|
|
|
- const SizedBox(width: 8),
|
|
|
GestureDetector(
|
|
|
onTap: () {
|
|
|
final dir = ref.read(expenseApplySortDirProvider);
|
|
|
@@ -147,8 +149,6 @@ class _ExpenseApplyListPageState extends ConsumerState<ExpenseApplyListPage> {
|
|
|
},
|
|
|
child: Container(width: 40, height: 40, decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)), child: Center(child: Icon(ref.watch(expenseApplySortDirProvider) == 'ASC' ? Icons.arrow_upward : Icons.arrow_downward, color: Colors.white, size: 20))),
|
|
|
),
|
|
|
- const SizedBox(width: 8),
|
|
|
- GestureDetector(onTap: _applyFilter, child: Container(width: 40, height: 40, decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(20)), child: const Icon(Icons.search, color: Colors.white, size: 22))),
|
|
|
])),
|
|
|
]),
|
|
|
),
|
|
|
@@ -184,16 +184,13 @@ class _ExpenseApplyListContent extends ConsumerWidget {
|
|
|
Widget _buildContent(AsyncValue<List<ExpenseApplyModel>> itemsAsync, BuildContext context, WidgetRef ref) {
|
|
|
final l10n = AppLocalizations.of(context);
|
|
|
final colors = Theme.of(context).extension<AppColorsExtension>()!;
|
|
|
- final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
- final tagBg = isDark ? colors.bgSecondaryContainer : Colors.grey.shade200;
|
|
|
- final tagFg = isDark ? colors.textSecondary : Colors.grey.shade600;
|
|
|
if (itemsAsync.isReloading) {
|
|
|
final oldItems = itemsAsync.valueOrNull ?? [];
|
|
|
if (oldItems.isEmpty) return ListView(children: [const SizedBox(height: 120), EmptyState(message: l10n.get('noExpenseApplications'))]);
|
|
|
return ListView.builder(padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), itemCount: oldItems.length, itemBuilder: (_, i) {
|
|
|
final m = oldItems[i];
|
|
|
final card = ListCard(
|
|
|
- cardNo: m.expenseApplyNo, amount: '¥${m.estimatedAmount.toStringAsFixed(2)}',
|
|
|
+ cardNo: m.expenseApplyNo, amount: formatAmount(m.estimatedAmount),
|
|
|
applicant: m.deptName.isNotEmpty ? '${m.applicantName} · ${m.deptName}' : m.applicantName,
|
|
|
description: m.remark.isNotEmpty ? '${m.purpose}\n${l10n.get('remark')}: ${m.remark}' : m.purpose,
|
|
|
date: du.DateUtils.formatDate(m.createTime),
|
|
|
@@ -201,10 +198,12 @@ class _ExpenseApplyListContent extends ConsumerWidget {
|
|
|
if (m.isTransferred == 1)
|
|
|
Container(
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
|
- decoration: BoxDecoration(color: tagBg, borderRadius: BorderRadius.circular(4)),
|
|
|
- child: Text(l10n.get('statusConvertedToExpense'), style: TextStyle(fontSize: 11, color: tagFg)),
|
|
|
+ decoration: BoxDecoration(color: colors.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4)),
|
|
|
+ child: Text(l10n.get('statusConvertedToExpense'), style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.primary)),
|
|
|
),
|
|
|
if (m.isTransferred == 1) const SizedBox(width: 6),
|
|
|
+ if (m.isTransferred != 1) _buildAuditTag(m, l10n, colors),
|
|
|
+ if (m.isTransferred != 1) const SizedBox(width: 6),
|
|
|
_buildUrgencyChip(m.urgency, l10n, colors),
|
|
|
]),
|
|
|
onTap: () { context.push('/expense-apply/detail/${m.expenseApplyNo}'); },
|
|
|
@@ -219,7 +218,7 @@ class _ExpenseApplyListContent extends ConsumerWidget {
|
|
|
if (i == items.length) return ListFooter(itemCount: items.length);
|
|
|
final m = items[i];
|
|
|
final card = ListCard(
|
|
|
- cardNo: m.expenseApplyNo, amount: '¥${m.estimatedAmount.toStringAsFixed(2)}',
|
|
|
+ cardNo: m.expenseApplyNo, amount: formatAmount(m.estimatedAmount),
|
|
|
applicant: m.deptName.isNotEmpty ? '${m.applicantName} · ${m.deptName}' : m.applicantName,
|
|
|
description: m.remark.isNotEmpty ? '${m.purpose}\n${l10n.get('remark')}: ${m.remark}' : m.purpose,
|
|
|
date: du.DateUtils.formatDate(m.createTime),
|
|
|
@@ -227,10 +226,12 @@ class _ExpenseApplyListContent extends ConsumerWidget {
|
|
|
if (m.isTransferred == 1)
|
|
|
Container(
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
|
- decoration: BoxDecoration(color: tagBg, borderRadius: BorderRadius.circular(4)),
|
|
|
- child: Text(l10n.get('statusConvertedToExpense'), style: TextStyle(fontSize: 11, color: tagFg)),
|
|
|
+ decoration: BoxDecoration(color: colors.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4)),
|
|
|
+ child: Text(l10n.get('statusConvertedToExpense'), style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.primary)),
|
|
|
),
|
|
|
if (m.isTransferred == 1) const SizedBox(width: 6),
|
|
|
+ if (m.isTransferred != 1) _buildAuditTag(m, l10n, colors),
|
|
|
+ if (m.isTransferred != 1) const SizedBox(width: 6),
|
|
|
_buildUrgencyChip(m.urgency, l10n, colors),
|
|
|
]),
|
|
|
onTap: () { context.push('/expense-apply/detail/${m.expenseApplyNo}'); },
|
|
|
@@ -256,4 +257,31 @@ class _ExpenseApplyListContent extends ConsumerWidget {
|
|
|
child: Text(label, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: color)),
|
|
|
);
|
|
|
}
|
|
|
+
|
|
|
+ Widget _buildAuditTag(ExpenseApplyModel item, AppLocalizations l10n, AppColorsExtension colors) {
|
|
|
+ if (item.isAuditApproved) {
|
|
|
+ return Container(
|
|
|
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
|
+ decoration: BoxDecoration(
|
|
|
+ color: colors.success.withValues(alpha: 0.1),
|
|
|
+ borderRadius: BorderRadius.circular(4),
|
|
|
+ ),
|
|
|
+ child: Text(
|
|
|
+ l10n.get('statusApproved'),
|
|
|
+ style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.success),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return Container(
|
|
|
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
|
+ decoration: BoxDecoration(
|
|
|
+ color: colors.statusGray.withValues(alpha: 0.1),
|
|
|
+ borderRadius: BorderRadius.circular(4),
|
|
|
+ ),
|
|
|
+ child: Text(
|
|
|
+ l10n.get('statusPending'),
|
|
|
+ style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.statusGray),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
}
|