mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
feat(budget): make a settle-up payment's date editable
The settle-up form only exposed from/to/amount and silently dated the payment by created_at, unlike a regular expense which has an editable date. Add a settled_at column (independent of created_at, same split as expense_date) through the schema, migration, service, REST and MCP surfaces, and the desktop + mobile forms; the ledger groups a payment by settled_at, falling back to created_at for rows recorded before the column existed.
This commit is contained in:
@@ -15,6 +15,8 @@ import MCostSheet from '../sheets/MCostSheet'
|
||||
import { ReceiptPreviewModal } from '../../../../components/Budget/ReceiptPreviewModal'
|
||||
import { readUserNote } from '../../../../components/Budget/CostsPanel.helpers'
|
||||
import { catMeta, COST_CAT_META } from '../../../../components/Budget/costsCategories'
|
||||
import { CustomDatePicker } from '../../../../components/shared/CustomDateTimePicker'
|
||||
import { localToday } from '../../../../components/Planner/today'
|
||||
import MConfirmSheet from '../../settings/MConfirmSheet'
|
||||
import MSheet from '../../../components/MSheet'
|
||||
import MChip from '../../../components/MChip'
|
||||
@@ -737,7 +739,9 @@ function MemberAvatar({ name, avatarUrl, isMe, variant, size, t }: {
|
||||
* No pixel spec exists for this form (the demo only toasts "Demo: add payment",
|
||||
* 03-trip-tabs.md §3.8) and no mobile/exported-desktop sheet covers it, so this
|
||||
* is a small local sheet built from the trip form-sheet chrome, kept to the
|
||||
* fields the settle-up card itself needs: from, to, amount in the display currency.
|
||||
* fields the settle-up card itself needs: from, to, amount in the display
|
||||
* currency, and the day it happened (editable like an expense's, unlike the
|
||||
* legacy created_at-only date — see `settlementDate` in CostsPanel.helpers.ts).
|
||||
*/
|
||||
function AddPaymentSheet({ open, onClose, tripId, base, people, me, toast, t, onSaved }: {
|
||||
open: boolean
|
||||
@@ -753,6 +757,7 @@ function AddPaymentSheet({ open, onClose, tripId, base, people, me, toast, t, on
|
||||
const [fromId, setFromId] = useState(me)
|
||||
const [toId, setToId] = useState(() => people.find(p => p.id !== me)?.id ?? me)
|
||||
const [amount, setAmount] = useState('')
|
||||
const [day, setDay] = useState(localToday())
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -760,6 +765,7 @@ function AddPaymentSheet({ open, onClose, tripId, base, people, me, toast, t, on
|
||||
setFromId(me)
|
||||
setToId(people.find(p => p.id !== me)?.id ?? me)
|
||||
setAmount('')
|
||||
setDay(localToday())
|
||||
setSaving(false)
|
||||
}, [open, me, people])
|
||||
|
||||
@@ -770,7 +776,7 @@ function AddPaymentSheet({ open, onClose, tripId, base, people, me, toast, t, on
|
||||
if (!valid || saving) return
|
||||
setSaving(true)
|
||||
try {
|
||||
await budgetApi.createSettlement(tripId, { from_user_id: fromId, to_user_id: toId, amount: amt, currency: base })
|
||||
await budgetApi.createSettlement(tripId, { from_user_id: fromId, to_user_id: toId, amount: amt, currency: base, settled_at: day })
|
||||
onSaved()
|
||||
} catch {
|
||||
toast.error(t('common.unknownError'))
|
||||
@@ -808,6 +814,9 @@ function AddPaymentSheet({ open, onClose, tripId, base, people, me, toast, t, on
|
||||
<input type="text" inputMode="decimal" value={amount} onChange={e => setAmount(e.target.value)} placeholder="0.00" className={FIELD_CLS} />
|
||||
<span className="flex-none font-geist text-[0.75rem] font-bold text-m-faint">{base}</span>
|
||||
</div>
|
||||
|
||||
<Eyebrow className="mb-[7px] mt-[14px] uppercase">{t('costs.day')}</Eyebrow>
|
||||
<CustomDatePicker value={day} onChange={setDay} style={{ width: '100%' }} />
|
||||
</div>
|
||||
<FormSheetFooter onCancel={onClose} cancelLabel={t('common.cancel')} onSubmit={save} submitLabel={t('costs.addPayment')} submitDisabled={!valid || saving} />
|
||||
</MSheet>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CostCategory } from '@trek/shared'
|
||||
import { readUserNote, splitEqualShares } from '../../../../components/Budget/CostsPanel.helpers'
|
||||
import { readUserNote, settlementDate, splitEqualShares } from '../../../../components/Budget/CostsPanel.helpers'
|
||||
import { catMeta, COST_CATEGORY_LIST } from '../../../../components/Budget/costsCategories'
|
||||
import { currencyDecimals } from '../../../../utils/formatters'
|
||||
import type { BudgetItem } from '../../../../types'
|
||||
@@ -87,6 +87,9 @@ export interface CostsSettlement {
|
||||
// Legacy rows predate this column (null) and are read as the display currency.
|
||||
currency?: string | null
|
||||
created_at?: string
|
||||
// The day the transfer actually happened; editable, unlike created_at (when it
|
||||
// was recorded). Null/absent on rows predating this field.
|
||||
settled_at?: string | null
|
||||
}
|
||||
|
||||
export interface CostsSettlementResponse {
|
||||
@@ -183,7 +186,7 @@ export function filterSettlements(settlements: CostsSettlement[], f: CostsFilter
|
||||
if (f.segment === 'owed') return []
|
||||
let list = settlements.slice()
|
||||
if (f.segment === 'mine') list = list.filter(s => s.from_user_id === me || s.to_user_id === me)
|
||||
if (f.dayKey) list = list.filter(s => (s.created_at || '').slice(0, 10) === f.dayKey)
|
||||
if (f.dayKey) list = list.filter(s => settlementDate(s) === f.dayKey)
|
||||
return list
|
||||
}
|
||||
|
||||
@@ -199,14 +202,14 @@ export interface CostsLedgerDayGroup {
|
||||
|
||||
/**
|
||||
* Like {@link groupByDay}, but also folds in settlement payments (see
|
||||
* {@link filterSettlements}) as their own ledger entries, keyed by the day
|
||||
* they were recorded — the mobile counterpart to desktop's unified
|
||||
* {@link filterSettlements}) as their own ledger entries, keyed by
|
||||
* {@link settlementDate} — the mobile counterpart to desktop's unified
|
||||
* `LedgerEntry` grouping, so a payment shows up even on a day with no expense.
|
||||
*/
|
||||
export function groupLedgerByDay(items: BudgetItem[], settlements: CostsSettlement[]): CostsLedgerDayGroup[] {
|
||||
const entries: CostsLedgerEntry[] = [
|
||||
...items.map(item => ({ kind: 'expense' as const, date: item.expense_date || '', item })),
|
||||
...settlements.map(settlement => ({ kind: 'payment' as const, date: (settlement.created_at || '').slice(0, 10), settlement })),
|
||||
...settlements.map(settlement => ({ kind: 'payment' as const, date: settlementDate(settlement), settlement })),
|
||||
]
|
||||
const byDate = new Map<string, CostsLedgerEntry[]>()
|
||||
for (const en of entries) {
|
||||
|
||||
Reference in New Issue
Block a user