Files
khata-ui/src/Dashboard.tsx

168 lines
5.1 KiB
TypeScript

import * as React from "react";
import {
Box,
Container,
CircularProgress,
Alert,
TextField,
Paper,
Autocomplete,
Button
} from "@mui/material";
import ConfigurableDashboard from "./components/Dashboard";
import { configuration } from "./dashboard-config";
import {
useReport,
prepareReport,
} from "./features/report";
export default function Dashboard() {
const [appliedPayees, setAppliedPayees] = React.useState<string[]>([]);
const [appliedTags, setAppliedTags] = React.useState<string[]>([]);
const [payeeInput, setPayeeInput] = React.useState<string[]>([]);
const [tagsInput, setTagsInput] = React.useState<string[]>([]);
const [loadedPayees, setLoadedPayees] = React.useState<string[]>([]);
const [loadedTags, setLoadedTags] = React.useState<string[]>([]);
const report = useReport({
periods: ["weekly", "monthly", "full"],
rolling: true,
include_transactions: true,
group_by: ["tags"],
payee: appliedPayees.length > 0 ? appliedPayees : undefined,
tags: appliedTags.length > 0 ? appliedTags : undefined,
});
React.useEffect(() => {
if (report.data?.data) {
setLoadedPayees(prev => {
const pSet = new Set<string>(prev);
report.data.data.buckets.forEach((b: any) => {
Object.values(b.periods).forEach((periodArray: any) => {
periodArray?.forEach((p: any) => {
p.expenses?.transactions?.forEach((t: any) => {
if (t.payee?.name) pSet.add(t.payee.name);
});
p.incomes?.transactions?.forEach((t: any) => {
if (t.payee?.name) pSet.add(t.payee.name);
});
});
});
});
return Array.from(pSet).sort();
});
setLoadedTags(prev => {
const tSet = new Set<string>(prev);
report.data.data.buckets.forEach((b: any) => {
Object.values(b.periods).forEach((periodArray: any) => {
periodArray?.forEach((p: any) => {
p.expenses?.transactions?.forEach((t: any) => {
t.tags?.forEach((tag: any) => tSet.add(tag.name || tag));
});
p.incomes?.transactions?.forEach((t: any) => {
t.tags?.forEach((tag: any) => tSet.add(tag.name || tag));
});
});
});
});
return Array.from(tSet).sort();
});
}
}, [report.data?.data]);
const isLoading = report.isLoading;
const error = report.error;
if (isLoading && !report.data) {
return (
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", height: "60vh" }}>
<CircularProgress />
</Box>
);
}
if (error) {
return (
<Container sx={{ mt: 4 }}>
<Alert severity="error">{String(error)}</Alert>
</Container>
);
}
if (!report.data) {
return null;
}
const data = prepareReport(report.data.data);
return (
<Box>
<Container>
<Paper
sx={{
mt: 4,
p: 2,
display: "flex",
flexDirection: { xs: "column", sm: "row" },
gap: 2,
alignItems: { xs: "stretch", sm: "flex-end" },
borderRadius: 4,
mb: -2 // pull up to be closer to the dashboard container below
}}
elevation={0}
variant="outlined"
>
<Box sx={{ display: 'flex', flexDirection: 'column', flex: 1, minWidth: { sm: 250 } }}>
<Box sx={{ typography: 'caption', mb: 1, color: 'text.secondary' }}>
Filter by Payee
</Box>
<Autocomplete
multiple
freeSolo
options={loadedPayees}
value={payeeInput}
onChange={(_, val) => setPayeeInput(val as string[])}
renderInput={(params) => <TextField {...params} placeholder="Add payees..." />}
sx={{ '& .MuiOutlinedInput-root': { height: 'auto', minHeight: '2.5rem', py: 0.5 } }}
/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', flex: 1, minWidth: { sm: 250 } }}>
<Box sx={{ typography: 'caption', mb: 1, color: 'text.secondary' }}>
Filter by Tags
</Box>
<Autocomplete
multiple
freeSolo
options={loadedTags}
value={tagsInput}
onChange={(_, val) => setTagsInput(val as string[])}
renderInput={(params) => <TextField {...params} placeholder="Add tags..." />}
sx={{ '& .MuiOutlinedInput-root': { height: 'auto', minHeight: '2.5rem', py: 0.5 } }}
/>
</Box>
<Button
variant="contained"
size="large"
onClick={() => {
setAppliedPayees(payeeInput);
setAppliedTags(tagsInput);
}}
disabled={isLoading}
sx={{ height: 40, borderRadius: 2 }} // Changed from 56 to 40 to match minHeight of inputs
>
Apply
</Button>
</Paper>
</Container>
<ConfigurableDashboard
config={configuration}
data={data}
/>
</Box>
);
}