shared page: expandable receipt details on split transactions
ci / lint-test (push) Successful in 57s
ci / lint-test (push) Successful in 57s
The item list existed only on the owner's transactions page — the person a meal or order was split WITH could see the amount but never what was in it. getSharedTransactions now carries the same order_platform LATERAL the transactions page uses, and rows with a receipt get the same disclosure arrow + OrderDetails expansion. The shared viewer is a split participant, so canAccessTransactions already authorises the order API for them; meal rows keep the verdict UI (the partner records their own), bridge rows stay review-free.
This commit is contained in:
+33
-3
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { Fragment, useState, useRef, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
useSharedTransactions,
|
useSharedTransactions,
|
||||||
useParticipantBalances,
|
useParticipantBalances,
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from "@/lib/hooks";
|
} from "@/lib/hooks";
|
||||||
import type { SharedTransactionRow } from "@/lib/queries";
|
import type { SharedTransactionRow } from "@/lib/queries";
|
||||||
import { EditTransactionModal } from "@/components/edit-transaction-modal";
|
import { EditTransactionModal } from "@/components/edit-transaction-modal";
|
||||||
|
import { OrderDetails } from "@/components/order-details";
|
||||||
import { formatCategory } from "@/lib/categories";
|
import { formatCategory } from "@/lib/categories";
|
||||||
import { CATEGORY_COLORS } from "@/lib/category-colors";
|
import { CATEGORY_COLORS } from "@/lib/category-colors";
|
||||||
|
|
||||||
@@ -370,6 +371,10 @@ export default function SharedPage() {
|
|||||||
const [paymentModal, setPaymentModal] = useState<{ id: number; name: string; balance: number } | null>(null);
|
const [paymentModal, setPaymentModal] = useState<{ id: number; name: string; balance: number } | null>(null);
|
||||||
const [showHistory, setShowHistory] = useState<number | null>(null);
|
const [showHistory, setShowHistory] = useState<number | null>(null);
|
||||||
const [editModal, setEditModal] = useState<SharedTransactionRow | null>(null);
|
const [editModal, setEditModal] = useState<SharedTransactionRow | null>(null);
|
||||||
|
// Receipt expansion, same pattern as the transactions page. The shared
|
||||||
|
// viewer is a split participant, so /api/transactions/[id]/order
|
||||||
|
// authorises them — the item list is part of what was shared.
|
||||||
|
const [expanded, setExpanded] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -543,11 +548,28 @@ export default function SharedPage() {
|
|||||||
{(transactions as SharedTransactionRow[]).map((tx) => {
|
{(transactions as SharedTransactionRow[]).map((tx) => {
|
||||||
const splits = Array.isArray(tx.splits) ? tx.splits : [];
|
const splits = Array.isArray(tx.splits) ? tx.splits : [];
|
||||||
return (
|
return (
|
||||||
<tr key={tx.id} className="border-b border-zinc-800/50 hover:bg-zinc-800/30">
|
<Fragment key={tx.id}>
|
||||||
|
<tr className="border-b border-zinc-800/50 hover:bg-zinc-800/30">
|
||||||
<td className="px-4 py-3 text-zinc-400 whitespace-nowrap">{formatDate(tx.transaction_date)}</td>
|
<td className="px-4 py-3 text-zinc-400 whitespace-nowrap">{formatDate(tx.transaction_date)}</td>
|
||||||
<td className="px-4 py-3 text-zinc-500 text-xs whitespace-nowrap">{formatDate(tx.created_at)}</td>
|
<td className="px-4 py-3 text-zinc-500 text-xs whitespace-nowrap">{formatDate(tx.created_at)}</td>
|
||||||
<td className="px-4 py-3 max-w-xs sticky left-0 z-10 bg-zinc-900 border-r border-zinc-800/80">
|
<td className="px-4 py-3 max-w-xs sticky left-0 z-10 bg-zinc-900 border-r border-zinc-800/80">
|
||||||
<p className="font-medium break-words">{tx.effective_merchant || tx.description}</p>
|
<div className="flex items-start gap-1.5">
|
||||||
|
{tx.order_platform && (
|
||||||
|
<button
|
||||||
|
onClick={() => setExpanded((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(tx.id)) next.delete(tx.id); else next.add(tx.id);
|
||||||
|
return next;
|
||||||
|
})}
|
||||||
|
className="text-zinc-600 hover:text-zinc-300 leading-none mt-0.5 shrink-0"
|
||||||
|
title={expanded.has(tx.id) ? "Hide receipt" : "Show the receipt this came from"}
|
||||||
|
aria-expanded={expanded.has(tx.id)}
|
||||||
|
>
|
||||||
|
{expanded.has(tx.id) ? "▾" : "▸"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<p className="font-medium break-words">{tx.effective_merchant || tx.description}</p>
|
||||||
|
</div>
|
||||||
{tx.effective_merchant && (
|
{tx.effective_merchant && (
|
||||||
<p className="text-xs text-zinc-500 break-words">{tx.description}</p>
|
<p className="text-xs text-zinc-500 break-words">{tx.description}</p>
|
||||||
)}
|
)}
|
||||||
@@ -615,6 +637,14 @@ export default function SharedPage() {
|
|||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{expanded.has(tx.id) && (
|
||||||
|
<tr className="border-b border-zinc-800/50 bg-zinc-900/40">
|
||||||
|
<td colSpan={8} className="px-4 py-3">
|
||||||
|
<OrderDetails transactionId={tx.id} currency={tx.currency ?? null} bare />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
+12
-1
@@ -866,6 +866,11 @@ export async function getSharedTransactions(ownerId: number, tagIds?: number[],
|
|||||||
COALESCE(t.owner_id, s.owner_id) as owner_id,
|
COALESCE(t.owner_id, s.owner_id) as owner_id,
|
||||||
p_owner.name as owner_name,
|
p_owner.name as owner_name,
|
||||||
COALESCE(src.created_at, t.created_at) as created_at,
|
COALESCE(src.created_at, t.created_at) as created_at,
|
||||||
|
-- Receipt flag, same LATERAL the transactions page uses: the shared
|
||||||
|
-- viewer is a split participant, so the order API authorises them —
|
||||||
|
-- without the flag here the disclosure arrow (and the item list behind
|
||||||
|
-- it) existed only on the owner's page.
|
||||||
|
order_ctx.platform as order_platform,
|
||||||
json_agg(json_build_object(
|
json_agg(json_build_object(
|
||||||
'split_id', ts.id,
|
'split_id', ts.id,
|
||||||
'participant_id', ts.participant_id,
|
'participant_id', ts.participant_id,
|
||||||
@@ -879,6 +884,12 @@ export async function getSharedTransactions(ownerId: number, tagIds?: number[],
|
|||||||
LEFT JOIN transaction_overrides o ON o.transaction_id = t.id
|
LEFT JOIN transaction_overrides o ON o.transaction_id = t.id
|
||||||
LEFT JOIN statements s ON s.id = t.statement_id
|
LEFT JOIN statements s ON s.id = t.statement_id
|
||||||
LEFT JOIN participants p_owner ON p_owner.id = COALESCE(t.owner_id, s.owner_id)
|
LEFT JOIN participants p_owner ON p_owner.id = COALESCE(t.owner_id, s.owner_id)
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT em.platform
|
||||||
|
FROM expense_metadata em
|
||||||
|
WHERE em.transaction_id = t.id OR em.matched_transaction_id = t.id
|
||||||
|
LIMIT 1
|
||||||
|
) order_ctx ON true
|
||||||
LEFT JOIN transactions src ON src.reconciled_with_id = t.id AND src.statement_id IS NULL
|
LEFT JOIN transactions src ON src.reconciled_with_id = t.id AND src.statement_id IS NULL
|
||||||
WHERE (
|
WHERE (
|
||||||
(
|
(
|
||||||
@@ -892,7 +903,7 @@ export async function getSharedTransactions(ownerId: number, tagIds?: number[],
|
|||||||
AND ${EXCLUDE_RECONCILED_SOURCE}
|
AND ${EXCLUDE_RECONCILED_SOURCE}
|
||||||
${tagClause}
|
${tagClause}
|
||||||
${participantClause}
|
${participantClause}
|
||||||
GROUP BY t.id, o.category_override, o.merchant_normalized, o.notes, s.bank_name, s.currency, s.owner_id, p_owner.name, src.created_at
|
GROUP BY t.id, o.category_override, o.merchant_normalized, o.notes, s.bank_name, s.currency, s.owner_id, p_owner.name, src.created_at, order_ctx.platform
|
||||||
ORDER BY t.transaction_date DESC
|
ORDER BY t.transaction_date DESC
|
||||||
`, params);
|
`, params);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user