feat(shared): replace settle buttons with payment ledger
- New split_payments table records actual payments between participants - Balance = total split obligations - total payments (splits never marked settled) - Record Payment modal per participant: direction toggle, amount pre-filled with balance, date, notes - Payment history inline on each balance card with +/- display and delete - Per-transaction Settle button removed; Action column removed from shared table - Splits always show the true cost breakdown regardless of payment state
This commit is contained in:
+50
-4
@@ -257,11 +257,43 @@ export function useSetSplits() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useSettleSplits() {
|
||||
export interface SplitPayment {
|
||||
id: number;
|
||||
from_participant_id: number;
|
||||
from_name: string;
|
||||
to_participant_id: number;
|
||||
to_name: string;
|
||||
amount: number;
|
||||
payment_date: string;
|
||||
notes: string | null;
|
||||
linked_transaction_id: number | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export function usePaymentHistory(participantId: number | null) {
|
||||
return useQuery<SplitPayment[]>({
|
||||
queryKey: ["split-payments", participantId],
|
||||
queryFn: async () => {
|
||||
if (!participantId) return [];
|
||||
const res = await fetch(`/api/split-payments?participant_id=${participantId}`);
|
||||
return res.json();
|
||||
},
|
||||
enabled: !!participantId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRecordPayment() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (body: { participant_id?: number; split_ids?: number[] }) => {
|
||||
const res = await fetch("/api/splits/settle", {
|
||||
mutationFn: async (body: {
|
||||
from_participant_id: number;
|
||||
to_participant_id: number;
|
||||
amount: number;
|
||||
payment_date: string;
|
||||
notes?: string;
|
||||
linked_transaction_id?: number;
|
||||
}) => {
|
||||
const res = await fetch("/api/split-payments", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
@@ -269,8 +301,22 @@ export function useSettleSplits() {
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["shared-transactions"] });
|
||||
qc.invalidateQueries({ queryKey: ["participant-balances"] });
|
||||
qc.invalidateQueries({ queryKey: ["split-payments"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeletePayment() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
const res = await fetch(`/api/split-payments?id=${id}`, { method: "DELETE" });
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["participant-balances"] });
|
||||
qc.invalidateQueries({ queryKey: ["split-payments"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+22
-12
@@ -276,16 +276,17 @@ export interface ParticipantBalance {
|
||||
export async function getParticipantBalances(ownerId: number) {
|
||||
return queryRaw<ParticipantBalance>(`
|
||||
SELECT p.id, p.name,
|
||||
COALESCE(SUM(combined.signed_amount), 0)::numeric(12,2) as total_owed,
|
||||
COALESCE(SUM(combined.unsettled_count), 0)::int as unsettled_count
|
||||
COALESCE(SUM(splits.signed_amount), 0)::numeric(12,2)
|
||||
- COALESCE(payments.net_paid, 0)::numeric(12,2) AS total_owed,
|
||||
COALESCE(SUM(splits.split_count), 0)::int AS unsettled_count
|
||||
FROM participants p
|
||||
|
||||
-- All split obligations (settled flag ignored — payments are the source of truth)
|
||||
LEFT JOIN (
|
||||
-- They owe me: their splits on transactions I own
|
||||
SELECT ts.participant_id AS pid,
|
||||
CASE WHEN ts.settled = false
|
||||
THEN (CASE WHEN t.transaction_type IN ('debit', 'fee', 'interest') THEN t.amount ELSE -t.amount END) * ts.share_percent / 100
|
||||
ELSE 0 END AS signed_amount,
|
||||
CASE WHEN ts.settled = false AND t.transaction_type IN ('debit', 'fee', 'interest') THEN 1 ELSE 0 END AS unsettled_count
|
||||
(CASE WHEN t.transaction_type IN ('debit', 'fee', 'interest') THEN t.amount ELSE -t.amount END) * ts.share_percent / 100 AS signed_amount,
|
||||
1 AS split_count
|
||||
FROM transaction_splits ts
|
||||
JOIN transactions t ON t.id = ts.transaction_id
|
||||
LEFT JOIN statements s ON s.id = t.statement_id
|
||||
@@ -295,17 +296,26 @@ export async function getParticipantBalances(ownerId: number) {
|
||||
|
||||
-- I owe them: my splits on transactions they own
|
||||
SELECT COALESCE(t.owner_id, s.owner_id) AS pid,
|
||||
CASE WHEN ts.settled = false
|
||||
THEN -(CASE WHEN t.transaction_type IN ('debit', 'fee', 'interest') THEN t.amount ELSE -t.amount END) * ts.share_percent / 100
|
||||
ELSE 0 END AS signed_amount,
|
||||
0 AS unsettled_count
|
||||
-((CASE WHEN t.transaction_type IN ('debit', 'fee', 'interest') THEN t.amount ELSE -t.amount END) * ts.share_percent / 100) AS signed_amount,
|
||||
0 AS split_count
|
||||
FROM transaction_splits ts
|
||||
JOIN transactions t ON t.id = ts.transaction_id
|
||||
LEFT JOIN statements s ON s.id = t.statement_id
|
||||
WHERE ts.participant_id = $1 AND COALESCE(t.owner_id, s.owner_id) != $1
|
||||
) combined ON combined.pid = p.id
|
||||
) splits ON splits.pid = p.id
|
||||
|
||||
-- Net payments: positive = they paid me, negative = I paid them
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
CASE WHEN sp.from_participant_id != $1 THEN sp.from_participant_id ELSE sp.to_participant_id END AS pid,
|
||||
SUM(CASE WHEN sp.to_participant_id = $1 THEN sp.amount ELSE -sp.amount END) AS net_paid
|
||||
FROM split_payments sp
|
||||
WHERE sp.from_participant_id = $1 OR sp.to_participant_id = $1
|
||||
GROUP BY pid
|
||||
) payments ON payments.pid = p.id
|
||||
|
||||
WHERE p.id != $1
|
||||
GROUP BY p.id, p.name
|
||||
GROUP BY p.id, p.name, payments.net_paid
|
||||
ORDER BY p.name
|
||||
`, [ownerId]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user