Compare commits
7 Commits
e6c7778c08
...
69c9fd6bef
| Author | SHA1 | Date | |
|---|---|---|---|
| 69c9fd6bef | |||
| 00c8da629c | |||
| ce0c34d014 | |||
| 6c305e0cdd | |||
| b587f8aeb6 | |||
| 6602d29299 | |||
| f4e5979c00 |
@@ -51,7 +51,7 @@ function Dashboard({ basePath }: { basePath: string }) {
|
|||||||
transition: 'transform 0.2s',
|
transition: 'transform 0.2s',
|
||||||
'&:hover': { transform: 'translateY(-4px)', boxShadow: 4 }
|
'&:hover': { transform: 'translateY(-4px)', boxShadow: 4 }
|
||||||
}}
|
}}
|
||||||
onClick={() => navigate(`${basePath}/${res.name}`)}
|
onClick={() => navigate(`/admin/${res.name}`)}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" color="primary">{res.pluralLabel}</Typography>
|
<Typography variant="h6" color="primary">{res.pluralLabel}</Typography>
|
||||||
<Typography variant="body2" color="text.secondary">Manage {res.pluralLabel.toLowerCase()}</Typography>
|
<Typography variant="body2" color="text.secondary">Manage {res.pluralLabel.toLowerCase()}</Typography>
|
||||||
@@ -88,7 +88,7 @@ function AdminApp({ basePath }: { basePath: string }) {
|
|||||||
<AdminLayout
|
<AdminLayout
|
||||||
username={currentUser.username}
|
username={currentUser.username}
|
||||||
onLogout={logout}
|
onLogout={logout}
|
||||||
onSelectResource={(name) => navigate(`${basePath}/${name}`)}
|
onSelectResource={(name) => navigate(`/admin/${name}`)}
|
||||||
resources={config?.resources || []}
|
resources={config?.resources || []}
|
||||||
>
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|||||||
@@ -104,12 +104,12 @@ export default function EnhancedTable({
|
|||||||
<GridActionsCellItem
|
<GridActionsCellItem
|
||||||
icon={<VisibilityIcon />}
|
icon={<VisibilityIcon />}
|
||||||
label="View"
|
label="View"
|
||||||
onClick={() => navigate(`/${config.name}/${params.id}`)}
|
onClick={() => navigate(`/admin/${config.name}/${params.id}`)}
|
||||||
/>,
|
/>,
|
||||||
<GridActionsCellItem
|
<GridActionsCellItem
|
||||||
icon={<EditIcon />}
|
icon={<EditIcon />}
|
||||||
label="Edit"
|
label="Edit"
|
||||||
onClick={() => navigate(`/${config.name}/edit/${params.id}`)}
|
onClick={() => navigate(`/admin/${config.name}/edit/${params.id}`)}
|
||||||
/>,
|
/>,
|
||||||
<GridActionsCellItem
|
<GridActionsCellItem
|
||||||
icon={<DeleteIcon />}
|
icon={<DeleteIcon />}
|
||||||
@@ -222,8 +222,8 @@ function MobileCardRow({ row, config, onDelete, onNavigate, navigate }: any) {
|
|||||||
<MoreVertIcon fontSize="small" />
|
<MoreVertIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
|
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
|
||||||
<MenuItem onClick={() => { handleClose(); navigate(`/${config.name}/${id}`); }}>View</MenuItem>
|
<MenuItem onClick={() => { handleClose(); navigate(`/admin/${config.name}/${id}`); }}>View</MenuItem>
|
||||||
<MenuItem onClick={() => { handleClose(); navigate(`/${config.name}/edit/${id}`); }}>Edit</MenuItem>
|
<MenuItem onClick={() => { handleClose(); navigate(`/admin/${config.name}/edit/${id}`); }}>Edit</MenuItem>
|
||||||
<MenuItem onClick={() => { handleClose(); onDelete(id); }} sx={{ color: 'error.main' }}>Delete</MenuItem>
|
<MenuItem onClick={() => { handleClose(); onDelete(id); }} sx={{ color: 'error.main' }}>Delete</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -242,7 +242,7 @@ function MobileCardRow({ row, config, onDelete, onNavigate, navigate }: any) {
|
|||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardActions sx={{ justifyContent: 'flex-end', px: 2, pb: 2 }}>
|
<CardActions sx={{ justifyContent: 'flex-end', px: 2, pb: 2 }}>
|
||||||
<Button size="small" onClick={() => navigate(`/${config.name}/${id}`)}>View Details</Button>
|
<Button size="small" onClick={() => navigate(`/admin/${config.name}/${id}`)}>View Details</Button>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -359,7 +359,7 @@ function FieldRenderer({ params, field, fieldKey, config, onNavigate, navigate,
|
|||||||
label={value}
|
label={value}
|
||||||
size="small"
|
size="small"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={(e) => { e.stopPropagation(); navigate(`/${config.name}/${params.row[config.primaryKey]}`); }}
|
onClick={(e) => { e.stopPropagation(); navigate(`/admin/${config.name}/${params.row[config.primaryKey]}`); }}
|
||||||
sx={{ cursor: 'pointer', fontWeight: 'bold' }}
|
sx={{ cursor: 'pointer', fontWeight: 'bold' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,11 +48,11 @@ export default function ResourceView({ config, onNavigateToResource }: ResourceV
|
|||||||
const deleteMutation = useDelete();
|
const deleteMutation = useDelete();
|
||||||
|
|
||||||
const handleEdit = (item: any) => {
|
const handleEdit = (item: any) => {
|
||||||
navigate(`/${config.name}/edit/${item[config.primaryKey]}`);
|
navigate(`/admin/${config.name}/edit/${item[config.primaryKey]}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
navigate(`/${config.name}/create`);
|
navigate(`/admin/${config.name}/create`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async (formData: any) => {
|
const handleSave = async (formData: any) => {
|
||||||
@@ -62,7 +62,7 @@ export default function ResourceView({ config, onNavigateToResource }: ResourceV
|
|||||||
} else {
|
} else {
|
||||||
await createMutation.mutateAsync(formData);
|
await createMutation.mutateAsync(formData);
|
||||||
}
|
}
|
||||||
navigate(`/${config.name}`);
|
navigate(`/admin/${config.name}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Save failed:', err);
|
console.error('Save failed:', err);
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ export default function ResourceView({ config, onNavigateToResource }: ResourceV
|
|||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onCreate={handleCreate}
|
onCreate={handleCreate}
|
||||||
onNavigateToResource={(res, id) => navigate(`/${res}/${id}`)}
|
onNavigateToResource={(res, id) => navigate(`/admin/${res}/${id}`)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Paper sx={{ p: 4 }}>
|
<Paper sx={{ p: 4 }}>
|
||||||
@@ -98,10 +98,10 @@ export default function ResourceView({ config, onNavigateToResource }: ResourceV
|
|||||||
config={config}
|
config={config}
|
||||||
initialData={isCreate ? null : itemQuery.data}
|
initialData={isCreate ? null : itemQuery.data}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onCancel={() => navigate(`/${config.name}`)}
|
onCancel={() => navigate(`/admin/${config.name}`)}
|
||||||
loading={createMutation.isPending || updateMutation.isPending}
|
loading={createMutation.isPending || updateMutation.isPending}
|
||||||
readOnly={isView}
|
readOnly={isView}
|
||||||
onEditClick={() => navigate(`/${config.name}/edit/${id}`)}
|
onEditClick={() => navigate(`/admin/${config.name}/edit/${id}`)}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import {
|
|||||||
|
|
||||||
import LatestItemsList, { LatestItem } from "./components/LatestItemsList";
|
import LatestItemsList, { LatestItem } from "./components/LatestItemsList";
|
||||||
import HistoryChart from "./components/HistoryChart";
|
import HistoryChart from "./components/HistoryChart";
|
||||||
|
import {
|
||||||
|
AggregatedDashboardData,
|
||||||
|
} from "./components/HistoryChart";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchLatestTransactions,
|
fetchLatestTransactions,
|
||||||
fetchAggregatedExpenses,
|
fetchAggregatedExpenses,
|
||||||
fetchAggregatedIncome,
|
fetchAggregatedIncome,
|
||||||
AggregatedDashboardData
|
|
||||||
} from "./utils/dashboardLoader";
|
} from "./utils/dashboardLoader";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export interface ChartDataPoint {
|
|||||||
id: string;
|
id: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
compareAmount?: number;
|
compareAmount?: number;
|
||||||
count?: number;
|
compareLabel?: string;
|
||||||
highlighted?: boolean;
|
highlighted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +31,54 @@ export interface HistoryChartProps {
|
|||||||
setComparison: (mode: boolean) => void;
|
setComparison: (mode: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatDisplay = (
|
||||||
|
point: ChartDataPoint,
|
||||||
|
tab: string,
|
||||||
|
comparison: boolean
|
||||||
|
) => {
|
||||||
|
const base = point.amount;
|
||||||
|
const cmp = point.compareAmount ?? 0;
|
||||||
|
|
||||||
|
const formatShort = (val: number) => {
|
||||||
|
if (tab === "monthly") {
|
||||||
|
if (val >= 100000) return `${(val / 100000).toFixed(2)}L`;
|
||||||
|
}
|
||||||
|
if (tab === "weekly") {
|
||||||
|
if (val >= 1000) return `${(val / 1000).toFixed(1)}K`;
|
||||||
|
}
|
||||||
|
return val.toLocaleString("en-IN");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only hide diff when comparison OFF or compare is undefined
|
||||||
|
if (!comparison) {
|
||||||
|
return `₹ ${formatShort(base)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const diff = base - cmp;
|
||||||
|
const sign = diff >= 0 ? "+" : "-";
|
||||||
|
const absDiff = Math.abs(diff);
|
||||||
|
|
||||||
|
return `₹ ${formatShort(base)} (${sign}${formatShort(absDiff)})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatLabel = (label: string, type: string) => {
|
||||||
|
if (type === "monthly") return label;
|
||||||
|
|
||||||
|
if (type === "weekly") {
|
||||||
|
const parts = label.split(" - ");
|
||||||
|
if (parts.length === 2) {
|
||||||
|
const [start, end] = parts;
|
||||||
|
const startDay = start.split(" ")[0];
|
||||||
|
const endParts = end.split(" ");
|
||||||
|
const endDay = endParts[0];
|
||||||
|
const month = endParts[1];
|
||||||
|
return `${startDay}–${endDay} ${month}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return label;
|
||||||
|
};
|
||||||
|
|
||||||
export default function HistoryChart({
|
export default function HistoryChart({
|
||||||
header,
|
header,
|
||||||
summary,
|
summary,
|
||||||
@@ -44,9 +92,7 @@ export default function HistoryChart({
|
|||||||
const [activeTab, setActiveTab] = React.useState<string>(tabs[0] || "");
|
const [activeTab, setActiveTab] = React.useState<string>(tabs[0] || "");
|
||||||
|
|
||||||
const handleTabChange = (_: React.MouseEvent<HTMLElement>, newTab: string | null) => {
|
const handleTabChange = (_: React.MouseEvent<HTMLElement>, newTab: string | null) => {
|
||||||
if (newTab !== null) {
|
if (newTab !== null) setActiveTab(newTab);
|
||||||
setActiveTab(newTab);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeDataKey = activeTab.toLowerCase() as keyof ChartData;
|
const activeDataKey = activeTab.toLowerCase() as keyof ChartData;
|
||||||
@@ -72,24 +118,6 @@ export default function HistoryChart({
|
|||||||
)
|
)
|
||||||
: 1;
|
: 1;
|
||||||
|
|
||||||
const formatAmount = (amount: number) => {
|
|
||||||
const tab = activeTab.toLowerCase();
|
|
||||||
|
|
||||||
if (amount === 0) return "";
|
|
||||||
|
|
||||||
if (tab === "monthly") {
|
|
||||||
if (amount >= 100000) return `₹ ${(amount / 100000).toFixed(2)} L`;
|
|
||||||
return `₹ ${amount.toLocaleString("en-IN")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tab === "weekly") {
|
|
||||||
if (amount >= 1000) return `₹ ${(amount / 1000).toFixed(1)} K`;
|
|
||||||
return `₹ ${amount.toLocaleString("en-IN")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `₹ ${amount.toLocaleString("en-IN")}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
sx={{
|
sx={{
|
||||||
@@ -111,38 +139,12 @@ export default function HistoryChart({
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tabs */}
|
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
exclusive
|
exclusive
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{
|
sx={{ mb: 4 }}
|
||||||
mb: 4,
|
|
||||||
bgcolor: (theme) =>
|
|
||||||
theme.palette.mode === "dark"
|
|
||||||
? "rgba(255,255,255,0.05)"
|
|
||||||
: "rgba(0,0,0,0.02)",
|
|
||||||
borderRadius: 8,
|
|
||||||
p: 0.5,
|
|
||||||
"& .MuiToggleButton-root": {
|
|
||||||
border: "none",
|
|
||||||
borderRadius: 8,
|
|
||||||
textTransform: "capitalize",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "text.secondary",
|
|
||||||
"&.Mui-selected": {
|
|
||||||
bgcolor: (theme) =>
|
|
||||||
theme.palette.mode === "dark"
|
|
||||||
? "primary.dark"
|
|
||||||
: "primary.light",
|
|
||||||
color: (theme) =>
|
|
||||||
theme.palette.mode === "dark"
|
|
||||||
? "primary.contrastText"
|
|
||||||
: "primary.main"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<ToggleButton key={tab} value={tab}>
|
<ToggleButton key={tab} value={tab}>
|
||||||
@@ -151,7 +153,6 @@ export default function HistoryChart({
|
|||||||
))}
|
))}
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
|
|
||||||
{/* Period Toggle */}
|
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup
|
||||||
value={period}
|
value={period}
|
||||||
exclusive
|
exclusive
|
||||||
@@ -176,7 +177,6 @@ export default function HistoryChart({
|
|||||||
<ToggleButton value="on">Compare</ToggleButton>
|
<ToggleButton value="on">Compare</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
|
|
||||||
{/* Chart */}
|
|
||||||
{currentData.length > 0 ? (
|
{currentData.length > 0 ? (
|
||||||
<Box sx={{ display: "flex", alignItems: "flex-end", height: 220, mt: 4 }}>
|
<Box sx={{ display: "flex", alignItems: "flex-end", height: 220, mt: 4 }}>
|
||||||
{currentData.map((point) => {
|
{currentData.map((point) => {
|
||||||
@@ -195,21 +195,15 @@ export default function HistoryChart({
|
|||||||
height: "100%"
|
height: "100%"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Values */}
|
|
||||||
<Typography variant="caption" sx={{ mb: 1, fontSize: "0.65rem" }}>
|
|
||||||
{formatAmount(point.amount)}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* Bars */}
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-end",
|
alignItems: "flex-end",
|
||||||
gap: comparison ? 0.5 : 0,
|
gap: comparison ? 0.5 : 0,
|
||||||
height: "100%"
|
height: "100%",
|
||||||
|
position: "relative"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Compare */}
|
|
||||||
{comparison && (
|
{comparison && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -221,31 +215,69 @@ export default function HistoryChart({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Current */}
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: 10,
|
width: 10,
|
||||||
height: `${currentHeight}%`,
|
height: `${currentHeight}%`,
|
||||||
bgcolor: point.highlighted ? "error.main" : "primary.main",
|
bgcolor: point.highlighted ? "error.main" : "primary.main",
|
||||||
borderRadius: 2
|
borderRadius: 2,
|
||||||
}}
|
position: "relative"
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Label */}
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{
|
|
||||||
mt: 1,
|
|
||||||
fontSize: "0.7rem",
|
|
||||||
textAlign: "center",
|
|
||||||
whiteSpace: "pre-line"
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{point.id}
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: -18,
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
fontSize: "0.65rem",
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatDisplay(point, activeTab.toLowerCase(), comparison)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
textAlign: "center",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
lineHeight: 1.1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
fontSize: "0.7rem",
|
||||||
|
opacity: 0.7,
|
||||||
|
color: "text.primary",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatLabel(point.id, activeDataKey)}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
fontSize: "0.65rem",
|
||||||
|
color: "grey.400",
|
||||||
|
visibility:
|
||||||
|
comparison && point.compareLabel && activeDataKey !== "daily"
|
||||||
|
? "visible"
|
||||||
|
: "hidden"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{point.compareLabel
|
||||||
|
? formatLabel(point.compareLabel, activeDataKey)
|
||||||
|
: "placeholder"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -2,40 +2,21 @@ import { api } from "../../react-openapi";
|
|||||||
import { LatestItem } from "../components/LatestItemsList";
|
import { LatestItem } from "../components/LatestItemsList";
|
||||||
import { ChartDataPoint } from "../components/HistoryChart";
|
import { ChartDataPoint } from "../components/HistoryChart";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { format } from "./dateUtils";
|
||||||
import MonetizationOnIcon from "@mui/icons-material/MonetizationOn";
|
import MonetizationOnIcon from "@mui/icons-material/MonetizationOn";
|
||||||
|
|
||||||
// ---------------- ICON ----------------
|
import {
|
||||||
|
buildDailyBuckets,
|
||||||
|
buildWeeklyRolling,
|
||||||
|
buildWeeklyCalendar,
|
||||||
|
buildMonthlyRolling,
|
||||||
|
buildMonthlyCalendar
|
||||||
|
} from "./periodBuilders";
|
||||||
|
|
||||||
const DEFAULT_ICON = React.createElement(MonetizationOnIcon, {
|
const DEFAULT_ICON = React.createElement(MonetizationOnIcon, {
|
||||||
sx: { color: "#388e3c" }
|
sx: { color: "#388e3c" }
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------- HELPERS ----------------
|
|
||||||
const format = (d: Date) =>
|
|
||||||
`${d.getDate()} ${d.toLocaleString("default", { month: "short" })}`;
|
|
||||||
|
|
||||||
const startOfDay = (d: Date) => {
|
|
||||||
const x = new Date(d);
|
|
||||||
x.setHours(0, 0, 0, 0);
|
|
||||||
return x;
|
|
||||||
};
|
|
||||||
|
|
||||||
const endOfDay = (d: Date) => {
|
|
||||||
const x = new Date(d);
|
|
||||||
x.setHours(23, 59, 59, 999);
|
|
||||||
return x;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStartOfWeek = (d: Date) => {
|
|
||||||
const date = new Date(d);
|
|
||||||
const day = date.getDay() || 7;
|
|
||||||
if (day !== 1) date.setDate(date.getDate() - (day - 1));
|
|
||||||
return startOfDay(date);
|
|
||||||
};
|
|
||||||
|
|
||||||
const shiftDate = (d: Date, days: number) =>
|
|
||||||
new Date(d.getTime() + days * 86400000);
|
|
||||||
|
|
||||||
// ---------------- LATEST ----------------
|
|
||||||
export async function fetchLatestTransactions(
|
export async function fetchLatestTransactions(
|
||||||
type: "expense" | "income"
|
type: "expense" | "income"
|
||||||
): Promise<LatestItem[]> {
|
): Promise<LatestItem[]> {
|
||||||
@@ -66,35 +47,17 @@ export async function fetchLatestTransactions(
|
|||||||
iconBgColor:
|
iconBgColor:
|
||||||
type === "expense" ? "#ffebee" : "#e8f5e9",
|
type === "expense" ? "#ffebee" : "#e8f5e9",
|
||||||
title: exp.payee?.name || exp.payee || "Unknown Payee",
|
title: exp.payee?.name || exp.payee || "Unknown Payee",
|
||||||
subtitle: exp.category?.name || exp.account?.name || "Transaction",
|
subtitle:
|
||||||
|
exp.category?.name || exp.account?.name || "Transaction",
|
||||||
amount: `Rs ${Math.abs(exp.amount || 0)}`,
|
amount: `Rs ${Math.abs(exp.amount || 0)}`,
|
||||||
timeAgo: diffDays === 0 ? "Today" : `${diffDays} days ago`
|
timeAgo: diffDays === 0 ? "Today" : `${diffDays} days ago`
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- TYPES ----------------
|
|
||||||
export interface ChartSeries {
|
|
||||||
rolling: ChartDataPoint[];
|
|
||||||
calendar: ChartDataPoint[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChartData {
|
|
||||||
daily: ChartDataPoint[];
|
|
||||||
weekly: ChartSeries;
|
|
||||||
monthly: ChartSeries;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AggregatedDashboardData {
|
|
||||||
chartData: ChartData;
|
|
||||||
totalAmount: number;
|
|
||||||
topPayees: Array<{ payeeName: string; amount: number }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------- AGGREGATION ----------------
|
|
||||||
export async function fetchAggregatedData(
|
export async function fetchAggregatedData(
|
||||||
type: "expense" | "income"
|
type: "expense" | "income"
|
||||||
): Promise<AggregatedDashboardData> {
|
) {
|
||||||
const res = await api.get("/expenses", { params: { limit: 0 } });
|
const res = await api.get("/expenses", { params: { limit: 0 } });
|
||||||
const all: any[] = res.data?.items || res.data || [];
|
const all: any[] = res.data?.items || res.data || [];
|
||||||
|
|
||||||
@@ -108,126 +71,19 @@ export async function fetchAggregatedData(
|
|||||||
|
|
||||||
const normalize = (amt: number) => Math.abs(amt);
|
const normalize = (amt: number) => Math.abs(amt);
|
||||||
|
|
||||||
// ---------------- DAILY ----------------
|
const {
|
||||||
const dailyBuckets: Record<string, any> = {
|
buckets: dailyBuckets,
|
||||||
Mon: { amount: 0, compare: 0 },
|
weekStart,
|
||||||
Tue: { amount: 0, compare: 0 },
|
weekEnd,
|
||||||
Wed: { amount: 0, compare: 0 },
|
prevWeekStart,
|
||||||
Thu: { amount: 0, compare: 0 },
|
prevWeekEnd
|
||||||
Fri: { amount: 0, compare: 0 },
|
} = buildDailyBuckets(now);
|
||||||
Sat: { amount: 0, compare: 0 },
|
|
||||||
Sun: { amount: 0, compare: 0 }
|
|
||||||
};
|
|
||||||
|
|
||||||
const weekStart = getStartOfWeek(now);
|
const weeklyRolling = buildWeeklyRolling(now);
|
||||||
const weekEnd = endOfDay(new Date(weekStart.getTime() + 6 * 86400000));
|
const weeklyCalendar = buildWeeklyCalendar(now);
|
||||||
const prevWeekStart = shiftDate(weekStart, -7);
|
const monthlyRolling = buildMonthlyRolling(now);
|
||||||
const prevWeekEnd = shiftDate(weekEnd, -7);
|
const monthlyCalendar = buildMonthlyCalendar(now);
|
||||||
|
|
||||||
// ---------------- WEEKLY ----------------
|
|
||||||
const weeklyRolling: any[] = [];
|
|
||||||
const weeklyCalendar: any[] = [];
|
|
||||||
|
|
||||||
const currentWeekStart = getStartOfWeek(now);
|
|
||||||
|
|
||||||
// rolling (last 5 weeks)
|
|
||||||
for (let i = 4; i >= 0; i--) {
|
|
||||||
const start = new Date(currentWeekStart.getTime() - i * 7 * 86400000);
|
|
||||||
const end = endOfDay(new Date(start.getTime() + 6 * 86400000));
|
|
||||||
|
|
||||||
weeklyRolling.push({
|
|
||||||
label: `${format(start)} - ${format(end)}`,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
amount: 0,
|
|
||||||
compare: 0,
|
|
||||||
prevStart: shiftDate(start, -7),
|
|
||||||
prevEnd: shiftDate(end, -7)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// calendar weeks
|
|
||||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
||||||
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
||||||
|
|
||||||
const firstWeekStart = getStartOfWeek(startOfMonth);
|
|
||||||
|
|
||||||
const totalWeeks = Math.ceil(
|
|
||||||
(endOfMonth.getTime() - firstWeekStart.getTime()) / (7 * 86400000)
|
|
||||||
) + 1;
|
|
||||||
|
|
||||||
for (let i = 0; i < totalWeeks; i++) {
|
|
||||||
const start = new Date(firstWeekStart.getTime() + i * 7 * 86400000);
|
|
||||||
const end = endOfDay(new Date(start.getTime() + 6 * 86400000));
|
|
||||||
|
|
||||||
weeklyCalendar.push({
|
|
||||||
label: `${format(start)} - ${format(end)}`,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
amount: 0,
|
|
||||||
compare: 0,
|
|
||||||
prevStart: shiftDate(start, -7),
|
|
||||||
prevEnd: shiftDate(end, -7)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------- MONTHLY ----------------
|
|
||||||
const monthlyRolling: any[] = [];
|
|
||||||
const monthlyCalendar: any[] = [];
|
|
||||||
|
|
||||||
// rolling (last 12 months)
|
|
||||||
for (let i = 11; i >= 0; i--) {
|
|
||||||
const d = new Date(now);
|
|
||||||
d.setMonth(d.getMonth() - i);
|
|
||||||
|
|
||||||
const start = new Date(d.getFullYear(), d.getMonth(), 1);
|
|
||||||
const end =
|
|
||||||
i === 0
|
|
||||||
? endOfDay(now)
|
|
||||||
: endOfDay(new Date(d.getFullYear(), d.getMonth() + 1, 0));
|
|
||||||
|
|
||||||
const prevStart = new Date(start);
|
|
||||||
prevStart.setMonth(prevStart.getMonth() - 1);
|
|
||||||
const prevEnd = new Date(end);
|
|
||||||
prevEnd.setMonth(prevEnd.getMonth() - 1);
|
|
||||||
|
|
||||||
monthlyRolling.push({
|
|
||||||
label: `${d.toLocaleString("default", { month: "short" })}-${String(
|
|
||||||
d.getFullYear()
|
|
||||||
).slice(2)}`,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
amount: 0,
|
|
||||||
compare: 0,
|
|
||||||
prevStart,
|
|
||||||
prevEnd
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// calendar (Jan–Dec)
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
const start = new Date(now.getFullYear(), i, 1);
|
|
||||||
const end = endOfDay(new Date(now.getFullYear(), i + 1, 0));
|
|
||||||
|
|
||||||
const prevStart = new Date(start);
|
|
||||||
prevStart.setFullYear(prevStart.getFullYear() - 1);
|
|
||||||
const prevEnd = new Date(end);
|
|
||||||
prevEnd.setFullYear(prevEnd.getFullYear() - 1);
|
|
||||||
|
|
||||||
monthlyCalendar.push({
|
|
||||||
label: `${start.toLocaleString("default", { month: "short" })}-${String(
|
|
||||||
start.getFullYear()
|
|
||||||
).slice(2)}`,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
amount: 0,
|
|
||||||
compare: 0,
|
|
||||||
prevStart,
|
|
||||||
prevEnd
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------- LOOP ----------------
|
|
||||||
for (const item of all) {
|
for (const item of all) {
|
||||||
const d = new Date(
|
const d = new Date(
|
||||||
item.occurred_at || item.created_at || Date.now()
|
item.occurred_at || item.created_at || Date.now()
|
||||||
@@ -237,56 +93,50 @@ export async function fetchAggregatedData(
|
|||||||
if (!isValid(amtRaw)) continue;
|
if (!isValid(amtRaw)) continue;
|
||||||
|
|
||||||
const amt = normalize(amtRaw);
|
const amt = normalize(amtRaw);
|
||||||
|
|
||||||
totalAmount += amt;
|
totalAmount += amt;
|
||||||
|
|
||||||
const payee = item.payee?.name || item.payee || "Unknown";
|
const payee = item.payee?.name || item.payee || "Unknown";
|
||||||
payeeMap[payee] = (payeeMap[payee] || 0) + amt;
|
payeeMap[payee] = (payeeMap[payee] || 0) + amt;
|
||||||
|
|
||||||
// DAILY
|
|
||||||
if (d >= weekStart && d <= weekEnd) {
|
|
||||||
const day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()];
|
const day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()];
|
||||||
|
|
||||||
|
if (d >= weekStart && d <= weekEnd) {
|
||||||
if (dailyBuckets[day]) {
|
if (dailyBuckets[day]) {
|
||||||
dailyBuckets[day].amount += amt;
|
dailyBuckets[day].amount += amt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (d >= prevWeekStart && d <= prevWeekEnd) {
|
if (d >= prevWeekStart && d <= prevWeekEnd) {
|
||||||
const day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()];
|
|
||||||
if (dailyBuckets[day]) {
|
if (dailyBuckets[day]) {
|
||||||
dailyBuckets[day].compare += amt;
|
dailyBuckets[day].compare += amt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WEEKLY
|
const apply = (arr: any[]) => {
|
||||||
for (const b of weeklyRolling) {
|
for (const b of arr) {
|
||||||
if (d >= b.start && d <= b.end) b.amount += amt;
|
if (d >= b.start && d <= b.end) b.amount += amt;
|
||||||
if (d >= b.prevStart && d <= b.prevEnd) b.compare += amt;
|
if (d >= b.prevStart && d <= b.prevEnd)
|
||||||
}
|
b.compare += amt;
|
||||||
for (const b of weeklyCalendar) {
|
|
||||||
if (d >= b.start && d <= b.end) b.amount += amt;
|
|
||||||
if (d >= b.prevStart && d <= b.prevEnd) b.compare += amt;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// MONTHLY
|
apply(weeklyRolling);
|
||||||
for (const b of monthlyRolling) {
|
apply(weeklyCalendar);
|
||||||
if (d >= b.start && d <= b.end) b.amount += amt;
|
apply(monthlyRolling);
|
||||||
if (d >= b.prevStart && d <= b.prevEnd) b.compare += amt;
|
apply(monthlyCalendar);
|
||||||
}
|
|
||||||
for (const b of monthlyCalendar) {
|
|
||||||
if (d >= b.start && d <= b.end) b.amount += amt;
|
|
||||||
if (d >= b.prevStart && d <= b.prevEnd) b.compare += amt;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toPoints = (arr: any[]): ChartDataPoint[] =>
|
const toPoints = (arr: any[]): ChartDataPoint[] =>
|
||||||
arr.map((x) => ({
|
arr.map((x) => ({
|
||||||
id: x.label,
|
id: x.label,
|
||||||
amount: x.amount,
|
amount: x.amount,
|
||||||
compareAmount: x.compare
|
compareAmount: x.compare,
|
||||||
|
compareLabel: x.prevStart && x.prevEnd
|
||||||
|
? `${format(x.prevStart)} - ${format(x.prevEnd)}`
|
||||||
|
: undefined
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const chartData: ChartData = {
|
const chartData = {
|
||||||
daily: Object.entries(dailyBuckets).map(([k, v]: any) => ({
|
daily: Object.entries(dailyBuckets).map(([k, v]: any) => ({
|
||||||
id: k,
|
id: k,
|
||||||
amount: v.amount,
|
amount: v.amount,
|
||||||
@@ -302,7 +152,6 @@ export async function fetchAggregatedData(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// highlight max (current only)
|
|
||||||
Object.values(chartData).forEach((group: any) => {
|
Object.values(chartData).forEach((group: any) => {
|
||||||
const arr = Array.isArray(group) ? group : group.rolling;
|
const arr = Array.isArray(group) ? group : group.rolling;
|
||||||
if (!arr?.length) return;
|
if (!arr?.length) return;
|
||||||
@@ -322,7 +171,6 @@ export async function fetchAggregatedData(
|
|||||||
return { chartData, totalAmount, topPayees };
|
return { chartData, totalAmount, topPayees };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- EXPORTS ----------------
|
|
||||||
export const fetchAggregatedExpenses = () =>
|
export const fetchAggregatedExpenses = () =>
|
||||||
fetchAggregatedData("expense");
|
fetchAggregatedData("expense");
|
||||||
|
|
||||||
|
|||||||
33
src/utils/dateUtils.ts
Normal file
33
src/utils/dateUtils.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export const format = (d: Date) =>
|
||||||
|
`${d.getDate()} ${d.toLocaleString("default", { month: "short" })}`;
|
||||||
|
|
||||||
|
export const startOfDay = (d: Date) => {
|
||||||
|
const x = new Date(d);
|
||||||
|
x.setHours(0, 0, 0, 0);
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const endOfDay = (d: Date) => {
|
||||||
|
const x = new Date(d);
|
||||||
|
x.setHours(23, 59, 59, 999);
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStartOfWeek = (d: Date) => {
|
||||||
|
const date = new Date(d);
|
||||||
|
const day = date.getDay() || 7;
|
||||||
|
if (day !== 1) date.setDate(date.getDate() - (day - 1));
|
||||||
|
return startOfDay(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shiftDate = (d: Date, days: number) =>
|
||||||
|
new Date(d.getTime() + days * 86400000);
|
||||||
|
|
||||||
|
export const getWeekIndex = (date: Date) => {
|
||||||
|
const firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
|
||||||
|
const firstWeekStart = getStartOfWeek(firstDay);
|
||||||
|
return Math.floor(
|
||||||
|
(startOfDay(date).getTime() - firstWeekStart.getTime()) /
|
||||||
|
(7 * 86400000)
|
||||||
|
);
|
||||||
|
};
|
||||||
178
src/utils/periodBuilders.ts
Normal file
178
src/utils/periodBuilders.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import {
|
||||||
|
format,
|
||||||
|
endOfDay,
|
||||||
|
getStartOfWeek,
|
||||||
|
shiftDate,
|
||||||
|
getWeekIndex
|
||||||
|
} from "./dateUtils";
|
||||||
|
|
||||||
|
export const buildDailyBuckets = (now: Date) => {
|
||||||
|
const buckets: Record<string, any> = {
|
||||||
|
Mon: { amount: 0, compare: 0 },
|
||||||
|
Tue: { amount: 0, compare: 0 },
|
||||||
|
Wed: { amount: 0, compare: 0 },
|
||||||
|
Thu: { amount: 0, compare: 0 },
|
||||||
|
Fri: { amount: 0, compare: 0 },
|
||||||
|
Sat: { amount: 0, compare: 0 },
|
||||||
|
Sun: { amount: 0, compare: 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const weekStart = getStartOfWeek(now);
|
||||||
|
const weekEnd = endOfDay(new Date(weekStart.getTime() + 6 * 86400000));
|
||||||
|
const prevWeekStart = shiftDate(weekStart, -7);
|
||||||
|
const prevWeekEnd = shiftDate(weekEnd, -7);
|
||||||
|
|
||||||
|
return { buckets, weekStart, weekEnd, prevWeekStart, prevWeekEnd };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPrevMonthWeek = (start: Date) => {
|
||||||
|
const prevMonthDate = new Date(start);
|
||||||
|
prevMonthDate.setMonth(prevMonthDate.getMonth() - 1);
|
||||||
|
|
||||||
|
const prevMonthFirst = new Date(
|
||||||
|
prevMonthDate.getFullYear(),
|
||||||
|
prevMonthDate.getMonth(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
const prevFirstWeekStart = getStartOfWeek(prevMonthFirst);
|
||||||
|
const weekIndex = getWeekIndex(start);
|
||||||
|
|
||||||
|
const prevStart = new Date(
|
||||||
|
prevFirstWeekStart.getTime() + weekIndex * 7 * 86400000
|
||||||
|
);
|
||||||
|
const prevEnd = endOfDay(new Date(prevStart.getTime() + 6 * 86400000));
|
||||||
|
|
||||||
|
return { prevStart, prevEnd };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildWeeklyRolling = (now: Date) => {
|
||||||
|
const arr: any[] = [];
|
||||||
|
const currentWeekStart = getStartOfWeek(now);
|
||||||
|
|
||||||
|
for (let i = 4; i >= 0; i--) {
|
||||||
|
const start = new Date(
|
||||||
|
currentWeekStart.getTime() - i * 7 * 86400000
|
||||||
|
);
|
||||||
|
const end = endOfDay(new Date(start.getTime() + 6 * 86400000));
|
||||||
|
|
||||||
|
const { prevStart, prevEnd } = getPrevMonthWeek(start);
|
||||||
|
|
||||||
|
arr.push({
|
||||||
|
label: `${format(start)} - ${format(end)}`,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
amount: 0,
|
||||||
|
compare: 0,
|
||||||
|
prevStart,
|
||||||
|
prevEnd
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildWeeklyCalendar = (now: Date) => {
|
||||||
|
const arr: any[] = [];
|
||||||
|
|
||||||
|
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
|
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||||
|
const firstWeekStart = getStartOfWeek(startOfMonth);
|
||||||
|
|
||||||
|
const totalWeeks =
|
||||||
|
Math.ceil(
|
||||||
|
(endOfMonth.getTime() - firstWeekStart.getTime()) /
|
||||||
|
(7 * 86400000)
|
||||||
|
) + 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < totalWeeks; i++) {
|
||||||
|
const start = new Date(
|
||||||
|
firstWeekStart.getTime() + i * 7 * 86400000
|
||||||
|
);
|
||||||
|
const end = endOfDay(new Date(start.getTime() + 6 * 86400000));
|
||||||
|
|
||||||
|
const { prevStart, prevEnd } = getPrevMonthWeek(start);
|
||||||
|
|
||||||
|
arr.push({
|
||||||
|
label: `${format(start)} - ${format(end)}`,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
amount: 0,
|
||||||
|
compare: 0,
|
||||||
|
prevStart,
|
||||||
|
prevEnd
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildMonthlyRolling = (now: Date) => {
|
||||||
|
const arr: any[] = [];
|
||||||
|
|
||||||
|
for (let i = 11; i >= 0; i--) {
|
||||||
|
const d = new Date(now);
|
||||||
|
d.setMonth(d.getMonth() - i);
|
||||||
|
|
||||||
|
const start = new Date(d.getFullYear(), d.getMonth(), 1);
|
||||||
|
const end =
|
||||||
|
i === 0
|
||||||
|
? endOfDay(now)
|
||||||
|
: endOfDay(new Date(d.getFullYear(), d.getMonth() + 1, 0));
|
||||||
|
|
||||||
|
const prevStart = new Date(start);
|
||||||
|
prevStart.setFullYear(prevStart.getFullYear() - 1);
|
||||||
|
|
||||||
|
let prevEnd = new Date(end);
|
||||||
|
prevEnd.setFullYear(prevEnd.getFullYear() - 1);
|
||||||
|
|
||||||
|
if (i === 0) {
|
||||||
|
prevEnd = new Date(prevStart);
|
||||||
|
prevEnd.setDate(now.getDate());
|
||||||
|
prevEnd = endOfDay(prevEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
arr.push({
|
||||||
|
label: `${d.toLocaleString("default", {
|
||||||
|
month: "short"
|
||||||
|
})}-${String(d.getFullYear()).slice(2)}`,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
amount: 0,
|
||||||
|
compare: 0,
|
||||||
|
prevStart,
|
||||||
|
prevEnd
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildMonthlyCalendar = (now: Date) => {
|
||||||
|
const arr: any[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const start = new Date(now.getFullYear(), i, 1);
|
||||||
|
const end = endOfDay(new Date(now.getFullYear(), i + 1, 0));
|
||||||
|
|
||||||
|
const prevStart = new Date(start);
|
||||||
|
prevStart.setFullYear(prevStart.getFullYear() - 1);
|
||||||
|
|
||||||
|
const prevEnd = new Date(end);
|
||||||
|
prevEnd.setFullYear(prevEnd.getFullYear() - 1);
|
||||||
|
|
||||||
|
arr.push({
|
||||||
|
label: `${start.toLocaleString("default", {
|
||||||
|
month: "short"
|
||||||
|
})}-${String(start.getFullYear()).slice(2)}`,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
amount: 0,
|
||||||
|
compare: 0,
|
||||||
|
prevStart,
|
||||||
|
prevEnd
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user