feat(orders): show where an Uber trip went, in the list
ci / lint-test (push) Failing after 41s

Five rows all reading "Order - Uber Trip" are indistinguishable — the list gives
you a date and an amount and nothing to tell one ride from another (user,
2026-07-27). Where the trip went is exactly what separates them, and it was
already stored on expense_metadata.route since this morning; nothing in the list
read it.

getTransactions now joins the receipt (both directions — transaction_id OR
matched_transaction_id, since a card-settled order points at the statement line
instead) and the description cell renders "Terminal 2, Melbourne Airport (MEL)
→ 19 Lady Penrhyn Dr" in the same italic sub-line notes use.

Two deliberate limits:

- **A note the user wrote always wins.** This only fills an empty sub-line; it
  never occupies the notes field, which is theirs.
- **Deliveries are excluded.** Their merchant already identifies them, so the
  restaurant's street address would be clutter on every food order. Gated on
  platform = 'uber'.

The summary keeps the first two comma-segments of each address — a truncation,
not a guess about geography. Uber puts the venue or street first, which is the
identifying part; the full stops with their times stay in the title attribute.
This commit is contained in:
2026-07-27 11:43:55 +10:00
parent 6161ddc9de
commit 3bb67f370d
3 changed files with 107 additions and 3 deletions
+32 -2
View File
@@ -9,7 +9,7 @@ import { TagPicker } from "@/components/tag-picker";
import { AddTransactionModal } from "@/components/add-transaction-modal";
import { EditTransactionModal } from "@/components/edit-transaction-modal";
import { CsvImportModal } from "@/components/csv-import-modal";
import type { TransactionRow } from "@/lib/queries";
import type { TransactionRow, RoutePointRow } from "@/lib/queries";
import type { RuleRow } from "@/lib/hooks";
function formatDate(d: string) {
@@ -475,6 +475,25 @@ function MultiSelect({
);
}
/**
* "Melbourne Airport (MEL) → Wyndham Vale VIC 3024" from the two stops on an
* Uber *trip* receipt. Deliveries are excluded by the caller: their merchant
* already identifies them, so the restaurant's street address would be clutter
* on every food order.
*
* Keeps the first two comma-segments of each address — a truncation, not a
* guess about geography. The venue or street comes first in Uber's format and
* is the identifying part; the full text stays in the title attribute.
*/
function routeSummary(route: RoutePointRow[] | null | undefined): string | null {
if (!route || route.length < 2) return null;
const short = (a: string) => a.split(",").slice(0, 2).join(",").trim();
const from = short(route[0].address);
const to = short(route[route.length - 1].address);
if (!from || !to) return null;
return `${from}${to}`;
}
export default function TransactionsPage() {
return (
<Suspense fallback={<p className="text-zinc-500 text-sm">Loading...</p>}>
@@ -929,8 +948,19 @@ function TransactionsContent() {
<td className="p-2 whitespace-nowrap text-zinc-500 text-xs">{formatDate(t.created_at)}</td>
<td className="p-2 max-w-xs">
<p className="truncate" title={t.description}>{t.description}</p>
{t.notes && (
{t.notes ? (
<p className="truncate text-xs text-zinc-500 italic mt-0.5" title={t.notes}>{t.notes}</p>
) : t.order_platform === "uber" && routeSummary(t.order_route) && (
// Five rows all reading "Order - Uber Trip" are
// indistinguishable. Where the trip went is what tells
// them apart, and it was already stored. A note the user
// wrote always wins — this only fills an empty line.
<p
className="truncate text-xs text-zinc-500 italic mt-0.5"
title={t.order_route!.map((r) => `${r.label}${r.time ? ` ${r.time}` : ""}: ${r.address}`).join("\n")}
>
{routeSummary(t.order_route)}
</p>
)}
</td>
<td className="p-2 max-w-[150px]">