fix(shared): tag filter SQL precedence, balance cards filter by tag
This commit is contained in:
@@ -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 balances = await getParticipantBalances(user.id);
|
||||
const tagParam = req.nextUrl.searchParams.get("tag_ids");
|
||||
const tagIds = tagParam ? tagParam.split(",").map(Number).filter(Boolean) : undefined;
|
||||
const balances = await getParticipantBalances(user.id, tagIds);
|
||||
return NextResponse.json(balances);
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ function PaymentHistory({ participantId, currentUserId }: { participantId: numbe
|
||||
export default function SharedPage() {
|
||||
const [tagIds, setTagIds] = useState<string[]>([]);
|
||||
const { data: transactions = [], isLoading: txLoading } = useSharedTransactions(tagIds);
|
||||
const { data: balances = [], isLoading: balLoading } = useParticipantBalances();
|
||||
const { data: balances = [], isLoading: balLoading } = useParticipantBalances(tagIds);
|
||||
const { data: me } = useCurrentUser();
|
||||
const [addingParticipant, setAddingParticipant] = useState(false);
|
||||
const [paymentModal, setPaymentModal] = useState<{ id: number; name: string; balance: number } | null>(null);
|
||||
|
||||
+4
-3
@@ -198,11 +198,12 @@ export function useParticipants() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useParticipantBalances() {
|
||||
export function useParticipantBalances(tagIds?: string[]) {
|
||||
return useQuery<{ id: number; name: string; total_owed: number; unsettled_count: number }[]>({
|
||||
queryKey: ["participant-balances"],
|
||||
queryKey: ["participant-balances", tagIds],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/participants/balances");
|
||||
const params = tagIds?.length ? `?tag_ids=${tagIds.join(",")}` : "";
|
||||
const res = await fetch(`/api/participants/balances${params}`);
|
||||
return res.json();
|
||||
},
|
||||
});
|
||||
|
||||
+14
-4
@@ -273,7 +273,14 @@ export interface ParticipantBalance {
|
||||
unsettled_count: number;
|
||||
}
|
||||
|
||||
export async function getParticipantBalances(ownerId: number) {
|
||||
export async function getParticipantBalances(ownerId: number, tagIds?: number[]) {
|
||||
const params: unknown[] = [ownerId];
|
||||
let tagFilter = "";
|
||||
if (tagIds?.length) {
|
||||
params.push(tagIds);
|
||||
tagFilter = `AND EXISTS (SELECT 1 FROM transaction_tags tt WHERE tt.transaction_id = t.id AND tt.tag_id = ANY($2::int[]))`;
|
||||
}
|
||||
|
||||
return queryRaw<ParticipantBalance>(`
|
||||
SELECT p.id, p.name,
|
||||
COALESCE(SUM(splits.signed_amount), 0)::numeric(12,2)
|
||||
@@ -281,7 +288,6 @@ export async function getParticipantBalances(ownerId: number) {
|
||||
COALESCE(SUM(splits.split_count), 0)::int AS unsettled_count
|
||||
FROM participants p
|
||||
|
||||
-- All split obligations (settled flag ignored — payments are the source of truth)
|
||||
LEFT JOIN (
|
||||
-- They owe me: their splits on transactions I own
|
||||
SELECT ts.participant_id AS pid,
|
||||
@@ -291,6 +297,7 @@ export async function getParticipantBalances(ownerId: number) {
|
||||
JOIN transactions t ON t.id = ts.transaction_id
|
||||
LEFT JOIN statements s ON s.id = t.statement_id
|
||||
WHERE COALESCE(t.owner_id, s.owner_id) = $1 AND ts.participant_id != $1
|
||||
${tagFilter}
|
||||
|
||||
UNION ALL
|
||||
|
||||
@@ -302,9 +309,10 @@ export async function getParticipantBalances(ownerId: number) {
|
||||
JOIN transactions t ON t.id = ts.transaction_id
|
||||
LEFT JOIN statements s ON s.id = t.statement_id
|
||||
WHERE ts.participant_id = $1 AND COALESCE(t.owner_id, s.owner_id) != $1
|
||||
${tagFilter}
|
||||
) splits ON splits.pid = p.id
|
||||
|
||||
-- Net payments: positive = they paid me, negative = I paid them
|
||||
-- Net payments always unfiltered (payments are against total debt, not per-tag)
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
CASE WHEN sp.from_participant_id != $1 THEN sp.from_participant_id ELSE sp.to_participant_id END AS pid,
|
||||
@@ -317,7 +325,7 @@ export async function getParticipantBalances(ownerId: number) {
|
||||
WHERE p.id != $1
|
||||
GROUP BY p.id, p.name, payments.net_paid
|
||||
ORDER BY p.name
|
||||
`, [ownerId]);
|
||||
`, params);
|
||||
}
|
||||
|
||||
export interface SharedTransactionRow extends TransactionRow {
|
||||
@@ -365,12 +373,14 @@ export async function getSharedTransactions(ownerId: number, tagIds?: 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 (
|
||||
(
|
||||
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 (
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user