|
|
@@ -122,8 +122,7 @@ class ExpenseDetailDialog extends StatefulWidget {
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
- State<ExpenseDetailDialog> createState() =>
|
|
|
- _ExpenseDetailDialogState();
|
|
|
+ State<ExpenseDetailDialog> createState() => _ExpenseDetailDialogState();
|
|
|
}
|
|
|
|
|
|
class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
@@ -142,8 +141,39 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
EmployeeItem? _selEmployee;
|
|
|
late final AttachmentPickerController _attachmentCtrl;
|
|
|
final ScrollController _scrollCtrl = ScrollController();
|
|
|
+ final _remarkFocus = FocusNode();
|
|
|
+ final _bankNameFocus = FocusNode();
|
|
|
+ final _bankAccountNameFocus = FocusNode();
|
|
|
+ final _bankAccountFocus = FocusNode();
|
|
|
bool _attachAvailable = false;
|
|
|
|
|
|
+ void _ensureVisible(FocusNode node) {
|
|
|
+ if (!node.hasFocus) return;
|
|
|
+ _doEnsureVisible(node, 0, -1);
|
|
|
+ }
|
|
|
+
|
|
|
+ void _doEnsureVisible(FocusNode node, int attempt, double lastInsets) {
|
|
|
+ if (attempt >= 15) return;
|
|
|
+ WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
+ if (!mounted || !node.hasFocus || !_scrollCtrl.hasClients) return;
|
|
|
+ final insets = MediaQuery.of(context).viewInsets.bottom;
|
|
|
+ if (insets != lastInsets) {
|
|
|
+ _doEnsureVisible(node, attempt + 1, insets);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Future.delayed(const Duration(milliseconds: 500), () {
|
|
|
+ if (!mounted || !node.hasFocus || !_scrollCtrl.hasClients) return;
|
|
|
+ final ctx = node.context;
|
|
|
+ if (ctx == null) return;
|
|
|
+ Scrollable.ensureVisible(
|
|
|
+ ctx,
|
|
|
+ alignment: 0.5,
|
|
|
+ duration: const Duration(milliseconds: 300),
|
|
|
+ );
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
static const _taxOptions = [0.0, 0.06, 0.09, 0.13];
|
|
|
static const _taxLabels = ['0%', '6%', '9%', '13%'];
|
|
|
|
|
|
@@ -159,17 +189,29 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
super.initState();
|
|
|
final d = widget.initialData;
|
|
|
_cat = d != null
|
|
|
- ? (_cats.any((c) => c.code == d.category) ? d.category : _cats.first.code)
|
|
|
- : _cats.isNotEmpty ? _cats.first.code : 'other';
|
|
|
+ ? (_cats.any((c) => c.code == d.category)
|
|
|
+ ? d.category
|
|
|
+ : _cats.first.code)
|
|
|
+ : _cats.isNotEmpty
|
|
|
+ ? _cats.first.code
|
|
|
+ : 'other';
|
|
|
_descCtrl = TextEditingController(text: d?.purpose ?? '');
|
|
|
- _amountCtrl = TextEditingController(text: d != null && d.amount > 0 ? d.amount.toStringAsFixed(2) : '');
|
|
|
+ _amountCtrl = TextEditingController(
|
|
|
+ text: d != null && d.amount > 0 ? d.amount.toStringAsFixed(2) : '',
|
|
|
+ );
|
|
|
if (d != null && d.customerVendorName.isNotEmpty) {
|
|
|
_selCustomer = CustomerVendor(id: '', name: d.customerVendorName);
|
|
|
}
|
|
|
- _approvedAmountCtrl = TextEditingController(text: d != null && d.approvedAmount > 0 ? d.approvedAmount.toStringAsFixed(2) : '');
|
|
|
+ _approvedAmountCtrl = TextEditingController(
|
|
|
+ text: d != null && d.approvedAmount > 0
|
|
|
+ ? d.approvedAmount.toStringAsFixed(2)
|
|
|
+ : '',
|
|
|
+ );
|
|
|
_remarkCtrl = TextEditingController(text: d?.remark ?? '');
|
|
|
_bankNameCtrl = TextEditingController(text: d?.bankName ?? '');
|
|
|
- _bankAccountNameCtrl = TextEditingController(text: d?.bankAccountName ?? '');
|
|
|
+ _bankAccountNameCtrl = TextEditingController(
|
|
|
+ text: d?.bankAccountName ?? '',
|
|
|
+ );
|
|
|
_bankAccountCtrl = TextEditingController(text: d?.bankAccount ?? '');
|
|
|
_taxRate = d?.taxRate ?? 0;
|
|
|
if (d != null && d.sqMan.isNotEmpty && widget.employees.isNotEmpty) {
|
|
|
@@ -178,10 +220,16 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
}
|
|
|
if (d != null) {
|
|
|
if (d.projectId.isNotEmpty && widget.projects.isNotEmpty) {
|
|
|
- _selProject = widget.projects.firstWhere((p) => p.id.toString() == d.projectId, orElse: () => widget.projects.first);
|
|
|
+ _selProject = widget.projects.firstWhere(
|
|
|
+ (p) => p.id.toString() == d.projectId,
|
|
|
+ orElse: () => widget.projects.first,
|
|
|
+ );
|
|
|
}
|
|
|
if (d.costDeptId.isNotEmpty && widget.costDepts.isNotEmpty) {
|
|
|
- _selDept = widget.costDepts.firstWhere((dept) => dept.id == d.costDeptId, orElse: () => widget.costDepts.first);
|
|
|
+ _selDept = widget.costDepts.firstWhere(
|
|
|
+ (dept) => dept.id == d.costDeptId,
|
|
|
+ orElse: () => widget.costDepts.first,
|
|
|
+ );
|
|
|
}
|
|
|
if (d.attachmentPaths.isNotEmpty) {
|
|
|
// Restore attachments will be handled after build
|
|
|
@@ -192,6 +240,12 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
}
|
|
|
_attachmentCtrl = AttachmentPickerController(maxCount: 9)
|
|
|
..addListener(() => setState(() {}));
|
|
|
+ _remarkFocus.addListener(() => _ensureVisible(_remarkFocus));
|
|
|
+ _bankNameFocus.addListener(() => _ensureVisible(_bankNameFocus));
|
|
|
+ _bankAccountNameFocus.addListener(
|
|
|
+ () => _ensureVisible(_bankAccountNameFocus),
|
|
|
+ );
|
|
|
+ _bankAccountFocus.addListener(() => _ensureVisible(_bankAccountFocus));
|
|
|
_checkAttachHealth();
|
|
|
}
|
|
|
|
|
|
@@ -217,6 +271,10 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
_bankAccountCtrl.dispose();
|
|
|
_attachmentCtrl.dispose();
|
|
|
_scrollCtrl.dispose();
|
|
|
+ _remarkFocus.dispose();
|
|
|
+ _bankNameFocus.dispose();
|
|
|
+ _bankAccountNameFocus.dispose();
|
|
|
+ _bankAccountFocus.dispose();
|
|
|
super.dispose();
|
|
|
}
|
|
|
|
|
|
@@ -266,81 +324,90 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
Widget build(BuildContext context) {
|
|
|
final colors = Theme.of(context).extension<AppColorsExtension>()!;
|
|
|
|
|
|
- return SafeArea(
|
|
|
- child: ConstrainedBox(
|
|
|
- constraints: BoxConstraints(
|
|
|
- maxHeight: MediaQuery.of(context).size.height * 0.8,
|
|
|
- ),
|
|
|
- child: Container(
|
|
|
- decoration: BoxDecoration(
|
|
|
- color: colors.bgPage,
|
|
|
- borderRadius:
|
|
|
- const BorderRadius.vertical(top: Radius.circular(16)),
|
|
|
+ return AnimatedPadding(
|
|
|
+ padding: EdgeInsets.only(
|
|
|
+ bottom: MediaQuery.of(context).viewInsets.bottom,
|
|
|
+ ),
|
|
|
+ duration: const Duration(milliseconds: 200),
|
|
|
+ child: SafeArea(
|
|
|
+ child: ConstrainedBox(
|
|
|
+ constraints: BoxConstraints(
|
|
|
+ maxHeight: MediaQuery.of(context).size.height * 0.8,
|
|
|
),
|
|
|
- child: Column(
|
|
|
- mainAxisSize: MainAxisSize.min,
|
|
|
- crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
- children: [
|
|
|
- _buildHeader(colors),
|
|
|
- Flexible(
|
|
|
- child: GestureDetector(
|
|
|
- onTap: () => FocusScope.of(context).unfocus(),
|
|
|
- behavior: HitTestBehavior.translucent,
|
|
|
- child: SingleChildScrollView(
|
|
|
- controller: _scrollCtrl,
|
|
|
- keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
|
|
- padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
|
|
- child: Column(
|
|
|
- mainAxisSize: MainAxisSize.min,
|
|
|
- crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
- children: [
|
|
|
- if (_isEdit && widget.initialData!.aeNo.isNotEmpty) ...[
|
|
|
- _buildAeInfoCard(colors),
|
|
|
- const SizedBox(height: 12),
|
|
|
- ],
|
|
|
- _buildCategoryCard(colors),
|
|
|
- const SizedBox(height: 12),
|
|
|
- _buildAcctSubjectCard(colors),
|
|
|
- const SizedBox(height: 12),
|
|
|
- _buildAmountCard(),
|
|
|
- const SizedBox(height: 12),
|
|
|
- _buildTaxRateCard(colors),
|
|
|
- if ((double.tryParse(_amountCtrl.text) ?? 0) > 0) ...[
|
|
|
- const SizedBox(height: 12),
|
|
|
- _buildCalcInfo(colors),
|
|
|
- ],
|
|
|
- const SizedBox(height: 12),
|
|
|
- _buildApprovedAmountCard(),
|
|
|
- const SizedBox(height: 12),
|
|
|
- _buildProjectCard(colors),
|
|
|
- const SizedBox(height: 12),
|
|
|
- _buildCostDeptCard(colors),
|
|
|
- const SizedBox(height: 12),
|
|
|
- _buildCustomerCard(colors),
|
|
|
- const SizedBox(height: 12),
|
|
|
- _buildEmployeeCard(colors),
|
|
|
- const SizedBox(height: 12),
|
|
|
- _buildBankInfoCard(colors),
|
|
|
- const SizedBox(height: 12),
|
|
|
- _buildRemarkInput(colors),
|
|
|
- const SizedBox(height: 12),
|
|
|
- _buildAttachmentCard(colors),
|
|
|
- ],
|
|
|
+ child: Container(
|
|
|
+ decoration: BoxDecoration(
|
|
|
+ color: colors.bgPage,
|
|
|
+ borderRadius: const BorderRadius.vertical(
|
|
|
+ top: Radius.circular(16),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ child: Column(
|
|
|
+ mainAxisSize: MainAxisSize.min,
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
+ children: [
|
|
|
+ _buildHeader(colors),
|
|
|
+ Flexible(
|
|
|
+ child: GestureDetector(
|
|
|
+ onTap: () => FocusScope.of(context).unfocus(),
|
|
|
+ behavior: HitTestBehavior.translucent,
|
|
|
+ child: SingleChildScrollView(
|
|
|
+ controller: _scrollCtrl,
|
|
|
+ keyboardDismissBehavior:
|
|
|
+ ScrollViewKeyboardDismissBehavior.manual,
|
|
|
+ padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
|
|
+ child: Column(
|
|
|
+ mainAxisSize: MainAxisSize.min,
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
+ children: [
|
|
|
+ if (_isEdit &&
|
|
|
+ widget.initialData!.aeNo.isNotEmpty) ...[
|
|
|
+ _buildAeInfoCard(colors),
|
|
|
+ const SizedBox(height: 12),
|
|
|
+ ],
|
|
|
+ _buildCategoryCard(colors),
|
|
|
+ const SizedBox(height: 12),
|
|
|
+ _buildAcctSubjectCard(colors),
|
|
|
+ const SizedBox(height: 12),
|
|
|
+ _buildAmountCard(),
|
|
|
+ const SizedBox(height: 12),
|
|
|
+ _buildTaxRateCard(colors),
|
|
|
+ if ((double.tryParse(_amountCtrl.text) ?? 0) > 0) ...[
|
|
|
+ const SizedBox(height: 12),
|
|
|
+ _buildCalcInfo(colors),
|
|
|
+ ],
|
|
|
+ const SizedBox(height: 12),
|
|
|
+ _buildApprovedAmountCard(),
|
|
|
+ const SizedBox(height: 12),
|
|
|
+ _buildProjectCard(colors),
|
|
|
+ const SizedBox(height: 12),
|
|
|
+ _buildCostDeptCard(colors),
|
|
|
+ const SizedBox(height: 12),
|
|
|
+ _buildCustomerCard(colors),
|
|
|
+ const SizedBox(height: 12),
|
|
|
+ _buildEmployeeCard(colors),
|
|
|
+ const SizedBox(height: 12),
|
|
|
+ _buildBankInfoCard(colors),
|
|
|
+ const SizedBox(height: 12),
|
|
|
+ _buildRemarkInput(colors),
|
|
|
+ const SizedBox(height: 12),
|
|
|
+ _buildAttachmentCard(colors),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
),
|
|
|
),
|
|
|
- ),
|
|
|
- ),
|
|
|
- Container(
|
|
|
- padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
|
|
- decoration: BoxDecoration(
|
|
|
- color: colors.bgCard,
|
|
|
- border: Border(
|
|
|
- top: BorderSide(color: colors.border, width: 0.5),
|
|
|
+ Container(
|
|
|
+ padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
|
|
+ decoration: BoxDecoration(
|
|
|
+ color: colors.bgCard,
|
|
|
+ border: Border(
|
|
|
+ top: BorderSide(color: colors.border, width: 0.5),
|
|
|
+ ),
|
|
|
),
|
|
|
+ child: _buildActions(),
|
|
|
),
|
|
|
- child: _buildActions(),
|
|
|
- ),
|
|
|
- ],
|
|
|
+ ],
|
|
|
+ ),
|
|
|
),
|
|
|
),
|
|
|
),
|
|
|
@@ -416,6 +483,7 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
TDToast.showText(_l10n.get('noData'), context: context);
|
|
|
return;
|
|
|
}
|
|
|
+ FocusManager.instance.primaryFocus?.unfocus();
|
|
|
TDPicker.showMultiPicker(
|
|
|
context,
|
|
|
title: label,
|
|
|
@@ -433,7 +501,12 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
);
|
|
|
},
|
|
|
child: Container(
|
|
|
- padding: const EdgeInsets.only(left: 16, right: 10, top: 12, bottom: 12),
|
|
|
+ padding: const EdgeInsets.only(
|
|
|
+ left: 16,
|
|
|
+ right: 10,
|
|
|
+ top: 12,
|
|
|
+ bottom: 12,
|
|
|
+ ),
|
|
|
decoration: BoxDecoration(
|
|
|
color: tdTheme.bgColorContainer,
|
|
|
borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
|
|
|
@@ -441,21 +514,62 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
),
|
|
|
child: Row(
|
|
|
children: [
|
|
|
- TDText(label, maxLines: 1, overflow: TextOverflow.visible, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: const TextStyle(letterSpacing: 0)),
|
|
|
+ TDText(
|
|
|
+ label,
|
|
|
+ maxLines: 1,
|
|
|
+ overflow: TextOverflow.visible,
|
|
|
+ font: tdTheme.fontBodyLarge,
|
|
|
+ fontWeight: FontWeight.w400,
|
|
|
+ style: const TextStyle(letterSpacing: 0),
|
|
|
+ ),
|
|
|
if (required)
|
|
|
- Padding(padding: const EdgeInsets.only(left: 4), child: TDText('*', font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: TextStyle(color: tdTheme.errorColor6))),
|
|
|
+ Padding(
|
|
|
+ padding: const EdgeInsets.only(left: 4),
|
|
|
+ child: TDText(
|
|
|
+ '*',
|
|
|
+ font: tdTheme.fontBodyLarge,
|
|
|
+ fontWeight: FontWeight.w400,
|
|
|
+ style: TextStyle(color: tdTheme.errorColor6),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
const SizedBox(width: 12),
|
|
|
Expanded(
|
|
|
- child: Row(mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.max, children: [
|
|
|
- Flexible(child: TDText(currentLabel, maxLines: 1, overflow: TextOverflow.ellipsis, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, textColor: tdTheme.textColorPrimary, textAlign: TextAlign.end)),
|
|
|
- const SizedBox(width: 4),
|
|
|
- SizedBox(
|
|
|
- width: 18, height: 18,
|
|
|
- child: hasValue
|
|
|
- ? GestureDetector(onTap: onClear, child: Icon(Icons.close, size: 18, color: tdTheme.textColorPlaceholder))
|
|
|
- : Icon(Icons.chevron_right, size: 18, color: tdTheme.textColorPlaceholder),
|
|
|
- ),
|
|
|
- ]),
|
|
|
+ child: Row(
|
|
|
+ mainAxisAlignment: MainAxisAlignment.end,
|
|
|
+ mainAxisSize: MainAxisSize.max,
|
|
|
+ children: [
|
|
|
+ Flexible(
|
|
|
+ child: TDText(
|
|
|
+ currentLabel,
|
|
|
+ maxLines: 1,
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
+ font: tdTheme.fontBodyLarge,
|
|
|
+ fontWeight: FontWeight.w400,
|
|
|
+ textColor: tdTheme.textColorPrimary,
|
|
|
+ textAlign: TextAlign.end,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(width: 4),
|
|
|
+ SizedBox(
|
|
|
+ width: 18,
|
|
|
+ height: 18,
|
|
|
+ child: hasValue
|
|
|
+ ? GestureDetector(
|
|
|
+ onTap: onClear,
|
|
|
+ child: Icon(
|
|
|
+ Icons.close,
|
|
|
+ size: 18,
|
|
|
+ color: tdTheme.textColorPlaceholder,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ : Icon(
|
|
|
+ Icons.chevron_right,
|
|
|
+ size: 18,
|
|
|
+ color: tdTheme.textColorPlaceholder,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
@@ -484,6 +598,7 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
required AppColorsExtension colors,
|
|
|
TextInputType? keyboardType,
|
|
|
List<TextInputFormatter>? inputFormatters,
|
|
|
+ FocusNode? focusNode,
|
|
|
}) {
|
|
|
final tdTheme = TDTheme.of(context);
|
|
|
final hasValue = controller.text.isNotEmpty;
|
|
|
@@ -496,40 +611,71 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
),
|
|
|
child: Row(
|
|
|
children: [
|
|
|
- TDText(label, maxLines: 1, overflow: TextOverflow.visible, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: const TextStyle(letterSpacing: 0)),
|
|
|
+ TDText(
|
|
|
+ label,
|
|
|
+ maxLines: 1,
|
|
|
+ overflow: TextOverflow.visible,
|
|
|
+ font: tdTheme.fontBodyLarge,
|
|
|
+ fontWeight: FontWeight.w400,
|
|
|
+ style: const TextStyle(letterSpacing: 0),
|
|
|
+ ),
|
|
|
if (required)
|
|
|
- Padding(padding: const EdgeInsets.only(left: 4), child: TDText('*', font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: TextStyle(color: tdTheme.errorColor6))),
|
|
|
+ Padding(
|
|
|
+ padding: const EdgeInsets.only(left: 4),
|
|
|
+ child: TDText(
|
|
|
+ '*',
|
|
|
+ font: tdTheme.fontBodyLarge,
|
|
|
+ fontWeight: FontWeight.w400,
|
|
|
+ style: TextStyle(color: tdTheme.errorColor6),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
const SizedBox(width: 12),
|
|
|
Expanded(
|
|
|
- child: Row(mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.max, children: [
|
|
|
- Flexible(
|
|
|
- child: TextField(
|
|
|
- controller: controller,
|
|
|
- textAlign: TextAlign.end,
|
|
|
- keyboardType: keyboardType,
|
|
|
- inputFormatters: inputFormatters,
|
|
|
- style: TextStyle(fontSize: 16, color: colors.textPrimary),
|
|
|
- decoration: InputDecoration(
|
|
|
- hintText: hintText,
|
|
|
- hintStyle: TextStyle(fontSize: 16, color: colors.textPlaceholder),
|
|
|
- border: InputBorder.none,
|
|
|
- isDense: true,
|
|
|
- contentPadding: EdgeInsets.zero,
|
|
|
+ child: Row(
|
|
|
+ mainAxisAlignment: MainAxisAlignment.end,
|
|
|
+ mainAxisSize: MainAxisSize.max,
|
|
|
+ children: [
|
|
|
+ Flexible(
|
|
|
+ child: TextField(
|
|
|
+ controller: controller,
|
|
|
+ focusNode: focusNode,
|
|
|
+ textAlign: TextAlign.end,
|
|
|
+ keyboardType: keyboardType,
|
|
|
+ inputFormatters: inputFormatters,
|
|
|
+ style: TextStyle(fontSize: 16, color: colors.textPrimary),
|
|
|
+ decoration: InputDecoration(
|
|
|
+ hintText: hintText,
|
|
|
+ hintStyle: TextStyle(
|
|
|
+ fontSize: 16,
|
|
|
+ color: colors.textPlaceholder,
|
|
|
+ ),
|
|
|
+ border: InputBorder.none,
|
|
|
+ isDense: true,
|
|
|
+ contentPadding: EdgeInsets.zero,
|
|
|
+ ),
|
|
|
+ onChanged: (_) => setState(() {}),
|
|
|
),
|
|
|
- onChanged: (_) => setState(() {}),
|
|
|
),
|
|
|
- ),
|
|
|
- const SizedBox(width: 4),
|
|
|
- SizedBox(
|
|
|
- width: 18, height: 18,
|
|
|
- child: hasValue
|
|
|
- ? GestureDetector(
|
|
|
- onTap: () { controller.clear(); setState(() {}); },
|
|
|
- child: Icon(Icons.close, size: 18, color: tdTheme.textColorPlaceholder),
|
|
|
- )
|
|
|
- : null,
|
|
|
- ),
|
|
|
- ]),
|
|
|
+ const SizedBox(width: 4),
|
|
|
+ SizedBox(
|
|
|
+ width: 18,
|
|
|
+ height: 18,
|
|
|
+ child: hasValue
|
|
|
+ ? GestureDetector(
|
|
|
+ onTap: () {
|
|
|
+ controller.clear();
|
|
|
+ setState(() {});
|
|
|
+ },
|
|
|
+ child: Icon(
|
|
|
+ Icons.close,
|
|
|
+ size: 18,
|
|
|
+ color: tdTheme.textColorPlaceholder,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ : null,
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
@@ -545,14 +691,15 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
hintText: '>0',
|
|
|
colors: Theme.of(context).extension<AppColorsExtension>()!,
|
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
|
- inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$'))],
|
|
|
+ inputFormatters: [
|
|
|
+ FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')),
|
|
|
+ ],
|
|
|
);
|
|
|
}
|
|
|
|
|
|
// ── 税率 ──
|
|
|
Widget _buildTaxRateCard(AppColorsExtension colors) {
|
|
|
- final currentLabel =
|
|
|
- '${(_taxRate * 100).toStringAsFixed(0)}%';
|
|
|
+ final currentLabel = '${(_taxRate * 100).toStringAsFixed(0)}%';
|
|
|
return _pickerCard(
|
|
|
label: _l10n.get('taxRate'),
|
|
|
required: true,
|
|
|
@@ -616,11 +763,28 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
}
|
|
|
|
|
|
Widget _readOnlyRow(TDThemeData tdTheme, String label, String value) {
|
|
|
- return Row(children: [
|
|
|
- TDText(label, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: const TextStyle(letterSpacing: 0)),
|
|
|
- const SizedBox(width: 12),
|
|
|
- Expanded(child: TDText(value, maxLines: 1, overflow: TextOverflow.ellipsis, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, textColor: tdTheme.textColorPrimary, textAlign: TextAlign.end)),
|
|
|
- ]);
|
|
|
+ return Row(
|
|
|
+ children: [
|
|
|
+ TDText(
|
|
|
+ label,
|
|
|
+ font: tdTheme.fontBodyLarge,
|
|
|
+ fontWeight: FontWeight.w400,
|
|
|
+ style: const TextStyle(letterSpacing: 0),
|
|
|
+ ),
|
|
|
+ const SizedBox(width: 12),
|
|
|
+ Expanded(
|
|
|
+ child: TDText(
|
|
|
+ value,
|
|
|
+ maxLines: 1,
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
+ font: tdTheme.fontBodyLarge,
|
|
|
+ fontWeight: FontWeight.w400,
|
|
|
+ textColor: tdTheme.textColorPrimary,
|
|
|
+ textAlign: TextAlign.end,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
BoxDecoration _cardDecoration(TDThemeData tdTheme) => BoxDecoration(
|
|
|
@@ -635,7 +799,9 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
return _pickerCard(
|
|
|
label: _l10n.get('applicant'),
|
|
|
required: false,
|
|
|
- currentLabel: _selEmployee != null ? '${_selEmployee!.salNo}/${_selEmployee!.name}' : _l10n.get('pleaseSelect'),
|
|
|
+ currentLabel: _selEmployee != null
|
|
|
+ ? '${_selEmployee!.salNo}/${_selEmployee!.name}'
|
|
|
+ : _l10n.get('pleaseSelect'),
|
|
|
labels: employees.map((e) => '${e.salNo}/${e.name}').toList(),
|
|
|
colors: colors,
|
|
|
onSelected: (idx) => setState(() {
|
|
|
@@ -644,12 +810,14 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
_bankAccountNameCtrl.text = _selEmployee!.accName;
|
|
|
_bankAccountCtrl.text = _selEmployee!.bnkId;
|
|
|
}),
|
|
|
- onClear: _selEmployee != null ? () => setState(() {
|
|
|
- _selEmployee = null;
|
|
|
- _bankNameCtrl.clear();
|
|
|
- _bankAccountNameCtrl.clear();
|
|
|
- _bankAccountCtrl.clear();
|
|
|
- }) : null,
|
|
|
+ onClear: _selEmployee != null
|
|
|
+ ? () => setState(() {
|
|
|
+ _selEmployee = null;
|
|
|
+ _bankNameCtrl.clear();
|
|
|
+ _bankAccountNameCtrl.clear();
|
|
|
+ _bankAccountCtrl.clear();
|
|
|
+ })
|
|
|
+ : null,
|
|
|
);
|
|
|
}
|
|
|
|
|
|
@@ -659,11 +827,15 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
return _pickerCard(
|
|
|
label: _l10n.get('customerVendor'),
|
|
|
required: false,
|
|
|
- currentLabel: _selCustomer != null ? '${_selCustomer!.id}/${_selCustomer!.name}' : _l10n.get('pleaseSelect'),
|
|
|
+ currentLabel: _selCustomer != null
|
|
|
+ ? '${_selCustomer!.id}/${_selCustomer!.name}'
|
|
|
+ : _l10n.get('pleaseSelect'),
|
|
|
labels: vendors.map((v) => '${v.id}/${v.name}').toList(),
|
|
|
colors: colors,
|
|
|
onSelected: (idx) => setState(() => _selCustomer = vendors[idx]),
|
|
|
- onClear: _selCustomer != null ? () => setState(() => _selCustomer = null) : null,
|
|
|
+ onClear: _selCustomer != null
|
|
|
+ ? () => setState(() => _selCustomer = null)
|
|
|
+ : null,
|
|
|
);
|
|
|
}
|
|
|
|
|
|
@@ -676,7 +848,9 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
hintText: '0',
|
|
|
colors: Theme.of(context).extension<AppColorsExtension>()!,
|
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
|
- inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$'))],
|
|
|
+ inputFormatters: [
|
|
|
+ FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')),
|
|
|
+ ],
|
|
|
);
|
|
|
}
|
|
|
|
|
|
@@ -685,6 +859,7 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
final tdTheme = TDTheme.of(context);
|
|
|
return TDTextarea(
|
|
|
controller: _remarkCtrl,
|
|
|
+ focusNode: _remarkFocus,
|
|
|
label: _l10n.get('remark'),
|
|
|
hintText: _l10n.get('enterRemark'),
|
|
|
maxLines: 3,
|
|
|
@@ -726,15 +901,27 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
),
|
|
|
const SizedBox(height: 4),
|
|
|
if (!_attachAvailable)
|
|
|
- TDText(_l10n.get('attachServiceUnavailable'),
|
|
|
- font: tdTheme.fontBodyMedium, fontWeight: FontWeight.w400,
|
|
|
- textColor: tdTheme.textColorPlaceholder)
|
|
|
+ TDText(
|
|
|
+ _l10n.get('attachServiceUnavailable'),
|
|
|
+ font: tdTheme.fontBodyMedium,
|
|
|
+ fontWeight: FontWeight.w400,
|
|
|
+ textColor: tdTheme.textColorPlaceholder,
|
|
|
+ )
|
|
|
else
|
|
|
AttachmentPicker(
|
|
|
controller: _attachmentCtrl,
|
|
|
maxImageSizeMB: 10,
|
|
|
maxFileSizeMB: 20,
|
|
|
- allowedExtensions: const ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'],
|
|
|
+ allowedExtensions: const [
|
|
|
+ 'pdf',
|
|
|
+ 'doc',
|
|
|
+ 'docx',
|
|
|
+ 'xls',
|
|
|
+ 'xlsx',
|
|
|
+ 'ppt',
|
|
|
+ 'pptx',
|
|
|
+ 'txt',
|
|
|
+ ],
|
|
|
onFileRejected: (file, reason) {
|
|
|
if (context.mounted) TDToast.showText(reason, context: context);
|
|
|
},
|
|
|
@@ -767,11 +954,25 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
),
|
|
|
child: Row(
|
|
|
children: [
|
|
|
- TDText(label, maxLines: 1, overflow: TextOverflow.visible, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400, style: const TextStyle(letterSpacing: 0)),
|
|
|
+ TDText(
|
|
|
+ label,
|
|
|
+ maxLines: 1,
|
|
|
+ overflow: TextOverflow.visible,
|
|
|
+ font: tdTheme.fontBodyLarge,
|
|
|
+ fontWeight: FontWeight.w400,
|
|
|
+ style: const TextStyle(letterSpacing: 0),
|
|
|
+ ),
|
|
|
const SizedBox(width: 12),
|
|
|
Expanded(
|
|
|
- child: TDText(value, maxLines: 1, overflow: TextOverflow.ellipsis, font: tdTheme.fontBodyLarge, fontWeight: FontWeight.w400,
|
|
|
- textColor: tdTheme.textColorPrimary, textAlign: TextAlign.end),
|
|
|
+ child: TDText(
|
|
|
+ value,
|
|
|
+ maxLines: 1,
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
+ font: tdTheme.fontBodyLarge,
|
|
|
+ fontWeight: FontWeight.w400,
|
|
|
+ textColor: tdTheme.textColorPrimary,
|
|
|
+ textAlign: TextAlign.end,
|
|
|
+ ),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
@@ -784,11 +985,15 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
return _pickerCard(
|
|
|
label: _l10n.get('relatedProject'),
|
|
|
required: false,
|
|
|
- currentLabel: _selProject != null ? '${_selProject!.id}/${_selProject!.name}' : _l10n.get('pleaseSelect'),
|
|
|
+ currentLabel: _selProject != null
|
|
|
+ ? '${_selProject!.id}/${_selProject!.name}'
|
|
|
+ : _l10n.get('pleaseSelect'),
|
|
|
labels: projects.map((p) => '${p.id}/${p.name}').toList(),
|
|
|
colors: colors,
|
|
|
onSelected: (idx) => setState(() => _selProject = projects[idx]),
|
|
|
- onClear: _selProject != null ? () => setState(() => _selProject = null) : null,
|
|
|
+ onClear: _selProject != null
|
|
|
+ ? () => setState(() => _selProject = null)
|
|
|
+ : null,
|
|
|
);
|
|
|
}
|
|
|
|
|
|
@@ -798,7 +1003,9 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
return _pickerCard(
|
|
|
label: _l10n.get('costDept'),
|
|
|
required: false,
|
|
|
- currentLabel: _selDept != null ? '${_selDept!.id}/${_selDept!.name}' : _l10n.get('pleaseSelect'),
|
|
|
+ currentLabel: _selDept != null
|
|
|
+ ? '${_selDept!.id}/${_selDept!.name}'
|
|
|
+ : _l10n.get('pleaseSelect'),
|
|
|
labels: depts.map((d) => '${d.id}/${d.name}').toList(),
|
|
|
colors: colors,
|
|
|
onSelected: (idx) => setState(() => _selDept = depts[idx]),
|
|
|
@@ -811,11 +1018,32 @@ class _ExpenseDetailDialogState extends State<ExpenseDetailDialog> {
|
|
|
return Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
- _inputCard(label: _l10n.get('bankName'), required: false, controller: _bankNameCtrl, hintText: _l10n.get('pleaseEnter'), colors: colors),
|
|
|
+ _inputCard(
|
|
|
+ label: _l10n.get('bankName'),
|
|
|
+ required: false,
|
|
|
+ controller: _bankNameCtrl,
|
|
|
+ hintText: _l10n.get('pleaseEnter'),
|
|
|
+ colors: colors,
|
|
|
+ focusNode: _bankNameFocus,
|
|
|
+ ),
|
|
|
const SizedBox(height: 12),
|
|
|
- _inputCard(label: _l10n.get('bankAccountName'), required: false, controller: _bankAccountNameCtrl, hintText: _l10n.get('pleaseEnter'), colors: colors),
|
|
|
+ _inputCard(
|
|
|
+ label: _l10n.get('bankAccountName'),
|
|
|
+ required: false,
|
|
|
+ controller: _bankAccountNameCtrl,
|
|
|
+ hintText: _l10n.get('pleaseEnter'),
|
|
|
+ colors: colors,
|
|
|
+ focusNode: _bankAccountNameFocus,
|
|
|
+ ),
|
|
|
const SizedBox(height: 12),
|
|
|
- _inputCard(label: _l10n.get('bankAccount'), required: false, controller: _bankAccountCtrl, hintText: _l10n.get('pleaseEnter'), colors: colors),
|
|
|
+ _inputCard(
|
|
|
+ label: _l10n.get('bankAccount'),
|
|
|
+ required: false,
|
|
|
+ controller: _bankAccountCtrl,
|
|
|
+ hintText: _l10n.get('pleaseEnter'),
|
|
|
+ colors: colors,
|
|
|
+ focusNode: _bankAccountFocus,
|
|
|
+ ),
|
|
|
],
|
|
|
);
|
|
|
}
|