diff --git a/src/app/api/shared-transactions/route.ts b/src/app/api/shared-transactions/route.ts index 0ab22b9..52b9ccb 100644 --- a/src/app/api/shared-transactions/route.ts +++ b/src/app/api/shared-transactions/route.ts @@ -6,6 +6,8 @@ export async function GET(req: NextRequest) { const user = await getCurrentUser(req); if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - const transactions = await getSharedTransactions(user.id); + const tagParam = req.nextUrl.searchParams.get("tag_ids"); + const tagIds = tagParam ? tagParam.split(",").map(Number).filter(Boolean) : undefined; + const transactions = await getSharedTransactions(user.id, tagIds); return NextResponse.json(transactions); } diff --git a/src/app/shared/page.tsx b/src/app/shared/page.tsx index 0901c54..b9e465a 100644 --- a/src/app/shared/page.tsx +++ b/src/app/shared/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useRef, useEffect } from "react"; import { useSharedTransactions, useParticipantBalances, @@ -9,6 +9,7 @@ import { usePaymentHistory, useDeletePayment, useCurrentUser, + useTags, type SplitPayment, } from "@/lib/hooks"; import type { SharedTransactionRow } from "@/lib/queries"; @@ -24,6 +25,55 @@ function formatAmount(n: number, type?: string) { return type && !SPEND_TYPES.has(type) ? `+${formatted}` : formatted; } +// ── Tag multi-select ────────────────────────────────────────────────────────── +function TagFilter({ value, onChange }: { value: string[]; onChange: (v: string[]) => void }) { + const { data: tags = [] } = useTags(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + function handler(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + } + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, []); + + if (!tags.length) return null; + + const toggle = (id: string) => + onChange(value.includes(id) ? value.filter((x) => x !== id) : [...value, id]); + + const label = value.length === 0 ? "All Tags" + : value.length === 1 ? (tags.find((t) => String(t.id) === value[0])?.name ?? "1 tag") + : `${value.length} tags`; + + return ( +
+ + {open && ( +
+ {tags.map((t) => ( + + ))} +
+ )} +
+ ); +} + // ── Add Participant ─────────────────────────────────────────────────────────── function AddParticipantForm({ onDone }: { onDone: () => void }) { const [name, setName] = useState(""); @@ -207,7 +257,8 @@ function PaymentHistory({ participantId, currentUserId }: { participantId: numbe // ── Main page ───────────────────────────────────────────────────────────────── export default function SharedPage() { - const { data: transactions = [], isLoading: txLoading } = useSharedTransactions(); + const [tagIds, setTagIds] = useState([]); + const { data: transactions = [], isLoading: txLoading } = useSharedTransactions(tagIds); const { data: balances = [], isLoading: balLoading } = useParticipantBalances(); const { data: me } = useCurrentUser(); const [addingParticipant, setAddingParticipant] = useState(false); @@ -216,14 +267,17 @@ export default function SharedPage() { return (
-
+

Shared Expenses

- {!addingParticipant && ( - - )} +
+ + {!addingParticipant && ( + + )} +
{addingParticipant && setAddingParticipant(false)} />} diff --git a/src/app/transactions/page.tsx b/src/app/transactions/page.tsx index 214e33c..9f1f9f0 100644 --- a/src/app/transactions/page.tsx +++ b/src/app/transactions/page.tsx @@ -225,7 +225,7 @@ function MarkAsPaymentModal({ const [direction, setDirection] = useState<"received" | "sent">( SPEND_TYPES.has(transaction.transaction_type) ? "sent" : "received" ); - const [amount, setAmount] = useState(transaction.amount.toFixed(2)); + const [amount, setAmount] = useState(Number(transaction.amount).toFixed(2)); const [date, setDate] = useState(transaction.transaction_date.slice(0, 10)); const [notes, setNotes] = useState(""); const [error, setError] = useState(""); diff --git a/src/lib/hooks.ts b/src/lib/hooks.ts index 8939a3a..3237058 100644 --- a/src/lib/hooks.ts +++ b/src/lib/hooks.ts @@ -208,11 +208,12 @@ export function useParticipantBalances() { }); } -export function useSharedTransactions() { +export function useSharedTransactions(tagIds?: string[]) { return useQuery({ - queryKey: ["shared-transactions"], + queryKey: ["shared-transactions", tagIds], queryFn: async () => { - const res = await fetch("/api/shared-transactions"); + const params = tagIds?.length ? `?tag_ids=${tagIds.join(",")}` : ""; + const res = await fetch(`/api/shared-transactions${params}`); return res.json(); }, }); diff --git a/src/lib/queries.ts b/src/lib/queries.ts index d259ce9..2bce73b 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -335,7 +335,14 @@ export async function getTags() { `); } -export async function getSharedTransactions(ownerId: number) { +export async function getSharedTransactions(ownerId: number, tagIds?: number[]) { + const params: unknown[] = [ownerId]; + let tagClause = ""; + if (tagIds?.length) { + params.push(tagIds); + tagClause = `AND EXISTS (SELECT 1 FROM transaction_tags tt WHERE tt.transaction_id = t.id AND tt.tag_id = ANY($2::int[]))`; + } + const rows = await queryRaw(` SELECT t.*, o.category_override, o.merchant_normalized as merchant_override, o.notes, @@ -358,17 +365,16 @@ export async function getSharedTransactions(ownerId: number) { 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) WHERE ( - -- I own this transaction and at least one other person has a split COALESCE(t.owner_id, s.owner_id) = $1 AND EXISTS (SELECT 1 FROM transaction_splits ts2 WHERE ts2.transaction_id = t.id AND ts2.participant_id != $1) ) OR ( - -- Someone else owns this transaction and I have a split on it COALESCE(t.owner_id, s.owner_id) != $1 AND EXISTS (SELECT 1 FROM transaction_splits ts_me WHERE ts_me.transaction_id = t.id AND ts_me.participant_id = $1) ) + ${tagClause} GROUP BY t.id, o.category_override, o.merchant_normalized, o.notes, s.bank_name, s.owner_id, p_owner.name ORDER BY t.transaction_date DESC - `, [ownerId]); + `, params); return rows.map((r) => ({ ...r,