Compare commits

..

2 Commits

Author SHA1 Message Date
67d4c85146 fixes 2026-04-25 12:49:58 +05:30
89ad8e376e Progress Cards for Top Payees 2026-04-25 12:41:05 +05:30
9 changed files with 114 additions and 53 deletions

View File

@@ -16,6 +16,7 @@ export function useResource<T = any>(config: ResourceConfig | undefined) {
queryKey: [name, "list", params], queryKey: [name, "list", params],
queryFn: async () => { queryFn: async () => {
if (!endpoint) return { data: [], total: 0 }; if (!endpoint) return { data: [], total: 0 };
console.log('params:', params);
// @ts-ignore // @ts-ignore
const res = await api.get<T[]>(endpoint, { params }); const res = await api.get<T[]>(endpoint, { params });
const total = res.headers ? parseInt(res.headers['x-total-count'] || res.headers['X-Total-Count']) : undefined; const total = res.headers ? parseInt(res.headers['x-total-count'] || res.headers['X-Total-Count']) : undefined;

View File

@@ -6,11 +6,13 @@ import {
CircularProgress, CircularProgress,
Alert, Alert,
ToggleButton, ToggleButton,
ToggleButtonGroup ToggleButtonGroup,
Typography
} from "@mui/material"; } from "@mui/material";
import LatestItems from "./components/LatestItems"; import LatestItems from "./components/LatestItems";
import HistoryChart from "./components/HistoryChart"; import HistoryChart from "./components/HistoryChart";
import ProgressCard from "./components/ProgressCard";
import { useDashboardData } from "./features/dashboard"; import { useDashboardData } from "./features/dashboard";
@@ -108,6 +110,29 @@ export default function Dashboard() {
/> />
</Grid> </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}> <Grid size={12}>
<LatestItems <LatestItems
title={`Recent ${mode === "expense" ? "Expenses" : "Income"}`} title={`Recent ${mode === "expense" ? "Expenses" : "Income"}`}

View File

@@ -4,4 +4,5 @@ export interface ProgressCardProps {
progressAmount: number; progressAmount: number;
totalAmount: number; totalAmount: number;
colorTheme?: "primary" | "secondary" | "error" | "info" | "success" | "warning"; colorTheme?: "primary" | "secondary" | "error" | "info" | "success" | "warning";
compact?: boolean;
} }

View File

@@ -1,23 +1,23 @@
import * as React from "react";
import ProgressCardView from "./ProgressCard.view"; import ProgressCardView from "./ProgressCard.view";
import { ProgressCardProps } from "./ProgressCard.models"; import { ProgressCardProps } from "./ProgressCard.models";
import { getPercentage, parseSummary } from "./ProgressCard.utils"; import { getPercentage, formatCurrency } from "./ProgressCard.utils";
export default function ProgressCard(props: ProgressCardProps) { export default function ProgressCard(props: ProgressCardProps) {
const { progressAmount, totalAmount, summary } = props; const { progressAmount, totalAmount, compact = false } = props;
const percentage = getPercentage(progressAmount, totalAmount); const percentage = getPercentage(progressAmount, totalAmount);
const { prefixAmount, suffixString } = parseSummary(
summary, const formattedProgress = formatCurrency(progressAmount);
progressAmount, const formattedTotal = formatCurrency(totalAmount);
totalAmount
);
return ( return (
<ProgressCardView <ProgressCardView
{...props} {...props}
percentage={percentage} percentage={percentage}
prefixAmount={prefixAmount} formattedProgress={formattedProgress}
suffixString={suffixString} formattedTotal={formattedTotal}
compact={compact}
/> />
); );
} }

View File

@@ -3,17 +3,13 @@ export const getPercentage = (progressAmount: number, totalAmount: number) => {
return Math.min(100, Math.max(0, (progressAmount / totalAmount) * 100)); return Math.min(100, Math.max(0, (progressAmount / totalAmount) * 100));
}; };
export const parseSummary = ( export const formatCurrency = (val: number) => {
summary: string | undefined, const absVal = Math.abs(val);
progressAmount: number, if (absVal >= 100000) {
totalAmount: number return `${(val / 100000).toFixed(2)}L`;
) => { }
const displaySummary = summary ?? `Rs ${progressAmount} / Rs ${totalAmount}`; if (absVal >= 1000) {
return `${(val / 1000).toFixed(2)}k`;
const parts = displaySummary.split("/"); }
const prefixAmount = parts[0]?.trim() || ""; return `${val.toFixed(2)}`;
const suffixString =
parts.length > 1 ? `/ ${parts.slice(1).join("/").trim()}` : "";
return { prefixAmount, suffixString };
}; };

View File

@@ -4,30 +4,32 @@ import {
Typography, Typography,
Paper, Paper,
LinearProgress, LinearProgress,
Divider,
linearProgressClasses linearProgressClasses
} from "@mui/material"; } from "@mui/material";
import { ProgressCardProps } from "./ProgressCard.models"; import { ProgressCardProps } from "./ProgressCard.models";
interface ViewProps extends ProgressCardProps { interface ViewProps extends ProgressCardProps {
percentage: number; percentage: number;
prefixAmount: string; formattedProgress: string;
suffixString: string; formattedTotal: string;
} }
export default function ProgressCardView({ export default function ProgressCardView({
header, header,
colorTheme = "info", colorTheme = "info",
percentage, percentage,
prefixAmount, formattedProgress,
suffixString, formattedTotal,
compact = false,
}: ViewProps) { }: ViewProps) {
return ( return (
<Paper <Paper
elevation={4} elevation={compact ? 2 : 4}
sx={{ sx={{
width: "100%", width: "100%",
p: { xs: 3, md: 4 }, p: compact ? { xs: 2.5, md: 3 } : { xs: 3, md: 4 },
borderRadius: 4, borderRadius: compact ? 3 : 4,
background: (theme) => background: (theme) =>
colorTheme === "info" colorTheme === "info"
? "linear-gradient(135deg, #0284c7 0%, #06b6d4 100%)" ? "linear-gradient(135deg, #0284c7 0%, #06b6d4 100%)"
@@ -35,41 +37,69 @@ export default function ProgressCardView({
color: "#fff", color: "#fff",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: compact ? "flex-start" : "center",
justifyContent: "center", justifyContent: "center",
position: "relative", position: "relative",
overflow: "hidden", overflow: "hidden",
boxShadow: (theme) => boxShadow: (theme) =>
`0 12px 24px -10px ${ `0 ${compact ? 6 : 12}px ${compact ? 12 : 24}px -10px ${
theme.palette.mode === "dark" theme.palette.mode === "dark"
? "#000" ? "#000"
: theme.palette[colorTheme].main : theme.palette[colorTheme].main
}`, }`,
}} }}
> >
<Typography variant="subtitle1" fontWeight={600} sx={{ opacity: 0.9, mb: 1 }}> <Typography
variant={compact ? "body2" : "subtitle1"}
fontWeight={700}
sx={{
opacity: 0.9,
mb: compact ? 1.5 : 2,
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
letterSpacing: 0.5
}}
>
{header} {header}
</Typography> </Typography>
<Typography variant="h3" fontWeight={800} sx={{ mb: 3 }}> <Box sx={{ mb: compact ? 2 : 3, width: '100%' }}>
{prefixAmount}{" "}
{suffixString && (
<Typography <Typography
component="span" variant={compact ? "h5" : "h3"}
variant="subtitle1" fontWeight={900}
sx={{ opacity: 0.7, fontWeight: 500 }} sx={{ mb: 0.5, lineHeight: 1.2 }}
> >
{suffixString} {formattedProgress}
</Typography>
)}
</Typography> </Typography>
<Box sx={{ width: "85%" }}> <Divider
sx={{
my: 1,
borderColor: "rgba(255,255,255,0.4)",
width: "100%",
}}
/>
<Typography
variant={compact ? "caption" : "body2"}
sx={{
opacity: 0.8,
fontWeight: 400,
display: "block"
}}
>
{formattedTotal}
</Typography>
</Box>
<Box sx={{ width: "100%", mt: 'auto' }}>
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={percentage} value={percentage}
sx={{ sx={{
height: 10, height: compact ? 6 : 10,
borderRadius: 5, borderRadius: 5,
[`&.${linearProgressClasses.colorPrimary}`]: { [`&.${linearProgressClasses.colorPrimary}`]: {
backgroundColor: "rgba(0, 0, 0, 0.2)", backgroundColor: "rgba(0, 0, 0, 0.2)",

View File

@@ -1,2 +1,2 @@
export { default } from "./LatestItems"; export { default } from "./ProgressCard";
export * from "./LatestItems.models"; export * from "./ProgressCard.models";

View File

@@ -15,26 +15,30 @@ export function useDashboardData(type: "expense" | "income") {
// Fetch reports for aggregation // Fetch reports for aggregation
const weeklyReport = useReportList({ period: "weekly", rolling: true }); const weeklyReport = useReportList({ period: "weekly", rolling: true });
const monthlyReport = useReportList({ period: "monthly", rolling: true }); const monthlyReport = useReportList({ period: "monthly", rolling: true });
const payeeReport = useReportList({ period: "weekly", rolling: true, group_by: "payee" });
const isLoading = const isLoading =
latestQuery.isLoading || latestQuery.isLoading ||
weeklyReport.isLoading || weeklyReport.isLoading ||
monthlyReport.isLoading; monthlyReport.isLoading ||
payeeReport.isLoading;
const error = const error =
latestQuery.error || latestQuery.error ||
weeklyReport.error || weeklyReport.error ||
monthlyReport.error; monthlyReport.error ||
payeeReport.error;
const latest = latestQuery.data?.data const latest = latestQuery.data?.data
? mapToLatestItems(latestQuery.data.data, type) ? mapToLatestItems(latestQuery.data.data, type)
: []; : [];
const aggregatedData = const aggregatedData =
weeklyReport.data?.data && monthlyReport.data?.data weeklyReport.data?.data && monthlyReport.data?.data && payeeReport.data?.data
? mapReportToDashboard( ? mapReportToDashboard(
(weeklyReport.data.data as any).buckets, (weeklyReport.data.data as any).buckets,
(monthlyReport.data.data as any).buckets, (monthlyReport.data.data as any).buckets,
(payeeReport.data.data as any).buckets,
type type
) )
: null; : null;

View File

@@ -50,6 +50,7 @@ const toPoints = (
export function mapReportToDashboard( export function mapReportToDashboard(
weekly: ReportBucket[], weekly: ReportBucket[],
monthly: ReportBucket[], monthly: ReportBucket[],
payeeBuckets: ReportBucket[],
type: "expense" | "income" type: "expense" | "income"
): AggregatedDashboardData { ): AggregatedDashboardData {
const flow = type === "expense" ? "expenses" : "incomes"; const flow = type === "expense" ? "expenses" : "incomes";
@@ -75,7 +76,9 @@ export function mapReportToDashboard(
const payeeMap: Record<string, number> = {}; 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) { for (const g of b.groups) {
const key = g.group_key || "Unknown"; const key = g.group_key || "Unknown";
const amt = g?.[flow]?.sum || 0; const amt = g?.[flow]?.sum || 0;
@@ -84,6 +87,7 @@ export function mapReportToDashboard(
} }
const topPayees = Object.entries(payeeMap) const topPayees = Object.entries(payeeMap)
// .filter(([name]) => name !== "Unknown")
.map(([payeeName, amount]) => ({ payeeName, amount })) .map(([payeeName, amount]) => ({ payeeName, amount }))
.sort((a, b) => b.amount - a.amount) .sort((a, b) => b.amount - a.amount)
.slice(0, 5); .slice(0, 5);