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>
This commit is contained in:
parent
334f975deb
commit
3b70abdb9e
4 changed files with 83 additions and 3 deletions
|
|
@ -5,6 +5,7 @@ export interface HighlightsTopTransactionsListProps {
|
|||
transactions: RecentTransaction[];
|
||||
windowDays: 30 | 60 | 90;
|
||||
onWindowChange: (days: 30 | 60 | 90) => void;
|
||||
onContextMenuRow?: (event: React.MouseEvent, transaction: RecentTransaction) => void;
|
||||
}
|
||||
|
||||
function formatAmount(amount: number, language: string): string {
|
||||
|
|
@ -18,6 +19,7 @@ export default function HighlightsTopTransactionsList({
|
|||
transactions,
|
||||
windowDays,
|
||||
onWindowChange,
|
||||
onContextMenuRow,
|
||||
}: HighlightsTopTransactionsListProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
|
|
@ -50,7 +52,11 @@ export default function HighlightsTopTransactionsList({
|
|||
) : (
|
||||
<ul className="divide-y divide-[var(--border)]">
|
||||
{transactions.map((tx) => (
|
||||
<li key={tx.id} className="flex items-center gap-3 px-4 py-2 text-sm">
|
||||
<li
|
||||
key={tx.id}
|
||||
onContextMenu={onContextMenuRow ? (e) => onContextMenuRow(e, tx) : undefined}
|
||||
className="flex items-center gap-3 px-4 py-2 text-sm"
|
||||
>
|
||||
<span
|
||||
className="w-2 h-2 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: tx.category_color ?? "#9ca3af" }}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ interface TransactionTableProps {
|
|||
onLoadSplitChildren: (parentId: number) => Promise<SplitChild[]>;
|
||||
onSaveSplit: (parentId: number, entries: Array<{ category_id: number; amount: number; description: string }>) => Promise<void>;
|
||||
onDeleteSplit: (parentId: number) => Promise<void>;
|
||||
onRowContextMenu?: (event: React.MouseEvent, row: TransactionRow) => void;
|
||||
}
|
||||
|
||||
function SortIcon({
|
||||
|
|
@ -50,6 +51,7 @@ export default function TransactionTable({
|
|||
onLoadSplitChildren,
|
||||
onSaveSplit,
|
||||
onDeleteSplit,
|
||||
onRowContextMenu,
|
||||
}: TransactionTableProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
||||
|
|
@ -135,6 +137,7 @@ export default function TransactionTable({
|
|||
{rows.map((row) => (
|
||||
<Fragment key={row.id}>
|
||||
<tr
|
||||
onContextMenu={onRowContextMenu ? (e) => onRowContextMenu(e, row) : undefined}
|
||||
className="hover:bg-[var(--muted)] transition-colors"
|
||||
>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{row.date}</td>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
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";
|
||||
|
||||
|
|
@ -18,9 +21,16 @@ export default function ReportsHighlightsPage() {
|
|||
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">
|
||||
|
|
@ -87,10 +97,35 @@ export default function ReportsHighlightsPage() {
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,28 @@
|
|||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Wand2 } from "lucide-react";
|
||||
import { Wand2, Tag } from "lucide-react";
|
||||
import { PageHelp } from "../components/shared/PageHelp";
|
||||
import { useTransactions } from "../hooks/useTransactions";
|
||||
import TransactionFilterBar from "../components/transactions/TransactionFilterBar";
|
||||
import TransactionSummaryBar from "../components/transactions/TransactionSummaryBar";
|
||||
import TransactionTable from "../components/transactions/TransactionTable";
|
||||
import TransactionPagination from "../components/transactions/TransactionPagination";
|
||||
import ContextMenu from "../components/shared/ContextMenu";
|
||||
import AddKeywordDialog from "../components/categories/AddKeywordDialog";
|
||||
import type { TransactionRow } from "../shared/types";
|
||||
|
||||
export default function TransactionsPage() {
|
||||
const { t } = useTranslation();
|
||||
const { state, setFilter, setSort, setPage, updateCategory, saveNotes, autoCategorize, addKeywordToCategory, loadSplitChildren, saveSplit, deleteSplit } =
|
||||
useTransactions();
|
||||
const [resultMessage, setResultMessage] = useState<string | null>(null);
|
||||
const [menu, setMenu] = useState<{ x: number; y: number; row: TransactionRow } | null>(null);
|
||||
const [pending, setPending] = useState<TransactionRow | null>(null);
|
||||
|
||||
const handleRowContextMenu = (e: React.MouseEvent, row: TransactionRow) => {
|
||||
e.preventDefault();
|
||||
setMenu({ x: e.clientX, y: e.clientY, row });
|
||||
};
|
||||
|
||||
const handleAutoCategorize = async () => {
|
||||
setResultMessage(null);
|
||||
|
|
@ -84,6 +94,7 @@ export default function TransactionsPage() {
|
|||
onLoadSplitChildren={loadSplitChildren}
|
||||
onSaveSplit={saveSplit}
|
||||
onDeleteSplit={deleteSplit}
|
||||
onRowContextMenu={handleRowContextMenu}
|
||||
/>
|
||||
|
||||
<TransactionPagination
|
||||
|
|
@ -94,6 +105,31 @@ export default function TransactionsPage() {
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{menu && (
|
||||
<ContextMenu
|
||||
x={menu.x}
|
||||
y={menu.y}
|
||||
header={menu.row.description}
|
||||
onClose={() => setMenu(null)}
|
||||
items={[
|
||||
{
|
||||
icon: <Tag size={14} />,
|
||||
label: t("reports.keyword.addFromTransaction"),
|
||||
onClick: () => setPending(menu.row),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{pending && (
|
||||
<AddKeywordDialog
|
||||
initialKeyword={pending.description.split(/\s+/)[0] ?? ""}
|
||||
initialCategoryId={pending.category_id}
|
||||
onClose={() => setPending(null)}
|
||||
onApplied={() => setPending(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue