shared page: expandable receipt details on split transactions
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:
2026-08-10 18:27:16 +10:00
parent 7f8e363b5b
commit 0e5422919e
2 changed files with 45 additions and 4 deletions
+33 -3
View File
@@ -1,6 +1,6 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { Fragment, useState, useRef, useEffect } from "react";
import {
useSharedTransactions,
useParticipantBalances,
@@ -16,6 +16,7 @@ import {
} from "@/lib/hooks";
import type { SharedTransactionRow } from "@/lib/queries";
import { EditTransactionModal } from "@/components/edit-transaction-modal";
import { OrderDetails } from "@/components/order-details";
import { formatCategory } from "@/lib/categories";
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 [showHistory, setShowHistory] = useState<number | 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 (
<div className="space-y-6">
@@ -543,11 +548,28 @@ export default function SharedPage() {
{(transactions as SharedTransactionRow[]).map((tx) => {
const splits = Array.isArray(tx.splits) ? tx.splits : [];
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-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">
<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 && (
<p className="text-xs text-zinc-500 break-words">{tx.description}</p>
)}
@@ -615,6 +637,14 @@ export default function SharedPage() {
</button>
</td>
</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>