Compare commits
2 Commits
67d4c85146
...
8a3ebdb1be
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a3ebdb1be | |||
| a36d9119bb |
@@ -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 ConfigurableDashboard 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
|
<ConfigurableDashboard
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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";
|
||||||
60
src/dashboard-config.ts
Normal file
60
src/dashboard-config.ts
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user