Compare commits
2 Commits
71afc157ff
...
67d4c85146
| Author | SHA1 | Date | |
|---|---|---|---|
| 67d4c85146 | |||
| 89ad8e376e |
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
@@ -107,6 +109,29 @@ export default function Dashboard() {
|
|||||||
colorScheme={colors}
|
colorScheme={colors}
|
||||||
/>
|
/>
|
||||||
</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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}{" "}
|
<Typography
|
||||||
{suffixString && (
|
variant={compact ? "h5" : "h3"}
|
||||||
<Typography
|
fontWeight={900}
|
||||||
component="span"
|
sx={{ mb: 0.5, lineHeight: 1.2 }}
|
||||||
variant="subtitle1"
|
>
|
||||||
sx={{ opacity: 0.7, fontWeight: 500 }}
|
{formattedProgress}
|
||||||
>
|
</Typography>
|
||||||
{suffixString}
|
|
||||||
</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)",
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { default } from "./LatestItems";
|
export { default } from "./ProgressCard";
|
||||||
export * from "./LatestItems.models";
|
export * from "./ProgressCard.models";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user