Simpl-Resultat/src/hooks/useReports.ts
Le-King-Fu 29a1a15120
Some checks failed
Release / build (windows-latest) (push) Has been cancelled
feat: add chart patterns, context menu, and import preview popup (v0.2.3)
- Add SVG fill patterns to differentiate chart categories beyond color
- Add right-click context menu on charts to hide categories or view transactions
- Add transaction detail modal showing all transactions for a category
- Change import preview from wizard step to popup modal
- Add direct "Check Duplicates" button skipping preview step
- Bump version to 0.2.3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 23:55:19 +00:00

147 lines
4.6 KiB
TypeScript

import { useReducer, useCallback, useEffect, useRef } from "react";
import type {
ReportTab,
DashboardPeriod,
MonthlyTrendItem,
CategoryBreakdownItem,
CategoryOverTimeData,
} from "../shared/types";
import { getMonthlyTrends, getCategoryOverTime } from "../services/reportService";
import { getExpensesByCategory } from "../services/dashboardService";
interface ReportsState {
tab: ReportTab;
period: DashboardPeriod;
monthlyTrends: MonthlyTrendItem[];
categorySpending: CategoryBreakdownItem[];
categoryOverTime: CategoryOverTimeData;
isLoading: boolean;
error: string | null;
}
type ReportsAction =
| { type: "SET_TAB"; payload: ReportTab }
| { type: "SET_PERIOD"; payload: DashboardPeriod }
| { type: "SET_LOADING"; payload: boolean }
| { type: "SET_ERROR"; payload: string | null }
| { type: "SET_MONTHLY_TRENDS"; payload: MonthlyTrendItem[] }
| { type: "SET_CATEGORY_SPENDING"; payload: CategoryBreakdownItem[] }
| { type: "SET_CATEGORY_OVER_TIME"; payload: CategoryOverTimeData };
const initialState: ReportsState = {
tab: "trends",
period: "6months",
monthlyTrends: [],
categorySpending: [],
categoryOverTime: { categories: [], data: [], colors: {}, categoryIds: {} },
isLoading: false,
error: null,
};
function reducer(state: ReportsState, action: ReportsAction): ReportsState {
switch (action.type) {
case "SET_TAB":
return { ...state, tab: action.payload };
case "SET_PERIOD":
return { ...state, period: action.payload };
case "SET_LOADING":
return { ...state, isLoading: action.payload };
case "SET_ERROR":
return { ...state, error: action.payload, isLoading: false };
case "SET_MONTHLY_TRENDS":
return { ...state, monthlyTrends: action.payload, isLoading: false };
case "SET_CATEGORY_SPENDING":
return { ...state, categorySpending: action.payload, isLoading: false };
case "SET_CATEGORY_OVER_TIME":
return { ...state, categoryOverTime: action.payload, isLoading: false };
default:
return state;
}
}
function computeDateRange(period: DashboardPeriod): { dateFrom?: string; dateTo?: string } {
if (period === "all") return {};
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const day = now.getDate();
const dateTo = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
let from: Date;
switch (period) {
case "month":
from = new Date(year, month, 1);
break;
case "3months":
from = new Date(year, month - 2, 1);
break;
case "6months":
from = new Date(year, month - 5, 1);
break;
case "12months":
from = new Date(year, month - 11, 1);
break;
}
const dateFrom = `${from.getFullYear()}-${String(from.getMonth() + 1).padStart(2, "0")}-${String(from.getDate()).padStart(2, "0")}`;
return { dateFrom, dateTo };
}
export function useReports() {
const [state, dispatch] = useReducer(reducer, initialState);
const fetchIdRef = useRef(0);
const fetchData = useCallback(async (tab: ReportTab, period: DashboardPeriod) => {
const fetchId = ++fetchIdRef.current;
dispatch({ type: "SET_LOADING", payload: true });
dispatch({ type: "SET_ERROR", payload: null });
try {
const { dateFrom, dateTo } = computeDateRange(period);
switch (tab) {
case "trends": {
const data = await getMonthlyTrends(dateFrom, dateTo);
if (fetchId !== fetchIdRef.current) return;
dispatch({ type: "SET_MONTHLY_TRENDS", payload: data });
break;
}
case "byCategory": {
const data = await getExpensesByCategory(dateFrom, dateTo);
if (fetchId !== fetchIdRef.current) return;
dispatch({ type: "SET_CATEGORY_SPENDING", payload: data });
break;
}
case "overTime": {
const data = await getCategoryOverTime(dateFrom, dateTo);
if (fetchId !== fetchIdRef.current) return;
dispatch({ type: "SET_CATEGORY_OVER_TIME", payload: data });
break;
}
}
} catch (e) {
if (fetchId !== fetchIdRef.current) return;
dispatch({
type: "SET_ERROR",
payload: e instanceof Error ? e.message : String(e),
});
}
}, []);
useEffect(() => {
fetchData(state.tab, state.period);
}, [state.tab, state.period, fetchData]);
const setTab = useCallback((tab: ReportTab) => {
dispatch({ type: "SET_TAB", payload: tab });
}, []);
const setPeriod = useCallback((period: DashboardPeriod) => {
dispatch({ type: "SET_PERIOD", payload: period });
}, []);
return { state, setTab, setPeriod };
}