major refactor of the dashboard and react-openapi integration (#1)

## Summary

This MR introduces a major refactor of the dashboard and react-openapi integration, focusing on configurability, separation of concerns, and improved extensibility.

---

## Key Changes

### 1. OpenAPI / Admin Refactor

* Extracted `ConfigContext` into a dedicated provider.
* Introduced `AppProvider` to encapsulate:

  * Config loading
  * API client initialization
  * React Query setup
* Removed internal `QueryClientProvider` from `Admin` for better composability.
* `Admin` now supports both:

  * Standalone usage
  * Nested usage inside an existing provider

### 2. Resource System Improvements

* Added `hidden` flag to `ResourceConfig` and overrides.
* Admin UI now filters out hidden resources.
* Added `useResourceByName` helper for dynamic resource access.
* Improved `useResource`:

  * Handles undefined config safely
  * Adds guards for missing endpoints
  * Disables queries when endpoint is absent

### 3. API Client Enhancements

* Added custom `paramsSerializer`:

  * Serializes arrays without `[]`
  * Ensures backend-compatible query formats

### 4. Dashboard Architecture Overhaul

* Replaced hardcoded dashboard with **config-driven system**:

  * Introduced `ConfigurableDashboard`
  * Dashboard sections defined via config
* New state model:

  * `mode` (expense/income)
  * `periodType` (rolling/calendar)
  * `comparison`
  * `selectedPeriodId`

### 5. Component Refactor (View / Logic Split)

* Split major components into:

  * `.tsx` (logic/controller)
  * `.view.tsx` (presentation)
  * `.models.ts` (types)
* Applied to:

  * Dashboard
  * HistoryChart
  * ProgressCard
  * LatestItems

### 6. HistoryChart Redesign

* Fully rebuilt using report-driven data
* Supports:

  * Weekly / Monthly / Yearly / FY / Full views
  * Rolling vs Calendar periods
  * Comparison mode (auto-aligned offsets)
* Introduced:

  * Bucket merging logic
  * Dynamic comparison attachment

### 7. Reporting Integration

* Dashboard now powered by:

  * `useReport`
  * `prepareReport`
* Removes need for multiple manual API calls

### 8. UI / UX Improvements

* Theme-aware color system
* Dynamic accent colors per mode
* Cleaner layout using section-based rendering
* Improved selection and interaction in charts

### 9. Cleanup & Removals

* Removed legacy components:

  * Old `HistoryChart`
  * Old `ProgressCard`
* Simplified Header layout spacing

---

## Behavior Changes

* Hidden resources are no longer visible in Admin UI.
* Dashboard is now entirely configuration-driven.
* API query params for arrays no longer use `[]`.
* Resource hooks no longer crash on missing config.

---

## Risks / Considerations

* Dashboard depends on correct configuration structure.
* Hidden flag may unintentionally hide resources if misconfigured.
* Query param serialization change must align with backend expectations.

---

## Follow-ups

* Add typing improvements to remove `@ts-ignore` in `GenericForm`.
* Extend dashboard config with more reusable section presets.
* Add tests for report aggregation and comparison logic.

---

Reviewed-on: #1
Co-authored-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
Co-committed-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
This commit is contained in:
2026-05-07 11:00:54 +00:00
committed by aetos
parent b1db439dda
commit 009ab50b47
45 changed files with 1600 additions and 1099 deletions

View File

@@ -0,0 +1,208 @@
import * as React from "react";
import {
Box,
Typography,
ToggleButtonGroup,
ToggleButton,
Paper
} from "@mui/material";
import { useTheme, alpha } from "@mui/material/styles";
import IconButton from "@mui/material/IconButton";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import {
ChartDataPoint,
HistoryChartProps,
} from "./HistoryChart.models";
import { formatDisplay } from "./HistoryChart.utils";
interface ViewProps extends HistoryChartProps {
activeTab: string;
setActiveTab: (v: string) => void;
currentData: ChartDataPoint[];
visibleData: ChartDataPoint[];
maxAmount: number;
visibleCount: number;
startIndex: number;
setStartIndex: React.Dispatch<React.SetStateAction<number>>;
activeDataKey: string;
}
export default function HistoryChartView(props: ViewProps) {
const {
header,
summary,
tabs,
colorScheme,
mode,
periodType,
selectedPeriodId,
comparison,
togglePeriodType,
setSelectedPeriodId,
toggleComparison,
activeTab,
setActiveTab,
currentData,
visibleData,
maxAmount,
visibleCount,
startIndex,
setStartIndex,
activeDataKey,
} = props;
const theme = useTheme();
const isDark = theme.palette.mode === "dark";
const handleTabChange = (_: React.MouseEvent<HTMLElement>, newTab: string | null) => {
if (newTab !== null) setActiveTab(newTab);
};
const canGoLeft = startIndex > 0;
const canGoRight = startIndex + visibleCount < currentData.length;
const handlePrev = () => {
if (canGoLeft) setStartIndex((prev) => prev - visibleCount);
};
const handleNext = () => {
if (canGoRight) setStartIndex((prev) => prev + visibleCount);
};
return (
<Paper
sx={{
p: { xs: 2.5, sm: 4 },
borderRadius: 4,
width: "100%",
boxShadow: "none",
border: "1px solid",
borderColor: "divider",
bgcolor: isDark ? "background.paper" : colorScheme.light,
}}
>
<Typography variant="h6" fontWeight={700} gutterBottom>
{header}
</Typography>
{summary && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{summary}
</Typography>
)}
<ToggleButtonGroup value={activeTab} exclusive onChange={handleTabChange} fullWidth sx={{ mb: 4 }}>
{tabs.map((tab) => (
<ToggleButton key={tab} value={tab}>
{tab}
</ToggleButton>
))}
</ToggleButtonGroup>
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 3 }}>
<ToggleButtonGroup value={periodType} exclusive onChange={togglePeriodType} size="small">
<ToggleButton value="rolling">Rolling</ToggleButton>
<ToggleButton value="calendar">Calendar</ToggleButton>
</ToggleButtonGroup>
<ToggleButton
value="compare"
selected={comparison}
onChange={toggleComparison}
size="small"
>
Compare
</ToggleButton>
</Box>
{currentData.length > 0 ? (
<Box sx={{ position: "relative", mt: 4 }}>
{canGoLeft && (
<IconButton onClick={handlePrev} size="small" sx={{ position: "absolute", left: 0, top: "50%" }}>
<ChevronLeftIcon fontSize="small" />
</IconButton>
)}
<Box sx={{ display: "flex", alignItems: "flex-end", height: 220, mt: 4 }}>
{visibleData.map((point) => {
const currentHeight = (point.amount / maxAmount) * 100;
const compareHeight = comparison
? ((point.compare?.amount ?? 0) / maxAmount) * 100
: 0;
const isSelected = selectedPeriodId === point.id;
const display = formatDisplay(point, activeDataKey, comparison);
return (
<Box
key={point.id}
onClick={() =>
setSelectedPeriodId(isSelected ? null : point.id)
}
sx={{
flex: 1,
display: "flex",
flexDirection: "column",
alignItems: "center",
cursor: "pointer",
height: "100%"
}}
>
<Box sx={{ display: "flex", alignItems: "flex-end", gap: 1, height: "100%" }}>
{comparison && (
<Box
sx={{
width: 8,
height: `${compareHeight}%`,
bgcolor: alpha(colorScheme.primary, 0.4),
borderRadius: "4px 4px 0 0"
}}
/>
)}
<Box
sx={{
width: 12,
height: `${currentHeight}%`,
bgcolor: isSelected ? "warning.main" : colorScheme.primary,
borderRadius: "4px 4px 0 0"
}}
/>
</Box>
<Typography variant="caption">
{point.label}
</Typography>
{comparison && point.compare && (
<Typography variant="caption" color="text.secondary">
{point.compare.label}
</Typography>
)}
<Typography variant="caption">
{display}
</Typography>
</Box>
);
})}
</Box>
{canGoRight && (
<IconButton onClick={handleNext} size="small" sx={{ position: "absolute", right: 0, top: "50%" }}>
<ChevronRightIcon fontSize="small" />
</IconButton>
)}
</Box>
) : (
<Box sx={{ height: 200, display: "flex", alignItems: "center", justifyContent: "center" }}>
<Typography color="text.secondary">No Data Available</Typography>
</Box>
)}
</Paper>
);
}