From 3d4d876283dcc511fa683c6a5ea5f9ee5f2ded37 Mon Sep 17 00:00:00 2001 From: 2ManyProjects Date: Mon, 28 Apr 2025 20:59:04 -0500 Subject: [PATCH] reporting dashboard --- frontend/package-lock.json | 273 +++++++- frontend/package.json | 3 +- frontend/src/App.jsx | 2 + frontend/src/pages/Admin/ReportsPage.jsx | 809 +++++++++++++++++++++++ 4 files changed, 1085 insertions(+), 2 deletions(-) create mode 100644 frontend/src/pages/Admin/ReportsPage.jsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f716c70..34a5e11 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,7 +24,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^9.0.2", - "react-router-dom": "^6.20.1" + "react-router-dom": "^6.20.1", + "recharts": "^2.10.3" }, "devDependencies": { "@types/react": "^18.2.37", @@ -1951,6 +1952,60 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "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==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@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==" + }, + "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==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "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==" + }, + "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==" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -2520,6 +2575,116 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "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==", + "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", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "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==", + "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==", + "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-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "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==", + "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==", + "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", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -2596,6 +2761,11 @@ } } }, + "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==" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3176,12 +3346,25 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3680,6 +3863,14 @@ "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==", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -4197,6 +4388,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4756,6 +4952,20 @@ "react-dom": ">=16.8" } }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -4784,6 +4994,41 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/recharts": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz", + "integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", @@ -5360,6 +5605,11 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5536,6 +5786,27 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "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/vite": { "version": "5.4.18", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz", diff --git a/frontend/package.json b/frontend/package.json index fc40a51..4c7061a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,7 +26,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^9.0.2", - "react-router-dom": "^6.20.1" + "react-router-dom": "^6.20.1", + "recharts": "^2.10.3" }, "devDependencies": { "@types/react": "^18.2.37", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 518a2b9..90f877f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -28,6 +28,7 @@ const AdminCategoriesPage = lazy(() => import('@pages/Admin/CategoriesPage')); const AdminCustomersPage = lazy(() => import('@pages/Admin/CustomersPage')); const AdminOrdersPage = lazy(() => import('@pages/Admin/OrdersPage')); const AdminSettingsPage = lazy(() => import('@pages/Admin/SettingsPage')); +const AdminReportsPage = lazy(() => import('@pages/Admin/ReportsPage')); const UserOrdersPage = lazy(() => import('@pages/UserOrdersPage')); const NotFoundPage = lazy(() => import('@pages/NotFoundPage')); @@ -100,6 +101,7 @@ function App() { } /> } /> } /> + } /> {/* Catch-all route for 404s */} diff --git a/frontend/src/pages/Admin/ReportsPage.jsx b/frontend/src/pages/Admin/ReportsPage.jsx new file mode 100644 index 0000000..0327277 --- /dev/null +++ b/frontend/src/pages/Admin/ReportsPage.jsx @@ -0,0 +1,809 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { + Box, + Typography, + Paper, + Grid, + Card, + CardContent, + CardHeader, + FormControl, + InputLabel, + Select, + MenuItem, + TextField, + CircularProgress, + Alert, + Tabs, + Tab, + Divider, + Button +} from '@mui/material'; +import { + LineChart, + Line, + BarChart, + Bar, + PieChart, + Pie, + Cell, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer +} from 'recharts'; +import { + startOfDay, + endOfDay, + startOfWeek, + endOfWeek, + startOfMonth, + endOfMonth, + startOfYear, + endOfYear, + addDays, + addWeeks, + addMonths, + addYears, + format, + isWithinInterval, + parseISO, + subMonths, + subWeeks, + subYears, + subDays, + isValid, + parse +} from 'date-fns'; +import { useAdminOrders } from '../../hooks/adminHooks'; +import { useQuery } from '@tanstack/react-query'; +import apiClient from '../../services/api'; + +// COLORS for charts +const COLORS = ['#8884d8', '#82ca9d', '#ffc658', '#ff8042', '#0088FE', '#00C49F', '#FFBB28', '#FF8042']; + +const ReportsPage = () => { + // Tab state + const [activeTab, setActiveTab] = useState(0); + + // Time period filter states + const [timelineType, setTimelineType] = useState('monthly'); + const [fromDate, setFromDate] = useState(subMonths(new Date(), 1)); + const [toDate, setToDate] = useState(new Date()); + + // State for date inputs + const [fromDateString, setFromDateString] = useState(format(fromDate, 'yyyy-MM-dd')); + const [toDateString, setToDateString] = useState(format(toDate, 'yyyy-MM-dd')); + + // Fetch data + const { data: orders, isLoading: ordersLoading } = useAdminOrders(); + const { data: products, isLoading: productsLoading } = useQuery({ + queryKey: ['admin-products'], + queryFn: async () => { + const response = await apiClient.get('/products'); + return response.data; + } + }); + + // Update date objects when string inputs change + useEffect(() => { + const parsedFromDate = parse(fromDateString, 'yyyy-MM-dd', new Date()); + if (isValid(parsedFromDate)) { + setFromDate(parsedFromDate); + } + }, [fromDateString]); + + useEffect(() => { + const parsedToDate = parse(toDateString, 'yyyy-MM-dd', new Date()); + if (isValid(parsedToDate)) { + setToDate(parsedToDate); + } + }, [toDateString]); + + // Handle tab change + const handleTabChange = (event, newValue) => { + setActiveTab(newValue); + }; + + // Handle timeline type change + const handleTimelineChange = (event) => { + const newTimelineType = event.target.value; + setTimelineType(newTimelineType); + + // Adjust the date range based on the new timeline type + const today = new Date(); + let newFromDate; + + switch (newTimelineType) { + case 'daily': + newFromDate = subDays(today, 7); // Last 7 days + break; + case 'weekly': + newFromDate = subWeeks(today, 4); // Last 4 weeks + break; + case 'monthly': + newFromDate = subMonths(today, 1); // Last month + break; + case 'annually': + newFromDate = subYears(today, 1); // Last year + break; + default: + newFromDate = subMonths(today, 1); + } + + setFromDate(newFromDate); + setToDate(today); + setFromDateString(format(newFromDate, 'yyyy-MM-dd')); + setToDateString(format(today, 'yyyy-MM-dd')); + }; + + // Handle date input changes + const handleFromDateChange = (e) => { + setFromDateString(e.target.value); + }; + + const handleToDateChange = (e) => { + setToDateString(e.target.value); + }; + + // Filter orders based on selected date range + const filteredOrders = useMemo(() => { + if (!orders) return []; + + return orders.filter(order => { + const orderDate = parseISO(order.created_at); + return isWithinInterval(orderDate, { + start: startOfDay(fromDate), + end: endOfDay(toDate) + }); + }); + }, [orders, fromDate, toDate]); + + // Calculate total revenue for the selected period + const totalRevenue = useMemo(() => { + return filteredOrders.reduce((sum, order) => { + if (order.payment_completed) { + return sum + parseFloat(order.total_amount || 0); + } + return sum; + }, 0); + }, [filteredOrders]); + + // Calculate average order value + const averageOrderValue = useMemo(() => { + const completedOrders = filteredOrders.filter(order => order.payment_completed); + if (completedOrders.length === 0) return 0; + + const total = completedOrders.reduce((sum, order) => sum + parseFloat(order.total_amount || 0), 0); + return total / completedOrders.length; + }, [filteredOrders]); + + // Group orders by status + const ordersByStatus = useMemo(() => { + const statusCounts = {}; + + filteredOrders.forEach(order => { + const status = order.status || 'unknown'; + statusCounts[status] = (statusCounts[status] || 0) + 1; + }); + + return Object.entries(statusCounts).map(([status, count]) => ({ + name: status.charAt(0).toUpperCase() + status.slice(1), + value: count + })); + }, [filteredOrders]); + + // Group orders by location (state/province) + const ordersByLocation = useMemo(() => { + const locationCounts = {}; + + filteredOrders.forEach(order => { + if (!order.shipping_address) return; + + // Basic location extraction from shipping address + let location = 'Unknown'; + + try { + // Try to extract state/province from the address + // This is a simple approach - might need to be refined based on your address format + const addressLines = order.shipping_address.split('\n'); + const stateLine = addressLines.find(line => line.includes(',')); + + if (stateLine) { + const statePart = stateLine.split(',')[1]; + if (statePart) { + // Extract state code (usually 2 letters before zip code) + const stateMatch = statePart.trim().match(/^([A-Z]{2})/); + location = stateMatch ? stateMatch[1] : statePart.trim().split(' ')[0]; + } + } + } catch (error) { + console.error('Error parsing location from address:', error); + } + + locationCounts[location] = (locationCounts[location] || 0) + 1; + }); + + return Object.entries(locationCounts) + .map(([location, count]) => ({ + name: location, + value: count + })) + .sort((a, b) => b.value - a.value) // Sort by count descending + .slice(0, 8); // Top 8 locations + }, [filteredOrders]); + + // Prepare time series data for revenue chart + const revenueTimeSeriesData = useMemo(() => { + if (!orders) return []; + + const dataMap = new Map(); + let dateFormat; + let step; + + // Determine format and grouping based on timeline type + switch (timelineType) { + case 'daily': + dateFormat = 'MMM d'; + step = (date) => addDays(date, 1); + break; + case 'weekly': + dateFormat = "'Week' w, yyyy"; + step = (date) => addWeeks(date, 1); + break; + case 'monthly': + dateFormat = 'MMM yyyy'; + step = (date) => addMonths(date, 1); + break; + case 'annually': + dateFormat = 'yyyy'; + step = (date) => addYears(date, 1); + break; + default: + dateFormat = 'MMM yyyy'; + step = (date) => addMonths(date, 1); + } + + // Initialize dataMap with all periods in the range + let currentDate = new Date(fromDate); + while (currentDate <= toDate) { + const key = format(currentDate, dateFormat); + dataMap.set(key, { name: key, revenue: 0, orders: 0 }); + currentDate = step(currentDate); + } + + // Add order data to the map + filteredOrders.forEach(order => { + const orderDate = parseISO(order.created_at); + const key = format(orderDate, dateFormat); + + if (dataMap.has(key)) { + const entry = dataMap.get(key); + if (order.payment_completed) { + entry.revenue += parseFloat(order.total_amount || 0); + } + entry.orders += 1; + dataMap.set(key, entry); + } + }); + + // Convert map to array and sort by date + return Array.from(dataMap.values()); + }, [filteredOrders, timelineType, fromDate, toDate]); + + // Prepare product sales data + const productSalesData = useMemo(() => { + if (!orders || !products) return []; + + const productSales = {}; + + // Initialize with all products + products.forEach(product => { + productSales[product.id] = { + id: product.id, + name: product.name, + category: product.category_name, + quantity: 0, + revenue: 0 + }; + }); + + // Aggregate sales data from orders + filteredOrders.forEach(order => { + if (order.items && Array.isArray(order.items)) { + order.items.forEach(item => { + if (productSales[item.product_id]) { + productSales[item.product_id].quantity += item.quantity; + productSales[item.product_id].revenue += parseFloat(item.price_at_purchase) * item.quantity; + } + }); + } + }); + + // Convert to array and sort by quantity + return Object.values(productSales) + .filter(product => product.quantity > 0) + .sort((a, b) => b.quantity - a.quantity); + }, [filteredOrders, products]); + + // Top selling products for the chart + const topSellingProducts = useMemo(() => { + return productSalesData.slice(0, 5); + }, [productSalesData]); + + // Category sales distribution + const categorySalesData = useMemo(() => { + if (!productSalesData.length) return []; + + const categoryMap = {}; + + productSalesData.forEach(product => { + const category = product.category || 'Uncategorized'; + + if (!categoryMap[category]) { + categoryMap[category] = { + name: category, + value: 0, + revenue: 0 + }; + } + + categoryMap[category].value += product.quantity; + categoryMap[category].revenue += product.revenue; + }); + + return Object.values(categoryMap).sort((a, b) => b.value - a.value); + }, [productSalesData]); + + // Loading state + const isLoading = ordersLoading || productsLoading; + + // Handle export reports + const handleExportCSV = () => { + // Generate CSV content + let csvContent = 'data:text/csv;charset=utf-8,'; + + // Add headers + csvContent += 'Period,Revenue,Orders\n'; + + // Add data rows + revenueTimeSeriesData.forEach(item => { + csvContent += `${item.name},${item.revenue.toFixed(2)},${item.orders}\n`; + }); + + // Create download link + const encodedUri = encodeURI(csvContent); + const link = document.createElement('a'); + link.setAttribute('href', encodedUri); + link.setAttribute('download', `sales-report-${format(new Date(), 'yyyy-MM-dd')}.csv`); + document.body.appendChild(link); + + // Trigger download + link.click(); + + // Clean up + document.body.removeChild(link); + }; + + if (isLoading) { + return ( + + + + ); + } + + return ( + + + Reports & Analytics + + + {/* Filters */} + + + + + Timeline + + + + + + + + + + + + + + + + + + + {/* Summary Cards */} + + + + + + Total Revenue + + + ${totalRevenue.toFixed(2)} + + + + + + + + + + Orders + + + {filteredOrders.length} + + + + + + + + + + Average Order Value + + + ${averageOrderValue.toFixed(2)} + + + + + + + + + + Completed Orders + + + {filteredOrders.filter(order => order.payment_completed).length} + + + + + + + {/* Tabs for different reports */} + + + + + + + + + + {/* Sales Overview Tab */} + {activeTab === 0 && ( + + + + Revenue & Orders Over Time + + + + + + + + + { + return name === 'revenue' ? `$${value.toFixed(2)}` : value; + }} /> + + + + + + + + + + + Order Status Distribution + + + + + `${name}: ${(percent * 100).toFixed(0)}%`} + outerRadius={100} + fill="#8884d8" + dataKey="value" + > + {ordersByStatus.map((entry, index) => ( + + ))} + + [`${value} orders`, props.payload.name]} /> + + + + + + + + + Category Sales Distribution + + + + + `${name}: ${(percent * 100).toFixed(0)}%`} + outerRadius={100} + fill="#8884d8" + dataKey="value" + > + {categorySalesData.map((entry, index) => ( + + ))} + + [`${value} items`, props.payload.name]} /> + + + + + + + )} + + {/* Product Performance Tab */} + {activeTab === 1 && ( + + + + Top Selling Products + + + + + + + + { + return name === 'revenue' ? `$${value.toFixed(2)}` : value; + }} /> + + + + + + + + + + + Product Sales Details + + + + + + + + + + + + + {productSalesData.map((product) => ( + + + + + + + ))} + +
ProductCategoryUnits SoldRevenue
{product.name}{product.category}{product.quantity}${product.revenue.toFixed(2)}
+
+
+
+ )} + + {/* Order Analysis Tab */} + {activeTab === 2 && ( + + + + Orders Over Time + + + + + + + + + + + + + + + + + + + + + + 0 ? (filteredOrders.filter(o => o.payment_completed).length / filteredOrders.length) * 100 : 0} + size={100} + thickness={5} + sx={{ color: '#82ca9d' }} + /> + + + {filteredOrders.length > 0 ? Math.round((filteredOrders.filter(o => o.payment_completed).length / filteredOrders.length) * 100) : 0}% + + + + + + {filteredOrders.filter(o => o.payment_completed).length} out of {filteredOrders.length} orders completed + + + + + + + + + + + + ({ + name: item.name, + avgValue: item.orders > 0 ? item.revenue / item.orders : 0 + }))} + margin={{ top: 5, right: 30, left: 20, bottom: 5 }} + > + + + + `$${value.toFixed(2)}`} /> + + + + + + + + + )} + + {/* Geographic Distribution Tab */} + {activeTab === 3 && ( + + + + Orders by Location + + + + + + + + + + + + + + + + + + Note: Location data is extracted from shipping addresses. The accuracy may vary based on address format. + + + + )} +
+
+
+ ); +}; + +export default ReportsPage; \ No newline at end of file