mercury-web/src/features/planner/CategoryTransactions.tsx

103 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import * as React from "react";
import Link from "next/link";
import { TextFieldRoot, TextFieldInput } from "@seed-design/react";
import { Card } from "@/ds";
import { tugrik } from "@/ds/money";
import { useTransactions } from "@/api/hooks/reads";
import type { Txn } from "@/api/schemas";
import { plannerStrings as s } from "./strings";
export interface CategoryTransactionsProps {
category: string;
}
/** MM.dd from an RFC3339/ISO timestamp — mirrors CategoryTransactionsView's
* `shortDate` on iOS. Falls back to an empty string on unparsable input. */
function shortDate(rfc: string): string {
const d = new Date(rfc);
if (Number.isNaN(d.getTime())) return "";
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
return `${mm}.${dd}`;
}
/** Every transaction in one category (server expands to sub-categories too) —
* opened by tapping a category-limit row on the planner hub. Ports
* `CategoryTransactionsView.swift`: header + filter + list, tap-free (the web
* port has no transaction detail cover yet). */
export function CategoryTransactions({ category }: CategoryTransactionsProps) {
const { data, isLoading } = useTransactions({
category,
from: "2020-01-01",
to: "2027-12-31",
limit: 300,
});
const [search, setSearch] = React.useState("");
const rows: Txn[] = React.useMemo(() => {
const all = data ?? [];
const needle = search.trim().toLowerCase();
if (!needle) return all;
return all.filter((t) => t.title.toLowerCase().includes(needle));
}, [data, search]);
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-3">
<Link href="/planner" aria-label={s.amountEntry.cancel} style={{ color: "var(--seed-color-fg-neutral)" }}>
</Link>
<h1 style={{ fontWeight: 700, fontSize: 16 }}>{category}</h1>
</div>
<TextFieldRoot value={search} onValueChange={setSearch}>
<TextFieldInput
placeholder={s.categoryTransactions.filterPlaceholder}
aria-label={s.categoryTransactions.filterPlaceholder}
/>
</TextFieldRoot>
<Card>
{isLoading && <p style={{ color: "var(--seed-color-fg-placeholder)" }}></p>}
{!isLoading && rows.length === 0 && (
<p style={{ color: "var(--seed-color-fg-placeholder)" }}>{s.categoryTransactions.empty}</p>
)}
{!isLoading && rows.length > 0 && (
<ul className="flex flex-col gap-3">
{rows.map((t, i) => {
const income = t.direction === "income";
return (
<li
key={`${t.txnId ?? i}`}
className="flex items-center justify-between gap-3"
style={{
borderBottom: i < rows.length - 1 ? "1px solid var(--seed-color-border-default, #eee)" : undefined,
paddingBottom: 12,
}}
>
<div className="flex flex-col">
<span style={{ fontWeight: 700 }}>{t.title || t.category}</span>
<span style={{ fontSize: 12, color: "var(--seed-color-fg-placeholder)" }}>
{shortDate(t.date)} · {t.category}
</span>
</div>
<span
style={{
fontWeight: 700,
color: income ? "var(--seed-color-fg-positive)" : "var(--seed-color-fg-critical)",
}}
>
{income ? "+" : ""}
{tugrik(t.amount)}
</span>
</li>
);
})}
</ul>
)}
</Card>
</div>
);
}