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'; import '../../core/i18n/app_localizations.dart'; import '../../shared/widgets/action_bar.dart'; import '../../shared/widgets/form_section.dart'; import '../../shared/widgets/form_field_row.dart'; import '../shell/nav_bar_config.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors_extension.dart'; class ExpenseApplicationApplyPage extends ConsumerStatefulWidget { final String? id; const ExpenseApplicationApplyPage({super.key, this.id}); @override ConsumerState createState() => _ExpenseApplicationApplyPageState(); } class _ExpenseApplicationApplyPageState extends ConsumerState { // ── 基本信息 ── int _urgency = 0; // 0=普通, 1=紧急, 2=特急 static const _urgencyLabels = ['普通', '紧急', '特急']; final Set _expenseTypes = {}; static const _expenseTypeOptions = [ ('travel', '差旅费'), ('entertainment', '业务招待费'), ('procurement', '日常采购'), ('activity', '活动经费'), ('office', '办公费'), ('meeting', '会议费'), ('training', '培训费'), ]; bool _isTaxIncluded = false; final _purposeController = TextEditingController(); // ── 关联管控 ── String? _selectedProjectName; int? _selectedProjectId; String? _selectedSubjectName; int? _selectedSubjectId; final _availableBudget = 50000.00; final _referenceNoController = TextEditingController(); // ── 费用明细 ── final List<_DetailItem> _details = []; int _detailIdCounter = 1; // ── 附件 ── final List _attachments = []; // mock file names // ── 专用字段 ── String _estimatedStartDate = ''; String _estimatedEndDate = ''; String _entertainmentTarget = ''; String _venue = ''; @override void dispose() { _purposeController.dispose(); _referenceNoController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: l10n.get('expenseApplyRequest'), showBack: true, onBack: () { if (_hasUnsaved()) { _showConfirmDialog( '确认退出', '当前内容尚未保存,是否退出?', '继续编辑', '放弃并退出', () => context.pop(), ); } else { context.pop(); } }, ), ); return PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) { if (!didPop) { if (_hasUnsaved()) { _showConfirmDialog( '确认退出', '当前内容尚未保存,是否退出?', '继续编辑', '放弃并退出', () => context.pop(), ); } else { context.pop(); } } }, child: Column( children: [ Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: [ _buildBasicInfo(l10n), const SizedBox(height: 16), _buildTypeSpecificFields(l10n), const SizedBox(height: 16), _buildControlSection(l10n), const SizedBox(height: 16), _buildDetailsSection(l10n), const SizedBox(height: 16), _buildAttachmentSection(l10n), const SizedBox(height: 80), ], ), ), ), _buildBottomBar(l10n), ], ), ); } // ═══ 1. 基本信息 ═══ Widget _buildBasicInfo(AppLocalizations l10n) { final colors = Theme.of(context).extension()!; return FormSection( title: l10n.get('basicInfo'), children: [ FormFieldRow( label: l10n.get('applicant'), value: '张三', readOnly: true, showArrow: false, ), FormFieldRow( label: l10n.get('department'), value: '技术部', readOnly: true, showArrow: false, ), FormFieldRow( label: l10n.get('date'), value: _today(), readOnly: true, showArrow: false, ), const SizedBox(height: 12), _label(l10n.get('emergencyLevel')), const SizedBox(height: 6), _buildUrgencyRadio(), const SizedBox(height: 12), _label(l10n.get('expenseType')), const SizedBox(height: 6), Wrap( spacing: 8, runSpacing: 8, children: _expenseTypeOptions.map((opt) { final sel = _expenseTypes.contains(opt.$1); return GestureDetector( onTap: () => setState( () => sel ? _expenseTypes.remove(opt.$1) : _expenseTypes.add(opt.$1), ), child: TDTag( opt.$2, size: TDTagSize.medium, theme: sel ? TDTagTheme.primary : TDTagTheme.defaultTheme, isOutline: !sel, ), ); }).toList(), ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _label(l10n.get('isTaxIncluded')), TDSwitch( isOn: _isTaxIncluded, onChanged: (v) { setState(() => _isTaxIncluded = v); return true; }, ), ], ), const SizedBox(height: 12), _label(l10n.get('feeReason')), const SizedBox(height: 4), TDTextarea( controller: _purposeController, hintText: l10n.get('enterFeeReason'), maxLength: 200, backgroundColor: colors.bgPage, ), const SizedBox(height: 12), FormFieldRow( label: l10n.get('validUntil'), hint: l10n.get('pleaseSelect'), onTap: () => _pickDate((d) {}), ), ], ); } Widget _buildUrgencyRadio() { final colors = Theme.of(context).extension()!; return Row( children: List.generate(3, (i) { final sel = _urgency == i; return Padding( padding: EdgeInsets.only(right: i < 2 ? 24 : 0), child: GestureDetector( onTap: () => setState(() => _urgency = i), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 18, height: 18, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: sel ? colors.primary : colors.textPlaceholder, width: 2, ), ), child: sel ? Center( child: Container( width: 8, height: 8, decoration: BoxDecoration( shape: BoxShape.circle, color: colors.primary, ), ), ) : null, ), const SizedBox(width: 6), Text( _urgencyLabels[i], style: TextStyle( fontSize: AppFontSizes.body, color: sel ? colors.primary : colors.textSecondary, ), ), ], ), ), ); }), ); } // ═══ 2. 类型专用字段 ═══ Widget _buildTypeSpecificFields(AppLocalizations l10n) { final ws = []; if (_expenseTypes.contains('travel')) ws.add(_buildTravelFields(l10n)); if (_expenseTypes.contains('entertainment')) { ws.add(const SizedBox(height: 16)); ws.add(_buildEntertainmentFields(l10n)); } if (_expenseTypes.contains('meeting')) { ws.add(const SizedBox(height: 16)); ws.add(_buildMeetingFields(l10n)); } return ws.isEmpty ? const SizedBox.shrink() : Column(children: ws); } Widget _buildTravelFields(AppLocalizations l10n) { return FormSection( title: '差旅费专用', children: [ FormFieldRow( label: l10n.get('estimatedStartDate'), value: _estimatedStartDate, hint: l10n.get('pleaseSelect'), onTap: () => _pickDate((d) => setState(() => _estimatedStartDate = d)), ), FormFieldRow( label: l10n.get('estimatedEndDate'), value: _estimatedEndDate, hint: l10n.get('pleaseSelect'), onTap: () => _pickDate((d) => setState(() => _estimatedEndDate = d)), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _label(l10n.get('isOvernight')), TDSwitch(onChanged: (_) => true), ], ), const SizedBox(height: 8), FormFieldRow( label: l10n.get('transportType'), value: '高铁/动车', onTap: () => _showListPicker('选择交通工具', [ '飞机', '高铁/动车', '火车(普速)', '自驾', ], (_) {}), ), ], ); } Widget _buildEntertainmentFields(AppLocalizations l10n) { return FormSection( title: '招待费专用', children: [ FormFieldRow( label: '招待对象单位', value: _entertainmentTarget, hint: '请输入', onTap: () => _showTextInput( '招待对象单位', (v) => setState(() => _entertainmentTarget = v), ), ), FormFieldRow( label: '招待层级', value: '普通', onTap: () => _showListPicker('选择招待层级', ['普通', '重要', 'VIP'], (_) {}), ), FormFieldRow( label: '外部人数', value: '3', onTap: () => _showNumberInput('外部人数', (_) {}), ), FormFieldRow( label: '内部陪同人数', value: '2', onTap: () => _showNumberInput('内部陪同人数', (_) {}), ), FormFieldRow( label: l10n.get('venue'), value: _venue, hint: '请输入地点', onTap: () => _showTextInput( l10n.get('venue'), (v) => setState(() => _venue = v), ), ), ], ); } Widget _buildMeetingFields(AppLocalizations l10n) { return FormSection( title: '会议费专用', children: [ FormFieldRow( label: l10n.get('estimatedStartDate'), hint: l10n.get('pleaseSelect'), onTap: () => _pickDate((_) {}), ), FormFieldRow( label: l10n.get('estimatedEndDate'), hint: l10n.get('pleaseSelect'), onTap: () => _pickDate((_) {}), ), FormFieldRow( label: l10n.get('venue'), value: _venue, hint: '请输入会议地点', onTap: () => _showTextInput('会议地点', (_) {}), ), ], ); } // ═══ 3. 关联管控 ═══ static const _mockProjects = [ ('华东市场拓展', 100), ('ERP系统升级', 101), ('新产品研发', 102), ('华南渠道建设', 103), ]; static const _mockSubjects = [('差旅费', 5), ('招待费', 6), ('办公费', 7), ('培训费', 8)]; Widget _buildControlSection(AppLocalizations l10n) { return FormSection( title: l10n.get('relatedControl'), children: [ FormFieldRow( label: l10n.get('relatedProject'), value: _selectedProjectName, hint: l10n.get('selectProject'), onTap: () { _showListPicker('选择关联项目', _mockProjects.map((p) => p.$1).toList(), ( v, ) { final p = _mockProjects.firstWhere((x) => x.$1 == v); setState(() { _selectedProjectId = p.$2; _selectedProjectName = p.$1; _selectedSubjectName = null; _selectedSubjectId = null; }); }); }, ), FormFieldRow( label: l10n.get('budgetSubject'), value: _selectedSubjectName, hint: l10n.get('selectSubject'), onTap: _selectedProjectId != null ? () { _showListPicker( '选择预算科目', _mockSubjects.map((s) => s.$1).toList(), (v) { final s = _mockSubjects.firstWhere((x) => x.$1 == v); setState(() { _selectedSubjectId = s.$2; _selectedSubjectName = s.$1; }); }, ); } : null, ), _buildBudgetRow(l10n), const SizedBox(height: 8), FormFieldRow( label: '关联合同号', value: _referenceNoController.text, hint: '选填', onTap: () => _showTextInput( '关联合同号', (v) => setState(() { _referenceNoController.text = v; _referenceNoController.selection = TextSelection.fromPosition( TextPosition(offset: v.length), ); }), ), ), ], ); } Widget _buildBudgetRow(AppLocalizations l10n) { final colors = Theme.of(context).extension()!; final over = _totalAmount() > _availableBudget; return SizedBox( height: 44, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( l10n.get('availableBudget'), style: TextStyle( fontSize: AppFontSizes.body, color: colors.textSecondary, ), ), Text( '¥${_availableBudget.toStringAsFixed(2)}', style: TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w700, color: over ? colors.danger : colors.amountPrimary, ), ), ], ), ); } // ═══ 4. 费用明细 ═══ Widget _buildDetailsSection(AppLocalizations l10n) { final colors = Theme.of(context).extension()!; return FormSection( title: l10n.get('expenseDetails'), showAction: true, actionText: l10n.get('add'), onActionTap: _showDetailDialog, children: [ if (_details.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text( l10n.get('noDetailHint'), style: TextStyle( fontSize: AppFontSizes.body, color: colors.textPlaceholder, ), ), ) else ..._details.asMap().entries.map((e) { final d = e.value; return Container( padding: const EdgeInsets.symmetric(vertical: 6), decoration: BoxDecoration( border: e.key < _details.length - 1 ? Border(bottom: BorderSide(color: colors.border)) : null, ), child: Row( children: [ Expanded( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( d.categoryName, style: TextStyle( fontSize: AppFontSizes.body, color: colors.textPrimary, ), ), if (d.remark.isNotEmpty) Text( d.remark, style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textPlaceholder, ), ), ], ), ), Text( '${d.quantity}×¥${d.unitPrice.toStringAsFixed(2)}', style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textSecondary, ), ), const SizedBox(width: 8), Text( '¥${d.amount.toStringAsFixed(2)}', style: TextStyle( fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: colors.amountPrimary, ), ), GestureDetector( onTap: () => setState(() => _details.removeAt(e.key)), child: Icon( Icons.close, size: 16, color: colors.textPlaceholder, ), ), ], ), ); }), Container(height: 1, color: colors.border), Container( height: 36, padding: const EdgeInsets.symmetric(vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( l10n.get('total'), style: TextStyle( fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: colors.textPrimary, ), ), Text( '¥${_totalAmount().toStringAsFixed(2)}', style: TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w700, color: colors.amountPrimary, ), ), ], ), ), if (_totalAmount() > _availableBudget) Padding( padding: const EdgeInsets.only(top: 8), child: Row( children: [ Icon(Icons.warning_amber, size: 14, color: colors.danger), const SizedBox(width: 6), Expanded( child: Text( '您的申请金额已超支,提交后将自动触发高管特批流程', style: TextStyle( fontSize: AppFontSizes.caption, color: colors.danger, ), ), ), ], ), ), ], ); } double _totalAmount() => _details.fold(0, (s, d) => s + d.amount); static const _detailCategories = [ ('transport', '交通费'), ('hotel', '住宿费'), ('office_supplies', '办公用品'), ('meals', '餐饮费'), ('materials', '材料费'), ('service', '服务费'), ('other', '其他'), ]; static const _units = ['张', '间', '人', '天', '套', '个']; void _showDetailDialog() { final colors = Theme.of(context).extension()!; String cat = 'transport'; String unit = '张'; final qtyCtrl = TextEditingController(text: '1'); final priceCtrl = TextEditingController(); final remarkCtrl = TextEditingController(); showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setDlg) => TDAlertDialog( title: '添加费用明细', contentWidget: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _label('费用类别'), const SizedBox(height: 4), GestureDetector( onTap: () { Navigator.pop(ctx); _showListPicker( '选择费用类别', _detailCategories.map((c) => c.$2).toList(), (v) { cat = _detailCategories.firstWhere((c) => c.$2 == v).$1; _showDetailDialog(); }, ); }, child: Container( height: 44, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: colors.bgPage, borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _detailCategories.firstWhere((c) => c.$1 == cat).$2, style: const TextStyle(fontSize: AppFontSizes.body), ), Icon( Icons.arrow_drop_down, color: colors.textPlaceholder, ), ], ), ), ), const SizedBox(height: 12), Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _label('数量'), const SizedBox(height: 4), TDInput(controller: qtyCtrl, hintText: '>0'), ], ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _label('单位'), const SizedBox(height: 4), GestureDetector( onTap: () { Navigator.pop(ctx); _showListPicker('选择单位', _units, (v) { unit = v; _showDetailDialog(); }); }, child: Container( height: 44, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: colors.bgPage, borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( unit, style: const TextStyle( fontSize: AppFontSizes.body, ), ), Icon( Icons.arrow_drop_down, color: colors.textPlaceholder, ), ], ), ), ), ], ), ), ], ), const SizedBox(height: 12), _label('单价'), const SizedBox(height: 4), TDInput(controller: priceCtrl, hintText: '>0'), const SizedBox(height: 12), _label('明细说明'), const SizedBox(height: 4), TDInput(controller: remarkCtrl, hintText: '选填'), ], ), leftBtn: TDDialogButtonOptions( title: '取消', action: () => Navigator.pop(ctx), ), rightBtn: TDDialogButtonOptions( title: '确定', titleColor: colors.primary, action: () { final q = int.tryParse(qtyCtrl.text) ?? 0; final p = double.tryParse(priceCtrl.text) ?? 0; if (q <= 0 || p <= 0) { TDToast.showText('数量和单价必须大于0', context: context); return; } setState( () => _details.add( _DetailItem( id: _detailIdCounter++, category: cat, categoryName: _detailCategories .firstWhere((c) => c.$1 == cat) .$2, quantity: q, unit: unit, unitPrice: p, amount: q * p, remark: remarkCtrl.text, ), ), ); Navigator.pop(ctx); }, ), ), ), ); } // ═══ 5. 附件上传 ═══ Widget _buildAttachmentSection(AppLocalizations l10n) { final colors = Theme.of(context).extension()!; return FormSection( title: l10n.get('attachmentUpload'), children: [ Text( l10n.get('maxAttachment'), style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textPlaceholder, ), ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ ..._attachments.asMap().entries.map( (e) => Stack( clipBehavior: Clip.none, children: [ Container( width: 80, height: 80, decoration: BoxDecoration( color: colors.primaryLight, borderRadius: BorderRadius.circular(4), ), child: Center( child: Icon(Icons.image, color: colors.primary, size: 32), ), ), Positioned( right: -4, top: -4, child: GestureDetector( onTap: () => setState(() => _attachments.removeAt(e.key)), child: Container( width: 20, height: 20, decoration: BoxDecoration( color: colors.danger, shape: BoxShape.circle, ), child: const Icon( Icons.close, size: 12, color: Colors.white, ), ), ), ), ], ), ), if (_attachments.length < 9) GestureDetector( onTap: () { // Mock: add attachment setState( () => _attachments.add( '附件_${DateTime.now().millisecondsSinceEpoch}.jpg', ), ); TDToast.showText('已添加附件(mock)', context: context); }, child: Container( width: 80, height: 80, decoration: BoxDecoration( color: colors.bgPage, borderRadius: BorderRadius.circular(4), border: Border.all(color: colors.border), ), child: Center( child: Icon( Icons.add, size: 24, color: colors.textPlaceholder, ), ), ), ), ], ), ], ); } // ═══ 6. 底部操作栏 ═══ Widget _buildBottomBar(AppLocalizations l10n) { final isDraft = widget.id != null; return ActionBar( leftLabel: isDraft ? l10n.get('reset') : null, centerLabel: l10n.get('saveDraft'), rightLabel: l10n.get('submitApproval'), showLeft: isDraft, onLeftTap: isDraft ? () => _showConfirmDialog( '确认重置', '将清空所有已填内容,此操作不可撤销', '取消', '确认重置', _resetAll, ) : null, onCenterTap: () { TDToast.showSuccess('已保存为草稿', context: context); context.pop(); }, onRightTap: () { final err = _validate(); if (err.isNotEmpty) { TDToast.showText(err.first, context: context); return; } TDToast.showSuccess('已提交,等待审批', context: context); context.pop(); }, ); } List _validate() { final e = []; if (_expenseTypes.isEmpty) e.add('请至少选择一项费用类型'); if (_purposeController.text.trim().isEmpty) e.add('请填写费用事由'); if (_selectedProjectId == null) e.add('请选择关联项目'); if (_selectedSubjectId == null) e.add('请选择预算科目'); if (_details.isEmpty) e.add('请至少添加一行费用明细'); if (_expenseTypes.contains('travel')) { if (_estimatedStartDate.isEmpty) e.add('请选择预计开始日期'); if (_estimatedEndDate.isEmpty) e.add('请选择预计结束日期'); } return e; } void _resetAll() => setState(() { _purposeController.clear(); _expenseTypes.clear(); _urgency = 0; _isTaxIncluded = false; _selectedProjectId = null; _selectedProjectName = null; _selectedSubjectId = null; _selectedSubjectName = null; _referenceNoController.clear(); _details.clear(); _attachments.clear(); _estimatedStartDate = ''; _estimatedEndDate = ''; _entertainmentTarget = ''; _venue = ''; }); bool _hasUnsaved() => _purposeController.text.isNotEmpty || _expenseTypes.isNotEmpty || _details.isNotEmpty || _attachments.isNotEmpty || _selectedProjectId != null; // ═══ 通用弹窗方法 ═══ void _showConfirmDialog( String title, String content, String leftText, String rightText, VoidCallback onConfirm, ) { final colors = Theme.of(context).extension()!; showDialog( context: context, builder: (ctx) => TDAlertDialog( title: title, content: content, leftBtn: TDDialogButtonOptions( title: leftText, titleColor: colors.primary, action: () => Navigator.pop(ctx), ), rightBtn: TDDialogButtonOptions( title: rightText, titleColor: colors.danger, action: () { Navigator.pop(ctx); onConfirm(); }, ), ), ); } void _showListPicker( String title, List items, Function(String) onPick, ) { showDialog( context: context, builder: (ctx) => TDAlertDialog( title: title, contentWidget: SizedBox( width: double.maxFinite, child: ListView( shrinkWrap: true, children: items .map( (item) => TDCell( title: item, onClick: (_) { onPick(item); Navigator.pop(ctx); }, ), ) .toList(), ), ), leftBtn: TDDialogButtonOptions( title: '取消', action: () => Navigator.pop(ctx), ), ), ); } void _showTextInput(String title, Function(String) onConfirm) { final colors = Theme.of(context).extension()!; final c = TextEditingController(); showDialog( context: context, builder: (ctx) => TDAlertDialog( title: title, contentWidget: TDInput(controller: c, hintText: '请输入'), leftBtn: TDDialogButtonOptions( title: '取消', action: () => Navigator.pop(ctx), ), rightBtn: TDDialogButtonOptions( title: '确定', titleColor: colors.primary, action: () { onConfirm(c.text); Navigator.pop(ctx); }, ), ), ); } void _showNumberInput(String title, Function(int) onConfirm) { final colors = Theme.of(context).extension()!; final c = TextEditingController(); showDialog( context: context, builder: (ctx) => TDAlertDialog( title: title, contentWidget: TDInput( controller: c, inputType: TextInputType.number, hintText: '请输入数字', ), leftBtn: TDDialogButtonOptions( title: '取消', action: () => Navigator.pop(ctx), ), rightBtn: TDDialogButtonOptions( title: '确定', titleColor: colors.primary, action: () { onConfirm(int.tryParse(c.text) ?? 0); Navigator.pop(ctx); }, ), ), ); } void _pickDate(Function(String) onPick) { final now = DateTime.now(); TDPicker.showDatePicker( context, title: '选择日期', useYear: true, useMonth: true, useDay: true, initialDate: [now.year, now.month, now.day], onConfirm: (selected) { onPick( '${selected['year']}-${selected['month']!.toString().padLeft(2, '0')}-${selected['day']!.toString().padLeft(2, '0')}', ); }, ); } Widget _label(String t) { final colors = Theme.of(context).extension()!; return Text( t, style: TextStyle( fontSize: AppFontSizes.body, color: colors.textSecondary, ), ); } String _today() { final n = DateTime.now(); return '${n.year}-${n.month.toString().padLeft(2, '0')}-${n.day.toString().padLeft(2, '0')}'; } } class _DetailItem { final int id; final String category; final String categoryName; final int quantity; final String unit; final double unitPrice; final double amount; final String remark; const _DetailItem({ required this.id, required this.category, required this.categoryName, required this.quantity, required this.unit, required this.unitPrice, required this.amount, required this.remark, }); }