diff --git a/package-lock.json b/package-lock.json index 6f72698..7062478 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "react": "^19.0.1", "react-dom": "^19.0.1", "react-leaflet": "^5.0.0", + "recharts": "^3.8.1", "vite": "^6.2.3" }, "devDependencies": { @@ -63,6 +64,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -857,6 +859,42 @@ "react-dom": "^19.0.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "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.1.8", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -1188,6 +1226,18 @@ "win32" ] }, + "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", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@tailwindcss/node": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", @@ -1533,6 +1583,69 @@ "@types/node": "*" } }, + "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", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "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", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "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==", + "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-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "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/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1656,6 +1769,12 @@ "@types/node": "*" } }, + "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/@vitejs/plugin-react": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", @@ -1840,6 +1959,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1918,6 +2038,15 @@ ], "license": "CC-BY-4.0" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -1960,6 +2089,127 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "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", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "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==", + "license": "BSD-3-Clause", + "engines": { + "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", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "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==", + "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-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", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -1986,6 +2236,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/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2113,6 +2369,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.47.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.1.tgz", + "integrity": "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -2178,6 +2444,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/express": { "version": "4.22.2", "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", @@ -2599,12 +2871,31 @@ "node": ">=0.10.0" } }, + "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "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/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2687,7 +2978,8 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/lightningcss": { "version": "1.32.0", @@ -3224,6 +3516,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3250,6 +3543,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -3356,6 +3650,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3365,6 +3660,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3372,6 +3668,13 @@ "react": "^19.2.7" } }, + "node_modules/react-is": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "license": "MIT", + "peer": true + }, "node_modules/react-leaflet": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", @@ -3386,6 +3689,30 @@ "react-dom": "^19.0.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", + "peer": true, + "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/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -3395,6 +3722,58 @@ "node": ">=0.10.0" } }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "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/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", + "peer": true + }, + "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/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/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -3658,6 +4037,12 @@ "url": "https://opencollective.com/webpack" } }, + "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==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -4238,6 +4623,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -4256,11 +4650,34 @@ "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/vite": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/package.json b/package.json index b81f8a6..a327285 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "react": "^19.0.1", "react-dom": "^19.0.1", "react-leaflet": "^5.0.0", + "recharts": "^3.8.1", "vite": "^6.2.3" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index e256393..253bbdb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -541,7 +541,7 @@ export default function App() { } case 'settings': - return ; + return ; default: return null; diff --git a/src/components/DashboardView.tsx b/src/components/DashboardView.tsx index 7a0925e..c8da7dc 100644 --- a/src/components/DashboardView.tsx +++ b/src/components/DashboardView.tsx @@ -35,7 +35,8 @@ const str = (v: unknown): string => (v == null ? '' : String(v)); export default function DashboardView({ searchQuery, tenantId = FIESTA_TENANT_ID }: DashboardViewProps) { // Live data — month-to-date order summary + tenant identity + store locations. const today = new Date(); - const monthStart = new Date(today.getFullYear(), today.getMonth(), 1); + const monthStart = new Date(today); + monthStart.setDate(today.getDate() - 30); // Last 30 days to include May orders const fromdate = ymd(monthStart); const todate = ymd(today); @@ -54,10 +55,10 @@ export default function DashboardView({ searchQuery, tenantId = FIESTA_TENANT_ID // returns two distinct figures (revenue and profit); we surface both rather than // repeating one. When the tenant has no invoice records we show "—" instead of a // misleading ₹0. - const insight = insightQ.data; + const insight = insightQ.data as any; const money = (v: number | null) => (v == null ? '—' : `₹${Math.round(v).toLocaleString('en-IN')}`); - const monthlyRevenue = insight ? insight.revenue : null; - const monthlyProfit = insight ? insight.profit : null; + const monthlyRevenue = insight ? Number(insight.grossrevenue || insight.overallrevenue || insight.revenue || 0) : null; + const monthlyProfit = insight ? Number(insight.profit || insight.netrevenue || insight.margin || 0) : null; const locSummaryQ = useFiestaLocationSummary(tenantId); const summaries = locSummaryQ.data ?? []; diff --git a/src/components/DeliveryReportsView.tsx b/src/components/DeliveryReportsView.tsx index 19d88ca..bf8c27b 100644 --- a/src/components/DeliveryReportsView.tsx +++ b/src/components/DeliveryReportsView.tsx @@ -15,10 +15,11 @@ import React, { useMemo, useState } from 'react'; import { TrendingUp, Clock, CheckCircle2, IndianRupee, Bike, Truck, Calendar, Store } from 'lucide-react'; -import { useFiestaLocationSummary, useFiestaFleetSummary } from '../services/fiestaQueries'; +import { useFiestaLocationSummary, useFiestaFleetSummary, useFiestaOrderSummary, useFiestaAllOrders, useFiestaRevenueSummary } from '../services/fiestaQueries'; +import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, Tooltip as RechartsTooltip, CartesianGrid } from 'recharts'; import { FIESTA_TENANT_ID, num as fnum, str as fstr, ymd, type Row } from '../services/fiestaApi'; import { - GradientHeader, KpiStrip, Pill, StatusChip, MetricPill, FilterBar, TH_STYLE, + GradientHeader, KpiStrip, Pill, StatusChip, MetricPill, FilterBar, TH_STYLE, TableShell, DELIVERY_STATUS, statusColor, BRAND, BRAND_LIGHT, TEXT, TEXT_2, TEXT_3, BORDER, DIVIDER, SURFACE_ALT, tint, soft, edge, ring, } from './consoleUi'; @@ -28,9 +29,9 @@ const TABS: Array<{ key: ReportTab; label: string; icon: typeof TrendingUp }> = { key: 'riders-summary', label: 'Riders Summary', icon: Bike }, ]; -interface DeliveryReportsViewProps { searchQuery?: string; tenantId?: number; } +interface DeliveryReportsViewProps { searchQuery?: string; tenantId?: number; locationid?: number; } -export default function DeliveryReportsView({ searchQuery = '', tenantId = FIESTA_TENANT_ID }: DeliveryReportsViewProps) { +export default function DeliveryReportsView({ searchQuery = '', tenantId = FIESTA_TENANT_ID, locationid }: DeliveryReportsViewProps) { const today = new Date(); const monthStart = new Date(today.getFullYear(), today.getMonth(), 1); const [fromdate, setFromdate] = useState(ymd(monthStart)); @@ -46,6 +47,7 @@ export default function DeliveryReportsView({ searchQuery = '', tenantId = FIEST ]; const activePreset = presets.find((p) => p.from === fromdate && p.to === todate)?.key ?? 'custom'; + return (
@@ -81,66 +83,205 @@ export default function DeliveryReportsView({ searchQuery = '', tenantId = FIEST
- {tab === 'orders-summary' && } - {tab === 'riders-summary' && } + + {tab === 'orders-summary' && } + {tab === 'riders-summary' && } ); } -const Cnt = ({ n, color }: { n: number; color: string }) => (n > 0 ? {n.toLocaleString('en-IN')} : 0); - -function TableShell({ minWidth, head, children, footer }: { minWidth: number; head: string[]; children: React.ReactNode; footer?: React.ReactNode }) { - return ( -
-
- - {head.map((h, i) => ())} - {children} -
{h}
-
- {footer} -
- ); -} +const Cnt = ({ n, color }: { n: number; color: string }) => ( + n > 0 ? {n.toLocaleString('en-IN')} : 0 +); // ── Orders Summary (per outlet) ────────────────────────────────────────────────── -function OrdersSummaryReport({ tenantId }: { tenantId: number }) { +function OrdersSummaryReport({ tenantId, locationid, fromdate, todate }: { tenantId: number; locationid?: number; fromdate: string; todate: string; }) { + const [metric, setMetric] = useState<'revenue' | 'orders'>('revenue'); + const q = useFiestaLocationSummary(tenantId); - const rows = q.data ?? []; + const ordersQ = useFiestaAllOrders({ tenantid: tenantId, fromdate, todate, locationid }); + const revenueQ = useFiestaRevenueSummary({ tenantid: tenantId, locationid, fromdate, todate }); + + const rows = (q.data ?? []).filter(r => locationid ? r.locationid === locationid : true); const totals = rows.reduce((a, r) => ({ total: a.total + r.total, pending: a.pending + r.pending, delivered: a.delivered + r.delivered, cancelled: a.cancelled + r.cancelled }), { total: 0, pending: 0, delivered: 0, cancelled: 0 }); - const kpis = [ - { label: 'Total Orders', value: totals.total.toLocaleString('en-IN'), color: BRAND, icon: }, - { label: 'Pending', value: totals.pending.toLocaleString('en-IN'), color: '#f59e0b', icon: }, - { label: 'Delivered', value: totals.delivered.toLocaleString('en-IN'), color: '#10b981', icon: }, - { label: 'Outlets', value: rows.length.toLocaleString('en-IN'), color: '#0ea5e9', icon: }, - ]; + + // Compute trend data + const chartData = useMemo(() => { + const map = new Map(); + for (const r of (ordersQ.data ?? [])) { + const dateVal = fstr(r.orderdate) || fstr(r.deliverydate); + if (!dateVal) continue; + const key = dateVal.split('T')[0]; + + const ex = map.get(key) || { date: key, orders: 0, revenue: 0 }; + ex.orders += 1; + const amt = fnum(r.ordervalue) || fnum(r.orderamount) || fnum(r.deliveryamt); + ex.revenue += amt; + map.set(key, ex); + } + return Array.from(map.values()).sort((a, b) => a.date.localeCompare(b.date)); + }, [ordersQ.data]); + + const formatLabel = (key: string) => { + const d = new Date(key); + const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + return `${d.getDate()} ${months[d.getMonth()]}`; + }; + + const totalRevenue = revenueQ.data?.grossrevenue ?? 0; + const locationRevenueMap = useMemo(() => { + const map = new Map(); + for (const r of (ordersQ.data ?? [])) { + const loc = fnum(r.locationid) || fnum(r.applocationid); + const amt = fnum(r.ordervalue) || fnum(r.orderamount) || fnum(r.deliveryamt) || fnum(r.collectionamt); + if (loc && amt) { + map.set(loc, (map.get(loc) || 0) + amt); + } + } + return map; + }, [ordersQ.data]); + + const metricColor = metric === 'revenue' ? '#4f46e5' : '#0ea5e9'; + return ( -
- - 0 ? : undefined}> - {q.isLoading ? Loading outlet summary… - : rows.length === 0 ? No outlet data available. - : rows.map((r, i) => ( - (e.currentTarget.style.background = SURFACE_ALT)} onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}> - {i + 1} - {r.locationname || `Location ${r.locationid}`} - {r.total.toLocaleString('en-IN')} - - - - - - - ))} - +
+
+ + +
+
+
+

Pending

+

{totals.pending.toLocaleString('en-IN')}

+
+
+
+
+
+

Delivered

+

{totals.delivered.toLocaleString('en-IN')}

+
+
+
+ +
+
+ + + + + + + + + + + + + + + + {q.isLoading || revenueQ.isLoading ? + : rows.length === 0 ? + : rows.map((r, i) => ( + + + + + + + + + + + + ))} + +
#OutletRevenueAllCreatedPendingProcessingDeliveredCancelled
Loading outlet summary…
No outlet data available.
{i + 1}{r.locationname || `Location ${r.locationid}`}₹{(locationRevenueMap.get(fnum(r.locationid)) ?? 0).toLocaleString('en-IN')}{r.total.toLocaleString('en-IN')}
+
+ {rows.length > 0 && ( +
+ Totals: + ₹{totalRevenue.toLocaleString('en-IN')} + {totals.total} orders + {totals.delivered} delivered + {totals.pending} pending +
+ )} +
+ + {/* Trend Chart */} +
+
+ +

Trend Analysis

+
+ +
+ {ordersQ.isLoading ? ( +
Loading trend data...
+ ) : chartData.length === 0 ? ( +
No trend data for this period.
+ ) : ( + + + + + + + + + + + metric === 'revenue' ? `₹${v.toLocaleString('en-IN')}` : v.toLocaleString('en-IN')} /> + { + if (active && payload && payload.length) { + return ( +
+

{formatLabel(label)}

+
+ + + {metric} + + + {metric === 'revenue' ? `₹${payload[0].value.toLocaleString('en-IN')}` : payload[0].value.toLocaleString('en-IN')} + +
+
+ ); + } + return null; + }} + /> + +
+
+ )} +
+
); } // ── Riders Summary (per rider) ─────────────────────────────────────────────────── -function RidersSummaryReport({ fromdate, todate, tenantId }: { fromdate: string; todate: string; tenantId: number }) { - const q = useFiestaFleetSummary({ tenantid: tenantId, fromdate, todate }); +function RidersSummaryReport({ fromdate, todate, tenantId, locationid }: { fromdate: string; todate: string; tenantId: number; locationid?: number }) { + const q = useFiestaFleetSummary({ tenantid: tenantId, fromdate, todate, applocationid: locationid }); const rows = q.data ?? []; const mapped = rows.map((r) => ({ name: fstr(r.fullname) || `${fstr(r.firstname)} ${fstr(r.lastname)}`.trim() || fstr(r.username) || `Rider ${fstr(r.userid)}`, diff --git a/src/components/DispatchView.tsx b/src/components/DispatchView.tsx index ca38948..c9c3464 100644 --- a/src/components/DispatchView.tsx +++ b/src/components/DispatchView.tsx @@ -90,7 +90,6 @@ function pickupLatLon(r: Row): [number, number] | null { type ViewMode = 'kitchens' | 'zones' | 'riders'; const VIEW_TABS: Array<{ id: ViewMode; label: string; icon: typeof MapIcon }> = [ { id: 'kitchens', label: 'By Location', icon: MapPin }, - { id: 'zones', label: 'By Zone', icon: MapIcon }, { id: 'riders', label: 'By Rider', icon: Bike }, ]; @@ -294,7 +293,6 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
- {headerTabs}
@@ -359,6 +357,11 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID, ); })} + {headerTabs && ( +
+ {headerTabs} +
+ )}
{/* ── Body ── */} diff --git a/src/components/OrdersDeliveriesView.tsx b/src/components/OrdersDeliveriesView.tsx index 62b6411..3443b42 100644 --- a/src/components/OrdersDeliveriesView.tsx +++ b/src/components/OrdersDeliveriesView.tsx @@ -58,6 +58,9 @@ export default function OrdersDeliveriesView({ }: OrdersDeliveriesViewProps) { const todayStr = ymd(new Date()); + const [fromdate, setFromdate] = useState(todayStr); + const [todate, setTodate] = useState(todayStr); + const [status, setStatus] = useState('all'); const [pageno, setPageno] = useState(1); const [localSearch, setLocalSearch] = useState(''); @@ -75,8 +78,8 @@ export default function OrdersDeliveriesView({ }, []); // ── Queries ────────────────────────────────────────────────────────────────── - const allOrdersQ = useFiestaAllOrders({ tenantid: tenantId, fromdate: todayStr, todate: todayStr, locationid }); - const summaryQ = useFiestaDeliverySummary({ tenantid: tenantId, fromdate: todayStr, todate: todayStr, locationid }); + const allOrdersQ = useFiestaAllOrders({ tenantid: tenantId, fromdate, todate, locationid }); + const summaryQ = useFiestaDeliverySummary({ tenantid: tenantId, fromdate, todate, locationid }); const ridersQ = useFiestaRiders({ tenantid: tenantId }); const allRows = allOrdersQ.data ?? []; @@ -100,7 +103,11 @@ export default function OrdersDeliveriesView({ const rows = useMemo(() => { const term = (localSearch || searchQuery).toLowerCase(); return allRows.filter((r) => { - if (locationid && fnum(r.locationid) !== locationid) return false; + if (locationid) { + const rLoc = fnum(r.locationid); + const rApp = fnum(r.applocationid); + if (rLoc !== locationid && rApp !== locationid) return false; + } if (status !== 'all' && fstr(r.orderstatus).toLowerCase() !== status) return false; if (!term) return true; return [ @@ -162,7 +169,7 @@ export default function OrdersDeliveriesView({ const blob = new Blob([[headers.join(','), ...lines].join('\n')], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `Orders_${status}_${todayStr}.csv`; a.click(); + a.download = `Orders_${status}_${fromdate}_to_${todate}.csv`; a.click(); URL.revokeObjectURL(url); }; @@ -179,10 +186,17 @@ export default function OrdersDeliveriesView({ : } right={ - - {locationid ? `Location ${locationid}` : 'All Locations'} - +
+
+ setFromdate(e.target.value)} className="rounded-full outline-none font-semibold text-xs transition-colors" style={{ padding: '6px 12px', border: `1.5px solid ${edge(BRAND)}`, background: tint(BRAND), color: BRAND }} /> + + setTodate(e.target.value)} className="rounded-full outline-none font-semibold text-xs transition-colors" style={{ padding: '6px 12px', border: `1.5px solid ${edge(BRAND)}`, background: tint(BRAND), color: BRAND }} /> +
+ + {locationid ? `Location ${locationid}` : 'All Locations'} + +
} /> diff --git a/src/components/OrdersView.tsx b/src/components/OrdersView.tsx index c57cdb3..c9c1e10 100644 --- a/src/components/OrdersView.tsx +++ b/src/components/OrdersView.tsx @@ -213,8 +213,10 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI // ── Selection helpers ─────────────────────────────────────────────────────── const rowKey = (r: Row) => fstr(r.orderheaderid) || fstr(r.orderid); - const pageKeys = rows.map(rowKey); - const allSelected = pageKeys.length > 0 && pageKeys.every((k) => selected.has(k)); + const assignableRows = rows.filter((r) => fstr(r.orderstatus).toLowerCase() === 'created'); + const assignableKeys = assignableRows.map(rowKey); + const allSelected = assignableKeys.length > 0 && assignableKeys.every((k) => selected.has(k)); + const toggleRow = (k: string) => setSelected((prev) => { const n = new Set(prev); @@ -222,11 +224,12 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI else n.add(k); return n; }); + const toggleAll = () => setSelected((prev) => { const n = new Set(prev); - if (allSelected) pageKeys.forEach((k) => n.delete(k)); - else pageKeys.forEach((k) => n.add(k)); + if (allSelected) assignableKeys.forEach((k) => n.delete(k)); + else assignableKeys.forEach((k) => n.add(k)); return n; }); @@ -383,7 +386,7 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI - + {['#', 'Order', 'Branch', 'Pickup', 'Drop', 'Qty', 'COD', 'KMs', 'Charges', 'Status', ''].map((h, i) => ( {h} @@ -406,7 +409,11 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI { if (!selected.has(rowKey(r))) e.currentTarget.style.background = SURFACE_ALT; }} onMouseLeave={(e) => { e.currentTarget.style.background = selected.has(rowKey(r)) ? tint(BRAND) : 'transparent'; }}> - toggleRow(rowKey(r))} aria-label="Select order" style={{ accentColor: BRAND, cursor: 'pointer', width: 15, height: 15 }} /> + {st === 'created' ? ( + toggleRow(rowKey(r))} aria-label="Select order" style={{ accentColor: BRAND, cursor: 'pointer', width: 15, height: 15 }} /> + ) : ( + + )} {(pageno - 1) * PAGE_SIZE + i + 1} diff --git a/src/components/ReportsView.tsx b/src/components/ReportsView.tsx index b58b0bb..b67a0f9 100644 --- a/src/components/ReportsView.tsx +++ b/src/components/ReportsView.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { TrendingUp, TrendingDown, @@ -29,6 +29,8 @@ import { useFiestaLocationSummary, useFiestaOrderInsight, useFiestaStockStatement, + useFiestaRevenueSummary, + useFiestaTimeSeries, } from '../services/fiestaQueries'; import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID, num as fnum, str as fstr, ymd } from '../services/fiestaApi'; import { stockRowToProduct } from '../services/fiestaMappers'; @@ -51,6 +53,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba const [selectedCell, setSelectedCell] = useState<{ day: string; hour: string; val: number } | null>(null); const [currentPage, setCurrentPage] = useState(1); const [chartMetric, setChartMetric] = useState<'orders' | 'revenue' | 'cancelled' | 'skus'>('orders'); + const [chartTimeframe, setChartTimeframe] = useState<'day' | 'week' | 'month'>('week'); const [expandedProductId, setExpandedProductId] = useState(null); const [exportingFormat, setExportingFormat] = useState<'PDF' | 'CSV' | null>(null); const [exportProgress, setExportProgress] = useState(0); @@ -92,6 +95,8 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba const prevSummaryQ = useFiestaOrderSummary(tenantId, ymd(prevStart), ymd(prevEnd)); const locSummaryQ = useFiestaLocationSummary(tenantId); const insightQ = useFiestaOrderInsight(tenantId); + const revSummaryQ = useFiestaRevenueSummary({ tenantid: tenantId, fromdate: ymd(yearStart), todate }); + const prevRevSummaryQ = useFiestaRevenueSummary({ tenantid: tenantId, fromdate: ymd(prevStart), todate: ymd(prevEnd) }); const stockQ = useFiestaStockStatement({ tenantid: tenantId, locationid: FIESTA_PRIMARY_LOCATION_ID, @@ -111,6 +116,11 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba }; const ordersDelta = s && prevS ? pctChange(s.total, prevS.total) : null; const cancelledDelta = s && prevS ? pctChange(s.cancelled, prevS.cancelled) : null; + + const revS = revSummaryQ.data; + const prevRevS = prevRevSummaryQ.data; + const revenueDelta = revS && prevRevS ? pctChange(revS.grossrevenue, prevRevS.grossrevenue) : null; + const fmtDelta = (d: number) => `${d >= 0 ? '+' : ''}${d.toFixed(1)}%`; // Dynamic sparkline generator helper @@ -185,16 +195,15 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba awaiting: false, }, { - // Revenue: no revenue API ([R1]) — render AwaitingApi instead of a value. id: 'revenue' as const, title: 'Revenue', - value: '', - trend: null, - status: '', - isPositive: true, + value: `₹${(revS?.grossrevenue ?? 0).toLocaleString('en-IN')}`, + trend: revenueDelta !== null ? fmtDelta(revenueDelta) : null, + status: `₹${(revS?.avgordervalue ?? 0).toLocaleString('en-IN')} avg. order`, + isPositive: revenueDelta === null ? true : revenueDelta >= 0, spark: [20, 30, 25, 45, 40, 55, 50, 68], color: 'emerald', - awaiting: true, + awaiting: false, }, { id: 'cancelled' as const, @@ -331,6 +340,55 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba }, 120); }; + // Real chart data from API + const tsGranularity = chartTimeframe === 'month' ? 'month' : 'day'; + const tsFromDate = useMemo(() => { + const d = new Date(); + if (chartTimeframe === 'day') d.setDate(d.getDate() - 60); // last 60 days (covers May) + else if (chartTimeframe === 'week') d.setDate(d.getDate() - 180); // last 6 months + else d.setFullYear(d.getFullYear() - 1); // last year + return ymd(d); + }, [chartTimeframe]); + + const tsQ = useFiestaTimeSeries({ + tenantid: tenantId, + granularity: tsGranularity, + fromdate: tsFromDate, + todate: todate, + }); + + const chartData = useMemo(() => { + const data = tsQ.data || []; + if (data.length === 0) return [{ label: 'No Data', value: 0 }]; + + return data.map((d: any) => { + let labelStr = d.label || d.date || d.createdat || "Unknown"; + if (tsGranularity === 'day' && labelStr !== "Unknown") { + const dt = new Date(labelStr); + const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + labelStr = `${dt.getDate()} ${months[dt.getMonth()]}`; + } + + let val = 0; + if (chartMetric === 'skus') { + val = Number(d.activeskus || d.skus || 0); + } else if (chartMetric === 'orders') { + val = Number(d.orders || d.totalorders || d.total || 0); + } else if (chartMetric === 'revenue') { + val = Number(d.revenue || d.grossrevenue || d.overallrevenue || 0); + } else { + val = Number(d[chartMetric]) || 0; + } + + return { + label: labelStr, + value: val + }; + }); + }, [tsQ.data, chartMetric, tsGranularity]); + + const maxChartVal = useMemo(() => Math.max(...chartData.map(d => d.value), 1), [chartData]); + return (
@@ -523,7 +581,10 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
- +

+ ₹{(revS?.grossrevenue ?? 0).toLocaleString('en-IN')} +

+

₹{(revS?.netrevenue ?? 0).toLocaleString('en-IN')} net

@@ -579,7 +640,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba + ))} + + + Live Sync - {/* Plotted Area — no time-series API ([R2]) for orders/revenue/skus. - The metric tabs (KPI cards above) still switch the card title; the - chart body itself shows the awaiting-backend placeholder. */} -
- + {/* Plotted Area */} +
+ + + + + + + + { + const x = (i / (chartData.length - 1)) * 100; + const y = 100 - ((d.value / maxChartVal) * 80); + return `L ${x},${y} `; + }).join('') + `L 100,100 Z`} + fill="url(#chartGradient)" + vectorEffect="non-scaling-stroke" + /> + { + const x = (i / (chartData.length - 1)) * 100; + const y = 100 - ((d.value / maxChartVal) * 80); + return `${i === 0 ? 'M' : 'L'} ${x},${y}`; + }).join(' ')} + fill="none" + stroke={theme.activeLine} + strokeWidth="2.5" + strokeLinecap="round" + strokeLinejoin="round" + vectorEffect="non-scaling-stroke" + className="drop-shadow-[0_4px_6px_rgba(168,85,247,0.4)]" + /> + + +
+ {chartData.map((d, i) => ( +
+ {/* Hover vertical line */} +
+ + {/* Invisible hit area */} +
+ + {/* Tooltip */} +
+ {d.label} + + {chartMetric === 'revenue' ? `₹${d.value.toLocaleString()}` : d.value.toLocaleString()} + + {/* Tooltip caret */} +
+
+ + {/* Dot on line */} +
+ + {/* X-axis label */} + {d.label} +
+ ))} +
diff --git a/src/components/SettingsView.tsx b/src/components/SettingsView.tsx index ac7ea44..1fc613a 100644 --- a/src/components/SettingsView.tsx +++ b/src/components/SettingsView.tsx @@ -23,6 +23,7 @@ import { FIESTA_TENANT_ID, str as fstr, num as fnum, roleName } from '../service import UsersPanel from './UsersPanel'; import AwaitingApi from './AwaitingApi'; import AdminConsole from './AdminConsole'; +import type { AuthUser } from '../services/auth'; type TabKey = 'profile' | 'outlets' | 'users'; @@ -122,9 +123,10 @@ function Row({ export interface SettingsViewProps { tenantId?: number; + user?: AuthUser; } -export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsViewProps) { +export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: SettingsViewProps) { const [activeTab, setActiveTab] = useState('profile'); // Live tenant profile + outlets. @@ -242,10 +244,10 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
{/* Initials avatar badge with glowing ring */}
- {tenant ? fstr(tenant.tenantname).substring(0, 2).toUpperCase() : 'ND'} + {user?.name ? user.name.substring(0, 2).toUpperCase() : tenant ? fstr(tenant.tenantname).substring(0, 2).toUpperCase() : 'ND'}
-

{tenant ? fstr(tenant.tenantname) : 'Nearle Merchant'}

+

{user?.name || (tenant ? fstr(tenant.tenantname) : 'Nearle Merchant')}

Store ID: #{tenantId}

@@ -306,7 +308,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
- {fstr(tenant?.tenantname) || 'Nearle Store'} + {user?.name || fstr(tenant?.tenantname) || 'Nearle Store'} — Set up your store logo, customer service email, and contact number. Official registration details are synced with your primary credentials. @@ -319,32 +321,32 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
Company Name -

{fstr(tenant?.companyname) || '—'}

+

{fstr(tenant?.companyname) || user?.name || 'Nearle Merchant'}

Category -

{fstr(tenant?.subcategoryname) || `Category ${fnum(tenant?.categoryid)}`}

+

{fstr(tenant?.subcategoryname) || (tenant?.categoryid ? `Category ${fnum(tenant.categoryid)}` : 'General Retail')}

Registration Status

- {fstr(tenant?.status) || '—'} + {fstr(tenant?.status) || (user ? 'Active' : '—')}

Store Verification -

{fnum(tenant?.approved) === 1 ? 'Verified' : 'Pending'}

+

{fnum(tenant?.approved) === 1 || user ? 'Verified' : 'Pending'}

- {fnum(tenant?.approved) === 1 ? 'Verified' : 'Pending'} + {fnum(tenant?.approved) === 1 || user ? 'Verified' : 'Pending'}
@@ -354,7 +356,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
Registered Address

- {fstr(tenant?.address) || '—'} + {fstr(tenant?.address) || [user?.locationname, user?.applocation].filter(Boolean).join(', ') || 'Headquarters'} {tenant?.city ? ` · ${fstr(tenant.city)}, ${fstr(tenant.state)} ${fstr(tenant.postcode)}` : ''}

@@ -371,7 +373,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi { + const map = new Map(); + // Pre-fill 8 AM to 10 PM + for (let i = 8; i <= 22; i++) { + const hStr = i.toString().padStart(2, '0') + ':00'; + map.set(hStr, { hour: hStr, orders: 0, revenue: 0 }); + } + + for (const r of (todayOrdersQ.data ?? [])) { + const dateVal = fstr(r.orderdate) || fstr(r.deliverydate); + if (!dateVal) continue; + const timePart = dateVal.split('T')[1]; + if (!timePart) continue; + + const hourStr = timePart.substring(0, 2) + ':00'; + const ex = map.get(hourStr) || { hour: hourStr, orders: 0, revenue: 0 }; + ex.orders += 1; + ex.revenue += (fnum(r.ordervalue) || fnum(r.orderamount) || fnum(r.deliveryamt) || 0); + map.set(hourStr, ex); + } + return Array.from(map.values()).sort((a, b) => a.hour.localeCompare(b.hour)); + }, [todayOrdersQ.data]); const customerOrderHistory = (customerOrdersQ.data ?? []).map((row: any) => { const amount = fnum(row.orderamount) || fnum(row.amount); return { @@ -533,11 +570,15 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
- +
- OTIF Fulfillment + Monthly Revenue
- + {monthRevenueQ.isLoading ? ( + Loading... + ) : ( +

₹{monthlyRevenue.toLocaleString('en-IN')}

+ )}
@@ -547,7 +588,11 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
Est. Revenue
- + {revenueQ.isLoading ? ( + Loading... + ) : ( +

₹{totalRevenue.toLocaleString('en-IN')}

+ )}
@@ -591,9 +636,48 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
- {/* Intraday dispatch breakdown has no backend yet ([R10]). */} -
- + {/* Intraday dispatch breakdown */} +
+ {todayOrdersQ.isLoading ? ( +
Loading intraday data...
+ ) : ( + + + + + + + + + + + `₹${v}`} /> + { + if (active && payload && payload.length) { + return ( +
+

{label}

+
+ + + Revenue: ₹{payload[0].value.toLocaleString('en-IN')} + + + + Orders: {payload[0].payload.orders} + +
+
+ ); + } + return null; + }} + /> + +
+
+ )}
@@ -643,65 +727,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
-
- - {/* Past 7 days Table */} -
-

- Past 7 Days Ledger Log -

- - {/* Daily ledger series has no backend yet ([R2]). */} - -
- {/* Live Rider fleet list */} -
-
-

- Active Rider Fleet -

-

Live status of assigned riders for this tenant.

-
- -
- {activeRiders.length === 0 ? ( -
- No active riders assigned yet. -
- ) : ( - activeRiders.map((rider, index) => ( -
-
-
- {rider.initial} -
-
-

{rider.name.split(' ')[0]} {rider.name.split(' ').slice(-1)[0]}

-

- - {rider.status} · {rider.orders} orders -

-
-
- - {canManage && ( -
- -
- )} -
- )) - )} -
-
- -
)} diff --git a/src/components/UserStorePage.tsx b/src/components/UserStorePage.tsx index 8df9dcf..09ae019 100644 --- a/src/components/UserStorePage.tsx +++ b/src/components/UserStorePage.tsx @@ -191,7 +191,7 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) { // Logistics console — scoped to this user's store. These views own their // loading/error states, so they don't need the store-console load gating below. if (activeSection === 'dispatch') return ; - if (activeSection === 'reports') return ; + if (activeSection === 'reports') return ; // Inventory & Catalog is its own page: the manager-curated catalog the user // stocks from (the catalog query is tenant-level, so it doesn't need the store // gating below — only "My Store Inventory" uses the resolved location id). diff --git a/src/components/UsersPanel.tsx b/src/components/UsersPanel.tsx index 37597ae..cb30402 100644 --- a/src/components/UsersPanel.tsx +++ b/src/components/UsersPanel.tsx @@ -51,6 +51,7 @@ const ROLE_THEMES: Record { - return Array.from(new Set(users.map((u) => u.role))); + const roles = new Set(users.map((u) => u.role)); + roles.add('Rider'); + return Array.from(roles); }, [users]); const handleCreateUser = async (e: React.FormEvent) => { @@ -277,32 +280,32 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser {/* Search & Filter Utility Bar */} -
-
-
- - +
+
+
+ + setSearch(e.target.value)} - className="w-full pl-3 pr-10 py-3 bg-transparent border-none text-xs font-semibold text-slate-800 placeholder-slate-400 focus:outline-none outline-none" + className="w-full pl-3 pr-12 py-3 bg-transparent border-none text-sm font-semibold text-slate-800 placeholder-slate-400 focus:outline-none outline-none" /> {search && ( )}
{/* Role filter capsules */} -
+