major refactor of the dashboard and react-openapi integration #1
@@ -16,6 +16,7 @@ export function useResource<T = any>(config: ResourceConfig | undefined) {
|
||||
queryKey: [name, "list", params],
|
||||
queryFn: async () => {
|
||||
if (!endpoint) return { data: [], total: 0 };
|
||||
console.log('params:', params);
|
||||
// @ts-ignore
|
||||
const res = await api.get<T[]>(endpoint, { params });
|
||||
const total = res.headers ? parseInt(res.headers['x-total-count'] || res.headers['X-Total-Count']) : undefined;
|
||||
|
||||
@@ -6,11 +6,13 @@ import {
|
||||
CircularProgress,
|
||||
Alert,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup
|
||||
ToggleButtonGroup,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
|
||||
import LatestItems from "./components/LatestItems";
|
||||
import HistoryChart from "./components/HistoryChart";
|
||||
import ProgressCard from "./components/ProgressCard";
|
||||
|
||||
import { useDashboardData } from "./features/dashboard";
|
||||
|
||||
@@ -107,6 +109,29 @@ export default function Dashboard() {
|
||||
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
|
||||
|
||||
@@ -4,4 +4,5 @@ export interface ProgressCardProps {
|
||||
progressAmount: number;
|
||||
totalAmount: number;
|
||||
colorTheme?: "primary" | "secondary" | "error" | "info" | "success" | "warning";
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ProgressCardProps } from "./ProgressCard.models";
|
||||
import { getPercentage, parseSummary } from "./ProgressCard.utils";
|
||||
|
||||
export default function ProgressCard(props: ProgressCardProps) {
|
||||
const { progressAmount, totalAmount, summary } = props;
|
||||
const { progressAmount, totalAmount, summary, compact = false } = props;
|
||||
|
||||
const percentage = getPercentage(progressAmount, totalAmount);
|
||||
const { prefixAmount, suffixString } = parseSummary(
|
||||
@@ -18,6 +18,7 @@ export default function ProgressCard(props: ProgressCardProps) {
|
||||
percentage={percentage}
|
||||
prefixAmount={prefixAmount}
|
||||
suffixString={suffixString}
|
||||
compact={compact}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,14 +20,15 @@ export default function ProgressCardView({
|
||||
percentage,
|
||||
prefixAmount,
|
||||
suffixString,
|
||||
compact = false,
|
||||
}: ViewProps) {
|
||||
return (
|
||||
<Paper
|
||||
elevation={4}
|
||||
elevation={compact ? 2 : 4}
|
||||
sx={{
|
||||
width: "100%",
|
||||
p: { xs: 3, md: 4 },
|
||||
borderRadius: 4,
|
||||
p: compact ? { xs: 2, md: 2.5 } : { xs: 3, md: 4 },
|
||||
borderRadius: compact ? 3 : 4,
|
||||
background: (theme) =>
|
||||
colorTheme === "info"
|
||||
? "linear-gradient(135deg, #0284c7 0%, #06b6d4 100%)"
|
||||
@@ -35,28 +36,32 @@ export default function ProgressCardView({
|
||||
color: "#fff",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
alignItems: compact ? "flex-start" : "center",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
boxShadow: (theme) =>
|
||||
`0 12px 24px -10px ${
|
||||
`0 ${compact ? 6 : 12}px ${compact ? 12 : 24}px -10px ${
|
||||
theme.palette.mode === "dark"
|
||||
? "#000"
|
||||
: theme.palette[colorTheme].main
|
||||
}`,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" fontWeight={600} sx={{ opacity: 0.9, mb: 1 }}>
|
||||
<Typography
|
||||
variant={compact ? "body2" : "subtitle1"}
|
||||
fontWeight={600}
|
||||
sx={{ opacity: 0.9, mb: compact ? 0.5 : 1, width: '100%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{header}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h3" fontWeight={800} sx={{ mb: 3 }}>
|
||||
<Typography variant={compact ? "h5" : "h3"} fontWeight={800} sx={{ mb: compact ? 2 : 3 }}>
|
||||
{prefixAmount}{" "}
|
||||
{suffixString && (
|
||||
<Typography
|
||||
component="span"
|
||||
variant="subtitle1"
|
||||
variant={compact ? "caption" : "subtitle1"}
|
||||
sx={{ opacity: 0.7, fontWeight: 500 }}
|
||||
>
|
||||
{suffixString}
|
||||
@@ -64,12 +69,12 @@ export default function ProgressCardView({
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ width: "85%" }}>
|
||||
<Box sx={{ width: compact ? "100%" : "85%" }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={percentage}
|
||||
sx={{
|
||||
height: 10,
|
||||
height: compact ? 6 : 10,
|
||||
borderRadius: 5,
|
||||
[`&.${linearProgressClasses.colorPrimary}`]: {
|
||||
backgroundColor: "rgba(0, 0, 0, 0.2)",
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { default } from "./LatestItems";
|
||||
export * from "./LatestItems.models";
|
||||
export { default } from "./ProgressCard";
|
||||
export * from "./ProgressCard.models";
|
||||
|
||||
@@ -15,26 +15,30 @@ export function useDashboardData(type: "expense" | "income") {
|
||||
// Fetch reports for aggregation
|
||||
const weeklyReport = useReportList({ period: "weekly", rolling: true });
|
||||
const monthlyReport = useReportList({ period: "monthly", rolling: true });
|
||||
const payeeReport = useReportList({ period: "weekly", rolling: true, group_by: "payee" });
|
||||
|
||||
const isLoading =
|
||||
latestQuery.isLoading ||
|
||||
weeklyReport.isLoading ||
|
||||
monthlyReport.isLoading;
|
||||
monthlyReport.isLoading ||
|
||||
payeeReport.isLoading;
|
||||
|
||||
const error =
|
||||
latestQuery.error ||
|
||||
weeklyReport.error ||
|
||||
monthlyReport.error;
|
||||
monthlyReport.error ||
|
||||
payeeReport.error;
|
||||
|
||||
const latest = latestQuery.data?.data
|
||||
? mapToLatestItems(latestQuery.data.data, type)
|
||||
: [];
|
||||
|
||||
const aggregatedData =
|
||||
weeklyReport.data?.data && monthlyReport.data?.data
|
||||
weeklyReport.data?.data && monthlyReport.data?.data && payeeReport.data?.data
|
||||
? mapReportToDashboard(
|
||||
(weeklyReport.data.data as any).buckets,
|
||||
(monthlyReport.data.data as any).buckets,
|
||||
(payeeReport.data.data as any).buckets,
|
||||
type
|
||||
)
|
||||
: null;
|
||||
|
||||
@@ -50,6 +50,7 @@ const toPoints = (
|
||||
export function mapReportToDashboard(
|
||||
weekly: ReportBucket[],
|
||||
monthly: ReportBucket[],
|
||||
payeeBuckets: ReportBucket[],
|
||||
type: "expense" | "income"
|
||||
): AggregatedDashboardData {
|
||||
const flow = type === "expense" ? "expenses" : "incomes";
|
||||
@@ -75,7 +76,9 @@ export function mapReportToDashboard(
|
||||
|
||||
const payeeMap: Record<string, number> = {};
|
||||
|
||||
for (const b of weekly) {
|
||||
const sourceForPayees = (payeeBuckets && payeeBuckets.length > 0) ? payeeBuckets : weekly;
|
||||
|
||||
for (const b of sourceForPayees) {
|
||||
for (const g of b.groups) {
|
||||
const key = g.group_key || "Unknown";
|
||||
const amt = g?.[flow]?.sum || 0;
|
||||
@@ -84,6 +87,7 @@ export function mapReportToDashboard(
|
||||
}
|
||||
|
||||
const topPayees = Object.entries(payeeMap)
|
||||
// .filter(([name]) => name !== "Unknown")
|
||||
.map(([payeeName, amount]) => ({ payeeName, amount }))
|
||||
.sort((a, b) => b.amount - a.amount)
|
||||
.slice(0, 5);
|
||||
|
||||
Reference in New Issue
Block a user