feat(filters): add No tags filter to transactions and shared pages
This commit is contained in:
@@ -7,7 +7,9 @@ export async function GET(req: NextRequest) {
|
|||||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
const tagParam = req.nextUrl.searchParams.get("tag_ids");
|
const tagParam = req.nextUrl.searchParams.get("tag_ids");
|
||||||
const tagIds = tagParam ? tagParam.split(",").map(Number).filter(Boolean) : undefined;
|
const rawIds = tagParam ? tagParam.split(",").filter(Boolean) : [];
|
||||||
const transactions = await getSharedTransactions(user.id, tagIds);
|
const noTags = rawIds.includes("untagged");
|
||||||
|
const tagIds = rawIds.filter((id) => id !== "untagged").map(Number).filter((n) => !isNaN(n));
|
||||||
|
const transactions = await getSharedTransactions(user.id, tagIds.length ? tagIds : undefined, noTags);
|
||||||
return NextResponse.json(transactions);
|
return NextResponse.json(transactions);
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-5
@@ -40,12 +40,20 @@ function TagFilter({ value, onChange }: { value: string[]; onChange: (v: string[
|
|||||||
return () => document.removeEventListener("mousedown", handler);
|
return () => document.removeEventListener("mousedown", handler);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!tags.length) return null;
|
const toggle = (id: string) => {
|
||||||
|
let next: string[];
|
||||||
const toggle = (id: string) =>
|
if (value.includes(id)) {
|
||||||
onChange(value.includes(id) ? value.filter((x) => x !== id) : [...value, id]);
|
next = value.filter((x) => x !== id);
|
||||||
|
} else if (id === "untagged") {
|
||||||
|
next = ["untagged"];
|
||||||
|
} else {
|
||||||
|
next = [...value.filter((x) => x !== "untagged"), id];
|
||||||
|
}
|
||||||
|
onChange(next);
|
||||||
|
};
|
||||||
|
|
||||||
const label = value.length === 0 ? "All Tags"
|
const label = value.length === 0 ? "All Tags"
|
||||||
|
: value.includes("untagged") ? "No tags"
|
||||||
: value.length === 1 ? (tags.find((t) => String(t.id) === value[0])?.name ?? "1 tag")
|
: value.length === 1 ? (tags.find((t) => String(t.id) === value[0])?.name ?? "1 tag")
|
||||||
: `${value.length} tags`;
|
: `${value.length} tags`;
|
||||||
|
|
||||||
@@ -61,6 +69,11 @@ function TagFilter({ value, onChange }: { value: string[]; onChange: (v: string[
|
|||||||
</button>
|
</button>
|
||||||
{open && (
|
{open && (
|
||||||
<div className="absolute top-full mt-1 z-20 bg-zinc-900 border border-zinc-700 rounded-lg shadow-xl min-w-[160px] max-h-56 overflow-y-auto">
|
<div className="absolute top-full mt-1 z-20 bg-zinc-900 border border-zinc-700 rounded-lg shadow-xl min-w-[160px] max-h-56 overflow-y-auto">
|
||||||
|
<label className="flex items-center gap-2 px-3 py-1.5 hover:bg-zinc-800 cursor-pointer text-sm border-b border-zinc-800">
|
||||||
|
<input type="checkbox" checked={value.includes("untagged")} onChange={() => toggle("untagged")}
|
||||||
|
className="accent-indigo-500 flex-shrink-0" />
|
||||||
|
<span className="text-zinc-400 italic">No tags</span>
|
||||||
|
</label>
|
||||||
{tags.map((t) => (
|
{tags.map((t) => (
|
||||||
<label key={t.id} className="flex items-center gap-2 px-3 py-1.5 hover:bg-zinc-800 cursor-pointer text-sm">
|
<label key={t.id} className="flex items-center gap-2 px-3 py-1.5 hover:bg-zinc-800 cursor-pointer text-sm">
|
||||||
<input type="checkbox" checked={value.includes(String(t.id))} onChange={() => toggle(String(t.id))}
|
<input type="checkbox" checked={value.includes(String(t.id))} onChange={() => toggle(String(t.id))}
|
||||||
@@ -259,8 +272,9 @@ function PaymentHistory({ participantId, currentUserId }: { participantId: numbe
|
|||||||
// ── Main page ─────────────────────────────────────────────────────────────────
|
// ── Main page ─────────────────────────────────────────────────────────────────
|
||||||
export default function SharedPage() {
|
export default function SharedPage() {
|
||||||
const [tagIds, setTagIds] = useState<string[]>([]);
|
const [tagIds, setTagIds] = useState<string[]>([]);
|
||||||
|
const realTagIds = tagIds.filter((id) => id !== "untagged");
|
||||||
const { data: transactions = [], isLoading: txLoading } = useSharedTransactions(tagIds);
|
const { data: transactions = [], isLoading: txLoading } = useSharedTransactions(tagIds);
|
||||||
const { data: balances = [], isLoading: balLoading } = useParticipantBalances(tagIds);
|
const { data: balances = [], isLoading: balLoading } = useParticipantBalances(realTagIds);
|
||||||
const { data: me } = useCurrentUser();
|
const { data: me } = useCurrentUser();
|
||||||
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);
|
||||||
|
|||||||
@@ -651,9 +651,14 @@ function TransactionsContent() {
|
|||||||
placeholder="All Banks"
|
placeholder="All Banks"
|
||||||
/>
|
/>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
options={(tags ?? []).map((t) => ({ value: String(t.id), label: t.name }))}
|
options={[{ value: "untagged", label: "No tags" }, ...(tags ?? []).map((t) => ({ value: String(t.id), label: t.name }))]}
|
||||||
value={filters.tag_ids}
|
value={filters.tag_ids}
|
||||||
onChange={(v) => setFilters((f) => ({ ...f, tag_ids: v, offset: 0 }))}
|
onChange={(v) => {
|
||||||
|
let next = v;
|
||||||
|
if (v.includes("untagged") && !filters.tag_ids.includes("untagged")) next = ["untagged"];
|
||||||
|
else if (filters.tag_ids.includes("untagged") && v.length > 1) next = v.filter((x) => x !== "untagged");
|
||||||
|
setFilters((f) => ({ ...f, tag_ids: next, offset: 0 }));
|
||||||
|
}}
|
||||||
placeholder="All Tags"
|
placeholder="All Tags"
|
||||||
/>
|
/>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
|
|||||||
+11
-3
@@ -115,8 +115,14 @@ export async function getTransactions(ownerId: number, filters: TransactionFilte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (filters.tag_ids?.length) {
|
if (filters.tag_ids?.length) {
|
||||||
|
const noTags = filters.tag_ids.includes("untagged");
|
||||||
|
const realTagIds = filters.tag_ids.filter((id) => id !== "untagged").map(Number);
|
||||||
|
if (noTags) {
|
||||||
|
conditions.push(`NOT EXISTS (SELECT 1 FROM transaction_tags tt2 WHERE tt2.transaction_id = t.id)`);
|
||||||
|
} else if (realTagIds.length > 0) {
|
||||||
conditions.push(`EXISTS (SELECT 1 FROM transaction_tags tt2 WHERE tt2.transaction_id = t.id AND tt2.tag_id = ANY($${paramIdx++}::int[]))`);
|
conditions.push(`EXISTS (SELECT 1 FROM transaction_tags tt2 WHERE tt2.transaction_id = t.id AND tt2.tag_id = ANY($${paramIdx++}::int[]))`);
|
||||||
params.push(filters.tag_ids.map(Number));
|
params.push(realTagIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (filters.transaction_types?.length) {
|
if (filters.transaction_types?.length) {
|
||||||
conditions.push(`t.transaction_type = ANY($${paramIdx++}::text[])`);
|
conditions.push(`t.transaction_type = ANY($${paramIdx++}::text[])`);
|
||||||
@@ -343,10 +349,12 @@ export async function getTags() {
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSharedTransactions(ownerId: number, tagIds?: number[]) {
|
export async function getSharedTransactions(ownerId: number, tagIds?: number[], noTags?: boolean) {
|
||||||
const params: unknown[] = [ownerId];
|
const params: unknown[] = [ownerId];
|
||||||
let tagClause = "";
|
let tagClause = "";
|
||||||
if (tagIds?.length) {
|
if (noTags) {
|
||||||
|
tagClause = `AND NOT EXISTS (SELECT 1 FROM transaction_tags tt WHERE tt.transaction_id = t.id)`;
|
||||||
|
} else if (tagIds?.length) {
|
||||||
params.push(tagIds);
|
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[]))`;
|
tagClause = `AND EXISTS (SELECT 1 FROM transaction_tags tt WHERE tt.transaction_id = t.id AND tt.tag_id = ANY($2::int[]))`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user