feat: improve quick bookkeeping and asset inclusion

Add commemorative assets, session undo, paired repayment, month/day markers and filters. Compact account cards, move transfer entry, persist backup metadata, and update Chinese documentation.
This commit is contained in:
陈煜 committed 2026-10-04 16:01:24 +08:00
1 parent ad6466cb6b
commit 096f1577c2
22 files changed
+1465 -73

No files matched your search

+165
View File
@@ -0,0 +1,165 @@
import React, { useState } from 'react';
import { AccountPicker } from './AccountPicker';
import { QuickTransfer } from './QuickTransfer';
import { money, positionAmount, type Position } from './api';
import { t as tr } from './i18n';
export type QuickEntry = {
key: string;
kind: 'balance' | 'transfer' | 'mark' | 'booked';
id: string;
positionId: string;
title: string;
currency: string;
before?: string;
after?: string;
amount?: string;
received?: string;
targetCurrency?: string;
date: string;
previousMonth?: string | null;
month?: string | null;
markUpdatedAt?: string;
previousDate?: string | null;
bookedDate?: string | null;
undone?: boolean;
};
export const isPaidForMonth = (
position: Pick<Position, 'kind' | 'side' | 'lastRepaymentMonth'>,
month: string,
) =>
position.kind === 'account' &&
position.side === 'liability' &&
position.lastRepaymentMonth === month;
export const isBookedToday = (position: Pick<Position, 'kind' | 'lastBookedDate'>, day: string) =>
position.kind === 'account' && position.lastBookedDate === day;
export const quickAccountVisible = (
position: Position,
month: string,
day: string,
unpaidOnly: boolean,
hideBooked: boolean,
) =>
(!unpaidOnly ||
(position.side === 'liability' && !position.archived && !isPaidForMonth(position, month))) &&
(!hideBooked || !isBookedToday(position, day));
export const canQuickRepay = (position: Pick<Position, 'kind' | 'side' | 'amount' | 'archived'>) =>
position.kind === 'account' &&
position.side === 'liability' &&
!position.archived &&
!position.amount.startsWith('-') &&
!/^0(?:\.0+)?$/.test(position.amount);
export function QuickJournal({
entries,
busy,
undo,
}: {
entries: QuickEntry[];
busy: boolean;
undo: (entry: QuickEntry) => void;
}) {
return (
<section className="quick-journal" aria-label={tr('本次快速记账')}>
<h2>{tr('本次快速记账')}</h2>
<p className="muted">{tr('本次保存成功的记录,可逐项撤回。')}</p>
{!entries.length ? (
<p className="muted">{tr('还没有保存记录')}</p>
) : (
<ol>
{entries.map((entry) => (
<li key={entry.key} className={entry.undone ? 'undone' : ''}>
<strong>{entry.title}</strong>
{entry.kind === 'balance' && (
<p>
{money(entry.before || '0', entry.currency)} →{' '}
{money(entry.after || '0', entry.currency)}
</p>
)}
{entry.kind === 'transfer' && (
<p>
{money(entry.amount || '0', entry.currency)}
{entry.targetCurrency !== entry.currency && (
<> → {money(entry.received || '0', entry.targetCurrency || entry.currency)}</>
)}
</p>
)}
{entry.kind === 'mark' && (
<p>{tr(entry.month ? '已标记本月还款' : '已取消本月标记')}</p>
)}
{entry.kind === 'booked' && (
<p>{tr(entry.bookedDate ? '已标记当天记账' : '已取消当天标记')}</p>
)}
<small>{entry.date.replace('T', ' ')} · UTC+8</small>
{entry.undone ? (
<span className="badge">{tr('已撤回')}</span>
) : (
<button
type="button"
className="secondary"
disabled={busy}
onClick={() => undo(entry)}
>
{tr('撤回')}
</button>
)}
</li>
))}
</ol>
)}
</section>
);
}
export function QuickRepayment({
target,
accounts,
busy,
submit,
}: {
target: Position;
accounts: Position[];
busy: boolean;
submit: (data: unknown) => void;
}) {
const choices = accounts.filter(
(p) => p.kind === 'account' && p.side === 'asset' && !p.archived && p.id !== target.id,
);
const [sourceId, setSourceId] = useState(choices[0]?.id || '');
const source = choices.find((p) => p.id === sourceId);
return (
<section>
<p>
{tr('还款账户')}:{target.name} · {money(positionAmount(target), target.currency)}
</p>
<p className="muted">
{tr('从付款账户转入欠款账户,双方余额同步更新;这里填写本次还款金额。')}
</p>
{!choices.length ? (
<p>{tr('请先添加启用的资产账户作为付款账户。')}</p>
) : (
<>
<label>
{tr('付款账户')}
<AccountPicker
name="quick-payment-source"
accounts={choices}
value={sourceId}
onChange={setSourceId}
/>
</label>
{source && (
<QuickTransfer
key={source.id + target.id}
source={source}
target={target}
busy={busy}
changeSource={() => setSourceId('')}
changeTarget={() => {}}
fixedTarget
actionLabel={tr('确认还款')}
submit={submit}
/>
)}
</>
)}
</section>
);
}