expense_detail_page.dart 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. import 'dart:typed_data';
  2. import 'package:flutter/material.dart';
  3. import 'package:tdesign_flutter/tdesign_flutter.dart';
  4. import 'package:flutter_riverpod/flutter_riverpod.dart';
  5. import '../../shared/widgets/loading_dialog.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 '../../shared/widgets/app_skeletons.dart';
  10. import 'expense_model.dart';
  11. import '../../core/i18n/app_localizations.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 'dart:io';
  17. import 'package:path_provider/path_provider.dart';
  18. import 'package:open_filex/open_filex.dart';
  19. import '../expense_apply/widgets/attachment_preview_page.dart';
  20. import 'widgets/expense_detail_view_dialog.dart';
  21. class ExpenseDetailPage extends ConsumerStatefulWidget {
  22. final String billNo;
  23. final int queryId;
  24. const ExpenseDetailPage({super.key, required this.billNo, this.queryId = 0});
  25. @override
  26. ConsumerState<ExpenseDetailPage> createState() => _ExpenseDetailPageState();
  27. }
  28. class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
  29. {
  30. ExpenseModel? _expense;
  31. List<BillAttachment> _attachments = [];
  32. bool _attachAvailable = false;
  33. bool _isLoading = true;
  34. String? _error;
  35. @override
  36. void initState() {
  37. super.initState();
  38. _loadData();
  39. }
  40. @override
  41. void dispose() {
  42. super.dispose();
  43. }
  44. Future<void> _loadData() async {
  45. setState(() {
  46. _isLoading = true;
  47. _error = null;
  48. });
  49. try {
  50. final api = ref.read(expenseApiProvider);
  51. // 1. 加载报销详情(主表 + 明细)
  52. final expense = await api.fetchDetail(widget.billNo);
  53. setState(() => _expense = expense);
  54. // 2. 加载附件(非致命)
  55. try {
  56. _attachAvailable = await api.checkAttachHealth();
  57. } catch (_) {
  58. _attachAvailable = false;
  59. }
  60. if (_attachAvailable) {
  61. try {
  62. _attachments = await api.getAttachments('BX', widget.billNo);
  63. } catch (_) {
  64. _attachments = [];
  65. }
  66. }
  67. } catch (e) {
  68. setState(() => _error = e.toString());
  69. } finally {
  70. setState(() { _isLoading = false; });
  71. }
  72. }
  73. @override
  74. Widget build(BuildContext context) {
  75. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  76. final l10n = AppLocalizations.of(context);
  77. if (_isLoading) {
  78. return const SkeletonDetailPage();
  79. }
  80. if (_error != null) {
  81. return Center(
  82. child: Column(
  83. mainAxisAlignment: MainAxisAlignment.center,
  84. children: [
  85. Icon(Icons.error_outline, size: 48, color: colors.danger),
  86. const SizedBox(height: 12),
  87. Text(
  88. _error!,
  89. style: TextStyle(fontSize: AppFontSizes.body, color: colors.danger),
  90. textAlign: TextAlign.center,
  91. ),
  92. const SizedBox(height: 16),
  93. TDButton(
  94. text: l10n.get('retry'),
  95. theme: TDButtonTheme.primary,
  96. onTap: _loadData,
  97. ),
  98. ],
  99. ),
  100. );
  101. }
  102. final expense = _expense!;
  103. return Expanded(
  104. child: SingleChildScrollView(
  105. physics: const AlwaysScrollableScrollPhysics(),
  106. padding: const EdgeInsets.all(16),
  107. child: Column(
  108. children: [
  109. _buildBasicInfoSection(expense, l10n, colors),
  110. const SizedBox(height: 16),
  111. _buildExpenseDetailSection(expense, l10n, colors),
  112. const SizedBox(height: 16),
  113. _buildAttachmentSection(l10n, colors),
  114. const SizedBox(height: 24),
  115. _buildPageFooter(colors),
  116. ],
  117. ),
  118. ),
  119. );
  120. }
  121. // ═══ 基本信息 ═══
  122. Widget _buildBasicInfoSection(
  123. ExpenseModel expense, AppLocalizations l10n, AppColorsExtension colors) {
  124. return FormSection(
  125. title: l10n.get('basicInfo'),
  126. leadingIcon: Icons.info_outline,
  127. children: [
  128. FormFieldRow(
  129. label: l10n.get('expenseNo'),
  130. value: expense.expenseNo,
  131. readOnly: true,
  132. showArrow: false),
  133. const SizedBox(height: 16),
  134. FormFieldRow(
  135. label: l10n.get('date'),
  136. value: du.DateUtils.formatDate(expense.createTime),
  137. readOnly: true,
  138. showArrow: false),
  139. const SizedBox(height: 16),
  140. FormFieldRow(
  141. label: l10n.get('expensePersonnel'),
  142. value: expense.applicantId.isNotEmpty
  143. ? '${expense.applicantId}/${expense.applicantName}'
  144. : expense.applicantName,
  145. readOnly: true,
  146. showArrow: false),
  147. const SizedBox(height: 16),
  148. FormFieldRow(
  149. label: l10n.get('expenseDept'),
  150. value: expense.deptId.isNotEmpty
  151. ? '${expense.deptId}/${expense.deptName}'
  152. : expense.deptName,
  153. readOnly: true,
  154. showArrow: false),
  155. const SizedBox(height: 16),
  156. SizedBox(
  157. height: 24,
  158. child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
  159. Text(l10n.get('expenseReason'),
  160. style: TextStyle(fontSize: AppFontSizes.subtitle, color: colors.textSecondary)),
  161. const SizedBox(width: 8),
  162. Expanded(
  163. child: Text(expense.purpose.isNotEmpty ? expense.purpose : '-',
  164. textAlign: TextAlign.end,
  165. maxLines: 2,
  166. overflow: TextOverflow.ellipsis,
  167. style: TextStyle(fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w500, color: colors.textPrimary)),
  168. ),
  169. ]),
  170. ),
  171. const SizedBox(height: 16),
  172. FormFieldRow(
  173. label: l10n.get('voucherNo'),
  174. value: expense.voucherNo.isNotEmpty ? expense.voucherNo : '-',
  175. readOnly: true,
  176. showArrow: false),
  177. const SizedBox(height: 16),
  178. FormFieldRow(
  179. label: l10n.get('currency'),
  180. value: expense.currencyCode.isNotEmpty ? expense.currencyCode : '-',
  181. readOnly: true,
  182. showArrow: false),
  183. const SizedBox(height: 16),
  184. FormFieldRow(
  185. label: l10n.get('paymentMethod'),
  186. value: expense.paymentMethod.isNotEmpty ? expense.paymentMethod : '-',
  187. readOnly: true,
  188. showArrow: false),
  189. const SizedBox(height: 16),
  190. SizedBox(
  191. height: 24,
  192. child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
  193. Text(l10n.get('remark'),
  194. style: TextStyle(fontSize: AppFontSizes.subtitle, color: colors.textSecondary)),
  195. const SizedBox(width: 8),
  196. Expanded(
  197. child: Text(expense.remark.isNotEmpty ? expense.remark : '-',
  198. textAlign: TextAlign.end,
  199. maxLines: 2,
  200. overflow: TextOverflow.ellipsis,
  201. style: TextStyle(fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w500, color: colors.textPrimary)),
  202. ),
  203. ]),
  204. ),
  205. ],
  206. );
  207. }
  208. // ═══ 费用明细 ═══
  209. Widget _buildExpenseDetailSection(
  210. ExpenseModel expense, AppLocalizations l10n, AppColorsExtension colors) {
  211. final totalAmount = expense.details.fold<double>(
  212. 0,
  213. (sum, d) => sum + d.totalAmount,
  214. );
  215. final totalApproved = expense.details.fold<double>(
  216. 0,
  217. (sum, d) => sum + d.approvedAmount,
  218. );
  219. return FormSection(
  220. title: l10n.get('expenseDetails'),
  221. leadingIcon: Icons.receipt_long_outlined,
  222. children: [
  223. if (expense.details.isEmpty)
  224. Padding(
  225. padding: const EdgeInsets.symmetric(vertical: 8),
  226. child: Text(l10n.get('noDetailData'),
  227. style: TextStyle(fontSize: AppFontSizes.body, color: colors.textPlaceholder)),
  228. )
  229. else
  230. ...expense.details.asMap().entries.map((e) {
  231. final d = e.value;
  232. final title = d.categoryName.isNotEmpty
  233. ? '${d.expenseCategory}/${d.categoryName}'
  234. : d.expenseCategory;
  235. return GestureDetector(
  236. onTap: () => _showExpenseDetailDialog(context, d),
  237. child: Container(
  238. margin: const EdgeInsets.symmetric(vertical: 6),
  239. padding: const EdgeInsets.all(12),
  240. decoration: BoxDecoration(color: colors.bgPage, borderRadius: BorderRadius.circular(8)),
  241. child: Row(
  242. children: [
  243. Expanded(
  244. child: Column(
  245. crossAxisAlignment: CrossAxisAlignment.start,
  246. children: [
  247. Row(
  248. children: [
  249. Expanded(
  250. child: Text(title,
  251. style: TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w500, color: colors.textPrimary)),
  252. ),
  253. Column(
  254. crossAxisAlignment: CrossAxisAlignment.end,
  255. children: [
  256. Text('¥${d.totalAmount.toStringAsFixed(2)}',
  257. style: TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: colors.amountPrimary)),
  258. if (d.approvedAmount > 0)
  259. Text('¥${d.approvedAmount.toStringAsFixed(2)}',
  260. style: TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: colors.success)),
  261. ],
  262. ),
  263. ],
  264. ),
  265. const SizedBox(height: 2),
  266. Text('${l10n.get('amountExcludingTax')}: ¥${d.amount.toStringAsFixed(2)}',
  267. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  268. if (d.taxAmount > 0)
  269. Text('${l10n.get('taxAmount')}: ¥${d.taxAmount.toStringAsFixed(2)}',
  270. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  271. if (d.taxRate > 0)
  272. Text('${l10n.get('taxRate')}: ${d.taxRate.toStringAsFixed(0)}%',
  273. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  274. if (d.acctSubjectName.isNotEmpty)
  275. Text('${l10n.get('acctSubject')}: ${d.acctSubjectId}/${d.acctSubjectName}',
  276. maxLines: 1, overflow: TextOverflow.ellipsis,
  277. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  278. if (d.aeNo.isNotEmpty)
  279. Text('${l10n.get('expenseApplyNo')}: ${d.aeNo}',
  280. maxLines: 1, overflow: TextOverflow.ellipsis,
  281. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  282. if (d.aeDd.isNotEmpty)
  283. Text('${l10n.get('applyDate')}: ${d.aeDd}',
  284. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  285. if (d.projectName.isNotEmpty)
  286. Text('${l10n.get('project')}: ${d.projectId}/${d.projectName}',
  287. maxLines: 1, overflow: TextOverflow.ellipsis,
  288. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  289. if (d.costDeptName.isNotEmpty)
  290. Text('${l10n.get('costDept')}: ${d.costDeptId}/${d.costDeptName}',
  291. maxLines: 1, overflow: TextOverflow.ellipsis,
  292. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  293. if (d.customerVendorName.isNotEmpty)
  294. Text('${l10n.get('customerVendor')}: ${d.customerVendorId}/${d.customerVendorName}',
  295. maxLines: 1, overflow: TextOverflow.ellipsis,
  296. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  297. if (d.sqManName.isNotEmpty)
  298. Text('${l10n.get('applicant')}: ${d.sqMan}/${d.sqManName}',
  299. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  300. if (d.bankAccountName.isNotEmpty)
  301. Text('${l10n.get('bankAccountName')}: ${d.bankAccountName}',
  302. maxLines: 1, overflow: TextOverflow.ellipsis,
  303. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  304. if (d.bankName.isNotEmpty)
  305. Text('${l10n.get('bankName')}: ${d.bankName}',
  306. maxLines: 1, overflow: TextOverflow.ellipsis,
  307. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  308. if (d.bankAccount.isNotEmpty)
  309. Text('${l10n.get('bankAccount')}: ${d.bankAccount}',
  310. maxLines: 1, overflow: TextOverflow.ellipsis,
  311. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  312. if (d.remark.isNotEmpty)
  313. Text('${l10n.get('remark')}: ${d.remark}',
  314. maxLines: 2, overflow: TextOverflow.ellipsis,
  315. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
  316. ],
  317. ),
  318. ),
  319. ],
  320. ),
  321. ),
  322. );
  323. }),
  324. if (expense.details.isNotEmpty) ...[
  325. const SizedBox(height: 8),
  326. Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
  327. Text(l10n.get('total'),
  328. style: TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: colors.textPrimary)),
  329. Column(
  330. crossAxisAlignment: CrossAxisAlignment.end,
  331. children: [
  332. Text('¥${totalAmount.toStringAsFixed(2)}',
  333. style: TextStyle(fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w700, color: colors.amountPrimary)),
  334. if (totalApproved > 0)
  335. Text('${l10n.get('approvedAmount')} ¥${totalApproved.toStringAsFixed(2)}',
  336. style: TextStyle(fontSize: AppFontSizes.caption, fontWeight: FontWeight.w600, color: colors.success)),
  337. ],
  338. ),
  339. ]),
  340. ],
  341. ],
  342. );
  343. }
  344. void _showExpenseDetailDialog(BuildContext context, ExpenseDetailModel d) {
  345. ExpenseDetailViewDialog.show(context, d);
  346. }
  347. // ═══ 附件 ═══
  348. Widget _buildAttachmentSection(AppLocalizations l10n, AppColorsExtension colors) {
  349. if (!_attachAvailable) {
  350. return FormSection(
  351. title: l10n.get('attachments'),
  352. leadingIcon: Icons.attach_file_outlined,
  353. children: [Text(l10n.get('attachServiceUnavailable'),
  354. style: TextStyle(fontSize: AppFontSizes.body, color: colors.textPlaceholder))],
  355. );
  356. }
  357. final headerAtts = _attachments.where((a) => a.isHeader).toList();
  358. final bodyGroups = <int, List<BillAttachment>>{};
  359. for (final a in _attachments.where((a) => a.isBody)) {
  360. bodyGroups.putIfAbsent(a.srcItm, () => []).add(a);
  361. }
  362. final children = <Widget>[];
  363. if (_attachments.isEmpty) {
  364. children.add(Text(l10n.get('noAttachment'),
  365. style: TextStyle(fontSize: AppFontSizes.body, color: colors.textPlaceholder)));
  366. } else {
  367. // 表头附件
  368. if (headerAtts.isNotEmpty) {
  369. children.add(Padding(
  370. padding: const EdgeInsets.only(bottom: 8),
  371. child: Text(l10n.get('headerAttachments'),
  372. style: TextStyle(fontSize: AppFontSizes.caption, fontWeight: FontWeight.w600, color: colors.textSecondary)),
  373. ));
  374. for (final a in headerAtts) {
  375. children.add(_buildAttachmentRow(a, colors));
  376. }
  377. }
  378. // 表身附件(按明细行分组)
  379. for (final entry in bodyGroups.entries) {
  380. children.add(const SizedBox(height: 8));
  381. children.add(Padding(
  382. padding: const EdgeInsets.only(bottom: 8),
  383. child: Text('${l10n.get('detailLine')} ${entry.key}',
  384. style: TextStyle(fontSize: AppFontSizes.caption, fontWeight: FontWeight.w600, color: colors.textSecondary)),
  385. ));
  386. for (final a in entry.value) {
  387. children.add(_buildAttachmentRow(a, colors));
  388. }
  389. }
  390. }
  391. return FormSection(
  392. title: l10n.get('attachments'),
  393. leadingIcon: Icons.attach_file_outlined,
  394. children: children,
  395. );
  396. }
  397. Widget _buildAttachmentRow(BillAttachment a, AppColorsExtension colors) {
  398. final isImage = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].contains(a.ext.toLowerCase());
  399. return GestureDetector(
  400. onTap: () => _openAttachment(a),
  401. child: Container(
  402. margin: const EdgeInsets.symmetric(vertical: 4),
  403. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  404. decoration: BoxDecoration(
  405. color: colors.bgPage,
  406. borderRadius: BorderRadius.circular(8),
  407. ),
  408. child: Row(children: [
  409. if (isImage)
  410. _ExpAttachmentThumbnail(api: ref.read(expenseApiProvider), attachment: a, size: 40)
  411. else
  412. Icon(_fileTypeIcon(a.ext), size: 40, color: colors.primary),
  413. const SizedBox(width: 10),
  414. Expanded(
  415. child: Text(a.fileName,
  416. maxLines: 1, overflow: TextOverflow.ellipsis,
  417. style: TextStyle(fontSize: AppFontSizes.body, color: colors.textPrimary)),
  418. ),
  419. ]),
  420. ),
  421. );
  422. }
  423. Future<void> _openAttachment(BillAttachment a) async {
  424. final l10n = AppLocalizations.of(context);
  425. final ext = a.ext.toLowerCase();
  426. final isImage = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].contains(ext);
  427. if (isImage) {
  428. // 图片 → 弹窗预览,内部自动下载并显示 loading
  429. final api = ref.read(expenseApiProvider);
  430. AttachmentPreview.show(context,
  431. loader: api.downloadAttachment(a.id),
  432. fileName: a.fileName,
  433. loadingText: l10n.get('loading'),
  434. );
  435. return;
  436. }
  437. // 非图片 → 下载后调用系统工具打开
  438. try {
  439. LoadingDialog.show(context, text: l10n.get('downloading'));
  440. final api = ref.read(expenseApiProvider);
  441. final bytes = await api.downloadAttachment(a.id);
  442. if (!mounted) return;
  443. LoadingDialog.hide(context);
  444. if (bytes == null) {
  445. TDToast.showText(l10n.get('downloadFailed'), context: context);
  446. return;
  447. }
  448. final dir = await getTemporaryDirectory();
  449. final file = File('${dir.path}/${a.fileName}');
  450. await file.writeAsBytes(bytes);
  451. await OpenFilex.open(file.path);
  452. } catch (_) {
  453. if (mounted) LoadingDialog.hide(context);
  454. if (mounted) TDToast.showText(l10n.get('openFailed'), context: context);
  455. }
  456. }
  457. IconData _fileTypeIcon(String ext) {
  458. switch (ext.toLowerCase()) {
  459. case 'pdf': return Icons.picture_as_pdf;
  460. case 'doc': case 'docx': return Icons.description;
  461. case 'xls': case 'xlsx': return Icons.table_chart;
  462. case 'jpg': case 'jpeg': case 'png': case 'gif': case 'bmp': return Icons.image_outlined;
  463. default: return Icons.insert_drive_file;
  464. }
  465. }
  466. Widget _buildPageFooter(AppColorsExtension colors) {
  467. final l10n = AppLocalizations.of(context);
  468. return Center(
  469. child: Padding(
  470. padding: const EdgeInsets.only(bottom: 16),
  471. child: Row(
  472. mainAxisSize: MainAxisSize.min,
  473. children: [
  474. Icon(Icons.rocket_launch_outlined, size: 16, color: colors.textPlaceholder),
  475. const SizedBox(width: 6),
  476. Text(l10n.get('pageFooter'),
  477. style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textPlaceholder)),
  478. ],
  479. ),
  480. ),
  481. );
  482. }
  483. }
  484. /// 附件缩略图 — 自动调用 DownloadAttachment 加载图片
  485. class _ExpAttachmentThumbnail extends StatefulWidget {
  486. final ExpenseApi api;
  487. final BillAttachment attachment;
  488. final double size;
  489. const _ExpAttachmentThumbnail({required this.api, required this.attachment, required this.size});
  490. @override
  491. State<_ExpAttachmentThumbnail> createState() => _ExpAttachmentThumbnailState();
  492. }
  493. class _ExpAttachmentThumbnailState extends State<_ExpAttachmentThumbnail> {
  494. Uint8List? _bytes;
  495. bool _loading = true;
  496. @override
  497. void initState() {
  498. super.initState();
  499. _load();
  500. }
  501. Future<void> _load() async {
  502. try {
  503. final bytes = await widget.api.downloadAttachment(widget.attachment.id);
  504. if (mounted) setState(() { _bytes = bytes; _loading = false; });
  505. } catch (_) {
  506. if (mounted) setState(() => _loading = false);
  507. }
  508. }
  509. @override
  510. Widget build(BuildContext context) {
  511. if (_loading) {
  512. return SizedBox(width: widget.size, height: widget.size,
  513. child: const Center(child: SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))));
  514. }
  515. if (_bytes != null) {
  516. return ClipRRect(
  517. borderRadius: BorderRadius.circular(4),
  518. child: Image.memory(_bytes!, width: widget.size, height: widget.size, fit: BoxFit.cover),
  519. );
  520. }
  521. return Icon(Icons.broken_image, size: widget.size * 0.6, color: Colors.grey);
  522. }
  523. }