Add transfer tracking and secure admin access
This commit is contained in:
@@ -135,12 +135,18 @@ class _ScreenshotParseSheetState extends State<ScreenshotParseSheet> {
|
||||
final type = switch (json['type']?.toString().toLowerCase()) {
|
||||
'income' => 'income',
|
||||
'expense' => 'expense',
|
||||
'transfer' => 'transfer',
|
||||
_ => 'unknown',
|
||||
};
|
||||
final rawTransferDirection = json['transferDirection']?.toString();
|
||||
final transferDirection =
|
||||
rawTransferDirection == 'in' || rawTransferDirection == 'out'
|
||||
? rawTransferDirection!
|
||||
: 'out';
|
||||
final categoryId = (json['categoryId'] as num?)?.toInt();
|
||||
final categoryName = json['categoryName']?.toString() ?? '其他';
|
||||
final categoryIcon = json['categoryIcon']?.toString() ?? 'tag';
|
||||
final categories = _categoriesFor(type);
|
||||
final categories = _categoriesFor(type, transferDirection);
|
||||
final exists = categories.any((category) => category.id == categoryId);
|
||||
if (type != 'unknown' && !exists && categoryId != null) {
|
||||
categories.add(
|
||||
@@ -148,7 +154,11 @@ class _ScreenshotParseSheetState extends State<ScreenshotParseSheet> {
|
||||
'id': categoryId,
|
||||
'name': categoryName,
|
||||
'iconKey': categoryIcon,
|
||||
'type': type,
|
||||
'type': type == 'transfer'
|
||||
? transferDirection == 'in'
|
||||
? 'income'
|
||||
: 'expense'
|
||||
: type,
|
||||
'sortOrder': 999,
|
||||
'isCustom': false,
|
||||
}),
|
||||
@@ -172,6 +182,8 @@ class _ScreenshotParseSheetState extends State<ScreenshotParseSheet> {
|
||||
occurredAt: parsedOccurredAt == null
|
||||
? ShanghaiTime.now
|
||||
: ShanghaiTime.toCivil(parsedOccurredAt),
|
||||
transferDirection: transferDirection,
|
||||
counterparty: json['counterparty']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -189,10 +201,11 @@ class _ScreenshotParseSheetState extends State<ScreenshotParseSheet> {
|
||||
);
|
||||
}
|
||||
|
||||
List<CategoryItem> _categoriesFor(String type) {
|
||||
List<CategoryItem> _categoriesFor(String type, [String direction = 'out']) {
|
||||
return switch (type) {
|
||||
'income' => _incomeCategories,
|
||||
'expense' => _expenseCategories,
|
||||
'transfer' => direction == 'in' ? _incomeCategories : _expenseCategories,
|
||||
_ => <CategoryItem>[],
|
||||
};
|
||||
}
|
||||
@@ -203,7 +216,7 @@ class _ScreenshotParseSheetState extends State<ScreenshotParseSheet> {
|
||||
|
||||
void _changeType(_ScreenshotDraft draft, String type) {
|
||||
if (draft.type == type) return;
|
||||
final categories = _categoriesFor(type);
|
||||
final categories = _categoriesFor(type, draft.transferDirection);
|
||||
setState(() {
|
||||
draft.type = type;
|
||||
draft.included = true;
|
||||
@@ -211,6 +224,15 @@ class _ScreenshotParseSheetState extends State<ScreenshotParseSheet> {
|
||||
});
|
||||
}
|
||||
|
||||
void _changeTransferDirection(_ScreenshotDraft draft, String direction) {
|
||||
if (draft.transferDirection == direction) return;
|
||||
final categories = _categoriesFor('transfer', direction);
|
||||
setState(() {
|
||||
draft.transferDirection = direction;
|
||||
draft.categoryId = categories.isEmpty ? null : categories.first.id;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pickOccurredAt(_ScreenshotDraft draft) async {
|
||||
FocusScope.of(context).unfocus();
|
||||
final value = await showJzDateTimeSheet(
|
||||
@@ -226,7 +248,7 @@ class _ScreenshotParseSheetState extends State<ScreenshotParseSheet> {
|
||||
}
|
||||
|
||||
Future<void> _selectCategory(_ScreenshotDraft draft, int billIndex) async {
|
||||
final categories = _categoriesFor(draft.type);
|
||||
final categories = _categoriesFor(draft.type, draft.transferDirection);
|
||||
if (categories.isEmpty) {
|
||||
_showMessage('当前收支类型暂无可选分类');
|
||||
return;
|
||||
@@ -308,8 +330,10 @@ class _ScreenshotParseSheetState extends State<ScreenshotParseSheet> {
|
||||
|
||||
for (var index = 0; index < selected.length; index++) {
|
||||
final draft = selected[index];
|
||||
if (draft.type != 'income' && draft.type != 'expense') {
|
||||
_showMessage('第 ${_drafts.indexOf(draft) + 1} 笔请先确认收入或支出');
|
||||
if (draft.type != 'income' &&
|
||||
draft.type != 'expense' &&
|
||||
draft.type != 'transfer') {
|
||||
_showMessage('第 ${_drafts.indexOf(draft) + 1} 笔请先确认账单类型');
|
||||
return;
|
||||
}
|
||||
if ((double.tryParse(draft.amountController.text) ?? 0) <= 0) {
|
||||
@@ -343,6 +367,14 @@ class _ScreenshotParseSheetState extends State<ScreenshotParseSheet> {
|
||||
source: 'screenshot',
|
||||
sourceText: '截屏识别',
|
||||
occurredAt: draft.occurredAt,
|
||||
transferDirection: draft.type == 'transfer'
|
||||
? draft.transferDirection
|
||||
: null,
|
||||
counterparty:
|
||||
draft.type == 'transfer' &&
|
||||
draft.counterpartyController.text.trim().isNotEmpty
|
||||
? draft.counterpartyController.text.trim()
|
||||
: null,
|
||||
);
|
||||
draft.saved = true;
|
||||
savedAny = true;
|
||||
@@ -540,7 +572,7 @@ class _ScreenshotParseSheetState extends State<ScreenshotParseSheet> {
|
||||
}
|
||||
|
||||
Widget _buildDraftCard(_ScreenshotDraft draft, int index) {
|
||||
final categories = _categoriesFor(draft.type);
|
||||
final categories = _categoriesFor(draft.type, draft.transferDirection);
|
||||
final selectedCategory = categories
|
||||
.where((category) => category.id == draft.categoryId)
|
||||
.firstOrNull;
|
||||
@@ -583,6 +615,8 @@ class _ScreenshotParseSheetState extends State<ScreenshotParseSheet> {
|
||||
label: switch (draft.type) {
|
||||
'income' => '收入',
|
||||
'expense' => '支出',
|
||||
'transfer' =>
|
||||
draft.transferDirection == 'in' ? '转入' : '转出',
|
||||
_ => '待确认',
|
||||
},
|
||||
color: draft.type == 'unknown'
|
||||
@@ -629,6 +663,15 @@ class _ScreenshotParseSheetState extends State<ScreenshotParseSheet> {
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _TypeButton(
|
||||
label: '转账',
|
||||
selected: draft.type == 'transfer',
|
||||
color: AppTheme.orange,
|
||||
onTap: () => _changeType(draft, 'transfer'),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _TypeButton(
|
||||
label: '收入',
|
||||
@@ -639,6 +682,37 @@ class _ScreenshotParseSheetState extends State<ScreenshotParseSheet> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (draft.type == 'transfer') ...[
|
||||
SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _TypeButton(
|
||||
label: '转出',
|
||||
selected: draft.transferDirection == 'out',
|
||||
color: AppTheme.orange,
|
||||
onTap: () =>
|
||||
_changeTransferDirection(draft, 'out'),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _TypeButton(
|
||||
label: '转入',
|
||||
selected: draft.transferDirection == 'in',
|
||||
color: AppTheme.primary,
|
||||
onTap: () =>
|
||||
_changeTransferDirection(draft, 'in'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: draft.counterpartyController,
|
||||
decoration: InputDecoration(labelText: '转账对方'),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 10),
|
||||
_CategoryField(
|
||||
category: selectedCategory,
|
||||
@@ -715,6 +789,7 @@ class _ScreenshotParseSheetState extends State<ScreenshotParseSheet> {
|
||||
|
||||
class _ScreenshotDraft {
|
||||
String type;
|
||||
String transferDirection;
|
||||
int? categoryId;
|
||||
bool included;
|
||||
bool saved = false;
|
||||
@@ -722,6 +797,7 @@ class _ScreenshotDraft {
|
||||
final TextEditingController amountController;
|
||||
final TextEditingController noteController;
|
||||
final TextEditingController paymentController;
|
||||
final TextEditingController counterpartyController;
|
||||
|
||||
_ScreenshotDraft({
|
||||
required this.type,
|
||||
@@ -731,15 +807,19 @@ class _ScreenshotDraft {
|
||||
required String note,
|
||||
required String paymentMethod,
|
||||
required this.occurredAt,
|
||||
this.transferDirection = 'out',
|
||||
String counterparty = '',
|
||||
}) : included = included,
|
||||
amountController = TextEditingController(text: amount),
|
||||
noteController = TextEditingController(text: note),
|
||||
paymentController = TextEditingController(text: paymentMethod);
|
||||
paymentController = TextEditingController(text: paymentMethod),
|
||||
counterpartyController = TextEditingController(text: counterparty);
|
||||
|
||||
void dispose() {
|
||||
amountController.dispose();
|
||||
noteController.dispose();
|
||||
paymentController.dispose();
|
||||
counterpartyController.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user