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:
208
src/components/HistoryChart/HistoryChart.view.tsx
Normal file
208
src/components/HistoryChart/HistoryChart.view.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user