configurable Dashboard.tsx

This commit is contained in:
2026-04-25 13:21:34 +05:30
parent 67d4c85146
commit a36d9119bb
6 changed files with 262 additions and 111 deletions

View File

@@ -2,44 +2,18 @@ import * as React from "react";
import { import {
Box, Box,
Container, Container,
Grid,
CircularProgress, CircularProgress,
Alert, Alert
ToggleButton,
ToggleButtonGroup,
Typography
} from "@mui/material"; } from "@mui/material";
import LatestItems from "./components/LatestItems"; import DashboardComponent from "./components/Dashboard";
import HistoryChart from "./components/HistoryChart"; import { configuration } from "./dashboard-config";
import ProgressCard from "./components/ProgressCard";
import { useDashboardData } from "./features/dashboard"; import { useDashboardData } from "./features/dashboard";
export default function Dashboard() { export default function Dashboard() {
const [mode, setMode] = React.useState<"expense" | "income">("expense"); const [mode, setMode] = React.useState<"expense" | "income">("expense");
const [period, setPeriod] = React.useState<"rolling" | "calendar">("rolling");
const [comparison, setComparison] = React.useState(false);
const palette = {
expense: {
primary: "#d32f2f",
light: "#fdecea",
dark: "#9a0007",
text: "#b71c1c"
},
income: {
primary: "#2e7d32",
light: "#e8f5e9",
dark: "#1b5e20",
text: "#1b5e20"
}
};
const { data, latest, isLoading, error } = useDashboardData(mode); const { data, latest, isLoading, error } = useDashboardData(mode);
const colors = palette[mode];
if (isLoading) { if (isLoading) {
return ( return (
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", height: "60vh" }}> <Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", height: "60vh" }}>
@@ -61,87 +35,11 @@ export default function Dashboard() {
} }
return ( return (
<Container <DashboardComponent
sx={{ config={configuration}
mt: 4, data={data}
mb: 4, latest={latest}
background: `linear-gradient(180deg, ${colors.light} 0%, transparent 100%)`, onModeChange={(newMode) => setMode(newMode)}
borderRadius: 4, />
p: 2
}}
>
<Box sx={{ display: "flex", justifyContent: "center", mb: 3 }}>
<ToggleButtonGroup
value={mode}
exclusive
onChange={(_, val) => val && setMode(val)}
sx={{
borderRadius: 3,
overflow: "hidden",
"& .MuiToggleButton-root": {
px: 3,
textTransform: "none",
color: "text.secondary"
},
"&.Mui-selected": {
bgcolor: colors.primary,
color: "white",
borderColor: colors.primary
},
}}
>
<ToggleButton value="expense">Expenses</ToggleButton>
<ToggleButton value="income">Income</ToggleButton>
</ToggleButtonGroup>
</Box>
<Grid container spacing={4} direction="row">
<Grid size={12}>
<HistoryChart
header={`${mode === "expense" ? "Expense" : "Income"} Breakdown`}
summary="Interactive chronological tracking"
tabs={["Daily", "Weekly", "Monthly"]}
data={data.chartData}
period={period}
onPeriodChange={setPeriod}
comparison={comparison}
setComparison={setComparison}
colorScheme={colors}
/>
</Grid>
{data.topPayees && data.topPayees.length > 0 && (
<Grid size={12}>
<Box sx={{ mb: 2 }}>
<Typography variant="h6" fontWeight={700}>
Top {mode === "expense" ? "Payees" : "Payors"}
</Typography>
</Box>
<Grid container spacing={2}>
{data.topPayees.map((payee: any) => (
<Grid key={payee.payeeName} size={{ xs: 12, sm: 6, md: 2.4 }}>
<ProgressCard
header={payee.payeeName}
progressAmount={payee.amount}
totalAmount={data.totalAmount}
colorTheme={mode === "expense" ? "error" : "success"}
compact
/>
</Grid>
))}
</Grid>
</Grid>
)}
<Grid size={12}>
<LatestItems
title={`Recent ${mode === "expense" ? "Expenses" : "Income"}`}
items={latest || []}
onViewAll={() => {}}
accentColor={colors.primary}
/>
</Grid>
</Grid>
</Container>
); );
} }

View File

@@ -0,0 +1,43 @@
import * as React from "react";
export type DashboardMode = "expense" | "income";
export type DashboardPeriod = "rolling" | "calendar";
export interface DashboardState {
mode: DashboardMode;
period: DashboardPeriod;
comparison: boolean;
}
export interface DashboardSection {
id: string;
title?: string;
summary?: string;
component: React.ComponentType<any>;
dataKey?: string;
settings?: Record<string, any>;
isList?: boolean;
style?: {
size?: number;
[key: string]: any;
};
}
export interface DashboardConfig {
sections: DashboardSection[];
style?: {
palette: Record<DashboardMode, {
primary: string;
light: string;
dark: string;
text: string;
}>;
};
}
export interface DashboardProps {
config: DashboardConfig;
data: any; // Aggregated data from features
latest: any[]; // Latest items from features
onModeChange?: (mode: DashboardMode) => void;
}

View File

@@ -0,0 +1,19 @@
import * as React from "react";
import DashboardView from "./Dashboard.view";
import { DashboardProps, DashboardState } from "./Dashboard.models";
export default function Dashboard(props: DashboardProps) {
const [state, setState] = React.useState<DashboardState>({
mode: "expense",
period: "rolling",
comparison: false,
});
return (
<DashboardView
{...props}
state={state}
setState={setState}
/>
);
}

View File

@@ -0,0 +1,129 @@
import * as React from "react";
import {
Box,
Container,
Grid,
Typography,
ToggleButton,
ToggleButtonGroup
} from "@mui/material";
import { DashboardProps, DashboardState } from "./Dashboard.models";
interface ViewProps extends DashboardProps {
state: DashboardState;
setState: React.Dispatch<React.SetStateAction<DashboardState>>;
}
export default function DashboardView({
config,
data,
latest,
state,
setState,
onModeChange
}: ViewProps) {
const { mode, period, comparison } = state;
const colors = config.style?.palette[mode] || { primary: '#000', light: '#fff' };
const handleModeChange = (_: any, newMode: any) => {
if (newMode && onModeChange) {
onModeChange(newMode);
setState(prev => ({ ...prev, mode: newMode }));
}
};
return (
<Container
sx={{
mt: 4,
mb: 4,
background: `linear-gradient(180deg, ${colors.light} 0%, transparent 100%)`,
borderRadius: 4,
p: 2
}}
>
<Box sx={{ display: "flex", justifyContent: "center", mb: 3 }}>
<ToggleButtonGroup
value={mode}
exclusive
onChange={handleModeChange}
sx={{
borderRadius: 3,
overflow: "hidden",
"& .MuiToggleButton-root": {
px: 3,
textTransform: "none",
color: "text.secondary"
},
"&.Mui-selected": {
bgcolor: colors.primary,
color: "white",
borderColor: colors.primary
},
}}
>
<ToggleButton value="expense">Expenses</ToggleButton>
<ToggleButton value="income">Income</ToggleButton>
</ToggleButtonGroup>
</Box>
<Grid container spacing={4}>
{config.sections.map((section) => {
const Component = section.component;
return (
<Grid key={section.id} size={section.style?.size || 12 as any}>
{section.title && !section.isList && (
<Box sx={{ mb: 2 }}>
<Typography variant="h6" fontWeight={700}>
{section.title}
</Typography>
</Box>
)}
{section.isList ? (
<Box>
{section.title && (
<Box sx={{ mb: 2 }}>
<Typography variant="h6" fontWeight={700}>
{section.title}
</Typography>
</Box>
)}
<Grid container spacing={2}>
{(data[section.dataKey || ""] || []).map((item: any, idx: number) => (
<Grid key={idx} size={{ xs: 12, sm: 6, md: 2.4 }}>
<Component
{...section.settings}
header={item.payeeName || item.name}
progressAmount={item.amount}
totalAmount={data.totalAmount}
colorTheme={mode === "expense" ? "error" : "success"}
/>
</Grid>
))}
</Grid>
</Box>
) : (
<Component
{...section.settings}
header={section.title}
summary={section.summary}
data={section.dataKey ? data[section.dataKey] : data.chartData}
items={section.dataKey === 'latest' ? latest : (data[section.dataKey || ''] || [])}
title={section.title}
accentColor={colors.primary}
colorScheme={colors}
period={period}
onPeriodChange={(p: any) => setState(prev => ({ ...prev, period: p }))}
comparison={comparison}
setComparison={(c: any) => setState(prev => ({ ...prev, comparison: c }))}
/>
)}
</Grid>
);
})}
</Grid>
</Container>
);
}

View File

@@ -0,0 +1,2 @@
export { default } from "./Dashboard";
export * from "./Dashboard.models";

60
src/dashboard-config.ts Normal file
View File

@@ -0,0 +1,60 @@
import HistoryChart from "./components/HistoryChart";
import ProgressCard from "./components/ProgressCard";
import LatestItems from "./components/LatestItems";
import { DashboardConfig } from "./components/Dashboard";
export const configuration: DashboardConfig = {
sections: [
{
id: "breakdown",
title: "Breakdown",
summary: "Interactive chronological tracking",
component: HistoryChart,
dataKey: "chartData",
settings: {
tabs: ["Daily", "Weekly", "Monthly"],
},
style: {
size: 12,
},
},
{
id: "top-payees",
title: 'Top Payees',
component: ProgressCard,
dataKey: "topPayees",
isList: true,
settings: {
compact: true,
},
style: {
size: 12,
},
},
{
id: "latest",
title: 'Recent Transactions',
component: LatestItems,
dataKey: "latest",
style: {
size: 12,
},
},
],
style: {
palette: {
expense: {
primary: "#d32f2f",
light: "#fdecea",
dark: "#9a0007",
text: "#b71c1c"
},
income: {
primary: "#2e7d32",
light: "#e8f5e9",
dark: "#1b5e20",
text: "#1b5e20"
}
}
}
};