|
@@ -30,8 +30,7 @@ class ExpenseApplyCreatePage extends ConsumerStatefulWidget {
|
|
|
_ExpenseApplyCreatePageState();
|
|
_ExpenseApplyCreatePageState();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-class _ExpenseApplyCreatePageState
|
|
|
|
|
- extends ConsumerState<ExpenseApplyCreatePage>
|
|
|
|
|
|
|
+class _ExpenseApplyCreatePageState extends ConsumerState<ExpenseApplyCreatePage>
|
|
|
with WidgetsBindingObserver {
|
|
with WidgetsBindingObserver {
|
|
|
static const _draftKey = 'expense_apply';
|
|
static const _draftKey = 'expense_apply';
|
|
|
|
|
|
|
@@ -64,12 +63,12 @@ class _ExpenseApplyCreatePageState
|
|
|
List<DepartmentItem> _departments = [];
|
|
List<DepartmentItem> _departments = [];
|
|
|
bool _refDataLoading = true;
|
|
bool _refDataLoading = true;
|
|
|
bool _addingDetail = false;
|
|
bool _addingDetail = false;
|
|
|
|
|
+ dynamic _acctTree;
|
|
|
|
|
|
|
|
// ── 申请部门 ──
|
|
// ── 申请部门 ──
|
|
|
String _selectedDeptId = '';
|
|
String _selectedDeptId = '';
|
|
|
String _selectedDeptName = '';
|
|
String _selectedDeptName = '';
|
|
|
|
|
|
|
|
-
|
|
|
|
|
@override
|
|
@override
|
|
|
void initState() {
|
|
void initState() {
|
|
|
super.initState();
|
|
super.initState();
|
|
@@ -85,7 +84,10 @@ class _ExpenseApplyCreatePageState
|
|
|
_checkAttachHealth();
|
|
_checkAttachHealth();
|
|
|
_purposeFocus.addListener(() => _ensureVisible(_purposeFocus));
|
|
_purposeFocus.addListener(() => _ensureVisible(_purposeFocus));
|
|
|
_remarkFocus.addListener(() => _ensureVisible(_remarkFocus));
|
|
_remarkFocus.addListener(() => _ensureVisible(_remarkFocus));
|
|
|
- _costTypes = []; _projects = []; _departments = [];
|
|
|
|
|
|
|
+ _costTypes = [];
|
|
|
|
|
+ _projects = [];
|
|
|
|
|
+ _departments = [];
|
|
|
|
|
+ _acctTree = null;
|
|
|
_refDataLoading = true;
|
|
_refDataLoading = true;
|
|
|
_refDataFuture = null;
|
|
_refDataFuture = null;
|
|
|
_draftFuture = DraftStorage.has(_draftKey);
|
|
_draftFuture = DraftStorage.has(_draftKey);
|
|
@@ -99,7 +101,10 @@ class _ExpenseApplyCreatePageState
|
|
|
final completer = Completer<void>();
|
|
final completer = Completer<void>();
|
|
|
_refDataFuture = completer.future;
|
|
_refDataFuture = completer.future;
|
|
|
if (showLoading) {
|
|
if (showLoading) {
|
|
|
- LoadingDialog.show(context, text: AppLocalizations.of(context).get('dataLoading'));
|
|
|
|
|
|
|
+ LoadingDialog.show(
|
|
|
|
|
+ context,
|
|
|
|
|
+ text: AppLocalizations.of(context).get('dataLoading'),
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
try {
|
|
try {
|
|
|
final api = ref.read(expenseApplyApiProvider);
|
|
final api = ref.read(expenseApplyApiProvider);
|
|
@@ -107,18 +112,24 @@ class _ExpenseApplyCreatePageState
|
|
|
api.getCostTypes(),
|
|
api.getCostTypes(),
|
|
|
api.getProjectCodes(),
|
|
api.getProjectCodes(),
|
|
|
api.getDepartments(),
|
|
api.getDepartments(),
|
|
|
|
|
+ api.getAcctSubjects(),
|
|
|
]);
|
|
]);
|
|
|
if (!mounted) return;
|
|
if (!mounted) return;
|
|
|
setState(() {
|
|
setState(() {
|
|
|
_costTypes = results[0] as List<CostTypeItem>;
|
|
_costTypes = results[0] as List<CostTypeItem>;
|
|
|
_projects = results[1] as List<ProjectCodeItem>;
|
|
_projects = results[1] as List<ProjectCodeItem>;
|
|
|
_departments = results[2] as List<DepartmentItem>;
|
|
_departments = results[2] as List<DepartmentItem>;
|
|
|
|
|
+ _acctTree = _convertAcctTree(results[3]);
|
|
|
|
|
+ debugPrint('[_loadRefData] _acctTree isNull: ${_acctTree == null}');
|
|
|
_refDataLoading = false;
|
|
_refDataLoading = false;
|
|
|
_autoSelectDept();
|
|
_autoSelectDept();
|
|
|
});
|
|
});
|
|
|
completer.complete();
|
|
completer.complete();
|
|
|
} catch (_) {
|
|
} catch (_) {
|
|
|
- if (!mounted) { completer.complete(); return; }
|
|
|
|
|
|
|
+ if (!mounted) {
|
|
|
|
|
+ completer.complete();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
setState(() => _refDataLoading = false);
|
|
setState(() => _refDataLoading = false);
|
|
|
completer.complete();
|
|
completer.complete();
|
|
|
} finally {
|
|
} finally {
|
|
@@ -188,7 +199,10 @@ class _ExpenseApplyCreatePageState
|
|
|
_selectedDeptId = '';
|
|
_selectedDeptId = '';
|
|
|
_selectedDeptName = '';
|
|
_selectedDeptName = '';
|
|
|
// 重置参考数据
|
|
// 重置参考数据
|
|
|
- _costTypes = []; _projects = []; _departments = [];
|
|
|
|
|
|
|
+ _costTypes = [];
|
|
|
|
|
+ _projects = [];
|
|
|
|
|
+ _departments = [];
|
|
|
|
|
+ _acctTree = null;
|
|
|
_refDataFuture = null;
|
|
_refDataFuture = null;
|
|
|
_refDataLoading = true;
|
|
_refDataLoading = true;
|
|
|
// 重置草稿状态
|
|
// 重置草稿状态
|
|
@@ -204,11 +218,15 @@ class _ExpenseApplyCreatePageState
|
|
|
@override
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
Widget build(BuildContext context) {
|
|
|
final l10n = AppLocalizations.of(context);
|
|
final l10n = AppLocalizations.of(context);
|
|
|
- setNavBarTitle(context, ref, NavBarConfig(
|
|
|
|
|
- title: l10n.get('expenseApplyRequest'),
|
|
|
|
|
- showBack: true,
|
|
|
|
|
- onBack: () => _doPop(),
|
|
|
|
|
- ));
|
|
|
|
|
|
|
+ setNavBarTitle(
|
|
|
|
|
+ context,
|
|
|
|
|
+ ref,
|
|
|
|
|
+ NavBarConfig(
|
|
|
|
|
+ title: l10n.get('expenseApplyRequest'),
|
|
|
|
|
+ showBack: true,
|
|
|
|
|
+ onBack: () => _doPop(),
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
return FutureBuilder<bool>(
|
|
return FutureBuilder<bool>(
|
|
|
future: _draftFuture,
|
|
future: _draftFuture,
|
|
@@ -280,45 +298,53 @@ class _ExpenseApplyCreatePageState
|
|
|
if (detailList != null) {
|
|
if (detailList != null) {
|
|
|
for (final d in detailList) {
|
|
for (final d in detailList) {
|
|
|
final m = d as Map<String, dynamic>;
|
|
final m = d as Map<String, dynamic>;
|
|
|
- _details.add(_DetailItem(
|
|
|
|
|
- id: m['id'] as int? ?? _detailIdCounter++,
|
|
|
|
|
- category: m['category'] as String? ?? '',
|
|
|
|
|
- categoryName: m['categoryName'] as String? ?? '',
|
|
|
|
|
- acctSubjectId: m['acctSubjectId'] as String? ?? '',
|
|
|
|
|
- acctSubjectName: m['acctSubjectName'] as String? ?? '',
|
|
|
|
|
- purpose: m['purpose'] as String? ?? '',
|
|
|
|
|
- projectId: m['projectId'] as int? ?? 0,
|
|
|
|
|
- projectName: m['projectName'] as String? ?? '',
|
|
|
|
|
- costDeptId: m['costDeptId'] as String? ?? '',
|
|
|
|
|
- costDeptName: m['costDeptName'] as String? ?? '',
|
|
|
|
|
- startDate: m['startDate'] as String? ?? '',
|
|
|
|
|
- endDate: m['endDate'] as String? ?? '',
|
|
|
|
|
- estimatedAmount: (m['estimatedAmount'] as num?)?.toDouble() ?? 0,
|
|
|
|
|
- remark: m['remark'] as String? ?? '',
|
|
|
|
|
- ));
|
|
|
|
|
|
|
+ _details.add(
|
|
|
|
|
+ _DetailItem(
|
|
|
|
|
+ id: m['id'] as int? ?? _detailIdCounter++,
|
|
|
|
|
+ category: m['category'] as String? ?? '',
|
|
|
|
|
+ categoryName: m['categoryName'] as String? ?? '',
|
|
|
|
|
+ acctSubjectId: m['acctSubjectId'] as String? ?? '',
|
|
|
|
|
+ acctSubjectName: m['acctSubjectName'] as String? ?? '',
|
|
|
|
|
+ purpose: m['purpose'] as String? ?? '',
|
|
|
|
|
+ projectId: m['projectId'] as int? ?? 0,
|
|
|
|
|
+ projectName: m['projectName'] as String? ?? '',
|
|
|
|
|
+ costDeptId: m['costDeptId'] as String? ?? '',
|
|
|
|
|
+ costDeptName: m['costDeptName'] as String? ?? '',
|
|
|
|
|
+ startDate: m['startDate'] as String? ?? '',
|
|
|
|
|
+ endDate: m['endDate'] as String? ?? '',
|
|
|
|
|
+ estimatedAmount: (m['estimatedAmount'] as num?)?.toDouble() ?? 0,
|
|
|
|
|
+ remark: m['remark'] as String? ?? '',
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- _detailIdCounter = _details.isEmpty ? 1 : _details.map((d) => d.id).reduce((a, b) => a > b ? a : b) + 1;
|
|
|
|
|
|
|
+ _detailIdCounter = _details.isEmpty
|
|
|
|
|
+ ? 1
|
|
|
|
|
+ : _details.map((d) => d.id).reduce((a, b) => a > b ? a : b) + 1;
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
Future<void> _saveDraftToStorage() async {
|
|
Future<void> _saveDraftToStorage() async {
|
|
|
- final detailList = _details.map((d) => {
|
|
|
|
|
- 'id': d.id,
|
|
|
|
|
- 'category': d.category,
|
|
|
|
|
- 'categoryName': d.categoryName,
|
|
|
|
|
- 'acctSubjectId': d.acctSubjectId,
|
|
|
|
|
- 'acctSubjectName': d.acctSubjectName,
|
|
|
|
|
- 'purpose': d.purpose,
|
|
|
|
|
- 'projectId': d.projectId,
|
|
|
|
|
- 'projectName': d.projectName,
|
|
|
|
|
- 'costDeptId': d.costDeptId,
|
|
|
|
|
- 'costDeptName': d.costDeptName,
|
|
|
|
|
- 'startDate': d.startDate,
|
|
|
|
|
- 'endDate': d.endDate,
|
|
|
|
|
- 'estimatedAmount': d.estimatedAmount,
|
|
|
|
|
- 'remark': d.remark,
|
|
|
|
|
- }).toList();
|
|
|
|
|
|
|
+ final detailList = _details
|
|
|
|
|
+ .map(
|
|
|
|
|
+ (d) => {
|
|
|
|
|
+ 'id': d.id,
|
|
|
|
|
+ 'category': d.category,
|
|
|
|
|
+ 'categoryName': d.categoryName,
|
|
|
|
|
+ 'acctSubjectId': d.acctSubjectId,
|
|
|
|
|
+ 'acctSubjectName': d.acctSubjectName,
|
|
|
|
|
+ 'purpose': d.purpose,
|
|
|
|
|
+ 'projectId': d.projectId,
|
|
|
|
|
+ 'projectName': d.projectName,
|
|
|
|
|
+ 'costDeptId': d.costDeptId,
|
|
|
|
|
+ 'costDeptName': d.costDeptName,
|
|
|
|
|
+ 'startDate': d.startDate,
|
|
|
|
|
+ 'endDate': d.endDate,
|
|
|
|
|
+ 'estimatedAmount': d.estimatedAmount,
|
|
|
|
|
+ 'remark': d.remark,
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
|
|
+ .toList();
|
|
|
await DraftStorage.save(_draftKey, {
|
|
await DraftStorage.save(_draftKey, {
|
|
|
'urgency': _urgency,
|
|
'urgency': _urgency,
|
|
|
'purpose': _purposeController.text,
|
|
'purpose': _purposeController.text,
|
|
@@ -380,7 +406,8 @@ class _ExpenseApplyCreatePageState
|
|
|
const SizedBox(height: 16),
|
|
const SizedBox(height: 16),
|
|
|
FormFieldRow(
|
|
FormFieldRow(
|
|
|
label: l10n.get('applicant'),
|
|
label: l10n.get('applicant'),
|
|
|
- value: HostAppChannel.usr.isNotEmpty && HostAppChannel.usrName.isNotEmpty
|
|
|
|
|
|
|
+ value:
|
|
|
|
|
+ HostAppChannel.usr.isNotEmpty && HostAppChannel.usrName.isNotEmpty
|
|
|
? '${HostAppChannel.usr}/${HostAppChannel.usrName}'
|
|
? '${HostAppChannel.usr}/${HostAppChannel.usrName}'
|
|
|
: '--',
|
|
: '--',
|
|
|
readOnly: true,
|
|
readOnly: true,
|
|
@@ -389,7 +416,9 @@ class _ExpenseApplyCreatePageState
|
|
|
const SizedBox(height: 16),
|
|
const SizedBox(height: 16),
|
|
|
FormFieldRow(
|
|
FormFieldRow(
|
|
|
label: l10n.get('applyDept'),
|
|
label: l10n.get('applyDept'),
|
|
|
- value: _selectedDeptId.isNotEmpty ? '$_selectedDeptId/$_selectedDeptName' : '',
|
|
|
|
|
|
|
+ value: _selectedDeptId.isNotEmpty
|
|
|
|
|
+ ? '$_selectedDeptId/$_selectedDeptName'
|
|
|
|
|
+ : '',
|
|
|
hint: l10n.get('pleaseSelect'),
|
|
hint: l10n.get('pleaseSelect'),
|
|
|
onTap: _refDataLoading ? null : () => _showDeptPicker(),
|
|
onTap: _refDataLoading ? null : () => _showDeptPicker(),
|
|
|
),
|
|
),
|
|
@@ -484,7 +513,11 @@ class _ExpenseApplyCreatePageState
|
|
|
final sel = _urgency == e.value.value;
|
|
final sel = _urgency == e.value.value;
|
|
|
final isCritical = e.value.value == Urgency.critical.value;
|
|
final isCritical = e.value.value == Urgency.critical.value;
|
|
|
final isUrgent = e.value.value == Urgency.urgent.value;
|
|
final isUrgent = e.value.value == Urgency.urgent.value;
|
|
|
- final activeColor = isCritical ? colors.danger : isUrgent ? colors.warning : colors.primary;
|
|
|
|
|
|
|
+ final activeColor = isCritical
|
|
|
|
|
+ ? colors.danger
|
|
|
|
|
+ : isUrgent
|
|
|
|
|
+ ? colors.warning
|
|
|
|
|
+ : colors.primary;
|
|
|
return Padding(
|
|
return Padding(
|
|
|
padding: EdgeInsets.only(left: e.key > 0 ? 18 : 0),
|
|
padding: EdgeInsets.only(left: e.key > 0 ? 18 : 0),
|
|
|
child: GestureDetector(
|
|
child: GestureDetector(
|
|
@@ -561,117 +594,120 @@ class _ExpenseApplyCreatePageState
|
|
|
return GestureDetector(
|
|
return GestureDetector(
|
|
|
onTap: () => _showDetailDialog(editIndex: e.key),
|
|
onTap: () => _showDetailDialog(editIndex: e.key),
|
|
|
child: Container(
|
|
child: Container(
|
|
|
- margin: const EdgeInsets.symmetric(vertical: 8),
|
|
|
|
|
- padding: const EdgeInsets.all(12),
|
|
|
|
|
- decoration: BoxDecoration(
|
|
|
|
|
- color: colors.bgPage,
|
|
|
|
|
- borderRadius: BorderRadius.circular(8),
|
|
|
|
|
- ),
|
|
|
|
|
- child: Row(
|
|
|
|
|
- crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
|
- children: [
|
|
|
|
|
- Expanded(
|
|
|
|
|
- child: Column(
|
|
|
|
|
- crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
- children: [
|
|
|
|
|
- Row(
|
|
|
|
|
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
- children: [
|
|
|
|
|
- Expanded(
|
|
|
|
|
- child: Text(
|
|
|
|
|
- '${d.category}/${d.categoryName}',
|
|
|
|
|
- maxLines: 1,
|
|
|
|
|
- overflow: TextOverflow.ellipsis,
|
|
|
|
|
|
|
+ margin: const EdgeInsets.symmetric(vertical: 8),
|
|
|
|
|
+ padding: const EdgeInsets.all(12),
|
|
|
|
|
+ decoration: BoxDecoration(
|
|
|
|
|
+ color: colors.bgPage,
|
|
|
|
|
+ borderRadius: BorderRadius.circular(8),
|
|
|
|
|
+ ),
|
|
|
|
|
+ child: Row(
|
|
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ Expanded(
|
|
|
|
|
+ child: Column(
|
|
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ Row(
|
|
|
|
|
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ Expanded(
|
|
|
|
|
+ child: Text(
|
|
|
|
|
+ '${d.category}/${d.categoryName}',
|
|
|
|
|
+ maxLines: 1,
|
|
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
|
|
+ style: TextStyle(
|
|
|
|
|
+ fontSize: AppFontSizes.subtitle,
|
|
|
|
|
+ color: colors.textPrimary,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(width: 12),
|
|
|
|
|
+ Text(
|
|
|
|
|
+ '¥${d.estimatedAmount.toStringAsFixed(2)}',
|
|
|
style: TextStyle(
|
|
style: TextStyle(
|
|
|
- fontSize: AppFontSizes.subtitle,
|
|
|
|
|
- color: colors.textPrimary,
|
|
|
|
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
|
|
+ fontWeight: FontWeight.w600,
|
|
|
|
|
+ color: colors.amountPrimary,
|
|
|
),
|
|
),
|
|
|
),
|
|
),
|
|
|
- ),
|
|
|
|
|
- const SizedBox(width: 12),
|
|
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ if (d.acctSubjectId.isNotEmpty &&
|
|
|
|
|
+ d.acctSubjectName.isNotEmpty) ...[
|
|
|
|
|
+ const SizedBox(height: 4),
|
|
|
Text(
|
|
Text(
|
|
|
- '¥${d.estimatedAmount.toStringAsFixed(2)}',
|
|
|
|
|
|
|
+ d.acctSubjectName,
|
|
|
|
|
+ maxLines: 1,
|
|
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
style: TextStyle(
|
|
style: TextStyle(
|
|
|
fontSize: AppFontSizes.caption,
|
|
fontSize: AppFontSizes.caption,
|
|
|
- fontWeight: FontWeight.w600,
|
|
|
|
|
- color: colors.amountPrimary,
|
|
|
|
|
|
|
+ color: colors.textSecondary,
|
|
|
),
|
|
),
|
|
|
),
|
|
),
|
|
|
],
|
|
],
|
|
|
- ),
|
|
|
|
|
- if (d.acctSubjectId.isNotEmpty && d.acctSubjectName.isNotEmpty) ...[
|
|
|
|
|
- const SizedBox(height: 4),
|
|
|
|
|
- Text(
|
|
|
|
|
- '${d.acctSubjectId}/${d.acctSubjectName}',
|
|
|
|
|
- maxLines: 1,
|
|
|
|
|
- overflow: TextOverflow.ellipsis,
|
|
|
|
|
- style: TextStyle(
|
|
|
|
|
- fontSize: AppFontSizes.caption,
|
|
|
|
|
- color: colors.textSecondary,
|
|
|
|
|
- ),
|
|
|
|
|
- ),
|
|
|
|
|
- ],
|
|
|
|
|
- if (d.projectId > 0 && d.projectName.isNotEmpty) ...[
|
|
|
|
|
- const SizedBox(height: 4),
|
|
|
|
|
- Text(
|
|
|
|
|
- '${d.projectId}/${d.projectName}',
|
|
|
|
|
- maxLines: 1,
|
|
|
|
|
- overflow: TextOverflow.ellipsis,
|
|
|
|
|
- style: TextStyle(
|
|
|
|
|
- fontSize: AppFontSizes.caption,
|
|
|
|
|
- color: colors.textSecondary,
|
|
|
|
|
|
|
+ if (d.projectId > 0 && d.projectName.isNotEmpty) ...[
|
|
|
|
|
+ const SizedBox(height: 4),
|
|
|
|
|
+ Text(
|
|
|
|
|
+ '${d.projectId}/${d.projectName}',
|
|
|
|
|
+ maxLines: 1,
|
|
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
|
|
+ style: TextStyle(
|
|
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
|
|
+ color: colors.textSecondary,
|
|
|
|
|
+ ),
|
|
|
),
|
|
),
|
|
|
- ),
|
|
|
|
|
- ],
|
|
|
|
|
- if (d.costDeptId.isNotEmpty && d.costDeptName.isNotEmpty) ...[
|
|
|
|
|
- const SizedBox(height: 4),
|
|
|
|
|
- Text(
|
|
|
|
|
- '${d.costDeptId}/${d.costDeptName}',
|
|
|
|
|
- maxLines: 1,
|
|
|
|
|
- overflow: TextOverflow.ellipsis,
|
|
|
|
|
- style: TextStyle(
|
|
|
|
|
- fontSize: AppFontSizes.caption,
|
|
|
|
|
- color: colors.textSecondary,
|
|
|
|
|
|
|
+ ],
|
|
|
|
|
+ if (d.costDeptId.isNotEmpty &&
|
|
|
|
|
+ d.costDeptName.isNotEmpty) ...[
|
|
|
|
|
+ const SizedBox(height: 4),
|
|
|
|
|
+ Text(
|
|
|
|
|
+ '${d.costDeptId}/${d.costDeptName}',
|
|
|
|
|
+ maxLines: 1,
|
|
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
|
|
+ style: TextStyle(
|
|
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
|
|
+ color: colors.textSecondary,
|
|
|
|
|
+ ),
|
|
|
),
|
|
),
|
|
|
- ),
|
|
|
|
|
- ],
|
|
|
|
|
- if (d.startDate.isNotEmpty && d.endDate.isNotEmpty) ...[
|
|
|
|
|
- const SizedBox(height: 4),
|
|
|
|
|
- Text(
|
|
|
|
|
- '${d.startDate} ~ ${d.endDate}',
|
|
|
|
|
- maxLines: 1,
|
|
|
|
|
- overflow: TextOverflow.ellipsis,
|
|
|
|
|
- style: TextStyle(
|
|
|
|
|
- fontSize: AppFontSizes.caption,
|
|
|
|
|
- color: colors.textSecondary,
|
|
|
|
|
|
|
+ ],
|
|
|
|
|
+ if (d.startDate.isNotEmpty &&
|
|
|
|
|
+ d.endDate.isNotEmpty) ...[
|
|
|
|
|
+ const SizedBox(height: 4),
|
|
|
|
|
+ Text(
|
|
|
|
|
+ '${d.startDate} ~ ${d.endDate}',
|
|
|
|
|
+ maxLines: 1,
|
|
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
|
|
+ style: TextStyle(
|
|
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
|
|
+ color: colors.textSecondary,
|
|
|
|
|
+ ),
|
|
|
),
|
|
),
|
|
|
- ),
|
|
|
|
|
- ],
|
|
|
|
|
- if (d.remark.isNotEmpty) ...[
|
|
|
|
|
- const SizedBox(height: 4),
|
|
|
|
|
- Text(
|
|
|
|
|
- d.remark,
|
|
|
|
|
- style: TextStyle(
|
|
|
|
|
- fontSize: AppFontSizes.caption,
|
|
|
|
|
- color: colors.textSecondary,
|
|
|
|
|
|
|
+ ],
|
|
|
|
|
+ if (d.remark.isNotEmpty) ...[
|
|
|
|
|
+ const SizedBox(height: 4),
|
|
|
|
|
+ Text(
|
|
|
|
|
+ d.remark,
|
|
|
|
|
+ style: TextStyle(
|
|
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
|
|
+ color: colors.textSecondary,
|
|
|
|
|
+ ),
|
|
|
),
|
|
),
|
|
|
- ),
|
|
|
|
|
|
|
+ ],
|
|
|
],
|
|
],
|
|
|
- ],
|
|
|
|
|
|
|
+ ),
|
|
|
),
|
|
),
|
|
|
- ),
|
|
|
|
|
- const SizedBox(width: 8),
|
|
|
|
|
- GestureDetector(
|
|
|
|
|
- onTap: () => setState(() => _details.removeAt(e.key)),
|
|
|
|
|
- child: Icon(
|
|
|
|
|
- Icons.close,
|
|
|
|
|
- size: 18,
|
|
|
|
|
- color: colors.textSecondary,
|
|
|
|
|
|
|
+ const SizedBox(width: 8),
|
|
|
|
|
+ GestureDetector(
|
|
|
|
|
+ onTap: () => setState(() => _details.removeAt(e.key)),
|
|
|
|
|
+ child: Icon(
|
|
|
|
|
+ Icons.close,
|
|
|
|
|
+ size: 18,
|
|
|
|
|
+ color: colors.textSecondary,
|
|
|
|
|
+ ),
|
|
|
),
|
|
),
|
|
|
- ),
|
|
|
|
|
- ],
|
|
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
),
|
|
),
|
|
|
- ),
|
|
|
|
|
);
|
|
);
|
|
|
}),
|
|
}),
|
|
|
const SizedBox(height: 8),
|
|
const SizedBox(height: 8),
|
|
@@ -704,8 +740,7 @@ class _ExpenseApplyCreatePageState
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- double _totalAmount() =>
|
|
|
|
|
- _details.fold(0, (s, d) => s + d.estimatedAmount);
|
|
|
|
|
|
|
+ double _totalAmount() => _details.fold(0, (s, d) => s + d.estimatedAmount);
|
|
|
|
|
|
|
|
Future<void> _showDetailDialog({int? editIndex}) async {
|
|
Future<void> _showDetailDialog({int? editIndex}) async {
|
|
|
if (_addingDetail) return;
|
|
if (_addingDetail) return;
|
|
@@ -720,60 +755,61 @@ class _ExpenseApplyCreatePageState
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- ExpenseDetailData? initialData;
|
|
|
|
|
- if (editIndex != null) {
|
|
|
|
|
- final d = _details[editIndex];
|
|
|
|
|
- initialData = ExpenseDetailData(
|
|
|
|
|
- category: d.category,
|
|
|
|
|
- categoryName: d.categoryName,
|
|
|
|
|
- acctSubjectId: d.acctSubjectId,
|
|
|
|
|
- acctSubjectName: d.acctSubjectName,
|
|
|
|
|
- purpose: d.purpose,
|
|
|
|
|
- projectId: d.projectId,
|
|
|
|
|
- projectName: d.projectName,
|
|
|
|
|
- costDeptId: d.costDeptId,
|
|
|
|
|
- costDeptName: d.costDeptName,
|
|
|
|
|
- startDate: d.startDate,
|
|
|
|
|
- endDate: d.endDate,
|
|
|
|
|
- estimatedAmount: d.estimatedAmount,
|
|
|
|
|
- remark: d.remark,
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- FocusManager.instance.primaryFocus?.unfocus();
|
|
|
|
|
- final result = await ExpenseApplyDetailDialog.show(
|
|
|
|
|
- // ignore: use_build_context_synchronously
|
|
|
|
|
- context,
|
|
|
|
|
- categories: _dialogCategories,
|
|
|
|
|
- projects: _dialogProjects,
|
|
|
|
|
- costDepts: _dialogCostDepts,
|
|
|
|
|
- l10n: l10n,
|
|
|
|
|
- initialData: initialData,
|
|
|
|
|
- );
|
|
|
|
|
- if (result != null && mounted) {
|
|
|
|
|
- setState(() {
|
|
|
|
|
- final item = _DetailItem(
|
|
|
|
|
- id: editIndex != null ? _details[editIndex].id : _detailIdCounter++,
|
|
|
|
|
- category: result.category,
|
|
|
|
|
- categoryName: result.categoryName,
|
|
|
|
|
- acctSubjectId: result.acctSubjectId,
|
|
|
|
|
- acctSubjectName: result.acctSubjectName,
|
|
|
|
|
- purpose: result.purpose,
|
|
|
|
|
- projectId: result.projectId,
|
|
|
|
|
- projectName: result.projectName,
|
|
|
|
|
- costDeptId: result.costDeptId,
|
|
|
|
|
- costDeptName: result.costDeptName,
|
|
|
|
|
- startDate: result.startDate,
|
|
|
|
|
- endDate: result.endDate,
|
|
|
|
|
- estimatedAmount: result.estimatedAmount,
|
|
|
|
|
- remark: result.remark,
|
|
|
|
|
|
|
+ ExpenseDetailData? initialData;
|
|
|
|
|
+ if (editIndex != null) {
|
|
|
|
|
+ final d = _details[editIndex];
|
|
|
|
|
+ initialData = ExpenseDetailData(
|
|
|
|
|
+ category: d.category,
|
|
|
|
|
+ categoryName: d.categoryName,
|
|
|
|
|
+ acctSubjectId: d.acctSubjectId,
|
|
|
|
|
+ acctSubjectName: d.acctSubjectName,
|
|
|
|
|
+ purpose: d.purpose,
|
|
|
|
|
+ projectId: d.projectId,
|
|
|
|
|
+ projectName: d.projectName,
|
|
|
|
|
+ costDeptId: d.costDeptId,
|
|
|
|
|
+ costDeptName: d.costDeptName,
|
|
|
|
|
+ startDate: d.startDate,
|
|
|
|
|
+ endDate: d.endDate,
|
|
|
|
|
+ estimatedAmount: d.estimatedAmount,
|
|
|
|
|
+ remark: d.remark,
|
|
|
);
|
|
);
|
|
|
- if (editIndex != null) {
|
|
|
|
|
- _details[editIndex] = item;
|
|
|
|
|
- } else {
|
|
|
|
|
- _details.add(item);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ FocusManager.instance.primaryFocus?.unfocus();
|
|
|
|
|
+ final result = await ExpenseApplyDetailDialog.show(
|
|
|
|
|
+ // ignore: use_build_context_synchronously
|
|
|
|
|
+ context,
|
|
|
|
|
+ categories: _dialogCategories,
|
|
|
|
|
+ projects: _dialogProjects,
|
|
|
|
|
+ costDepts: _dialogCostDepts,
|
|
|
|
|
+ l10n: l10n,
|
|
|
|
|
+ acctTree: _acctTree,
|
|
|
|
|
+ initialData: initialData,
|
|
|
|
|
+ );
|
|
|
|
|
+ if (result != null && mounted) {
|
|
|
|
|
+ setState(() {
|
|
|
|
|
+ final item = _DetailItem(
|
|
|
|
|
+ id: editIndex != null ? _details[editIndex].id : _detailIdCounter++,
|
|
|
|
|
+ category: result.category,
|
|
|
|
|
+ categoryName: result.categoryName,
|
|
|
|
|
+ acctSubjectId: result.acctSubjectId,
|
|
|
|
|
+ acctSubjectName: result.acctSubjectName,
|
|
|
|
|
+ purpose: result.purpose,
|
|
|
|
|
+ projectId: result.projectId,
|
|
|
|
|
+ projectName: result.projectName,
|
|
|
|
|
+ costDeptId: result.costDeptId,
|
|
|
|
|
+ costDeptName: result.costDeptName,
|
|
|
|
|
+ startDate: result.startDate,
|
|
|
|
|
+ endDate: result.endDate,
|
|
|
|
|
+ estimatedAmount: result.estimatedAmount,
|
|
|
|
|
+ remark: result.remark,
|
|
|
|
|
+ );
|
|
|
|
|
+ if (editIndex != null) {
|
|
|
|
|
+ _details[editIndex] = item;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ _details.add(item);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
} finally {
|
|
} finally {
|
|
|
_addingDetail = false;
|
|
_addingDetail = false;
|
|
|
}
|
|
}
|
|
@@ -781,6 +817,18 @@ class _ExpenseApplyCreatePageState
|
|
|
|
|
|
|
|
// ═══ 3. 附件上传 ═══
|
|
// ═══ 3. 附件上传 ═══
|
|
|
|
|
|
|
|
|
|
+ /// 深度转换 JSON 解析的 List<dynamic> 为 List<Map>,确保 TDCascader 类型匹配
|
|
|
|
|
+ List<Map<String, dynamic>> _convertAcctTree(dynamic tree) {
|
|
|
|
|
+ if (tree is! List) return [];
|
|
|
|
|
+ return tree.map<Map<String, dynamic>>((e) {
|
|
|
|
|
+ final map = Map<String, dynamic>.from(e as Map);
|
|
|
|
|
+ if (map['children'] != null) {
|
|
|
|
|
+ map['children'] = _convertAcctTree(map['children']);
|
|
|
|
|
+ }
|
|
|
|
|
+ return map;
|
|
|
|
|
+ }).toList();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
Future<void> _checkAttachHealth() async {
|
|
Future<void> _checkAttachHealth() async {
|
|
|
// 立即设为 false,等待 API 返回后再更新,避免缓存旧值
|
|
// 立即设为 false,等待 API 返回后再更新,避免缓存旧值
|
|
|
if (mounted) setState(() => _attachAvailable = false);
|
|
if (mounted) setState(() => _attachAvailable = false);
|
|
@@ -797,12 +845,24 @@ class _ExpenseApplyCreatePageState
|
|
|
final colors = Theme.of(context).extension<AppColorsExtension>()!;
|
|
final colors = Theme.of(context).extension<AppColorsExtension>()!;
|
|
|
final children = <Widget>[];
|
|
final children = <Widget>[];
|
|
|
if (!_attachAvailable) {
|
|
if (!_attachAvailable) {
|
|
|
- children.add(Text(l10n.get('attachServiceUnavailable'),
|
|
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textPlaceholder)));
|
|
|
|
|
|
|
+ children.add(
|
|
|
|
|
+ Text(
|
|
|
|
|
+ l10n.get('attachServiceUnavailable'),
|
|
|
|
|
+ style: TextStyle(
|
|
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
|
|
+ color: colors.textPlaceholder,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
} else {
|
|
} else {
|
|
|
children.addAll([
|
|
children.addAll([
|
|
|
- Text(l10n.get('maxAttachment'),
|
|
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textPlaceholder)),
|
|
|
|
|
|
|
+ Text(
|
|
|
|
|
+ l10n.get('maxAttachment'),
|
|
|
|
|
+ style: TextStyle(
|
|
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
|
|
+ color: colors.textPlaceholder,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
const SizedBox(height: 8),
|
|
const SizedBox(height: 8),
|
|
|
]);
|
|
]);
|
|
|
}
|
|
}
|
|
@@ -813,25 +873,35 @@ class _ExpenseApplyCreatePageState
|
|
|
...children,
|
|
...children,
|
|
|
if (_attachAvailable)
|
|
if (_attachAvailable)
|
|
|
AttachmentPicker(
|
|
AttachmentPicker(
|
|
|
- controller: _attachmentController,
|
|
|
|
|
- maxImageSizeMB: 10,
|
|
|
|
|
- maxFileSizeMB: 20,
|
|
|
|
|
- allowedExtensions: const [
|
|
|
|
|
- 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt',
|
|
|
|
|
- ],
|
|
|
|
|
- onFileRejected: (file, reason) {
|
|
|
|
|
- if (context.mounted) {
|
|
|
|
|
- TDToast.showText(reason, context: context);
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- ),
|
|
|
|
|
|
|
+ controller: _attachmentController,
|
|
|
|
|
+ maxImageSizeMB: 10,
|
|
|
|
|
+ maxFileSizeMB: 20,
|
|
|
|
|
+ allowedExtensions: const [
|
|
|
|
|
+ 'pdf',
|
|
|
|
|
+ 'doc',
|
|
|
|
|
+ 'docx',
|
|
|
|
|
+ 'xls',
|
|
|
|
|
+ 'xlsx',
|
|
|
|
|
+ 'ppt',
|
|
|
|
|
+ 'pptx',
|
|
|
|
|
+ 'txt',
|
|
|
|
|
+ ],
|
|
|
|
|
+ onFileRejected: (file, reason) {
|
|
|
|
|
+ if (context.mounted) {
|
|
|
|
|
+ TDToast.showText(reason, context: context);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ ),
|
|
|
],
|
|
],
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void _showDeptPicker() {
|
|
void _showDeptPicker() {
|
|
|
if (_departments.isEmpty) {
|
|
if (_departments.isEmpty) {
|
|
|
- TDToast.showText(AppLocalizations.of(context).get('noData'), context: context);
|
|
|
|
|
|
|
+ TDToast.showText(
|
|
|
|
|
+ AppLocalizations.of(context).get('noData'),
|
|
|
|
|
+ context: context,
|
|
|
|
|
+ );
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
final l10n = AppLocalizations.of(context);
|
|
final l10n = AppLocalizations.of(context);
|
|
@@ -861,21 +931,22 @@ class _ExpenseApplyCreatePageState
|
|
|
// ═══ API 数据 → 弹窗类型转换 ═══
|
|
// ═══ API 数据 → 弹窗类型转换 ═══
|
|
|
|
|
|
|
|
List<CostCategory> get _dialogCategories => _costTypes
|
|
List<CostCategory> get _dialogCategories => _costTypes
|
|
|
- .map((c) => CostCategory(
|
|
|
|
|
- code: c.typeNo,
|
|
|
|
|
- nameKey: c.typeName,
|
|
|
|
|
- acctSubjectId: c.accNo,
|
|
|
|
|
- acctSubjectName: c.accName,
|
|
|
|
|
- ))
|
|
|
|
|
|
|
+ .map(
|
|
|
|
|
+ (c) => CostCategory(
|
|
|
|
|
+ code: c.typeNo,
|
|
|
|
|
+ nameKey: c.typeName,
|
|
|
|
|
+ acctSubjectId: c.accNo,
|
|
|
|
|
+ acctSubjectName: c.accName,
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
.toList();
|
|
.toList();
|
|
|
|
|
|
|
|
List<Project> get _dialogProjects => _projects
|
|
List<Project> get _dialogProjects => _projects
|
|
|
.map((p) => Project(id: int.tryParse(p.objNo) ?? 0, name: p.name))
|
|
.map((p) => Project(id: int.tryParse(p.objNo) ?? 0, name: p.name))
|
|
|
.toList();
|
|
.toList();
|
|
|
|
|
|
|
|
- List<CostDept> get _dialogCostDepts => _departments
|
|
|
|
|
- .map((d) => CostDept(id: d.dep, name: d.name))
|
|
|
|
|
- .toList();
|
|
|
|
|
|
|
+ List<CostDept> get _dialogCostDepts =>
|
|
|
|
|
+ _departments.map((d) => CostDept(id: d.dep, name: d.name)).toList();
|
|
|
|
|
|
|
|
// ═══ 4. 底部操作栏 ═══
|
|
// ═══ 4. 底部操作栏 ═══
|
|
|
Widget _buildBottomBar(AppLocalizations l10n) {
|
|
Widget _buildBottomBar(AppLocalizations l10n) {
|
|
@@ -910,7 +981,8 @@ class _ExpenseApplyCreatePageState
|
|
|
// 上传表头附件(billNo 提取失败则跳过,不影响主流程)
|
|
// 上传表头附件(billNo 提取失败则跳过,不影响主流程)
|
|
|
if (billNo != null && _attachmentController.files.isNotEmpty) {
|
|
if (billNo != null && _attachmentController.files.isNotEmpty) {
|
|
|
final now = DateTime.now();
|
|
final now = DateTime.now();
|
|
|
- final effDd = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} '
|
|
|
|
|
|
|
+ final effDd =
|
|
|
|
|
+ '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} '
|
|
|
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}.'
|
|
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}.'
|
|
|
'${now.millisecond.toString().padLeft(3, '0')}';
|
|
'${now.millisecond.toString().padLeft(3, '0')}';
|
|
|
final usr = HostAppChannel.usr;
|
|
final usr = HostAppChannel.usr;
|
|
@@ -936,7 +1008,10 @@ class _ExpenseApplyCreatePageState
|
|
|
await DraftStorage.delete(_draftKey);
|
|
await DraftStorage.delete(_draftKey);
|
|
|
if (mounted) {
|
|
if (mounted) {
|
|
|
LoadingDialog.hide(context);
|
|
LoadingDialog.hide(context);
|
|
|
- TDToast.showSuccess(l10n.get('submittedAwaitingApproval'), context: context);
|
|
|
|
|
|
|
+ TDToast.showSuccess(
|
|
|
|
|
+ l10n.get('submittedAwaitingApproval'),
|
|
|
|
|
+ context: context,
|
|
|
|
|
+ );
|
|
|
GoRouter.of(context).go('/expense-apply/list');
|
|
GoRouter.of(context).go('/expense-apply/list');
|
|
|
}
|
|
}
|
|
|
} catch (_) {
|
|
} catch (_) {
|
|
@@ -982,7 +1057,7 @@ class _ExpenseApplyCreatePageState
|
|
|
'TYPE_NO': d.category,
|
|
'TYPE_NO': d.category,
|
|
|
'AMTN_YJ': d.estimatedAmount,
|
|
'AMTN_YJ': d.estimatedAmount,
|
|
|
'ACC_NO': d.acctSubjectId,
|
|
'ACC_NO': d.acctSubjectId,
|
|
|
- 'ACC_NAME': d.acctSubjectName,
|
|
|
|
|
|
|
+ //'ACC_NAME': d.acctSubjectName,
|
|
|
'DEP': d.costDeptId,
|
|
'DEP': d.costDeptId,
|
|
|
'OBJ_NO': d.projectId > 0 ? d.projectId.toString() : '',
|
|
'OBJ_NO': d.projectId > 0 ? d.projectId.toString() : '',
|
|
|
'START_DD': d.startDate,
|
|
'START_DD': d.startDate,
|
|
@@ -1140,11 +1215,18 @@ class _ExpenseApplyCreatePageState
|
|
|
child: Row(
|
|
child: Row(
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
children: [
|
|
children: [
|
|
|
- Icon(Icons.rocket_launch_outlined, size: 16, color: colors.textPlaceholder),
|
|
|
|
|
|
|
+ Icon(
|
|
|
|
|
+ Icons.rocket_launch_outlined,
|
|
|
|
|
+ size: 16,
|
|
|
|
|
+ color: colors.textPlaceholder,
|
|
|
|
|
+ ),
|
|
|
const SizedBox(width: 6),
|
|
const SizedBox(width: 6),
|
|
|
Text(
|
|
Text(
|
|
|
l10n.get('pageFooter'),
|
|
l10n.get('pageFooter'),
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textPlaceholder),
|
|
|
|
|
|
|
+ style: TextStyle(
|
|
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
|
|
+ color: colors.textPlaceholder,
|
|
|
|
|
+ ),
|
|
|
),
|
|
),
|
|
|
],
|
|
],
|
|
|
),
|
|
),
|