configurable Dashboard.tsx
This commit is contained in:
43
src/components/Dashboard/Dashboard.models.ts
Normal file
43
src/components/Dashboard/Dashboard.models.ts
Normal 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;
|
||||
}
|
||||
19
src/components/Dashboard/Dashboard.tsx
Normal file
19
src/components/Dashboard/Dashboard.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
129
src/components/Dashboard/Dashboard.view.tsx
Normal file
129
src/components/Dashboard/Dashboard.view.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
2
src/components/Dashboard/index.ts
Normal file
2
src/components/Dashboard/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from "./Dashboard";
|
||||
export * from "./Dashboard.models";
|
||||
Reference in New Issue
Block a user