expense_detail_page.dart 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. import 'package:flutter/material.dart';
  2. import 'package:tdesign_flutter/tdesign_flutter.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import 'package:go_router/go_router.dart';
  5. import '../../shared/widgets/nav_bar_config.dart';
  6. import '../../core/utils/date_utils.dart' as du;
  7. import '../../shared/widgets/form_section.dart';
  8. import '../../shared/widgets/form_field_row.dart';
  9. import 'expense_model.dart';
  10. import '../../core/i18n/app_localizations.dart';
  11. import '../../shared/widgets/loading_dialog.dart';
  12. import '../../shared/models/bill_attachment.dart';
  13. import '../../core/theme/app_colors.dart';
  14. import '../../core/theme/app_colors_extension.dart';
  15. import 'expense_api.dart';
  16. import '../../shared/widgets/approval_actions.dart';
  17. import '../../shared/widgets/approval_timeline.dart';
  18. import '../../shared/models/approval_status.dart';
  19. class ExpenseDetailPage extends ConsumerStatefulWidget {
  20. final String billNo;
  21. final int queryId;
  22. const ExpenseDetailPage({super.key, required this.billNo, this.queryId = 0});
  23. @override
  24. ConsumerState<ExpenseDetailPage> createState() => _ExpenseDetailPageState();
  25. }
  26. class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
  27. with WidgetsBindingObserver {
  28. ExpenseModel? _expense;
  29. List<BillAttachment> _attachments = [];
  30. bool _attachAvailable = false;
  31. List<ApprovalRecord> _timelineRecords = [];
  32. List<String> _timelineChain = [];
  33. String _timelineCurrentApproverId = '';
  34. bool _isLoading = true;
  35. String? _error;
  36. @override
  37. void initState() {
  38. super.initState();
  39. WidgetsBinding.instance.addObserver(this);
  40. _loadData();
  41. }
  42. @override
  43. void dispose() {
  44. WidgetsBinding.instance.removeObserver(this);
  45. super.dispose();
  46. }
  47. @override
  48. void didChangeAppLifecycleState(AppLifecycleState state) {
  49. if (state == AppLifecycleState.resumed) {
  50. _attachments = [];
  51. _attachAvailable = false;
  52. _loadData();
  53. }
  54. }
  55. Future<void> _loadData() async {
  56. setState(() {
  57. _isLoading = true;
  58. _error = null;
  59. });
  60. try {
  61. final api = ref.read(expenseApiProvider);
  62. // 1. 加载报销详情(主表 + 明细)
  63. final expense = await api.fetchDetail(widget.billNo);
  64. setState(() => _expense = expense);
  65. // 2. 加载审批时间线(非致命——失败时回退到 expense 模型数据)
  66. try {
  67. final timelineData = await api.fetchApprovalTimeline('BX', widget.billNo);
  68. setState(() {
  69. _timelineRecords = (timelineData['records'] as List<dynamic>?)
  70. ?.map((e) => ApprovalRecord.fromJson(e as Map<String, dynamic>))
  71. .toList() ??
  72. [];
  73. _timelineChain = (timelineData['chain'] as List<dynamic>?)
  74. ?.map((e) => e as String)
  75. .toList() ??
  76. [];
  77. _timelineCurrentApproverId =
  78. (timelineData['currentApproverId'] as String?) ?? '';
  79. });
  80. } catch (_) {
  81. // 回退到 expense 自带的审批数据
  82. setState(() {
  83. _timelineRecords = expense.approvalRecords;
  84. _timelineChain = expense.approvalChain;
  85. _timelineCurrentApproverId = expense.currentApproverId;
  86. });
  87. }
  88. // 3. 加载附件(非致命)
  89. try {
  90. if (mounted) setState(() => _attachAvailable = false);
  91. _attachAvailable = await api.checkAttachHealth();
  92. if (_attachAvailable) {
  93. _attachments = await api.getAttachments('BX', widget.billNo);
  94. }
  95. } catch (_) {
  96. _attachAvailable = false;
  97. _attachments = [];
  98. }
  99. } catch (e) {
  100. setState(() => _error = e.toString());
  101. } finally {
  102. setState(() => _isLoading = false);
  103. }
  104. }
  105. @override
  106. Widget build(BuildContext context) {
  107. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  108. final l10n = AppLocalizations.of(context);
  109. setNavBarTitle(context, ref, NavBarConfig(
  110. title: l10n.get('expenseDetail'),
  111. showBack: true,
  112. onBack: () => context.pop(),
  113. ));
  114. if (_isLoading) {
  115. return const Center(
  116. child: TDLoading(
  117. size: TDLoadingSize.large,
  118. icon: TDLoadingIcon.activity,
  119. ),
  120. );
  121. }
  122. if (_error != null) {
  123. return Center(
  124. child: Column(
  125. mainAxisAlignment: MainAxisAlignment.center,
  126. children: [
  127. Icon(Icons.error_outline, size: 48, color: colors.danger),
  128. const SizedBox(height: 12),
  129. Text(
  130. _error!,
  131. style: TextStyle(fontSize: AppFontSizes.body, color: colors.danger),
  132. textAlign: TextAlign.center,
  133. ),
  134. const SizedBox(height: 16),
  135. TDButton(
  136. text: l10n.get('retry'),
  137. theme: TDButtonTheme.primary,
  138. onTap: _loadData,
  139. ),
  140. ],
  141. ),
  142. );
  143. }
  144. final expense = _expense!;
  145. return Column(
  146. children: [
  147. Expanded(
  148. child: SingleChildScrollView(
  149. physics: const AlwaysScrollableScrollPhysics(),
  150. padding: const EdgeInsets.all(16),
  151. child: Column(
  152. children: [
  153. _buildBasicInfoSection(expense, l10n, colors),
  154. const SizedBox(height: 16),
  155. _buildExpenseDetailSection(expense, l10n, colors),
  156. const SizedBox(height: 16),
  157. _buildAttachmentSection(l10n, colors),
  158. const SizedBox(height: 16),
  159. _buildApprovalSection(l10n, colors),
  160. ],
  161. ),
  162. ),
  163. ),
  164. ApprovalActions(
  165. queryId: widget.queryId,
  166. onApprove: () => _handleApprove(),
  167. onReject: () => _handleReject(),
  168. onReverseAudit: () => _handleReverseAudit(),
  169. ),
  170. ],
  171. );
  172. }
  173. // ── 审核操作 handler ──
  174. void _handleApprove() async {
  175. final l10n = AppLocalizations.of(context);
  176. final rem = await _showOpinionDialog(
  177. title: l10n.get('confirmApprove'),
  178. hint: l10n.get('approvalComment'),
  179. );
  180. if (rem == null || !mounted) return;
  181. await _doAudit('approve', rem: rem);
  182. }
  183. void _handleReject() async {
  184. final l10n = AppLocalizations.of(context);
  185. final rem = await _showOpinionDialog(
  186. title: l10n.get('confirmReject'),
  187. hint: l10n.get('rejectReason'),
  188. );
  189. if (rem == null || !mounted) return;
  190. await _doAudit('reject', rem: rem);
  191. }
  192. void _handleReverseAudit() async {
  193. final l10n = AppLocalizations.of(context);
  194. final confirmed = await showDialog<bool>(
  195. context: context,
  196. builder: (ctx) => TDAlertDialog(
  197. title: l10n.get('withdrawConfirm'),
  198. content: l10n.get('withdrawConfirmTip'),
  199. leftBtn: TDDialogButtonOptions(title: l10n.get('cancel'), action: () => Navigator.pop(ctx, false)),
  200. rightBtn: TDDialogButtonOptions(title: l10n.get('confirm'), action: () => Navigator.pop(ctx, true)),
  201. ),
  202. );
  203. if (confirmed != true || !mounted) return;
  204. final rem = await _showOpinionDialog(
  205. title: l10n.get('withdrawConfirm'),
  206. hint: l10n.get('approvalComment'),
  207. );
  208. if (rem == null || !mounted) return;
  209. await _doAudit('reverseAudit', rem: rem);
  210. }
  211. /// 弹出审批意见输入框,返回 null 表示取消
  212. Future<String?> _showOpinionDialog({
  213. required String title,
  214. required String hint,
  215. }) async {
  216. final ctrl = TextEditingController();
  217. return showDialog<String>(
  218. context: context,
  219. builder: (ctx) => TDAlertDialog(
  220. title: title,
  221. content: hint,
  222. leftBtn: TDDialogButtonOptions(
  223. title: AppLocalizations.of(context).get('cancel'),
  224. action: () => Navigator.pop(ctx),
  225. ),
  226. rightBtn: TDDialogButtonOptions(
  227. title: AppLocalizations.of(context).get('confirm'),
  228. action: () => Navigator.pop(ctx, ctrl.text),
  229. ),
  230. ),
  231. );
  232. }
  233. Future<void> _doAudit(String action, {String rem = ''}) async {
  234. try {
  235. LoadingDialog.show(context);
  236. final api = ref.read(expenseApiProvider);
  237. await api.executeApproval(bilId: 'BX', bilNo: widget.billNo, action: action, rem: rem);
  238. if (mounted) {
  239. LoadingDialog.hide(context);
  240. TDToast.showSuccess(AppLocalizations.of(context).get('submitSuccess'), context: context);
  241. if (mounted) context.pop();
  242. }
  243. } catch (e) {
  244. if (mounted) {
  245. LoadingDialog.hide(context);
  246. TDToast.showFail(e.toString(), context: context);
  247. }
  248. }
  249. }
  250. // ═══ 基本信息 ═══
  251. Widget _buildBasicInfoSection(
  252. ExpenseModel expense, AppLocalizations l10n, AppColorsExtension colors) {
  253. return FormSection(
  254. title: l10n.get('basicInfo'),
  255. leadingIcon: Icons.info_outline,
  256. children: [
  257. FormFieldRow(
  258. label: l10n.get('expenseNo'),
  259. value: expense.expenseNo,
  260. readOnly: true,
  261. showArrow: false),
  262. const SizedBox(height: 16),
  263. FormFieldRow(
  264. label: l10n.get('voucherNo'),
  265. value: expense.voucherNo.isNotEmpty ? expense.voucherNo : '-',
  266. readOnly: true,
  267. showArrow: false),
  268. const SizedBox(height: 16),
  269. FormFieldRow(
  270. label: l10n.get('expensePersonnel'),
  271. value: expense.applicantId.isNotEmpty
  272. ? '${expense.applicantId}/${expense.applicantName}'
  273. : expense.applicantName,
  274. readOnly: true,
  275. showArrow: false),
  276. const SizedBox(height: 16),
  277. FormFieldRow(
  278. label: l10n.get('expenseDept'),
  279. value: expense.deptId.isNotEmpty
  280. ? '${expense.deptId}/${expense.deptName}'
  281. : expense.deptName,
  282. readOnly: true,
  283. showArrow: false),
  284. const SizedBox(height: 16),
  285. FormFieldRow(
  286. label: l10n.get('date'),
  287. value: du.DateUtils.formatDateTime(expense.createTime),
  288. readOnly: true,
  289. showArrow: false),
  290. const SizedBox(height: 16),
  291. FormFieldRow(
  292. label: l10n.get('currency'),
  293. value: expense.currencyCode.isNotEmpty ? expense.currencyCode : '-',
  294. readOnly: true,
  295. showArrow: false),
  296. const SizedBox(height: 16),
  297. SizedBox(
  298. height: 24,
  299. child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
  300. Text(l10n.get('feeReason'),
  301. style: TextStyle(fontSize: AppFontSizes.subtitle, color: colors.textSecondary)),
  302. const SizedBox(width: 8),
  303. Expanded(
  304. child: Text(expense.purpose.isNotEmpty ? expense.purpose : '-',
  305. textAlign: TextAlign.end,
  306. maxLines: 2,
  307. overflow: TextOverflow.ellipsis,
  308. style: TextStyle(fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w500, color: colors.textPrimary)),
  309. ),
  310. ]),
  311. ),
  312. const SizedBox(height: 16),
  313. FormFieldRow(
  314. label: l10n.get('paymentMethod'),
  315. value: expense.paymentMethod.isNotEmpty ? expense.paymentMethod : '-',
  316. readOnly: true,
  317. showArrow: false),
  318. const SizedBox(height: 16),
  319. SizedBox(
  320. height: 24,
  321. child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
  322. Text(l10n.get('remark'),
  323. style: TextStyle(fontSize: AppFontSizes.subtitle, color: colors.textSecondary)),
  324. const SizedBox(width: 8),
  325. Expanded(
  326. child: Text(expense.remark.isNotEmpty ? expense.remark : '-',
  327. textAlign: TextAlign.end,
  328. maxLines: 2,
  329. overflow: TextOverflow.ellipsis,
  330. style: TextStyle(fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w500, color: colors.textPrimary)),
  331. ),
  332. ]),
  333. ),
  334. ],
  335. );
  336. }
  337. // ═══ 费用明细 ═══
  338. Widget _buildExpenseDetailSection(
  339. ExpenseModel expense, AppLocalizations l10n, AppColorsExtension colors) {
  340. final totalAmount = expense.details.fold<double>(
  341. 0,
  342. (sum, d) => sum + d.totalAmount,
  343. );
  344. final totalApproved = expense.details.fold<double>(
  345. 0,
  346. (sum, d) => sum + d.approvedAmount,
  347. );
  348. return FormSection(
  349. title: l10n.get('expenseDetails'),
  350. leadingIcon: Icons.receipt_long_outlined,
  351. children: [
  352. if (expense.details.isEmpty)
  353. Padding(
  354. padding: const EdgeInsets.symmetric(vertical: 8),
  355. child: Text(l10n.get('noDetailData'),
  356. style: TextStyle(fontSize: AppFontSizes.body, color: colors.textPlaceholder)),
  357. )
  358. else
  359. ...expense.details.asMap().entries.map((e) {
  360. final d = e.value;
  361. final title = d.categoryName.isNotEmpty
  362. ? '${d.expenseCategory}/${d.categoryName}'
  363. : (d.remark.isNotEmpty ? d.remark : (d.acctSubjectId.isNotEmpty ? d.acctSubjectId : '--'));
  364. return Container(
  365. margin: const EdgeInsets.symmetric(vertical: 6),
  366. padding: const EdgeInsets.all(12),
  367. decoration: BoxDecoration(color: colors.bgPage, borderRadius: BorderRadius.circular(8)),
  368. child: Row(
  369. children: [
  370. Expanded(
  371. child: Column(
  372. crossAxisAlignment: CrossAxisAlignment.start,
  373. children: [
  374. Row(
  375. children: [
  376. Expanded(
  377. child: Text(title,
  378. style: TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w500, color: colors.textPrimary)),
  379. ),
  380. Column(
  381. crossAxisAlignment: CrossAxisAlignment.end,
  382. children: [
  383. Text('¥${d.totalAmount.toStringAsFixed(2)}',
  384. style: TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: colors.amountPrimary)),
  385. if (d.approvedAmount > 0)
  386. Text('¥${d.approvedAmount.toStringAsFixed(2)}',
  387. style: TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: colors.success)),
  388. ],
  389. ),
  390. ],
  391. ),
  392. const SizedBox(height: 2),
  393. Text('${l10n.get('amountExcludingTax')}: ¥${d.amount.toStringAsFixed(2)}',
  394. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  395. if (d.taxAmount > 0)
  396. Text('${l10n.get('taxAmount')}: ¥${d.taxAmount.toStringAsFixed(2)}',
  397. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  398. if (d.approvedAmount > 0)
  399. Text('${l10n.get('approvedAmount')}: ¥${d.approvedAmount.toStringAsFixed(2)}',
  400. style: TextStyle(fontSize: AppFontSizes.caption, fontWeight: FontWeight.w500, color: colors.success)),
  401. if (d.acctSubjectName.isNotEmpty)
  402. Text('${l10n.get('acctSubject')}: ${d.acctSubjectId}/${d.acctSubjectName}',
  403. maxLines: 1, overflow: TextOverflow.ellipsis,
  404. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  405. if (d.aeNo.isNotEmpty)
  406. Text('${l10n.get('expenseApplyNo')}: ${d.aeNo}',
  407. maxLines: 1, overflow: TextOverflow.ellipsis,
  408. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  409. if (d.aeDd.isNotEmpty)
  410. Text('${l10n.get('applyDate')}: ${d.aeDd}',
  411. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  412. if (d.projectName.isNotEmpty)
  413. Text('${l10n.get('project')}: ${d.projectId}/${d.projectName}',
  414. maxLines: 1, overflow: TextOverflow.ellipsis,
  415. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  416. if (d.costDeptName.isNotEmpty)
  417. Text('${l10n.get('dept')}: ${d.costDeptId}/${d.costDeptName}',
  418. maxLines: 1, overflow: TextOverflow.ellipsis,
  419. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  420. if (d.customerVendorName.isNotEmpty)
  421. Text('${l10n.get('customerVendor')}: ${d.customerVendorName}',
  422. maxLines: 1, overflow: TextOverflow.ellipsis,
  423. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  424. if (d.remark.isNotEmpty)
  425. Text(d.remark,
  426. maxLines: 2, overflow: TextOverflow.ellipsis,
  427. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  428. ],
  429. ),
  430. ),
  431. ],
  432. ),
  433. );
  434. }),
  435. if (expense.details.isNotEmpty) ...[
  436. const SizedBox(height: 8),
  437. Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
  438. Text(l10n.get('total'),
  439. style: TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: colors.textPrimary)),
  440. Column(
  441. crossAxisAlignment: CrossAxisAlignment.end,
  442. children: [
  443. Text('¥${totalAmount.toStringAsFixed(2)}',
  444. style: TextStyle(fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w700, color: colors.amountPrimary)),
  445. if (totalApproved > 0)
  446. Text('${l10n.get('approvedAmount')} ¥${totalApproved.toStringAsFixed(2)}',
  447. style: TextStyle(fontSize: AppFontSizes.caption, fontWeight: FontWeight.w600, color: colors.success)),
  448. ],
  449. ),
  450. ]),
  451. ],
  452. ],
  453. );
  454. }
  455. // ═══ 附件 ═══
  456. Widget _buildAttachmentSection(AppLocalizations l10n, AppColorsExtension colors) {
  457. if (!_attachAvailable) {
  458. return FormSection(
  459. title: l10n.get('attachments'),
  460. leadingIcon: Icons.attach_file_outlined,
  461. children: [Text(l10n.get('attachServiceUnavailable'),
  462. style: TextStyle(fontSize: AppFontSizes.body, color: colors.textPlaceholder))],
  463. );
  464. }
  465. final headerAtts = _attachments.where((a) => a.isHeader).toList();
  466. final bodyGroups = <int, List<BillAttachment>>{};
  467. for (final a in _attachments.where((a) => a.isBody)) {
  468. bodyGroups.putIfAbsent(a.srcItm, () => []).add(a);
  469. }
  470. final children = <Widget>[];
  471. if (_attachments.isEmpty) {
  472. children.add(Text(l10n.get('noAttachment'),
  473. style: TextStyle(fontSize: AppFontSizes.body, color: colors.textPlaceholder)));
  474. } else {
  475. // 表头附件
  476. if (headerAtts.isNotEmpty) {
  477. children.add(Padding(
  478. padding: const EdgeInsets.only(bottom: 8),
  479. child: Text(l10n.get('headerAttachments'),
  480. style: TextStyle(fontSize: AppFontSizes.caption, fontWeight: FontWeight.w600, color: colors.textSecondary)),
  481. ));
  482. for (final a in headerAtts) {
  483. children.add(_buildAttachmentRow(a, colors));
  484. }
  485. }
  486. // 表身附件(按明细行分组)
  487. for (final entry in bodyGroups.entries) {
  488. children.add(const SizedBox(height: 8));
  489. children.add(Padding(
  490. padding: const EdgeInsets.only(bottom: 8),
  491. child: Text('${l10n.get('detailLine')} ${entry.key}',
  492. style: TextStyle(fontSize: AppFontSizes.caption, fontWeight: FontWeight.w600, color: colors.textSecondary)),
  493. ));
  494. for (final a in entry.value) {
  495. children.add(_buildAttachmentRow(a, colors));
  496. }
  497. }
  498. }
  499. return FormSection(
  500. title: l10n.get('attachments'),
  501. leadingIcon: Icons.attach_file_outlined,
  502. children: children,
  503. );
  504. }
  505. Widget _buildAttachmentRow(BillAttachment a, AppColorsExtension colors) {
  506. return Container(
  507. margin: const EdgeInsets.symmetric(vertical: 4),
  508. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  509. decoration: BoxDecoration(
  510. color: colors.bgPage,
  511. borderRadius: BorderRadius.circular(8),
  512. ),
  513. child: Row(children: [
  514. Icon(_fileTypeIcon(a.ext), size: 24, color: colors.primary),
  515. const SizedBox(width: 10),
  516. Expanded(
  517. child: Text(a.fileName,
  518. maxLines: 1, overflow: TextOverflow.ellipsis,
  519. style: TextStyle(fontSize: AppFontSizes.body, color: colors.textPrimary)),
  520. ),
  521. ]),
  522. );
  523. }
  524. // ═══ 审核流程 — 使用 ApprovalTimeline 组件 ═══
  525. Widget _buildApprovalSection(
  526. AppLocalizations l10n, AppColorsExtension colors) {
  527. return FormSection(
  528. title: l10n.get('approvalFlow'),
  529. leadingIcon: Icons.fact_check_outlined,
  530. children: [
  531. ApprovalTimeline(
  532. records: _timelineRecords,
  533. chain: _timelineChain,
  534. currentApproverId: _timelineCurrentApproverId,
  535. ),
  536. ],
  537. );
  538. }
  539. IconData _fileTypeIcon(String ext) {
  540. switch (ext.toLowerCase()) {
  541. case 'pdf': return Icons.picture_as_pdf;
  542. case 'doc': case 'docx': return Icons.description;
  543. case 'xls': case 'xlsx': return Icons.table_chart;
  544. case 'jpg': case 'jpeg': case 'png': case 'gif': case 'bmp': return Icons.image_outlined;
  545. default: return Icons.insert_drive_file;
  546. }
  547. }
  548. }