Add transfer tracking and secure admin access

This commit is contained in:
2026-07-26 11:57:57 +08:00
parent 0738953e6d
commit 7df25edd96
111 changed files with 6379 additions and 1934 deletions
@@ -22,6 +22,7 @@ class _SearchPageState extends State<SearchPage> {
bool _loading = false;
String? _error;
String? _timeFilter; // today | week | month
String? _typeFilter;
double? _minAmount, _maxAmount;
Future<void> _search() async {
@@ -29,6 +30,7 @@ class _SearchPageState extends State<SearchPage> {
if (q.isEmpty &&
!_aiOnly &&
_timeFilter == null &&
_typeFilter == null &&
_minAmount == null &&
_maxAmount == null)
return;
@@ -58,6 +60,7 @@ class _SearchPageState extends State<SearchPage> {
maxAmount: _maxAmount,
from: from,
to: to,
type: _typeFilter,
);
if (mounted)
setState(() {
@@ -74,7 +77,7 @@ class _SearchPageState extends State<SearchPage> {
@override
Widget build(BuildContext context) {
final total = _results
.where((t) => !t.isIncome)
.where((t) => t.isExpense)
.fold<double>(0, (s, t) => s + t.amount);
return Scaffold(
appBar: AppBar(
@@ -84,7 +87,7 @@ class _SearchPageState extends State<SearchPage> {
textInputAction: TextInputAction.search,
onSubmitted: (_) => _search(),
decoration: InputDecoration(
hintText: '搜备注 / 分类 / 你说过的',
hintText: '搜备注 / 对方 / 分类 / ',
contentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 8),
),
),
@@ -122,6 +125,20 @@ class _SearchPageState extends State<SearchPage> {
},
),
),
for (final option in const [
('expense', '支出'),
('transfer', '转账'),
('income', '收入'),
])
FilterChip(
label: Text(option.$2, style: TextStyle(fontSize: 11)),
selected: _typeFilter == option.$1,
selectedColor: context.jz.primaryBackground,
onSelected: (selected) {
setState(() => _typeFilter = selected ? option.$1 : null);
_search();
},
),
if (_searched && !_loading)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
@@ -20,7 +20,9 @@ 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;
@@ -37,7 +39,9 @@ class _TransactionEditPageState extends State<TransactionEditPage> {
);
_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;
@@ -49,6 +53,7 @@ class _TransactionEditPageState extends State<TransactionEditPage> {
_amount.dispose();
_note.dispose();
_payment.dispose();
_counterparty.dispose();
super.dispose();
}
@@ -56,7 +61,12 @@ class _TransactionEditPageState extends State<TransactionEditPage> {
setState(() => _loading = true);
try {
await CurrentLedgerStore.instance.ensureLoaded();
final categories = await TxApi.categories(_type);
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;
@@ -75,6 +85,12 @@ class _TransactionEditPageState extends State<TransactionEditPage> {
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,
@@ -143,6 +159,11 @@ class _TransactionEditPageState extends State<TransactionEditPage> {
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);
@@ -178,10 +199,28 @@ class _TransactionEditPageState extends State<TransactionEditPage> {
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,
@@ -100,11 +100,13 @@ class TxDetailPage extends StatelessWidget {
// 类型
_Row(
k: '类型',
child: Text(
tx.isIncome ? '收入' : '支出',
style: TextStyle(fontSize: 13.5),
),
child: Text(tx.typeLabel, style: TextStyle(fontSize: 13.5)),
),
if (tx.isTransfer && tx.counterparty?.isNotEmpty == true)
_Row(
k: '转账对方',
child: Text(tx.counterparty!, style: TextStyle(fontSize: 13.5)),
),
// 备注
if (tx.note != null && tx.note!.isNotEmpty)
_Row(