Simpl-Resultat/src/pages/ReportsHighlightsPage.tsx
le king fu 3b70abdb9e
Some checks failed
PR Check / rust (push) Has been cancelled
PR Check / frontend (push) Has been cancelled
PR Check / rust (pull_request) Has been cancelled
PR Check / frontend (pull_request) Has been cancelled
feat: propagate right-click "add as keyword" to transactions page and highlights list (#75)
Wire the ContextMenu + AddKeywordDialog pair onto the remaining per-transaction
surfaces. No new business logic — pure composition of #69 / #74 pieces.

- HighlightsTopTransactionsList: optional onContextMenuRow prop, ReportsHighlightsPage
  renders ContextMenu + AddKeywordDialog on right-click
- TransactionTable: optional onRowContextMenu prop on each <tr>; TransactionsPage
  handles it and opens the dialog pre-filled with the row description + current
  category
- Aggregate tables (HighlightsTopMoversTable, ComparePeriodTable, MonthlyTrendsTable,
  CategoryOverTimeTable) are intentionally NOT wired: they show category / month
  aggregates, not individual transactions, so there is no keyword to extract from
  a row — the dialog would be nonsensical there

Fixes #75

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:15:33 -04:00

131 lines
4.9 KiB
TypeScript

import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { ArrowLeft, Tag } from "lucide-react";
import PeriodSelector from "../components/dashboard/PeriodSelector";
import HubNetBalanceTile from "../components/reports/HubNetBalanceTile";
import HighlightsTopMoversTable from "../components/reports/HighlightsTopMoversTable";
import HighlightsTopMoversChart from "../components/reports/HighlightsTopMoversChart";
import HighlightsTopTransactionsList from "../components/reports/HighlightsTopTransactionsList";
import ViewModeToggle, { readViewMode, type ViewMode } from "../components/reports/ViewModeToggle";
import ContextMenu from "../components/shared/ContextMenu";
import AddKeywordDialog from "../components/categories/AddKeywordDialog";
import { useHighlights } from "../hooks/useHighlights";
import { useReportsPeriod } from "../hooks/useReportsPeriod";
import type { RecentTransaction } from "../shared/types";
const STORAGE_KEY = "reports-viewmode-highlights";
export default function ReportsHighlightsPage() {
const { t } = useTranslation();
const { period, setPeriod, from, to, setCustomDates } = useReportsPeriod();
const { data, isLoading, error, windowDays, setWindowDays } = useHighlights();
const [viewMode, setViewMode] = useState<ViewMode>(() => readViewMode(STORAGE_KEY));
const [menu, setMenu] = useState<{ x: number; y: number; tx: RecentTransaction } | null>(null);
const [pending, setPending] = useState<RecentTransaction | null>(null);
const preserveSearch = typeof window !== "undefined" ? window.location.search : "";
const handleContextMenu = (e: React.MouseEvent, tx: RecentTransaction) => {
e.preventDefault();
setMenu({ x: e.clientX, y: e.clientY, tx });
};
return (
<div className={isLoading ? "opacity-60" : ""}>
<div className="flex items-center gap-3 mb-4">
<Link
to={`/reports${preserveSearch}`}
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] p-1 rounded-md hover:bg-[var(--muted)]"
aria-label={t("reports.hub.title")}
>
<ArrowLeft size={18} />
</Link>
<h1 className="text-2xl font-bold">{t("reports.hub.highlights")}</h1>
</div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
<PeriodSelector
value={period}
onChange={setPeriod}
customDateFrom={from}
customDateTo={to}
onCustomDateChange={setCustomDates}
/>
<ViewModeToggle value={viewMode} onChange={setViewMode} storageKey={STORAGE_KEY} />
</div>
{error && (
<div className="bg-[var(--negative)]/10 text-[var(--negative)] rounded-xl p-4 mb-6">
{error}
</div>
)}
{data && (
<div className="flex flex-col gap-6">
<section>
<h2 className="text-sm font-semibold uppercase tracking-wide text-[var(--muted-foreground)] mb-3">
{t("reports.highlights.balances")}
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<HubNetBalanceTile
label={t("reports.highlights.netBalanceCurrent")}
amount={data.netBalanceCurrent}
series={data.monthlyBalanceSeries.map((m) => m.netBalance)}
/>
<HubNetBalanceTile
label={t("reports.highlights.netBalanceYtd")}
amount={data.netBalanceYtd}
series={data.monthlyBalanceSeries.map((m) => m.netBalance)}
/>
</div>
</section>
<section>
<h2 className="text-sm font-semibold uppercase tracking-wide text-[var(--muted-foreground)] mb-3">
{t("reports.highlights.topMovers")}
</h2>
{viewMode === "chart" ? (
<HighlightsTopMoversChart movers={data.topMovers} />
) : (
<HighlightsTopMoversTable movers={data.topMovers} />
)}
</section>
<section>
<HighlightsTopTransactionsList
transactions={data.topTransactions}
windowDays={windowDays}
onWindowChange={setWindowDays}
onContextMenuRow={handleContextMenu}
/>
</section>
</div>
)}
{menu && (
<ContextMenu
x={menu.x}
y={menu.y}
header={menu.tx.description}
onClose={() => setMenu(null)}
items={[
{
icon: <Tag size={14} />,
label: t("reports.keyword.addFromTransaction"),
onClick: () => setPending(menu.tx),
},
]}
/>
)}
{pending && (
<AddKeywordDialog
initialKeyword={pending.description.split(/\s+/)[0] ?? ""}
onClose={() => setPending(null)}
onApplied={() => setPending(null)}
/>
)}
</div>
);
}