mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 12:04:02 +00:00
feat: add payer control for purchases
- Add explicit payerMemberId field to purchase ledger entries - Add 'Paid by' selector in mini app purchase add/edit forms - Default payer to current user when creating new purchases - Allow admins to change who made existing purchases - Update backend handlers to accept and persist payerMemberId - Add i18n translations for 'Paid by' label (EN/RU) All quality gates pass: build, typecheck, lint, format, test Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -169,6 +169,7 @@ export const dictionary = {
|
||||
participantExcluded: 'Excluded',
|
||||
purchaseCustomShareLabel: 'Custom amount',
|
||||
purchaseEditorBody: 'Review the purchase details and adjust the split only when needed.',
|
||||
purchasePayerLabel: 'Paid by',
|
||||
paymentsAdminTitle: 'Payments',
|
||||
paymentsAdminBody: 'Add, fix, or remove payment records for the current cycle.',
|
||||
paymentsAddAction: 'Add payment',
|
||||
@@ -516,6 +517,7 @@ export const dictionary = {
|
||||
purchaseCustomShareLabel: 'Своя сумма',
|
||||
purchaseEditorBody:
|
||||
'Проверь покупку и меняй детали разделения только если это действительно нужно.',
|
||||
purchasePayerLabel: 'Оплатил',
|
||||
paymentsAdminTitle: 'Оплаты',
|
||||
paymentsAdminBody: 'Добавляй, исправляй или удаляй оплаты за текущий цикл.',
|
||||
paymentsAddAction: 'Добавить оплату',
|
||||
|
||||
@@ -33,6 +33,7 @@ export type PurchaseDraft = {
|
||||
description: string
|
||||
amountMajor: string
|
||||
currency: 'USD' | 'GEL'
|
||||
payerMemberId?: string
|
||||
splitMode: 'equal' | 'custom_amounts'
|
||||
splitInputMode: 'equal' | 'exact' | 'percentage'
|
||||
participants: ParticipantShare[]
|
||||
@@ -110,6 +111,7 @@ export function purchaseDrafts(
|
||||
description: entry.title,
|
||||
amountMajor: entry.amountMajor,
|
||||
currency: entry.currency,
|
||||
...(entry.payerMemberId !== undefined ? { payerMemberId: entry.payerMemberId } : {}),
|
||||
splitMode: entry.purchaseSplitMode ?? 'equal',
|
||||
splitInputMode: (entry.purchaseSplitMode ?? 'equal') === 'equal' ? 'equal' : 'exact',
|
||||
participants:
|
||||
@@ -129,6 +131,7 @@ export function purchaseDraftForEntry(entry: MiniAppDashboard['ledger'][number])
|
||||
description: entry.title,
|
||||
amountMajor: entry.amountMajor,
|
||||
currency: entry.currency,
|
||||
...(entry.payerMemberId !== undefined ? { payerMemberId: entry.payerMemberId } : {}),
|
||||
splitMode: entry.purchaseSplitMode ?? 'equal',
|
||||
splitInputMode: (entry.purchaseSplitMode ?? 'equal') === 'equal' ? 'equal' : 'exact',
|
||||
participants:
|
||||
|
||||
@@ -152,6 +152,7 @@ export interface MiniAppDashboard {
|
||||
included: boolean
|
||||
shareAmountMajor: string | null
|
||||
}[]
|
||||
payerMemberId?: string
|
||||
}[]
|
||||
}
|
||||
|
||||
@@ -996,6 +997,7 @@ export async function addMiniAppPurchase(
|
||||
description: string
|
||||
amountMajor: string
|
||||
currency: 'USD' | 'GEL'
|
||||
payerMemberId?: string
|
||||
split?: {
|
||||
mode: 'equal' | 'custom_amounts'
|
||||
participants: readonly {
|
||||
@@ -1030,6 +1032,7 @@ export async function updateMiniAppPurchase(
|
||||
description: string
|
||||
amountMajor: string
|
||||
currency: 'USD' | 'GEL'
|
||||
payerMemberId?: string
|
||||
split?: {
|
||||
mode: 'equal' | 'custom_amounts'
|
||||
participants: readonly {
|
||||
|
||||
@@ -202,7 +202,7 @@ function ParticipantSplitInputs(props: ParticipantSplitInputsProps) {
|
||||
}
|
||||
|
||||
export default function LedgerRoute() {
|
||||
const { initData, refreshHouseholdData } = useSession()
|
||||
const { initData, refreshHouseholdData, session } = useSession()
|
||||
const { copy } = useI18n()
|
||||
const { dashboard, loading, effectiveIsAdmin, purchaseLedger, utilityLedger, paymentLedger } =
|
||||
useDashboard()
|
||||
@@ -384,6 +384,11 @@ export default function LedgerRoute() {
|
||||
description: draft.description,
|
||||
amountMajor: draft.amountMajor,
|
||||
currency: draft.currency,
|
||||
...(draft.payerMemberId
|
||||
? {
|
||||
payerMemberId: draft.payerMemberId
|
||||
}
|
||||
: {}),
|
||||
split: {
|
||||
mode: draft.splitMode,
|
||||
participants: draft.participants.map((p) => ({
|
||||
@@ -428,6 +433,11 @@ export default function LedgerRoute() {
|
||||
description: draft.description,
|
||||
amountMajor: draft.amountMajor,
|
||||
currency: draft.currency,
|
||||
...(draft.payerMemberId
|
||||
? {
|
||||
payerMemberId: draft.payerMemberId
|
||||
}
|
||||
: {}),
|
||||
...(draft.participants.length > 0
|
||||
? {
|
||||
split: {
|
||||
@@ -444,10 +454,12 @@ export default function LedgerRoute() {
|
||||
: {})
|
||||
})
|
||||
setAddPurchaseOpen(false)
|
||||
const currentSession = session()
|
||||
setNewPurchase({
|
||||
description: '',
|
||||
amountMajor: '',
|
||||
currency: (dashboard()?.currency as 'USD' | 'GEL') ?? 'GEL',
|
||||
...(currentSession.status === 'ready' ? { payerMemberId: currentSession.member.id } : {}),
|
||||
splitMode: 'equal',
|
||||
splitInputMode: 'equal',
|
||||
participants: []
|
||||
@@ -785,6 +797,25 @@ export default function LedgerRoute() {
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={copy().purchasePayerLabel}>
|
||||
<Select
|
||||
value={newPurchase().payerMemberId ?? ''}
|
||||
ariaLabel={copy().purchasePayerLabel}
|
||||
placeholder="—"
|
||||
options={[{ value: '', label: '—' }, ...memberOptions()]}
|
||||
onChange={(value) =>
|
||||
setNewPurchase((p) => {
|
||||
const base = { ...p }
|
||||
if (value) {
|
||||
return { ...base, payerMemberId: value }
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { payerMemberId, ...rest } = base
|
||||
return rest as PurchaseDraft
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<div style={{ 'grid-column': '1 / -1' }}>
|
||||
<Field label="Split By">
|
||||
<Select
|
||||
@@ -892,6 +923,26 @@ export default function LedgerRoute() {
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={copy().purchasePayerLabel}>
|
||||
<Select
|
||||
value={draft().payerMemberId ?? ''}
|
||||
ariaLabel={copy().purchasePayerLabel}
|
||||
placeholder="—"
|
||||
options={[{ value: '', label: '—' }, ...memberOptions()]}
|
||||
onChange={(value) =>
|
||||
setPurchaseDraft((d) => {
|
||||
if (!d) return d
|
||||
const base = { ...d }
|
||||
if (value) {
|
||||
return { ...base, payerMemberId: value }
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { payerMemberId, ...rest } = base
|
||||
return rest as PurchaseDraft
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<div style={{ 'grid-column': '1 / -1' }}>
|
||||
<Field label="Split By">
|
||||
<Select
|
||||
|
||||
Reference in New Issue
Block a user