filter-by-payee-and-tags #3
@@ -3,7 +3,11 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Container,
|
Container,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Alert
|
Alert,
|
||||||
|
TextField,
|
||||||
|
Paper,
|
||||||
|
Autocomplete,
|
||||||
|
Button
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
import ConfigurableDashboard from "./components/Dashboard";
|
import ConfigurableDashboard from "./components/Dashboard";
|
||||||
@@ -14,18 +18,67 @@ import {
|
|||||||
} from "./features/report";
|
} from "./features/report";
|
||||||
|
|
||||||
export default function Dashboard() {
|
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({
|
const report = useReport({
|
||||||
periods: ["weekly", "monthly", "full"],
|
periods: ["weekly", "monthly", "full"],
|
||||||
rolling: true,
|
rolling: true,
|
||||||
include_transactions: true,
|
include_transactions: true,
|
||||||
group_by: ["tags"],
|
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 isLoading = report.isLoading;
|
||||||
const error = report.error;
|
const error = report.error;
|
||||||
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading && !report.data) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", height: "60vh" }}>
|
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", height: "60vh" }}>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
@@ -41,15 +94,74 @@ export default function Dashboard() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!report) {
|
if (!report.data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = prepareReport(report.data?.data);
|
const data = prepareReport(report.data.data);
|
||||||
return (
|
return (
|
||||||
<ConfigurableDashboard
|
<Box>
|
||||||
config={configuration}
|
<Container>
|
||||||
data={data}
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,5 +86,14 @@ export interface ReportData {
|
|||||||
ignore_self: boolean;
|
ignore_self: boolean;
|
||||||
include_transactions: boolean;
|
include_transactions: boolean;
|
||||||
|
|
||||||
|
start_date?: string | null;
|
||||||
|
end_date?: string | null;
|
||||||
|
flow?: "expense" | "income" | null;
|
||||||
|
payee?: string[] | null;
|
||||||
|
account?: string[] | null;
|
||||||
|
tags?: string[] | null;
|
||||||
|
min_amount?: number | null;
|
||||||
|
max_amount?: number | null;
|
||||||
|
|
||||||
buckets: ReportBucket[];
|
buckets: ReportBucket[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ export interface ReportParams {
|
|||||||
group_by?: ("payee" | "tags")[];
|
group_by?: ("payee" | "tags")[];
|
||||||
ignore_self?: boolean;
|
ignore_self?: boolean;
|
||||||
include_transactions?: boolean;
|
include_transactions?: boolean;
|
||||||
|
start_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
flow?: "expense" | "income";
|
||||||
|
payee?: string[];
|
||||||
|
account?: string[];
|
||||||
|
tags?: string[];
|
||||||
|
min_amount?: number;
|
||||||
|
max_amount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useReport(params: ReportParams) {
|
export function useReport(params: ReportParams) {
|
||||||
|
|||||||
Reference in New Issue
Block a user