168 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
}
|