-
-
0 ? (
+ <>
+ {/* Mobile: stacked cards, most-scannable field (item/direction/qty) up top. */}
+
+ {movements.map((entry) => (
+
+
+
+
+ {entry.direction === "In" ? (
+
+ ) : (
+
+ )}
+
+
+
Item #{entry.itemId}
+
+ {warehousesById.get(entry.warehouseId)?.code ?? entry.warehouseId}
+
+
+
+
- {entry.direction === "In" ? (
-
- ) : (
-
- )}
+ {entry.direction}
+
+
+
+
+
-
-
Item #{entry.itemId}
-
- {warehousesById.get(entry.warehouseId)?.code ?? entry.warehouseId}
+
+
Date
+
{new Date(entry.createdAt).toLocaleDateString()}
+
+
+
Source
+
+ {entry.sourceDocType} #{entry.sourceDocId}
-
- {entry.direction}
-
+ ))}
+
-
-
-
Qty
-
{entry.qtyBase}
-
-
-
Date
-
{new Date(entry.createdAt).toLocaleDateString()}
-
-
-
Source
-
- {entry.sourceDocType} #{entry.sourceDocId}
-
-
-
+ {/* Desktop: table. */}
+
+
+
+
+
+ Item
+ Warehouse
+ Direction
+ Qty
+ Source
+ Date
+
+
+
+ {movements.map((entry) => (
+
+ {entry.itemId}
+
+ {warehousesById.get(entry.warehouseId)?.code ?? entry.warehouseId}
+
+
+
+ {entry.direction}
+
+
+ {entry.qtyBase}
+
+ {entry.sourceDocType} #{entry.sourceDocId}
+
+
+ {new Date(entry.createdAt).toLocaleDateString()}
+
+
+ ))}
+
+
+
+ >
+ ) : (
+
No stock movements yet.
+ )
+ ) : (
+ !error && (
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
))}
-
- {/* Desktop: table. */}
-
-
-
-
-
- Item
- Warehouse
- Direction
- Qty
- Source
- Date
-
-
-
- {movements.map((entry) => (
-
- {entry.itemId}
-
- {warehousesById.get(entry.warehouseId)?.code ?? entry.warehouseId}
-
-
-
- {entry.direction}
-
-
- {entry.qtyBase}
-
- {entry.sourceDocType} #{entry.sourceDocId}
-
-
- {new Date(entry.createdAt).toLocaleDateString()}
-
-
- ))}
-
-
-
-
- >
- ) : (
-
No stock movements yet.
- )
- ) : (
- !error && (
-
- {Array.from({ length: 5 }).map((_, i) => (
-
- ))}
-
- )
- )}
-
+ )
+ )}
+
+ )}
)
}
diff --git a/Frontend/erp-system/app/globals.css b/Frontend/erp-system/app/globals.css
index 76f7e56..47c09c0 100644
--- a/Frontend/erp-system/app/globals.css
+++ b/Frontend/erp-system/app/globals.css
@@ -81,6 +81,8 @@
--chart-3: oklch(0.582 0.183 142.495);
--chart-4: oklch(0.787 0.141 72.031);
--chart-5: oklch(0.577 0.245 27.325);
+ /* Dashboard charts: one slate ink, tinted lighter for extra series, so every graph matches. */
+ --chart-ink: #5b6b6e;
--radius: 0.5rem;
--sidebar: oklch(0.98 0 0);
--sidebar-foreground: oklch(0.15 0 0);
@@ -120,6 +122,7 @@
--chart-3: oklch(0.65 0.15 142.495);
--chart-4: oklch(0.85 0.12 72.031);
--chart-5: oklch(0.704 0.191 22.216);
+ --chart-ink: #a3b3b6;
--sidebar: oklch(0.125 0 0);
--sidebar-foreground: oklch(0.95 0 0);
--sidebar-primary: oklch(0.645 0.215 16.439);
diff --git a/Frontend/erp-system/components/ui/stat-card.tsx b/Frontend/erp-system/components/ui/stat-card.tsx
index 2b988ce..c9a66c3 100644
--- a/Frontend/erp-system/components/ui/stat-card.tsx
+++ b/Frontend/erp-system/components/ui/stat-card.tsx
@@ -58,10 +58,8 @@ export interface StatCardProps {
valueClassName?: string
/** Extra classes for the icon tile, e.g. a warning tint instead of the primary one. */
iconClassName?: string
- /** Makes the card a toggle button that filters the list below it (click again to clear). */
- onClick?: () => void
- /** The card's filter is the one currently applied — highlighted and announced as pressed. */
- active?: boolean
+ /** Tighter padding and a smaller icon / text, for dense rows such as the dashboard. */
+ compact?: boolean
}
/**
@@ -80,8 +78,7 @@ export function StatCard({
className,
valueClassName,
iconClassName,
- onClick,
- active = false,
+ compact = false,
}: StatCardProps) {
const hasDelta = typeof delta === "number" && delta !== 0
const isPositive = hasDelta && delta > 0
@@ -98,31 +95,36 @@ export function StatCard({
? { type: "button" as const, onClick, "aria-pressed": active, title: active ? "Click to clear this filter" : "Click to filter the list" }
: {})}
className={cn(
- "@container min-w-0 rounded-2xl bg-card px-4 py-5 text-left shadow-sm ring-1 ring-foreground/10 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg hover:ring-primary/30",
- onClick && "w-full cursor-pointer focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
- className,
- // After className so the "selected" look wins over a card's own tint.
- active && "bg-primary/5 ring-2 ring-primary"
+ "@container min-w-0 rounded-2xl bg-card shadow-sm ring-1 ring-foreground/10 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg hover:ring-primary/30",
+ compact ? "px-3 py-2.5" : "px-4 py-5",
+ className
)}
>
{/* Wide enough: icon beside the text. Narrow (e.g. two per row on a phone): icon above, so the
value keeps the full width instead of being cut off. */}
-
+
{Icon && (
-
+
)}
-
{label}
+
{label}
-
+
{formatValue(value)}
{trend && trend.length > 1 &&
}
diff --git a/Frontend/erp-system/lib/api/dashboard.ts b/Frontend/erp-system/lib/api/dashboard.ts
index c291a0b..527edf1 100644
--- a/Frontend/erp-system/lib/api/dashboard.ts
+++ b/Frontend/erp-system/lib/api/dashboard.ts
@@ -1,9 +1,15 @@
// Dashboard overview endpoint (Backend/ERPCore/Controllers/DashboardController.cs).
import { apiRequest, buildQuery } from "@/lib/api-client"
-import { DashboardStats } from "@/types/dashboard"
+import { DashboardChartsData, DashboardStats } from "@/types/dashboard"
export const dashboardApi = {
- stats(params: { branchId?: number } = {}): Promise
{
+ /** `from` / `to` (YYYY-MM-DD) set the period for the activity counts; omitted means today. */
+ stats(params: { branchId?: number; from?: string; to?: string } = {}): Promise {
return apiRequest(`/dashboard/stats${buildQuery(params)}`)
},
+
+ /** Datasets for the dashboard graphs; the time-based ones follow `from` / `to` (YYYY-MM-DD, default today). */
+ charts(params: { branchId?: number; from?: string; to?: string } = {}): Promise {
+ return apiRequest(`/dashboard/charts${buildQuery(params)}`)
+ },
}
diff --git a/Frontend/erp-system/package-lock.json b/Frontend/erp-system/package-lock.json
index 1c404c6..97c1bff 100644
--- a/Frontend/erp-system/package-lock.json
+++ b/Frontend/erp-system/package-lock.json
@@ -16,6 +16,7 @@
"chart.js": "^4.5.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "cn": "^0.5.0",
"date-fns": "^4.4.0",
"html-to-image": "^1.11.13",
"jspdf": "^4.2.1",
@@ -28,6 +29,7 @@
"react-dom": "19.2.4",
"react-hook-form": "^7.81.0",
"react-loading-skeleton": "^3.5.0",
+ "recharts": "^3.8.0",
"shadcn": "^4.13.0",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",
@@ -1909,6 +1911,42 @@
"react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.13.0",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.13.0.tgz",
+ "integrity": "sha512-f6jwVRfwW30+tnllz4YcsHHpPYYOE7abcHkyUCovD+P7DyhdAI5prz2Fh/8TLhakjpN+7aIEion1s5L/tvlYlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^11.0.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "11.2.3",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.2.3.tgz",
+ "integrity": "sha512-FbR9W6jf7+tvOCuDmOLJy3yStLCvoRDODRARzfeaWmn75+1fW4hFbMv5X5cWgEkxgWcC/eC5Nva00ZV1iv+0EA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1934,6 +1972,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
@@ -2398,6 +2442,12 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
@@ -2413,6 +2463,12 @@
"@types/d3-selection": "*"
}
},
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
@@ -2422,12 +2478,48 @@
"@types/d3-color": "*"
}
},
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
"node_modules/@types/d3-selection": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
"license": "MIT"
},
+ "node_modules/@types/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
"node_modules/@types/d3-transition": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
@@ -2518,6 +2610,12 @@
"license": "MIT",
"optional": true
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
"node_modules/@types/validate-npm-package-name": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz",
@@ -3916,6 +4014,18 @@
"node": ">=6"
}
},
+ "node_modules/cn": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cn/-/cn-0.5.0.tgz",
+ "integrity": "sha512-JGEixZEpfFXm1P5uXwvEobOFLlHwIWi5v4tjd1ZwfiYmcEilkuD2l+054rvJcYCR1B+uVThHeUbgmNz1FwuxuA==",
+ "license": "MIT",
+ "bin": {
+ "cn": "bin/cn.mjs"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
"node_modules/code-block-writer": {
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
@@ -4186,6 +4296,18 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
@@ -4226,6 +4348,15 @@
"node": ">=12"
}
},
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
@@ -4238,6 +4369,31 @@
"node": ">=12"
}
},
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
@@ -4247,6 +4403,42 @@
"node": ">=12"
}
},
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
@@ -4394,6 +4586,12 @@
}
}
},
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
"node_modules/dedent": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz",
@@ -4861,6 +5059,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/es-toolkit": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz",
+ "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks",
+ "tests/types",
+ "tests/browser-compat"
+ ]
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -5317,6 +5527,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
"node_modules/eventsource": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
@@ -6093,6 +6309,16 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -6140,6 +6366,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
@@ -8484,7 +8719,6 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/react-loading-skeleton": {
@@ -8496,6 +8730,29 @@
"react": ">=16.8.0"
}
},
+ "node_modules/react-redux": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
+ "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/recast": {
"version": "0.23.12",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.23.12.tgz",
@@ -8512,6 +8769,57 @@
"node": ">= 4"
}
},
+ "node_modules/recharts": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz",
+ "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==",
+ "license": "MIT",
+ "workspaces": [
+ "www"
+ ],
+ "dependencies": {
+ "@reduxjs/toolkit": "^1.9.0 || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^10.1.1",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.1.1",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/recharts/node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
+ },
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -10090,6 +10398,28 @@
"node": ">= 0.8"
}
},
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/Frontend/erp-system/package.json b/Frontend/erp-system/package.json
index 665b385..5d77f0e 100644
--- a/Frontend/erp-system/package.json
+++ b/Frontend/erp-system/package.json
@@ -17,6 +17,7 @@
"chart.js": "^4.5.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "cn": "^0.5.0",
"date-fns": "^4.4.0",
"html-to-image": "^1.11.13",
"jspdf": "^4.2.1",
@@ -29,6 +30,7 @@
"react-dom": "19.2.4",
"react-hook-form": "^7.81.0",
"react-loading-skeleton": "^3.5.0",
+ "recharts": "^3.8.0",
"shadcn": "^4.13.0",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",
diff --git a/Frontend/erp-system/types/dashboard.ts b/Frontend/erp-system/types/dashboard.ts
index 2a207aa..03ec93f 100644
--- a/Frontend/erp-system/types/dashboard.ts
+++ b/Frontend/erp-system/types/dashboard.ts
@@ -20,6 +20,36 @@ export interface DashboardStats {
pendingLeaveRequests: number
overdueSalesInvoices: number
monthlyPurchasesVsSales: MonthlyPurchasesVsSales[]
+ // Stock & warehouse
+ batchesExpiringSoon: number
+ transfersInTransit: number
+ draftStockAdjustments: number
+ wastagePendingApproval: number
+ // Approvals
+ pendingApprovals: number
+ priceAdjustmentsPending: number
+ // Sales (salesReturns and bundleSales are counted inside the requested from–to period)
+ draftSalesInvoices: number
+ draftSalesSlips: number
+ salesReturns: number
+ bundleSales: number
+ // Purchasing
+ purchaseReturnsPending: number
+ unpaidPurchaseInvoices: number
+ // Money (cashInHand is the closing figure at the end of the period; bankDeposits is summed inside it)
+ cashInHand: number
+ bankDeposits: number
+ // Service & warranty
+ openSupportTickets: number
+ warrantiesExpiringSoon: number
+ warrantyClaimsOpen: number
+ // Production
+ productionRunsInProgress: number
+ // HR (employeesPresent is counted inside the period)
+ payrollRunsPending: number
+ attendanceBatchesPending: number
+ employeesPresent: number
+ totalActiveEmployees: number
}
// One month of Posted purchase vs sales invoice GrandTotals; `month` is "yyyy-MM".
@@ -29,3 +59,53 @@ export interface MonthlyPurchasesVsSales {
purchases: number
sales: number
}
+
+// GET /dashboard/charts — every dataset behind the dashboard graphs (Backend/ERPCore/Dtos/Dashboard/DashboardChartsDtos.cs).
+// Time-based sets follow the requested from–to period; ageing, batches expiring, low stock and warranties are snapshots.
+export interface ChartPoint {
+ label: string
+ value: number
+}
+
+/** Two values under one label: stock in / out by warehouse, tickets opened / resolved by day. */
+export interface ChartPair {
+ label: string
+ first: number
+ second: number
+}
+
+export interface DailySalesPoint {
+ label: string
+ sales: number
+ grossProfit: number
+ expenses: number
+}
+
+export interface DocumentStatusPoint {
+ doc: string
+ draft: number
+ posted: number
+ cancelled: number
+}
+
+export interface DashboardChartsData {
+ dailySales: DailySalesPoint[]
+ paymentSplit: ChartPoint[]
+ topItems: ChartPoint[]
+ salesByCategory: ChartPoint[]
+ salesByCustomer: ChartPoint[]
+ documentStatus: DocumentStatusPoint[]
+ cashInHandByDay: ChartPoint[]
+ receivablesAgeing: ChartPoint[]
+ payablesAgeing: ChartPoint[]
+ expenseBreakdown: ChartPoint[]
+ topVendors: ChartPoint[]
+ stockInVsOut: ChartPair[]
+ batchesExpiring: ChartPoint[]
+ attendanceMix: ChartPoint[]
+ payrollCostByMonth: ChartPoint[]
+ leaveStatus: ChartPoint[]
+ ticketsOverTime: ChartPair[]
+ productionStatus: ChartPoint[]
+ warrantyExpiryByMonth: ChartPoint[]
+}