Compare commits
No commits in common. "0bbbcc541b8a518225f0ab6cc93e1737cf87ab8d" and "e1192beca38e5c4faf8582758af0f6a67acda45e" have entirely different histories.
0bbbcc541b
...
e1192beca3
10 changed files with 60 additions and 88 deletions
|
|
@ -387,12 +387,12 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
|||
</tr>
|
||||
{reorderRows(group, subtotalsOnTop).map((row) => renderRow(row))}
|
||||
<tr className="bg-[var(--muted)]/40 border-b border-[var(--border)]">
|
||||
<td className="py-2.5 px-3 sticky left-0 bg-[var(--muted)]/40 z-10 text-sm font-semibold">
|
||||
<td className="py-2 px-3 sticky left-0 bg-[var(--muted)]/40 z-10 text-xs font-semibold">
|
||||
{t(typeTotalKeys[type])}
|
||||
</td>
|
||||
<td className="py-2.5 px-2 text-right text-sm font-semibold">{formatSigned(sectionAnnualTotal)}</td>
|
||||
<td className="py-2 px-2 text-right text-xs font-semibold">{formatSigned(sectionAnnualTotal)}</td>
|
||||
{sectionMonthTotals.map((total, mIdx) => (
|
||||
<td key={mIdx} className="py-2.5 px-2 text-right text-sm font-semibold">
|
||||
<td key={mIdx} className="py-2 px-2 text-right text-xs font-semibold">
|
||||
{formatSigned(total)}
|
||||
</td>
|
||||
))}
|
||||
|
|
@ -401,11 +401,11 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
|
|||
);
|
||||
})}
|
||||
{/* Totals row */}
|
||||
<tr className="bg-[var(--muted)] font-bold border-t-2 border-[var(--border)]">
|
||||
<td className="py-3 px-3 sticky left-0 bg-[var(--muted)] z-10 text-sm">{t("common.total")}</td>
|
||||
<td className="py-3 px-2 text-right text-sm">{formatSigned(annualTotal)}</td>
|
||||
<tr className="bg-[var(--muted)] font-semibold">
|
||||
<td className="py-2.5 px-3 sticky left-0 bg-[var(--muted)] z-10 text-xs">{t("common.total")}</td>
|
||||
<td className="py-2.5 px-2 text-right text-xs">{formatSigned(annualTotal)}</td>
|
||||
{monthTotals.map((total, mIdx) => (
|
||||
<td key={mIdx} className="py-3 px-2 text-right text-sm">
|
||||
<td key={mIdx} className="py-2.5 px-2 text-right text-xs">
|
||||
{formatSigned(total)}
|
||||
</td>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -256,26 +256,26 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
|
|||
</tr>
|
||||
);
|
||||
})}
|
||||
<tr className="border-b border-[var(--border)] bg-[var(--muted)]/40 font-semibold text-sm">
|
||||
<td className="px-3 py-2.5">{t(typeTotalKeys[section.type])}</td>
|
||||
<td className="text-right px-3 py-2.5 border-l border-[var(--border)]/50">
|
||||
<tr className="border-b border-[var(--border)] bg-[var(--muted)]/40 font-semibold">
|
||||
<td className="px-3 py-2 text-xs">{t(typeTotalKeys[section.type])}</td>
|
||||
<td className="text-right px-3 py-2 border-l border-[var(--border)]/50">
|
||||
{cadFormatter(sectionTotals.monthActual)}
|
||||
</td>
|
||||
<td className="text-right px-3 py-2.5">{cadFormatter(sectionTotals.monthBudget)}</td>
|
||||
<td className={`text-right px-3 py-2.5 ${variationColor(sectionTotals.monthVariation)}`}>
|
||||
<td className="text-right px-3 py-2">{cadFormatter(sectionTotals.monthBudget)}</td>
|
||||
<td className={`text-right px-3 py-2 ${variationColor(sectionTotals.monthVariation)}`}>
|
||||
{cadFormatter(sectionTotals.monthVariation)}
|
||||
</td>
|
||||
<td className={`text-right px-3 py-2.5 ${variationColor(sectionTotals.monthVariation)}`}>
|
||||
<td className={`text-right px-3 py-2 ${variationColor(sectionTotals.monthVariation)}`}>
|
||||
{pctFormatter(sectionMonthPct)}
|
||||
</td>
|
||||
<td className="text-right px-3 py-2.5 border-l border-[var(--border)]/50">
|
||||
<td className="text-right px-3 py-2 border-l border-[var(--border)]/50">
|
||||
{cadFormatter(sectionTotals.ytdActual)}
|
||||
</td>
|
||||
<td className="text-right px-3 py-2.5">{cadFormatter(sectionTotals.ytdBudget)}</td>
|
||||
<td className={`text-right px-3 py-2.5 ${variationColor(sectionTotals.ytdVariation)}`}>
|
||||
<td className="text-right px-3 py-2">{cadFormatter(sectionTotals.ytdBudget)}</td>
|
||||
<td className={`text-right px-3 py-2 ${variationColor(sectionTotals.ytdVariation)}`}>
|
||||
{cadFormatter(sectionTotals.ytdVariation)}
|
||||
</td>
|
||||
<td className={`text-right px-3 py-2.5 ${variationColor(sectionTotals.ytdVariation)}`}>
|
||||
<td className={`text-right px-3 py-2 ${variationColor(sectionTotals.ytdVariation)}`}>
|
||||
{pctFormatter(sectionYtdPct)}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -283,26 +283,26 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
|
|||
);
|
||||
})}
|
||||
{/* Grand totals */}
|
||||
<tr className="border-t-2 border-[var(--border)] font-bold text-sm bg-[var(--muted)]/20">
|
||||
<td className="px-3 py-3">{t("common.total")}</td>
|
||||
<td className="text-right px-3 py-3 border-l border-[var(--border)]/50">
|
||||
<tr className="border-t-2 border-[var(--border)] font-bold bg-[var(--muted)]/20">
|
||||
<td className="px-3 py-2">{t("common.total")}</td>
|
||||
<td className="text-right px-3 py-2 border-l border-[var(--border)]/50">
|
||||
{cadFormatter(totals.monthActual)}
|
||||
</td>
|
||||
<td className="text-right px-3 py-3">{cadFormatter(totals.monthBudget)}</td>
|
||||
<td className={`text-right px-3 py-3 ${variationColor(totals.monthVariation)}`}>
|
||||
<td className="text-right px-3 py-2">{cadFormatter(totals.monthBudget)}</td>
|
||||
<td className={`text-right px-3 py-2 ${variationColor(totals.monthVariation)}`}>
|
||||
{cadFormatter(totals.monthVariation)}
|
||||
</td>
|
||||
<td className={`text-right px-3 py-3 ${variationColor(totals.monthVariation)}`}>
|
||||
<td className={`text-right px-3 py-2 ${variationColor(totals.monthVariation)}`}>
|
||||
{pctFormatter(totalMonthPct)}
|
||||
</td>
|
||||
<td className="text-right px-3 py-3 border-l border-[var(--border)]/50">
|
||||
<td className="text-right px-3 py-2 border-l border-[var(--border)]/50">
|
||||
{cadFormatter(totals.ytdActual)}
|
||||
</td>
|
||||
<td className="text-right px-3 py-3">{cadFormatter(totals.ytdBudget)}</td>
|
||||
<td className={`text-right px-3 py-3 ${variationColor(totals.ytdVariation)}`}>
|
||||
<td className="text-right px-3 py-2">{cadFormatter(totals.ytdBudget)}</td>
|
||||
<td className={`text-right px-3 py-2 ${variationColor(totals.ytdVariation)}`}>
|
||||
{cadFormatter(totals.ytdVariation)}
|
||||
</td>
|
||||
<td className={`text-right px-3 py-3 ${variationColor(totals.ytdVariation)}`}>
|
||||
<td className={`text-right px-3 py-2 ${variationColor(totals.ytdVariation)}`}>
|
||||
{pctFormatter(totalYtdPct)}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -80,8 +80,8 @@ export default function CategoryOverTimeTable({ data, hiddenCategories }: Catego
|
|||
</tr>
|
||||
);
|
||||
})}
|
||||
<tr className="border-t-2 border-[var(--border)] font-bold text-sm bg-[var(--muted)]/20">
|
||||
<td className="px-3 py-3 sticky left-0 bg-[var(--muted)]/20 z-10">{t("common.total")}</td>
|
||||
<tr className="border-t-2 border-[var(--border)] font-bold bg-[var(--muted)]/20">
|
||||
<td className="px-3 py-2 sticky left-0 bg-[var(--muted)]/20 z-10">{t("common.total")}</td>
|
||||
{months.map((month) => {
|
||||
const monthData = data.data.find((d) => d.month === month);
|
||||
const monthTotal = visibleCategories.reduce(
|
||||
|
|
@ -89,12 +89,12 @@ export default function CategoryOverTimeTable({ data, hiddenCategories }: Catego
|
|||
0,
|
||||
);
|
||||
return (
|
||||
<td key={month} className="text-right px-3 py-3">
|
||||
<td key={month} className="text-right px-3 py-2">
|
||||
{cadFormatter(monthTotal)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td className="text-right px-3 py-3 border-l border-[var(--border)]/50">
|
||||
<td className="text-right px-3 py-2 border-l border-[var(--border)]/50">
|
||||
{cadFormatter(
|
||||
visibleCategories.reduce(
|
||||
(sum, cat) => sum + data.data.reduce((s, d) => s + ((d as Record<string, unknown>)[cat] as number || 0), 0),
|
||||
|
|
|
|||
|
|
@ -61,10 +61,10 @@ export default function CategoryTable({ data, hiddenCategories }: CategoryTableP
|
|||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr className="border-t-2 border-[var(--border)] font-bold text-sm bg-[var(--muted)]/20">
|
||||
<td className="px-3 py-3">{t("common.total")}</td>
|
||||
<td className="text-right px-3 py-3">{cadFormatter(grandTotal)}</td>
|
||||
<td className="text-right px-3 py-3 text-[var(--muted-foreground)]">100%</td>
|
||||
<tr className="border-t-2 border-[var(--border)] font-bold bg-[var(--muted)]/20">
|
||||
<td className="px-3 py-2">{t("common.total")}</td>
|
||||
<td className="text-right px-3 py-2">{cadFormatter(grandTotal)}</td>
|
||||
<td className="text-right px-3 py-2 text-[var(--muted-foreground)]">100%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -192,13 +192,13 @@ export default function DynamicReportTable({ config, result }: DynamicReportTabl
|
|||
))
|
||||
)}
|
||||
{/* Grand total */}
|
||||
<tr className="border-t-2 border-[var(--border)] font-bold text-sm bg-[var(--muted)]/20">
|
||||
<td colSpan={rowDims.length || 1} className="px-3 py-3">
|
||||
<tr className="border-t-2 border-[var(--border)] font-bold bg-[var(--muted)]/20">
|
||||
<td colSpan={rowDims.length || 1} className="px-3 py-2">
|
||||
{t("reports.pivot.total")}
|
||||
</td>
|
||||
{colValues.map((colVal) =>
|
||||
measures.map((m) => (
|
||||
<td key={`total-${colVal}-${m}`} className="text-right px-3 py-3 border-l border-[var(--border)]/50">
|
||||
<td key={`total-${colVal}-${m}`} className="text-right px-3 py-2 border-l border-[var(--border)]/50">
|
||||
{cadFormatter(grandTotals[colVal]?.[m] || 0)}
|
||||
</td>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -61,11 +61,11 @@ export default function MonthlyTrendsTable({ data }: MonthlyTrendsTableProps) {
|
|||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr className="border-t-2 border-[var(--border)] font-bold text-sm bg-[var(--muted)]/20">
|
||||
<td className="px-3 py-3">{t("common.total")}</td>
|
||||
<td className="text-right px-3 py-3 text-[var(--positive)]">{cadFormatter(totals.income)}</td>
|
||||
<td className="text-right px-3 py-3 text-[var(--negative)]">{cadFormatter(totals.expenses)}</td>
|
||||
<td className={`text-right px-3 py-3 ${totals.income - totals.expenses >= 0 ? "text-[var(--positive)]" : "text-[var(--negative)]"}`}>
|
||||
<tr className="border-t-2 border-[var(--border)] font-bold bg-[var(--muted)]/20">
|
||||
<td className="px-3 py-2">{t("common.total")}</td>
|
||||
<td className="text-right px-3 py-2 text-[var(--positive)]">{cadFormatter(totals.income)}</td>
|
||||
<td className="text-right px-3 py-2 text-[var(--negative)]">{cadFormatter(totals.expenses)}</td>
|
||||
<td className={`text-right px-3 py-2 ${totals.income - totals.expenses >= 0 ? "text-[var(--positive)]" : "text-[var(--negative)]"}`}>
|
||||
{cadFormatter(totals.income - totals.expenses)}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -3,21 +3,18 @@ import type {
|
|||
DashboardPeriod,
|
||||
DashboardSummary,
|
||||
CategoryBreakdownItem,
|
||||
CategoryOverTimeData,
|
||||
BudgetVsActualRow,
|
||||
RecentTransaction,
|
||||
} from "../shared/types";
|
||||
import {
|
||||
getDashboardSummary,
|
||||
getExpensesByCategory,
|
||||
getRecentTransactions,
|
||||
} from "../services/dashboardService";
|
||||
import { getCategoryOverTime } from "../services/reportService";
|
||||
import { getBudgetVsActualData } from "../services/budgetService";
|
||||
|
||||
interface DashboardState {
|
||||
summary: DashboardSummary;
|
||||
categoryBreakdown: CategoryBreakdownItem[];
|
||||
categoryOverTime: CategoryOverTimeData;
|
||||
budgetVsActual: BudgetVsActualRow[];
|
||||
recentTransactions: RecentTransaction[];
|
||||
period: DashboardPeriod;
|
||||
customDateFrom: string;
|
||||
customDateTo: string;
|
||||
|
|
@ -33,8 +30,7 @@ type DashboardAction =
|
|||
payload: {
|
||||
summary: DashboardSummary;
|
||||
categoryBreakdown: CategoryBreakdownItem[];
|
||||
categoryOverTime: CategoryOverTimeData;
|
||||
budgetVsActual: BudgetVsActualRow[];
|
||||
recentTransactions: RecentTransaction[];
|
||||
};
|
||||
}
|
||||
| { type: "SET_PERIOD"; payload: DashboardPeriod }
|
||||
|
|
@ -42,15 +38,14 @@ type DashboardAction =
|
|||
|
||||
const now = new Date();
|
||||
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
||||
const yearStartStr = `${now.getFullYear()}-01-01`;
|
||||
const monthStartStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
|
||||
|
||||
const initialState: DashboardState = {
|
||||
summary: { totalCount: 0, totalAmount: 0, incomeTotal: 0, expenseTotal: 0 },
|
||||
categoryBreakdown: [],
|
||||
categoryOverTime: { categories: [], data: [], colors: {}, categoryIds: {} },
|
||||
budgetVsActual: [],
|
||||
period: "year",
|
||||
customDateFrom: yearStartStr,
|
||||
recentTransactions: [],
|
||||
period: "month",
|
||||
customDateFrom: monthStartStr,
|
||||
customDateTo: todayStr,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
|
@ -67,8 +62,7 @@ function reducer(state: DashboardState, action: DashboardAction): DashboardState
|
|||
...state,
|
||||
summary: action.payload.summary,
|
||||
categoryBreakdown: action.payload.categoryBreakdown,
|
||||
categoryOverTime: action.payload.categoryOverTime,
|
||||
budgetVsActual: action.payload.budgetVsActual,
|
||||
recentTransactions: action.payload.recentTransactions,
|
||||
isLoading: false,
|
||||
};
|
||||
case "SET_PERIOD":
|
||||
|
|
@ -135,17 +129,14 @@ export function useDashboard() {
|
|||
|
||||
try {
|
||||
const { dateFrom, dateTo } = computeDateRange(period, customFrom, customTo);
|
||||
const currentMonth = new Date().getMonth() + 1;
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [summary, categoryBreakdown, categoryOverTime, budgetVsActual] = await Promise.all([
|
||||
const [summary, categoryBreakdown, recentTransactions] = await Promise.all([
|
||||
getDashboardSummary(dateFrom, dateTo),
|
||||
getExpensesByCategory(dateFrom, dateTo),
|
||||
getCategoryOverTime(dateFrom, dateTo),
|
||||
getBudgetVsActualData(currentYear, currentMonth),
|
||||
getRecentTransactions(10),
|
||||
]);
|
||||
|
||||
if (fetchId !== fetchIdRef.current) return;
|
||||
dispatch({ type: "SET_DATA", payload: { summary, categoryBreakdown, categoryOverTime, budgetVsActual } });
|
||||
dispatch({ type: "SET_DATA", payload: { summary, categoryBreakdown, recentTransactions } });
|
||||
} catch (e) {
|
||||
if (fetchId !== fetchIdRef.current) return;
|
||||
dispatch({
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@
|
|||
"noData": "No data available. Start by importing your bank statements.",
|
||||
"expensesByCategory": "Expenses by Category",
|
||||
"recentTransactions": "Recent Transactions",
|
||||
"budgetVsActual": "Budget vs Actual",
|
||||
"expensesOverTime": "Expenses Over Time",
|
||||
"period": {
|
||||
"month": "This month",
|
||||
"3months": "3 months",
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@
|
|||
"noData": "Aucune donnée disponible. Commencez par importer vos relevés bancaires.",
|
||||
"expensesByCategory": "Dépenses par catégorie",
|
||||
"recentTransactions": "Transactions récentes",
|
||||
"budgetVsActual": "Budget vs Réel",
|
||||
"expensesOverTime": "Dépenses dans le temps",
|
||||
"period": {
|
||||
"month": "Ce mois",
|
||||
"3months": "3 mois",
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ import { useDashboard } from "../hooks/useDashboard";
|
|||
import { PageHelp } from "../components/shared/PageHelp";
|
||||
import PeriodSelector from "../components/dashboard/PeriodSelector";
|
||||
import CategoryPieChart from "../components/dashboard/CategoryPieChart";
|
||||
import CategoryOverTimeChart from "../components/reports/CategoryOverTimeChart";
|
||||
import BudgetVsActualTable from "../components/reports/BudgetVsActualTable";
|
||||
import RecentTransactionsList from "../components/dashboard/RecentTransactionsList";
|
||||
import TransactionDetailModal from "../components/shared/TransactionDetailModal";
|
||||
import type { CategoryBreakdownItem, DashboardPeriod } from "../shared/types";
|
||||
|
||||
|
|
@ -42,7 +41,7 @@ function computeDateRange(
|
|||
export default function DashboardPage() {
|
||||
const { t } = useTranslation();
|
||||
const { state, setPeriod, setCustomDates } = useDashboard();
|
||||
const { summary, categoryBreakdown, categoryOverTime, budgetVsActual, period, isLoading } = state;
|
||||
const { summary, categoryBreakdown, recentTransactions, period, isLoading } = state;
|
||||
|
||||
const [hiddenCategories, setHiddenCategories] = useState<Set<string>>(new Set());
|
||||
const [detailModal, setDetailModal] = useState<CategoryBreakdownItem | null>(null);
|
||||
|
|
@ -109,7 +108,7 @@ export default function DashboardPage() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
{cards.map((card) => (
|
||||
<div
|
||||
key={card.labelKey}
|
||||
|
|
@ -126,7 +125,7 @@ export default function DashboardPage() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<CategoryPieChart
|
||||
data={categoryBreakdown}
|
||||
hiddenCategories={hiddenCategories}
|
||||
|
|
@ -134,21 +133,7 @@ export default function DashboardPage() {
|
|||
onShowAll={showAll}
|
||||
onViewDetails={viewDetails}
|
||||
/>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-3">{t("dashboard.budgetVsActual")}</h2>
|
||||
<BudgetVsActualTable data={budgetVsActual} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-semibold mb-3">{t("dashboard.expensesOverTime")}</h2>
|
||||
<CategoryOverTimeChart
|
||||
data={categoryOverTime}
|
||||
hiddenCategories={hiddenCategories}
|
||||
onToggleHidden={toggleHidden}
|
||||
onShowAll={showAll}
|
||||
onViewDetails={viewDetails}
|
||||
/>
|
||||
<RecentTransactionsList transactions={recentTransactions} />
|
||||
</div>
|
||||
|
||||
{detailModal && (
|
||||
|
|
|
|||
Loading…
Reference in a new issue