fix(participants): show Me contextually per logged-in user

Participant id=1 was named "Me" in the DB, causing Sonu and other users
to see "Me" referring to Siddharth when viewing splits and shared expenses.

- Rename participant id=1 from "Me" to "Siddharth" in the DB
- /api/participants now substitutes "Me" for whichever participant matches
  the current user, so the label is always relative to the viewer
- split-modal: default split uses currentUser.id instead of name === "Me"
- transactions/page: filter and display logic uses participant ID not name
- shared/page: split chips show "Me" when participant_id === current user

Also includes add-transaction-modal tags support (pre-existing staged change).
This commit is contained in:
2026-04-01 18:36:29 +11:00
parent 0a1f6b48a2
commit 7491e70a15
5 changed files with 90 additions and 15 deletions
+8 -2
View File
@@ -1,11 +1,17 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/db"; import { prisma } from "@/lib/db";
import { queryRaw } from "@/lib/db"; import { getCurrentUser } from "@/lib/auth";
export async function GET() { export async function GET(req: NextRequest) {
const user = await getCurrentUser(req);
const participants = await prisma.participants.findMany({ const participants = await prisma.participants.findMany({
orderBy: { name: "asc" }, orderBy: { name: "asc" },
}); });
if (user) {
return NextResponse.json(
participants.map((p) => (p.id === user.id ? { ...p, name: "Me" } : p))
);
}
return NextResponse.json(participants); return NextResponse.json(participants);
} }
+19 -1
View File
@@ -13,6 +13,7 @@ import {
type SplitPayment, type SplitPayment,
} 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";
function formatDate(d: string) { function formatDate(d: string) {
return new Date(d).toLocaleDateString("en-AU", { day: "numeric", month: "short", year: "numeric" }); return new Date(d).toLocaleDateString("en-AU", { day: "numeric", month: "short", year: "numeric" });
@@ -264,6 +265,7 @@ export default function SharedPage() {
const [addingParticipant, setAddingParticipant] = useState(false); const [addingParticipant, setAddingParticipant] = useState(false);
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);
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -352,6 +354,7 @@ export default function SharedPage() {
<th className="text-left px-4 py-2 text-xs text-zinc-500 font-medium">Description</th> <th className="text-left px-4 py-2 text-xs text-zinc-500 font-medium">Description</th>
<th className="text-right px-4 py-2 text-xs text-zinc-500 font-medium">Amount</th> <th className="text-right px-4 py-2 text-xs text-zinc-500 font-medium">Amount</th>
<th className="text-left px-4 py-2 text-xs text-zinc-500 font-medium">Splits</th> <th className="text-left px-4 py-2 text-xs text-zinc-500 font-medium">Splits</th>
<th className="px-4 py-2"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -377,11 +380,19 @@ export default function SharedPage() {
{splits.map((s) => ( {splits.map((s) => (
<span key={s.participant_id} <span key={s.participant_id}
className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-zinc-800 text-zinc-300"> className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-zinc-800 text-zinc-300">
{s.name} {s.share_percent}% {s.participant_id === me?.id ? "Me" : s.name} {s.share_percent}%
</span> </span>
))} ))}
</div> </div>
</td> </td>
<td className="px-4 py-3">
<button
onClick={() => setEditModal(tx)}
className="text-xs text-zinc-500 hover:text-zinc-200 px-2 py-0.5 rounded hover:bg-zinc-800 transition-colors"
>
Edit
</button>
</td>
</tr> </tr>
); );
})} })}
@@ -399,6 +410,13 @@ export default function SharedPage() {
onClose={() => setPaymentModal(null)} onClose={() => setPaymentModal(null)}
/> />
)} )}
{editModal && (
<EditTransactionModal
transaction={editModal}
onClose={() => setEditModal(null)}
/>
)}
</div> </div>
); );
} }
+4 -3
View File
@@ -218,7 +218,7 @@ function MarkAsPaymentModal({
const { data: me } = useCurrentUser(); const { data: me } = useCurrentUser();
const record = useRecordPayment(); const record = useRecordPayment();
const others = participants.filter((p) => p.name !== "Me"); const others = participants.filter((p) => p.id !== me?.id);
const [participantId, setParticipantId] = useState<number | "">(others[0]?.id ?? ""); const [participantId, setParticipantId] = useState<number | "">(others[0]?.id ?? "");
@@ -517,6 +517,7 @@ function TransactionsContent() {
const { data, isLoading } = useTransactions(filters); const { data, isLoading } = useTransactions(filters);
const { data: banks } = useBanks(); const { data: banks } = useBanks();
const { data: tags } = useTags(); const { data: tags } = useTags();
const { data: me } = useCurrentUser();
const { data: statementInfo } = useStatement(parseInt(filters.statement_id) || 0); const { data: statementInfo } = useStatement(parseInt(filters.statement_id) || 0);
const updateTxn = useUpdateTransaction(); const updateTxn = useUpdateTransaction();
const bulkAction = useBulkAction(); const bulkAction = useBulkAction();
@@ -851,7 +852,7 @@ function TransactionsContent() {
</td> </td>
<td className="p-2 whitespace-nowrap"> <td className="p-2 whitespace-nowrap">
<div className="flex items-center gap-1 flex-wrap"> <div className="flex items-center gap-1 flex-wrap">
{t.splits?.filter((s) => s.name !== "Me").map((s) => ( {t.splits?.filter((s) => s.participant_id !== me?.id).map((s) => (
<span <span
key={s.participant_id} key={s.participant_id}
className={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${ className={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${
@@ -865,7 +866,7 @@ function TransactionsContent() {
<button <button
onClick={() => setSplitModal({ transactionId: t.id, amount: t.amount, description: t.description, merchant: t.effective_merchant || undefined, transactionIds: undefined })} onClick={() => setSplitModal({ transactionId: t.id, amount: t.amount, description: t.description, merchant: t.effective_merchant || undefined, transactionIds: undefined })}
className={`text-xs px-2 py-0.5 rounded transition-colors ${ className={`text-xs px-2 py-0.5 rounded transition-colors ${
t.splits?.some((s) => s.name !== "Me") t.splits?.some((s) => s.participant_id !== me?.id)
? "text-amber-400 hover:text-amber-200 hover:bg-zinc-800" ? "text-amber-400 hover:text-amber-200 hover:bg-zinc-800"
: "text-zinc-500 hover:text-zinc-200 hover:bg-zinc-800" : "text-zinc-500 hover:text-zinc-200 hover:bg-zinc-800"
}`} }`}
+52 -2
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { useCreateTransaction, useParticipants } from "@/lib/hooks"; import { useCreateTransaction, useParticipants, useTags } from "@/lib/hooks";
import { CATEGORIES, formatCategory } from "@/lib/categories"; import { CATEGORIES, formatCategory } from "@/lib/categories";
const TRANSACTION_TYPES = ["debit", "credit", "payment", "refund", "fee", "interest", "transfer"]; const TRANSACTION_TYPES = ["debit", "credit", "payment", "refund", "fee", "interest", "transfer"];
@@ -27,6 +27,7 @@ export function AddTransactionModal({
}) { }) {
const createTransaction = useCreateTransaction(); const createTransaction = useCreateTransaction();
const { data: participants = [] } = useParticipants(); const { data: participants = [] } = useParticipants();
const { data: allTags = [] } = useTags();
const [date, setDate] = useState(prefill?.date ?? new Date().toISOString().slice(0, 10)); const [date, setDate] = useState(prefill?.date ?? new Date().toISOString().slice(0, 10));
const [description, setDescription] = useState(prefill?.description ?? ""); const [description, setDescription] = useState(prefill?.description ?? "");
@@ -34,6 +35,7 @@ export function AddTransactionModal({
const [type, setType] = useState(prefill?.transaction_type ?? "debit"); const [type, setType] = useState(prefill?.transaction_type ?? "debit");
const [merchant, setMerchant] = useState(prefill?.merchant_normalized ?? ""); const [merchant, setMerchant] = useState(prefill?.merchant_normalized ?? "");
const [category, setCategory] = useState(prefill?.category ?? ""); const [category, setCategory] = useState(prefill?.category ?? "");
const [selectedTagIds, setSelectedTagIds] = useState<number[]>([]);
const [splits, setSplits] = useState<{ participant_id: number; share_percent: number }[]>( const [splits, setSplits] = useState<{ participant_id: number; share_percent: number }[]>(
prefill?.splits ?? [] prefill?.splits ?? []
); );
@@ -51,9 +53,15 @@ export function AddTransactionModal({
setSplits(splits.filter((_, idx) => idx !== i)); setSplits(splits.filter((_, idx) => idx !== i));
} }
function toggleTag(id: number) {
setSelectedTagIds((prev) =>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
);
}
async function handleSubmit(e: React.FormEvent) { async function handleSubmit(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
await createTransaction.mutateAsync({ const result = await createTransaction.mutateAsync({
date, date,
description, description,
amount: parseFloat(amount), amount: parseFloat(amount),
@@ -62,6 +70,17 @@ export function AddTransactionModal({
category: category || undefined, category: category || undefined,
splits: splits.length ? splits : undefined, splits: splits.length ? splits : undefined,
}); });
if (selectedTagIds.length && result?.id) {
await Promise.all(
selectedTagIds.map((tagId) =>
fetch(`/api/transactions/${result.id}/tags`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tag_id: tagId }),
})
)
);
}
onClose(); onClose();
} }
@@ -150,6 +169,37 @@ export function AddTransactionModal({
/> />
</div> </div>
{/* Tags */}
{allTags.length > 0 && (
<div>
<label className="block text-xs text-zinc-500 mb-1">Tags (optional)</label>
<div className="flex flex-wrap gap-1.5">
{allTags.map((tag) => {
const selected = selectedTagIds.includes(tag.id);
return (
<button
key={tag.id}
type="button"
onClick={() => toggleTag(tag.id)}
className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-xs border transition-colors ${
selected
? "border-transparent text-white"
: "border-zinc-600 text-zinc-400 hover:border-zinc-500 hover:text-zinc-300"
}`}
style={selected ? { backgroundColor: tag.color + "cc", borderColor: tag.color } : {}}
>
<span
className="w-1.5 h-1.5 rounded-full flex-shrink-0"
style={{ backgroundColor: tag.color }}
/>
{tag.name}
</button>
);
})}
</div>
</div>
)}
{/* Splits */} {/* Splits */}
<div> <div>
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
+7 -7
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useParticipants, useSetSplits, useTransactionSplits, useBulkAction, useCreateRule } from "@/lib/hooks"; import { useParticipants, useSetSplits, useTransactionSplits, useBulkAction, useCreateRule, useCurrentUser } from "@/lib/hooks";
interface Split { interface Split {
participant_id: number; participant_id: number;
@@ -22,6 +22,7 @@ export function SplitModal({ transactionId, transactionIds, amount, description,
const singleId = transactionId ?? 0; const singleId = transactionId ?? 0;
const { data: participants } = useParticipants(); const { data: participants } = useParticipants();
const { data: currentUser } = useCurrentUser();
const { data: existingSplits } = useTransactionSplits(isBulk ? 0 : singleId); const { data: existingSplits } = useTransactionSplits(isBulk ? 0 : singleId);
const setSplits = useSetSplits(); const setSplits = useSetSplits();
const bulkAction = useBulkAction(); const bulkAction = useBulkAction();
@@ -34,10 +35,9 @@ export function SplitModal({ transactionId, transactionIds, amount, description,
// Initialise: bulk always defaults to 100% Me; single loads existing splits // Initialise: bulk always defaults to 100% Me; single loads existing splits
useEffect(() => { useEffect(() => {
if (!participants || participants.length === 0) return; if (!participants || participants.length === 0 || !currentUser) return;
const me = participants.find((p) => p.name === "Me");
if (isBulk) { if (isBulk) {
if (me) setSplitsState([{ participant_id: me.id, share_percent: 100 }]); setSplitsState([{ participant_id: currentUser.id, share_percent: 100 }]);
} else if (existingSplits && existingSplits.length > 0) { } else if (existingSplits && existingSplits.length > 0) {
setSplitsState( setSplitsState(
existingSplits.map((s: { participant_id: number; share_percent: number }) => ({ existingSplits.map((s: { participant_id: number; share_percent: number }) => ({
@@ -45,10 +45,10 @@ export function SplitModal({ transactionId, transactionIds, amount, description,
share_percent: Number(s.share_percent), share_percent: Number(s.share_percent),
})) }))
); );
} else if (me) { } else {
setSplitsState([{ participant_id: me.id, share_percent: 100 }]); setSplitsState([{ participant_id: currentUser.id, share_percent: 100 }]);
} }
}, [existingSplits, participants, isBulk]); }, [existingSplits, participants, isBulk, currentUser]);
const total = splits.reduce((sum, s) => sum + s.share_percent, 0); const total = splits.reduce((sum, s) => sum + s.share_percent, 0);