Files
jizhi/frontend/lib/features/home/pages/transaction_edit_page.dart
T

328 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:miaoji_zhang/shared/api/api_client.dart';
import 'package:miaoji_zhang/shared/api/business_api.dart';
import 'package:miaoji_zhang/shared/services/current_ledger_store.dart';
import 'package:miaoji_zhang/shared/theme/app_theme.dart';
import 'package:miaoji_zhang/shared/widgets/app_controls.dart';
import 'package:miaoji_zhang/shared/widgets/category_icon.dart';
import 'package:miaoji_zhang/shared/services/shanghai_time.dart';
class TransactionEditPage extends StatefulWidget {
final TxItem transaction;
const TransactionEditPage({super.key, required this.transaction});
@override
State<TransactionEditPage> createState() => _TransactionEditPageState();
}
class _TransactionEditPageState extends State<TransactionEditPage> {
late final TextEditingController _amount;
late final TextEditingController _note;
late final TextEditingController _payment;
late final TextEditingController _counterparty;
late String _type;
late String _transferDirection;
late int _ledgerId;
late int _categoryId;
late DateTime _occurredAt;
List<CategoryItem> _categories = const [];
bool _loading = true;
bool _saving = false;
@override
void initState() {
super.initState();
final transaction = widget.transaction;
_amount = TextEditingController(
text: transaction.amount.toStringAsFixed(2),
);
_note = TextEditingController(text: transaction.note ?? '');
_payment = TextEditingController(text: transaction.paymentMethod ?? '');
_counterparty = TextEditingController(text: transaction.counterparty ?? '');
_type = transaction.type;
_transferDirection = transaction.transferDirection ?? 'out';
_ledgerId = transaction.ledgerId;
_categoryId = transaction.categoryId;
_occurredAt = transaction.occurredAt;
_loadCategories();
}
@override
void dispose() {
_amount.dispose();
_note.dispose();
_payment.dispose();
_counterparty.dispose();
super.dispose();
}
Future<void> _loadCategories() async {
setState(() => _loading = true);
try {
await CurrentLedgerStore.instance.ensureLoaded();
final categoryType = _type == 'transfer'
? _transferDirection == 'in'
? 'income'
: 'expense'
: _type;
final categories = await TxApi.categories(categoryType);
if (!categories.any((category) => category.id == _categoryId) &&
categories.isNotEmpty) {
_categoryId = categories.first.id;
}
if (mounted) setState(() => _categories = categories);
} catch (error) {
if (mounted) _showError(error);
} finally {
if (mounted) setState(() => _loading = false);
}
}
Future<void> _changeType(String type) async {
if (type == _type) return;
setState(() => _type = type);
await _loadCategories();
}
Future<void> _changeTransferDirection(String direction) async {
if (direction == _transferDirection) return;
setState(() => _transferDirection = direction);
await _loadCategories();
}
Future<void> _pickDateTime() async {
final value = await showJzDateTimeSheet(
context,
initial: _occurredAt,
firstDate: DateTime(2000),
lastDate: ShanghaiTime.now.add(const Duration(days: 1)),
title: '选择发生时间',
);
if (value != null && mounted) setState(() => _occurredAt = value);
}
Future<void> _selectCategory() async {
final value = await showJzOptionSheet<int>(
context,
title: '选择分类',
options: _categories
.map(
(category) => JzOption(
value: category.id,
label: category.name,
leading: CategoryIconBox(
iconKey: category.iconKey,
colorKey: category.colorKey,
size: 32,
),
),
)
.toList(),
selected: _categoryId,
);
if (value != null && mounted) setState(() => _categoryId = value);
}
Future<void> _selectLedger() async {
final ledgers = CurrentLedgerStore.instance.ledgers;
final value = await showJzOptionSheet<int>(
context,
title: '选择所属账本',
options: ledgers
.map(
(ledger) => JzOption(
value: ledger.id,
label: ledger.name,
subtitle: ledger.isDefault ? '当前默认账本' : null,
),
)
.toList(),
selected: _ledgerId,
);
if (value != null && mounted) setState(() => _ledgerId = value);
}
Future<void> _save() async {
final amount = double.tryParse(_amount.text.trim());
if (amount == null || amount <= 0) {
_showError(StateError('请输入正确金额'));
return;
}
setState(() => _saving = true);
try {
final updated = await TxApi.update(
widget.transaction.id,
ledgerId: _ledgerId,
categoryId: _categoryId,
type: _type,
amount: amount,
note: _note.text.trim(),
paymentMethod: _payment.text.trim(),
transferDirection: _type == 'transfer' ? _transferDirection : null,
counterparty:
_type == 'transfer' && _counterparty.text.trim().isNotEmpty
? _counterparty.text.trim()
: null,
occurredAt: _occurredAt,
);
if (mounted) Navigator.pop(context, updated);
} catch (error) {
if (mounted) _showError(error);
} finally {
if (mounted) setState(() => _saving = false);
}
}
void _showError(Object error) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(apiErrorMessage(error))));
}
@override
Widget build(BuildContext context) {
final ledgers = CurrentLedgerStore.instance.ledgers;
return Scaffold(
appBar: AppBar(title: Text('编辑账单')),
body: _loading
? Center(
child: CircularProgressIndicator(
color: AppTheme.primary,
strokeWidth: 2,
),
)
: ListView(
padding: const EdgeInsets.all(16),
children: [
JzSegmentedControl<String>(
value: _type,
options: const [
JzOption(value: 'expense', label: '支出'),
JzOption(value: 'transfer', label: '转账'),
JzOption(value: 'income', label: '收入'),
],
onChanged: _changeType,
),
if (_type == 'transfer') ...[
SizedBox(height: 12),
JzSegmentedControl<String>(
value: _transferDirection,
options: const [
JzOption(value: 'out', label: '转出'),
JzOption(value: 'in', label: '转入'),
],
onChanged: _changeTransferDirection,
),
SizedBox(height: 12),
TextField(
controller: _counterparty,
maxLength: 40,
decoration: InputDecoration(labelText: '转账对方'),
),
],
SizedBox(height: 14),
TextField(
controller: _amount,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
decoration: InputDecoration(
labelText: '金额',
prefixText: '¥ ',
),
),
SizedBox(height: 12),
_SelectField(
label: '分类',
value:
_categories
.where((category) => category.id == _categoryId)
.map((category) => category.name)
.firstOrNull ??
'请选择',
onTap: _selectCategory,
),
SizedBox(height: 12),
_SelectField(
label: '所属账本',
value:
ledgers
.where((ledger) => ledger.id == _ledgerId)
.map((ledger) => ledger.name)
.firstOrNull ??
'请选择',
onTap: _selectLedger,
),
SizedBox(height: 12),
TextField(
controller: _note,
maxLength: 50,
decoration: InputDecoration(labelText: '备注'),
),
SizedBox(height: 4),
TextField(
controller: _payment,
maxLength: 20,
decoration: InputDecoration(labelText: '支付方式'),
),
SizedBox(height: 4),
ListTile(
contentPadding: EdgeInsets.zero,
title: Text('发生时间'),
subtitle: Text(_occurredAt.toString().substring(0, 16)),
trailing: Icon(Icons.chevron_right),
onTap: _pickDateTime,
),
if (widget.transaction.sourceText?.isNotEmpty == true) ...[
SizedBox(height: 8),
InputDecorator(
decoration: InputDecoration(labelText: 'AI 原话(只读)'),
child: Text(widget.transaction.sourceText!),
),
],
SizedBox(height: 24),
FilledButton(
onPressed: _saving ? null : _save,
child: Text(_saving ? '保存中...' : '保存修改'),
),
],
),
);
}
}
class _SelectField extends StatelessWidget {
final String label;
final String value;
final VoidCallback onTap;
const _SelectField({
required this.label,
required this.value,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
child: InputDecorator(
decoration: InputDecoration(labelText: label),
child: Row(
children: [
Expanded(
child: Text(
value,
style: TextStyle(fontSize: 13.5, fontWeight: FontWeight.w600),
),
),
Icon(Icons.keyboard_arrow_down_rounded, color: context.jz.text3),
],
),
),
);
}
}