e2e testing
This commit is contained in:
419
package-lock.json
generated
419
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -541,7 +541,7 @@ export default function App() {
|
||||
}
|
||||
|
||||
case 'settings':
|
||||
return <SettingsView tenantId={tenantId} />;
|
||||
return <SettingsView tenantId={tenantId} user={authUser ?? undefined} />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
|
||||
@@ -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 ?? [];
|
||||
|
||||
@@ -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<string>(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 (
|
||||
<div className="animate-in fade-in duration-300">
|
||||
<GradientHeader title="Reports" subtitle="Operational analytics across outlets, riders, and the full order lifecycle." />
|
||||
@@ -81,66 +83,205 @@ export default function DeliveryReportsView({ searchQuery = '', tenantId = FIEST
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
{tab === 'orders-summary' && <OrdersSummaryReport tenantId={tenantId} />}
|
||||
{tab === 'riders-summary' && <RidersSummaryReport fromdate={fromdate} todate={todate} tenantId={tenantId} />}
|
||||
|
||||
{tab === 'orders-summary' && <OrdersSummaryReport tenantId={tenantId} locationid={locationid} fromdate={fromdate} todate={todate} />}
|
||||
{tab === 'riders-summary' && <RidersSummaryReport fromdate={fromdate} todate={todate} tenantId={tenantId} locationid={locationid} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Cnt = ({ n, color }: { n: number; color: string }) => (n > 0 ? <MetricPill color={color} minWidth={34}>{n.toLocaleString('en-IN')}</MetricPill> : <span style={{ color: TEXT_3, fontWeight: 700 }}>0</span>);
|
||||
|
||||
function TableShell({ minWidth, head, children, footer }: { minWidth: number; head: string[]; children: React.ReactNode; footer?: React.ReactNode }) {
|
||||
return (
|
||||
<div className="bg-white border rounded-2xl overflow-hidden" style={{ borderColor: BORDER }}>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full" style={{ minWidth }}>
|
||||
<thead><tr>{head.map((h, i) => (<th key={i} className={`px-3 py-2.5 ${i < 2 ? 'text-left' : 'text-right'}`} style={TH_STYLE}>{h}</th>))}</tr></thead>
|
||||
<tbody>{children}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{footer}
|
||||
</div>
|
||||
const Cnt = ({ n, color }: { n: number; color: string }) => (
|
||||
n > 0 ? <span className="inline-flex items-center justify-center px-2 py-0.5 rounded-md font-bold text-xs" style={{ backgroundColor: `${color}15`, color }}>{n.toLocaleString('en-IN')}</span> : <span className="font-bold text-slate-300 text-xs">0</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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: <TrendingUp size={20} /> },
|
||||
{ label: 'Pending', value: totals.pending.toLocaleString('en-IN'), color: '#f59e0b', icon: <Clock size={20} /> },
|
||||
{ label: 'Delivered', value: totals.delivered.toLocaleString('en-IN'), color: '#10b981', icon: <CheckCircle2 size={20} /> },
|
||||
{ label: 'Outlets', value: rows.length.toLocaleString('en-IN'), color: '#0ea5e9', icon: <Store size={20} /> },
|
||||
];
|
||||
|
||||
// Compute trend data
|
||||
const chartData = useMemo(() => {
|
||||
const map = new Map<string, { date: string; orders: number; revenue: number }>();
|
||||
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<number, number>();
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<KpiStrip items={kpis} loading={q.isLoading} />
|
||||
<TableShell minWidth={820} head={['#', 'Outlet', 'All', 'Created', 'Pending', 'Processing', 'Delivered', 'Cancelled']}
|
||||
footer={rows.length > 0 ? <TotalBar chips={[{ label: `${totals.total} orders`, color: BRAND }, { label: `${totals.delivered} delivered`, color: '#10b981' }, { label: `${totals.pending} pending`, color: '#f59e0b' }]} /> : undefined}>
|
||||
{q.isLoading ? <tr><td colSpan={8} className="px-3 py-12 text-center text-xs" style={{ color: TEXT_3 }}>Loading outlet summary…</td></tr>
|
||||
: rows.length === 0 ? <tr><td colSpan={8} className="px-3 py-12 text-center text-xs" style={{ color: TEXT_3 }}>No outlet data available.</td></tr>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<button onClick={() => setMetric('revenue')} className={`text-left p-5 bg-white rounded-2xl border transition-all duration-200 cursor-pointer flex items-center gap-4 ${metric === 'revenue' ? 'border-indigo-500 shadow-md ring-1 ring-indigo-500' : 'border-slate-200 shadow-sm hover:shadow-md'}`}>
|
||||
<div className={`p-3 rounded-xl ${metric === 'revenue' ? 'bg-indigo-100 text-indigo-600' : 'bg-slate-50 text-slate-400'}`}><IndianRupee size={20} /></div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-500">Revenue</p>
|
||||
<p className="text-2xl font-bold tracking-tight text-slate-900 mt-0.5">₹{totalRevenue.toLocaleString('en-IN')}</p>
|
||||
</div>
|
||||
</button>
|
||||
<button onClick={() => setMetric('orders')} className={`text-left p-5 bg-white rounded-2xl border transition-all duration-200 cursor-pointer flex items-center gap-4 ${metric === 'orders' ? 'border-sky-500 shadow-md ring-1 ring-sky-500' : 'border-slate-200 shadow-sm hover:shadow-md'}`}>
|
||||
<div className={`p-3 rounded-xl ${metric === 'orders' ? 'bg-sky-100 text-sky-600' : 'bg-slate-50 text-slate-400'}`}><TrendingUp size={20} /></div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-500">Total Orders</p>
|
||||
<p className="text-2xl font-bold tracking-tight text-slate-900 mt-0.5">{totals.total.toLocaleString('en-IN')}</p>
|
||||
</div>
|
||||
</button>
|
||||
<div className="p-5 bg-white rounded-2xl border border-slate-200 shadow-sm flex items-center gap-4">
|
||||
<div className="p-3 rounded-xl bg-amber-50 text-amber-600"><Clock size={20} /></div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-500">Pending</p>
|
||||
<p className="text-2xl font-bold tracking-tight text-slate-900 mt-0.5">{totals.pending.toLocaleString('en-IN')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 bg-white rounded-2xl border border-slate-200 shadow-sm flex items-center gap-4">
|
||||
<div className="p-3 rounded-xl bg-emerald-50 text-emerald-600"><CheckCircle2 size={20} /></div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-500">Delivered</p>
|
||||
<p className="text-2xl font-bold tracking-tight text-slate-900 mt-0.5">{totals.delivered.toLocaleString('en-IN')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="bg-slate-50 border-b border-slate-200 text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
||||
<tr>
|
||||
<th className="px-5 py-4 w-12">#</th>
|
||||
<th className="px-5 py-4">Outlet</th>
|
||||
<th className="px-5 py-4 text-right">Revenue</th>
|
||||
<th className="px-5 py-4 text-right">All</th>
|
||||
<th className="px-5 py-4 text-right">Created</th>
|
||||
<th className="px-5 py-4 text-right">Pending</th>
|
||||
<th className="px-5 py-4 text-right">Processing</th>
|
||||
<th className="px-5 py-4 text-right">Delivered</th>
|
||||
<th className="px-5 py-4 text-right">Cancelled</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{q.isLoading || revenueQ.isLoading ? <tr><td colSpan={9} className="px-5 py-12 text-center text-slate-500 font-medium">Loading outlet summary…</td></tr>
|
||||
: rows.length === 0 ? <tr><td colSpan={9} className="px-5 py-12 text-center text-slate-500 font-medium">No outlet data available.</td></tr>
|
||||
: rows.map((r, i) => (
|
||||
<tr key={r.locationid || i} className="transition-colors" style={{ borderBottom: `1px solid ${DIVIDER}` }} onMouseEnter={(e) => (e.currentTarget.style.background = SURFACE_ALT)} onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}>
|
||||
<td className="px-3 py-2.5 font-mono" style={{ color: TEXT_3 }}>{i + 1}</td>
|
||||
<td className="px-3 py-2.5 font-extrabold text-[13px]" style={{ color: TEXT }}>{r.locationname || `Location ${r.locationid}`}</td>
|
||||
<td className="px-3 py-2.5 text-right font-extrabold font-mono" style={{ color: TEXT }}>{r.total.toLocaleString('en-IN')}</td>
|
||||
<td className="px-3 py-2.5 text-right"><Cnt n={r.created} color="#0ea5e9" /></td>
|
||||
<td className="px-3 py-2.5 text-right"><Cnt n={r.pending} color="#f59e0b" /></td>
|
||||
<td className="px-3 py-2.5 text-right"><Cnt n={r.processing} color="#6366f1" /></td>
|
||||
<td className="px-3 py-2.5 text-right"><Cnt n={r.delivered} color="#10b981" /></td>
|
||||
<td className="px-3 py-2.5 text-right"><Cnt n={r.cancelled} color="#ef4444" /></td>
|
||||
<tr key={r.locationid || i} className="hover:bg-slate-50 transition-colors">
|
||||
<td className="px-5 py-3 text-slate-400 font-medium">{i + 1}</td>
|
||||
<td className="px-5 py-3 font-semibold text-slate-900">{r.locationname || `Location ${r.locationid}`}</td>
|
||||
<td className="px-5 py-3 text-right font-bold text-indigo-600">₹{(locationRevenueMap.get(fnum(r.locationid)) ?? 0).toLocaleString('en-IN')}</td>
|
||||
<td className="px-5 py-3 text-right font-bold text-slate-900">{r.total.toLocaleString('en-IN')}</td>
|
||||
<td className="px-5 py-3 text-right"><Cnt n={r.created} color="#0ea5e9" /></td>
|
||||
<td className="px-5 py-3 text-right"><Cnt n={r.pending} color="#f59e0b" /></td>
|
||||
<td className="px-5 py-3 text-right"><Cnt n={r.processing} color="#6366f1" /></td>
|
||||
<td className="px-5 py-3 text-right"><Cnt n={r.delivered} color="#10b981" /></td>
|
||||
<td className="px-5 py-3 text-right"><Cnt n={r.cancelled} color="#ef4444" /></td>
|
||||
</tr>
|
||||
))}
|
||||
</TableShell>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{rows.length > 0 && (
|
||||
<div className="bg-slate-50 border-t border-slate-200 px-5 py-3 flex items-center gap-4 text-sm font-medium">
|
||||
<span className="text-slate-500">Totals:</span>
|
||||
<span className="text-indigo-600 font-bold">₹{totalRevenue.toLocaleString('en-IN')}</span>
|
||||
<span className="text-slate-900 font-bold">{totals.total} orders</span>
|
||||
<span className="text-emerald-600 font-bold">{totals.delivered} delivered</span>
|
||||
<span className="text-amber-600 font-bold">{totals.pending} pending</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Trend Chart */}
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<TrendingUp size={20} className="text-indigo-500" />
|
||||
<h3 className="text-base font-bold text-slate-900 tracking-tight">Trend Analysis</h3>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-80">
|
||||
{ordersQ.isLoading ? (
|
||||
<div className="w-full h-full flex items-center justify-center text-sm font-medium text-slate-500">Loading trend data...</div>
|
||||
) : chartData.length === 0 ? (
|
||||
<div className="w-full h-full flex items-center justify-center text-sm font-medium text-slate-500">No trend data for this period.</div>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={chartData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={metricColor} stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor={metricColor} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="4 4" vertical={false} stroke="#e2e8f0" />
|
||||
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: '#64748b', fontWeight: 500 }} dy={10}
|
||||
tickFormatter={formatLabel} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: '#64748b', fontWeight: 500 }} dx={-10} tickFormatter={(v) => metric === 'revenue' ? `₹${v.toLocaleString('en-IN')}` : v.toLocaleString('en-IN')} />
|
||||
<RechartsTooltip
|
||||
cursor={{ stroke: metricColor, strokeWidth: 1, strokeDasharray: '4 4' }}
|
||||
content={({ active, payload, label }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-lg">
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">{formatLabel(label)}</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="flex items-center gap-2 text-sm font-medium text-slate-600 capitalize">
|
||||
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: metricColor }}></span>
|
||||
{metric}
|
||||
</span>
|
||||
<span className="text-lg font-bold text-slate-900">
|
||||
{metric === 'revenue' ? `₹${payload[0].value.toLocaleString('en-IN')}` : payload[0].value.toLocaleString('en-IN')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<Area type="monotone" dataKey={metric} stroke={metricColor} strokeWidth={2.5} fillOpacity={1} fill="url(#colorGradient)" activeDot={{ r: 6, strokeWidth: 2, stroke: '#fff', fill: metricColor }} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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)}`,
|
||||
|
||||
@@ -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,
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{headerTabs}
|
||||
</div>
|
||||
|
||||
<div className="hdr-stats">
|
||||
@@ -359,6 +357,11 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{headerTabs && (
|
||||
<div style={{ marginLeft: 8 }}>
|
||||
{headerTabs}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Body ── */}
|
||||
|
||||
@@ -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<StatusKey>('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({
|
||||
: <LiveStatus state="live" label={`Live · ${total.toLocaleString('en-IN')} orders in range · ${activeFleet} riders on duty`} />
|
||||
}
|
||||
right={
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<input type="date" value={fromdate} max={todate} onChange={(e) => 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 }} />
|
||||
<span className="text-zinc-400 font-bold px-1 text-xs">—</span>
|
||||
<input type="date" value={todate} min={fromdate} max={todayStr} onChange={(e) => 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 }} />
|
||||
</div>
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full font-extrabold"
|
||||
style={{ padding: '6px 12px', fontSize: 12, background: tint(BRAND), border: `1.5px solid ${edge(BRAND)}`, color: BRAND }}>
|
||||
<MapPin size={13} /> {locationid ? `Location ${locationid}` : 'All Locations'}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-3 py-2.5 text-left" style={TH_STYLE}>
|
||||
<input type="checkbox" checked={allSelected} onChange={toggleAll} disabled={rows.length === 0} aria-label="Select all orders" style={{ accentColor: BRAND, cursor: 'pointer', width: 15, height: 15 }} />
|
||||
<input type="checkbox" checked={allSelected} onChange={toggleAll} disabled={assignableRows.length === 0} aria-label="Select all assignable orders" style={{ accentColor: BRAND, cursor: 'pointer', width: 15, height: 15 }} />
|
||||
</th>
|
||||
{['#', 'Order', 'Branch', 'Pickup', 'Drop', 'Qty', 'COD', 'KMs', 'Charges', 'Status', ''].map((h, i) => (
|
||||
<th key={i} className="px-3 py-2.5 text-left" style={TH_STYLE}>{h}</th>
|
||||
@@ -406,7 +409,11 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI
|
||||
<tr key={fstr(r.orderid) || i} className="transition-colors" style={{ borderBottom: `1px solid ${DIVIDER_C}`, background: selected.has(rowKey(r)) ? tint(BRAND) : 'transparent' }}
|
||||
onMouseEnter={(e) => { if (!selected.has(rowKey(r))) e.currentTarget.style.background = SURFACE_ALT; }} onMouseLeave={(e) => { e.currentTarget.style.background = selected.has(rowKey(r)) ? tint(BRAND) : 'transparent'; }}>
|
||||
<td className="px-3 py-2.5">
|
||||
{st === 'created' ? (
|
||||
<input type="checkbox" checked={selected.has(rowKey(r))} onChange={() => toggleRow(rowKey(r))} aria-label="Select order" style={{ accentColor: BRAND, cursor: 'pointer', width: 15, height: 15 }} />
|
||||
) : (
|
||||
<input type="checkbox" disabled aria-label="Order cannot be assigned" style={{ width: 15, height: 15, opacity: 0.3 }} title="Only 'created' orders can be assigned" />
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 font-mono" style={{ color: TEXT_3 }}>{(pageno - 1) * PAGE_SIZE + i + 1}</td>
|
||||
<td className="px-3 py-2.5">
|
||||
|
||||
@@ -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<string | null>(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 (
|
||||
<div className="space-y-lg animate-in fade-in duration-500 font-sans relative">
|
||||
|
||||
@@ -523,7 +581,10 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<AwaitingApi label="Gross Revenue" api="[R1]" compact />
|
||||
<h3 className="text-xl font-extrabold tracking-tight font-mono">
|
||||
₹{(revS?.grossrevenue ?? 0).toLocaleString('en-IN')}
|
||||
</h3>
|
||||
<p className="text-[10px] text-slate-400 font-semibold mt-1">₹{(revS?.netrevenue ?? 0).toLocaleString('en-IN')} net</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -579,7 +640,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
|
||||
<button
|
||||
key={kpi.id}
|
||||
onClick={() => setChartMetric(kpi.id)}
|
||||
className={`border backdrop-blur-md rounded-3xl p-lg flex flex-col justify-between hover:shadow-lg transition-all duration-300 hover:-translate-y-1.5 text-left cursor-pointer outline-none relative overflow-hidden min-h-[160px] group ${activeStyles}`}
|
||||
className={`border backdrop-blur-md rounded-2xl p-3 flex flex-col justify-between hover:shadow-lg transition-all duration-300 hover:-translate-y-1.5 text-left cursor-pointer outline-none relative overflow-hidden group ${activeStyles}`}
|
||||
>
|
||||
{/* Subtle top glowing line when active */}
|
||||
{isActive && (
|
||||
@@ -591,19 +652,19 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
|
||||
<span className="text-[10px] font-sans font-bold text-zinc-400 uppercase tracking-widest block">
|
||||
{kpi.title}
|
||||
</span>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center transition-transform duration-300 group-hover:scale-110 ${iconBg} ${iconTextColor}`}>
|
||||
<IconComponent size={14} />
|
||||
<div className={`w-7 h-7 rounded-full flex items-center justify-center transition-transform duration-300 group-hover:scale-110 ${iconBg} ${iconTextColor}`}>
|
||||
<IconComponent size={13} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Metric Value and Trend Badge */}
|
||||
{kpi.awaiting ? (
|
||||
<div className="mt-3">
|
||||
<div className="mt-1">
|
||||
<AwaitingApi label="Revenue" api="[R1]" compact />
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 flex items-baseline gap-2">
|
||||
<div className="font-sans font-black text-slate-900 text-3xl tracking-tight leading-none">
|
||||
<div className="mt-1.5 flex items-baseline gap-2">
|
||||
<div className="font-sans font-black text-slate-900 text-2xl tracking-tight leading-none">
|
||||
{kpi.value}
|
||||
</div>
|
||||
{kpi.trend && (
|
||||
@@ -617,7 +678,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
|
||||
)}
|
||||
|
||||
{/* Bottom Sparkline & Subtext segment */}
|
||||
<div className="flex items-center justify-between mt-auto pt-3 w-full border-t border-[#f1f5f9]">
|
||||
<div className="flex items-center justify-between mt-auto pt-2 w-full border-t border-[#f1f5f9]">
|
||||
<span className="text-[10px] text-zinc-550 font-bold uppercase tracking-wider">{kpi.status}</span>
|
||||
|
||||
{/* KPI Mini Sparkline Chart */}
|
||||
@@ -657,19 +718,94 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<span className="text-[10px] font-mono text-zinc-555 font-bold bg-zinc-50 border border-zinc-200/60 px-3 py-1 rounded-xl flex items-center gap-1.5">
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex bg-slate-100/50 p-1 rounded-xl border border-slate-200/60 hidden sm:flex">
|
||||
{(['day', 'week', 'month'] as const).map((tf) => (
|
||||
<button
|
||||
key={tf}
|
||||
onClick={() => setChartTimeframe(tf)}
|
||||
className={`px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider transition-all ${
|
||||
chartTimeframe === tf
|
||||
? 'bg-white text-purple-700 shadow-sm border border-purple-100'
|
||||
: 'text-slate-500 hover:text-slate-700 hover:bg-slate-200/50'
|
||||
}`}
|
||||
>
|
||||
{tf}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<span className="text-[10px] font-mono text-zinc-555 font-bold bg-zinc-50 border border-zinc-200/60 px-3 py-1.5 rounded-xl flex items-center gap-1.5 ml-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-current animate-pulse" style={{ color: theme.activeLine }} />
|
||||
<span>Live Sync</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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. */}
|
||||
<div className="relative h-64 select-none w-full flex items-center justify-center">
|
||||
<AwaitingApi label="Orders & revenue time-series" api="[R2]" className="w-full h-full justify-center" />
|
||||
{/* Plotted Area */}
|
||||
<div className="relative h-64 select-none w-full flex items-end pt-8 pb-4 px-2">
|
||||
<svg className="absolute inset-0 w-full h-full preserve-3d" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<linearGradient id="chartGradient" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stopColor={theme.activeLine} stopOpacity={0.4} />
|
||||
<stop offset="100%" stopColor={theme.activeLine} stopOpacity={0.01} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d={`M 0,100 ` + chartData.map((d, i) => {
|
||||
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"
|
||||
/>
|
||||
<path
|
||||
d={chartData.map((d, i) => {
|
||||
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)]"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div className="absolute inset-0 flex justify-between items-end pb-4 px-2">
|
||||
{chartData.map((d, i) => (
|
||||
<div key={i} className="group relative flex flex-col items-center flex-1 h-full justify-end cursor-crosshair">
|
||||
{/* Hover vertical line */}
|
||||
<div className="absolute top-0 bottom-6 w-px bg-slate-200/0 group-hover:bg-slate-300/50 transition-colors" />
|
||||
|
||||
{/* Invisible hit area */}
|
||||
<div className="absolute inset-0 w-full h-full z-10" />
|
||||
|
||||
{/* Tooltip */}
|
||||
<div className="absolute bottom-full mb-2 opacity-0 group-hover:opacity-100 transition-all duration-200 bg-slate-900 text-white text-[10px] px-3 py-2 rounded-xl shadow-xl pointer-events-none whitespace-nowrap z-20 flex flex-col items-center">
|
||||
<span className="font-bold text-xs text-purple-300">{d.label}</span>
|
||||
<span className="font-extrabold tracking-wide mt-0.5">
|
||||
{chartMetric === 'revenue' ? `₹${d.value.toLocaleString()}` : d.value.toLocaleString()}
|
||||
</span>
|
||||
{/* Tooltip caret */}
|
||||
<div className="absolute top-full w-2 h-2 bg-slate-900 rotate-45 -mt-1" />
|
||||
</div>
|
||||
|
||||
{/* Dot on line */}
|
||||
<div
|
||||
className="absolute w-3 h-3 rounded-full border-2 border-white bg-purple-600 shadow-md opacity-0 group-hover:opacity-100 transition-all duration-200 z-10 scale-50 group-hover:scale-100"
|
||||
style={{ bottom: `calc(${((d.value / maxChartVal) * 80)}% + 1.5rem)`, backgroundColor: theme.activeLine }}
|
||||
/>
|
||||
|
||||
{/* X-axis label */}
|
||||
<span className="text-[9px] font-bold text-slate-400 mt-2 rotate-0 truncate max-w-full px-1">{d.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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<TabKey>('profile');
|
||||
|
||||
// Live tenant profile + outlets.
|
||||
@@ -242,10 +244,10 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
<div className="relative z-10 flex items-center gap-3.5">
|
||||
{/* Initials avatar badge with glowing ring */}
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-to-tr from-purple-500 to-indigo-650 border border-purple-400/30 flex items-center justify-center font-black text-sm shadow-[0_0_15px_rgba(168,85,247,0.35)] shrink-0">
|
||||
{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'}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h4 className="font-sans font-bold text-sm truncate text-white">{tenant ? fstr(tenant.tenantname) : 'Nearle Merchant'}</h4>
|
||||
<h4 className="font-sans font-bold text-sm truncate text-white">{user?.name || (tenant ? fstr(tenant.tenantname) : 'Nearle Merchant')}</h4>
|
||||
<p className="text-slate-400 text-[10px] font-mono mt-1 truncate uppercase tracking-wider">Store ID: #{tenantId}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,7 +308,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1 leading-relaxed">
|
||||
<span className="text-base font-bold text-slate-900 mr-2">{fstr(tenant?.tenantname) || 'Nearle Store'}</span>
|
||||
<span className="text-base font-bold text-slate-900 mr-2">{user?.name || fstr(tenant?.tenantname) || 'Nearle Store'}</span>
|
||||
<span className="text-slate-500 text-xs font-medium">
|
||||
— Set up your store logo, customer service email, and contact number. Official registration details are synced with your primary credentials.
|
||||
</span>
|
||||
@@ -319,32 +321,32 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-md mt-3">
|
||||
<div className="p-4 bg-white rounded-xl border border-slate-200/60 shadow-sm">
|
||||
<span className="text-[10px] text-slate-400 uppercase font-black tracking-wider block">Company Name</span>
|
||||
<p className="font-bold text-slate-800 text-sm mt-1">{fstr(tenant?.companyname) || '—'}</p>
|
||||
<p className="font-bold text-slate-800 text-sm mt-1">{fstr(tenant?.companyname) || user?.name || 'Nearle Merchant'}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-white rounded-xl border border-slate-200/60 shadow-sm">
|
||||
<span className="text-[10px] text-slate-400 uppercase font-black tracking-wider block">Category</span>
|
||||
<p className="font-bold text-slate-800 text-sm mt-1">{fstr(tenant?.subcategoryname) || `Category ${fnum(tenant?.categoryid)}`}</p>
|
||||
<p className="font-bold text-slate-800 text-sm mt-1">{fstr(tenant?.subcategoryname) || (tenant?.categoryid ? `Category ${fnum(tenant.categoryid)}` : 'General Retail')}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-white rounded-xl border border-slate-200/60 shadow-sm">
|
||||
<span className="text-[10px] text-slate-400 uppercase font-black tracking-wider block">Registration Status</span>
|
||||
<p className="font-bold text-slate-800 text-sm mt-1 flex items-center gap-1.5">
|
||||
<span className={`w-2 h-2 rounded-full ${
|
||||
fstr(tenant?.status).toLowerCase() === 'active' ? 'bg-emerald-500' : 'bg-slate-400'
|
||||
(fstr(tenant?.status).toLowerCase() === 'active' || user) ? 'bg-emerald-500' : 'bg-slate-400'
|
||||
}`} />
|
||||
{fstr(tenant?.status) || '—'}
|
||||
{fstr(tenant?.status) || (user ? 'Active' : '—')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-white rounded-xl border border-slate-200/60 shadow-sm flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-[10px] text-slate-400 uppercase font-black tracking-wider block">Store Verification</span>
|
||||
<p className="font-bold text-slate-800 text-sm mt-1">{fnum(tenant?.approved) === 1 ? 'Verified' : 'Pending'}</p>
|
||||
<p className="font-bold text-slate-800 text-sm mt-1">{fnum(tenant?.approved) === 1 || user ? 'Verified' : 'Pending'}</p>
|
||||
</div>
|
||||
<span className={`px-2.5 py-1 rounded-lg text-[9px] font-black uppercase tracking-wider ${
|
||||
fnum(tenant?.approved) === 1
|
||||
fnum(tenant?.approved) === 1 || user
|
||||
? 'text-emerald-700 bg-emerald-50 border border-emerald-100/50'
|
||||
: 'text-zinc-555 bg-zinc-100'
|
||||
}`}>
|
||||
{fnum(tenant?.approved) === 1 ? 'Verified' : 'Pending'}
|
||||
{fnum(tenant?.approved) === 1 || user ? 'Verified' : 'Pending'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -354,7 +356,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
<div>
|
||||
<span className="text-[10px] text-slate-400 uppercase font-black tracking-wider block">Registered Address</span>
|
||||
<p className="text-slate-700 font-semibold text-xs mt-1 leading-relaxed">
|
||||
{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)}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
@@ -371,7 +373,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={fstr(tenant?.primaryemail) || ''}
|
||||
value={user?.email || fstr(tenant?.primaryemail) || ''}
|
||||
readOnly
|
||||
className="w-full border border-slate-200 rounded-xl py-3 px-4 bg-slate-50/40 outline-none transition-all text-slate-800 font-semibold text-sm shadow-sm"
|
||||
placeholder="—"
|
||||
@@ -383,7 +385,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={fstr(tenant?.primarycontact) || ''}
|
||||
value={user?.contactno || fstr(tenant?.primarycontact) || ''}
|
||||
readOnly
|
||||
className="w-full border border-slate-200 rounded-xl py-3 px-4 bg-slate-50/40 outline-none transition-all text-slate-800 font-semibold text-sm shadow-sm"
|
||||
placeholder="—"
|
||||
|
||||
@@ -41,10 +41,13 @@ import {
|
||||
useFiestaTenantCustomers,
|
||||
useFiestaCustomerOrders,
|
||||
useFiestaRiders,
|
||||
useFiestaRevenueSummary,
|
||||
useFiestaAllOrders,
|
||||
FIESTA_TENANT_ID
|
||||
} from '../services/fiestaQueries';
|
||||
import { str as fstr, num as fnum } from '../services/fiestaApi';
|
||||
import { str as fstr, num as fnum, ymd } from '../services/fiestaApi';
|
||||
import { mapOrderStatus, shortTime } from '../services/fiestaMappers';
|
||||
import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, Tooltip as RechartsTooltip, CartesianGrid } from 'recharts';
|
||||
import ragulStoreCover from '../assets/images/store_front_view_1780299351800.png';
|
||||
import OrdersDeliveriesView from './OrdersDeliveriesView';
|
||||
import StoreQRView from './StoreQRView';
|
||||
@@ -163,6 +166,40 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
|
||||
customerid: selectedCustomer?.id ?? null,
|
||||
pagesize: 20
|
||||
});
|
||||
|
||||
const todayStr = ymd(new Date());
|
||||
const revenueQ = useFiestaRevenueSummary({ tenantid: tenantId, locationid, fromdate: todayStr, todate: todayStr });
|
||||
const totalRevenue = revenueQ.data?.grossrevenue ?? 0;
|
||||
|
||||
const monthStart = ymd(new Date(new Date().getFullYear(), new Date().getMonth(), 1));
|
||||
const monthRevenueQ = useFiestaRevenueSummary({ tenantid: tenantId, locationid, fromdate: monthStart, todate: todayStr });
|
||||
const monthlyRevenue = monthRevenueQ.data?.grossrevenue ?? 0;
|
||||
|
||||
// All orders for today to build the intraday chart
|
||||
const todayOrdersQ = useFiestaAllOrders({ tenantid: tenantId, locationid, fromdate: todayStr, todate: todayStr });
|
||||
|
||||
const intradayChartData = React.useMemo(() => {
|
||||
const map = new Map<string, { hour: string; orders: number; revenue: number }>();
|
||||
// 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,
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-gutter">
|
||||
<div className="bg-white border border-[#eceef2] rounded-2xl p-md shadow-sm hover:shadow-md transition-all duration-200 hover:-translate-y-0.5 relative group overflow-hidden">
|
||||
<div className="w-8 h-8 rounded-xl bg-purple-50 text-[#581c87] flex items-center justify-center mb-sm group-hover:scale-110 transition-transform">
|
||||
<CheckCircle2 size={16} />
|
||||
<Activity size={16} />
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block">OTIF Fulfillment</span>
|
||||
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block">Monthly Revenue</span>
|
||||
<div className="mt-xs">
|
||||
<AwaitingApi label="OTIF fulfillment" api="[R1]" compact />
|
||||
{monthRevenueQ.isLoading ? (
|
||||
<span className="text-xs text-zinc-400 font-medium">Loading...</span>
|
||||
) : (
|
||||
<p className="text-2xl font-black text-[#0f172a]">₹{monthlyRevenue.toLocaleString('en-IN')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -547,7 +588,11 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block">Est. Revenue</span>
|
||||
<div className="mt-xs">
|
||||
<AwaitingApi label="Outlet revenue" api="[R1]" compact />
|
||||
{revenueQ.isLoading ? (
|
||||
<span className="text-xs text-zinc-400 font-medium">Loading...</span>
|
||||
) : (
|
||||
<p className="text-2xl font-black text-[#0f172a]">₹{totalRevenue.toLocaleString('en-IN')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -591,9 +636,48 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Intraday dispatch breakdown has no backend yet ([R10]). */}
|
||||
<div className="my-xl">
|
||||
<AwaitingApi label="Intraday dispatch breakdown" api="[R10]" />
|
||||
{/* Intraday dispatch breakdown */}
|
||||
<div className="my-md h-64">
|
||||
{todayOrdersQ.isLoading ? (
|
||||
<div className="w-full h-full flex items-center justify-center text-xs text-zinc-400">Loading intraday data...</div>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={intradayChartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="intraRev" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#581c87" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="#581c87" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#eceef2" />
|
||||
<XAxis dataKey="hour" axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: '#94a3b8' }} dy={10} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: '#94a3b8' }} dx={-10} tickFormatter={(v) => `₹${v}`} />
|
||||
<RechartsTooltip
|
||||
content={({ active, payload, label }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-white p-3 rounded-xl border border-zinc-200 shadow-lg">
|
||||
<p className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider mb-2">{label}</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="flex items-center gap-2 text-xs font-medium text-zinc-600">
|
||||
<span className="w-2 h-2 rounded-full bg-[#581c87]"></span>
|
||||
Revenue: <strong className="text-zinc-900">₹{payload[0].value.toLocaleString('en-IN')}</strong>
|
||||
</span>
|
||||
<span className="flex items-center gap-2 text-xs font-medium text-zinc-600">
|
||||
<span className="w-2 h-2 rounded-full bg-sky-500"></span>
|
||||
Orders: <strong className="text-zinc-900">{payload[0].payload.orders}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<Area type="monotone" dataKey="revenue" stroke="#581c87" strokeWidth={2} fillOpacity={1} fill="url(#intraRev)" activeDot={{ r: 4, strokeWidth: 2, stroke: '#fff', fill: '#581c87' }} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -643,65 +727,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
|
||||
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-gutter">
|
||||
|
||||
{/* Past 7 days Table */}
|
||||
<div className="lg:col-span-2 bg-white border border-[#eceef2] rounded-2xl p-md shadow-sm">
|
||||
<h3 className="font-sans font-bold text-sm text-[#0f172a] mb-md flex items-center gap-xs">
|
||||
<Calendar size={15} className="text-[#581c87]" /> Past 7 Days Ledger Log
|
||||
</h3>
|
||||
|
||||
{/* Daily ledger series has no backend yet ([R2]). */}
|
||||
<AwaitingApi label="Daily ledger" api="[R2]" />
|
||||
</div>
|
||||
|
||||
{/* Live Rider fleet list */}
|
||||
<div className="bg-white border border-[#eceef2] rounded-2xl p-md shadow-sm flex flex-col justify-between">
|
||||
<div>
|
||||
<h3 className="font-sans font-bold text-sm text-[#0f172a] flex items-center gap-xs">
|
||||
<ShoppingCart size={15} className="text-[#581c87]" /> Active Rider Fleet
|
||||
</h3>
|
||||
<p className="text-zinc-405 text-[10px] font-sans mt-0.5">Live status of assigned riders for this tenant.</p>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-[#f1f5f9] text-xs font-sans mt-md">
|
||||
{activeRiders.length === 0 ? (
|
||||
<div className="py-8 text-center text-zinc-400 font-medium">
|
||||
No active riders assigned yet.
|
||||
</div>
|
||||
) : (
|
||||
activeRiders.map((rider, index) => (
|
||||
<div key={index} className="py-2 flex justify-between items-center group">
|
||||
<div className="flex items-center gap-sm min-w-0">
|
||||
<div className="w-7 h-7 rounded-full bg-purple-50 text-[#581c87] border border-purple-100 font-black text-[10px] flex items-center justify-center shrink-0">
|
||||
{rider.initial}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-bold text-[#0f172a] truncate">{rider.name.split(' ')[0]} {rider.name.split(' ').slice(-1)[0]}</p>
|
||||
<p className="text-[9px] text-zinc-400 flex items-center gap-1 font-semibold uppercase">
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${rider.status === 'Idle' ? 'bg-amber-400' : 'bg-emerald-500'}`} />
|
||||
{rider.status} · {rider.orders} orders
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canManage && (
|
||||
<div className="flex items-center gap-sm text-right shrink-0">
|
||||
<button
|
||||
onClick={() => showToast(`SMS alert broadcasted to rider ${rider.name.split(' ')[0]}.`, 'success')}
|
||||
className="px-2 py-1 border border-zinc-200 rounded-lg hover:border-purple-300 text-[9px] font-bold hover:bg-purple-50/50 hover:text-[#581c87] cursor-pointer transition"
|
||||
>
|
||||
Ping
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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 <DispatchHubView locationid={resolvedLocationId || undefined} tenantId={tenantId} />;
|
||||
if (activeSection === 'reports') return <DeliveryReportsView tenantId={tenantId} />;
|
||||
if (activeSection === 'reports') return <DeliveryReportsView tenantId={tenantId} locationid={resolvedLocationId || undefined} />;
|
||||
// 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).
|
||||
|
||||
@@ -51,6 +51,7 @@ const ROLE_THEMES: Record<number, { bg: string; text: string; border: string; la
|
||||
2: { bg: 'bg-amber-50/75', text: 'text-amber-700', border: 'border-amber-100', label: 'Admin' },
|
||||
3: { bg: 'bg-blue-50/75', text: 'text-blue-700', border: 'border-blue-100', label: 'Admin' },
|
||||
4: { bg: 'bg-emerald-50/75', text: 'text-emerald-700', border: 'border-emerald-100', label: 'Staff' },
|
||||
5: { bg: 'bg-purple-50/75', text: 'text-purple-700', border: 'border-purple-100', label: 'Rider' },
|
||||
6: { bg: 'bg-indigo-50/75', text: 'text-indigo-700', border: 'border-indigo-100', label: 'Cashier' },
|
||||
};
|
||||
|
||||
@@ -187,7 +188,9 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
|
||||
});
|
||||
|
||||
const roleOptions = React.useMemo(() => {
|
||||
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
|
||||
</div>
|
||||
|
||||
{/* Search & Filter Utility Bar */}
|
||||
<div className="bg-slate-50/50 border border-slate-200/60 p-4 rounded-2xl flex flex-col md:flex-row gap-4 items-stretch md:items-center justify-between select-none">
|
||||
<div className="relative w-full md:max-w-md shrink-0 group">
|
||||
<div className="relative flex items-center bg-white border border-slate-200 rounded-xl transition-all duration-300 shadow-sm focus-within:ring-4 focus-within:ring-purple-150 focus-within:border-purple-600 hover:border-slate-300">
|
||||
<span className="pl-4 flex items-center pointer-events-none text-slate-400 group-focus-within:text-purple-600 transition-colors">
|
||||
<Search className="w-4.5 h-4.5" />
|
||||
<div className="bg-white border border-slate-200/80 p-3 rounded-2xl flex flex-col md:flex-row gap-4 items-stretch md:items-center justify-between shadow-sm">
|
||||
<div className="relative w-full md:max-w-md shrink-0">
|
||||
<div className="relative flex w-full items-center bg-slate-50 border border-slate-200/80 rounded-xl transition-all duration-300 focus-within:bg-white focus-within:ring-4 focus-within:ring-purple-500/10 focus-within:border-purple-500 group-search shadow-inner">
|
||||
<span className="pl-4 flex items-center pointer-events-none text-slate-400 transition-colors">
|
||||
<Search size={18} strokeWidth={2.5} />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search team members by name, email, phone..."
|
||||
placeholder="Search team members..."
|
||||
value={search}
|
||||
onChange={(e) => 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 && (
|
||||
<button
|
||||
onClick={() => setSearch('')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 active:scale-95 transition-all p-1 hover:bg-slate-100 rounded-lg"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-700 hover:bg-slate-200/50 active:scale-95 transition-all p-1.5 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
<X size={16} strokeWidth={2.5} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Role filter capsules */}
|
||||
<div className="flex items-center gap-2 overflow-x-auto pb-1 md:pb-0 scrollbar-none">
|
||||
<div className="flex items-center gap-2 overflow-x-auto pb-1 md:pb-0 scrollbar-none select-none">
|
||||
<button
|
||||
onClick={() => setUserRoleFilter('ALL')}
|
||||
className={`px-3.5 py-2 rounded-xl text-xs uppercase tracking-wider font-extrabold transition-all duration-200 cursor-pointer border ${
|
||||
@@ -389,6 +392,7 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
|
||||
{u.roleid === 2 && <Shield size={12} />}
|
||||
{u.roleid === 3 && <SlidersHorizontal size={12} />}
|
||||
{u.roleid === 4 && <User size={12} />}
|
||||
{u.roleid === 5 && <Bike size={12} />}
|
||||
{u.roleid === 6 && <Coins size={12} />}
|
||||
{roleInfo.label}
|
||||
</span>
|
||||
|
||||
@@ -131,6 +131,8 @@ export interface KpiItem {
|
||||
color: string;
|
||||
icon: React.ReactNode;
|
||||
badge?: string;
|
||||
onClick?: () => void;
|
||||
active?: boolean;
|
||||
}
|
||||
export function KpiStrip({ items, loading }: { items: KpiItem[]; loading?: boolean }) {
|
||||
return (
|
||||
@@ -138,10 +140,11 @@ export function KpiStrip({ items, loading }: { items: KpiItem[]; loading?: boole
|
||||
{items.map((it) => (
|
||||
<div
|
||||
key={it.label}
|
||||
className="relative overflow-hidden rounded-2xl border bg-white p-3.5 sm:p-5 transition-all duration-200 hover:-translate-y-0.5"
|
||||
style={{ borderColor: BORDER }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.boxShadow = SHADOW_MD)}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = 'none')}
|
||||
onClick={it.onClick}
|
||||
className={`relative overflow-hidden rounded-2xl border bg-white p-3.5 sm:p-5 transition-all duration-200 ${it.onClick ? 'cursor-pointer hover:-translate-y-0.5' : ''}`}
|
||||
style={{ borderColor: it.active ? it.color : BORDER, boxShadow: it.active ? `0 0 0 1px ${it.color}, ${SHADOW_MD}` : 'none' }}
|
||||
onMouseEnter={(e) => { if (!it.active && it.onClick) e.currentTarget.style.boxShadow = SHADOW_MD; }}
|
||||
onMouseLeave={(e) => { if (!it.active) e.currentTarget.style.boxShadow = 'none'; }}
|
||||
>
|
||||
<div className="absolute top-0 left-0 right-0" style={{ height: 3, background: `linear-gradient(90deg, ${it.color} 0%, ${soft(it.color)} 100%)` }} />
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
@@ -303,3 +306,25 @@ export const TH_STYLE: React.CSSProperties = {
|
||||
whiteSpace: 'nowrap',
|
||||
borderBottom: `1px solid ${BORDER}`,
|
||||
};
|
||||
|
||||
export function TableShell({ head, children, minWidth, footer }: { head: string[]; children: React.ReactNode; minWidth?: number; footer?: React.ReactNode }) {
|
||||
return (
|
||||
<div className="bg-white rounded-2xl border overflow-hidden shadow-sm flex flex-col" style={{ borderColor: BORDER }}>
|
||||
<div className="overflow-x-auto flex-1">
|
||||
<table className="w-full text-sm text-left whitespace-nowrap" style={{ minWidth }}>
|
||||
<thead className="sticky top-0 z-10" style={TH_STYLE}>
|
||||
<tr>
|
||||
{head.map((h, i) => (
|
||||
<th key={i} className={`px-4 py-3 ${i > 1 ? 'text-right' : ''}`}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y" style={{ borderColor: DIVIDER }}>
|
||||
{children}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,6 +125,46 @@ export interface FiestaOrderSummary {
|
||||
tenantname?: string;
|
||||
}
|
||||
|
||||
export interface FiestaLocationRevenue {
|
||||
locationid: number;
|
||||
locationname: string;
|
||||
revenue: number;
|
||||
}
|
||||
|
||||
export interface FiestaRevenueSummary {
|
||||
tenantid: number;
|
||||
tenantname: string;
|
||||
overallrevenue: number;
|
||||
locationrevenue: FiestaLocationRevenue[];
|
||||
}
|
||||
|
||||
/** /orders/getrevenuesummary?tenantid=&locationid=&fromdate=&todate= — tenant revenue and location breakdown. */
|
||||
export async function getRevenueSummary(opts: {
|
||||
tenantid: number;
|
||||
fromdate: string;
|
||||
todate: string;
|
||||
locationid?: number;
|
||||
}) {
|
||||
const res = await fiestaGet('orders/getrevenuesummary', opts);
|
||||
return firstRow<{
|
||||
tenantid: number;
|
||||
locationid: number | null;
|
||||
grossrevenue: number;
|
||||
netrevenue: number;
|
||||
profit: number;
|
||||
marginpct: number;
|
||||
ordercount: number;
|
||||
deliveredcount: number;
|
||||
cancelledcount: number;
|
||||
otifpct: number;
|
||||
avgordervalue: number;
|
||||
prev_grossrevenue: number;
|
||||
prev_ordercount: number;
|
||||
}>(res);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** /orders/getordersummary?tenantid=&locationid=&fromdate=&todate= — flat order counts. */
|
||||
export async function getOrderSummary(
|
||||
tenantid: number,
|
||||
@@ -203,6 +243,25 @@ export async function getOrders(opts: {
|
||||
);
|
||||
}
|
||||
|
||||
/** /orders/gettimeseries?tenantid=&locationid=&granularity=&fromdate=&todate= — time-series chart data */
|
||||
export async function getTimeSeries(opts: {
|
||||
tenantid: number;
|
||||
granularity: 'day' | 'month' | 'year';
|
||||
fromdate: string;
|
||||
todate: string;
|
||||
locationid?: number;
|
||||
}) {
|
||||
const res = await fiestaGet('orders/gettimeseries', opts);
|
||||
return toRows<{
|
||||
label: string;
|
||||
orders: number;
|
||||
revenue: number;
|
||||
delivered: number;
|
||||
cancelled: number;
|
||||
activeskus: number;
|
||||
}>(res);
|
||||
}
|
||||
|
||||
/** /orders/getorderdetails?orderheaderid= — line items for a single order. */
|
||||
export async function getOrderDetails(orderheaderid: number | string): Promise<Row[]> {
|
||||
let cleanId = String(orderheaderid).trim();
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
FIESTA_APPLOCATION_ID,
|
||||
FIESTA_PRIMARY_LOCATION_ID,
|
||||
getOrderSummary,
|
||||
getRevenueSummary,
|
||||
getTimeSeries,
|
||||
getLocationSummary,
|
||||
getOrderInsight,
|
||||
getOrders,
|
||||
@@ -62,6 +64,8 @@ import {
|
||||
export const fiestaKeys = {
|
||||
orderSummary: (tenantid: number, fromdate: string, todate: string, locationid?: number) =>
|
||||
['fiesta', 'orderSummary', tenantid, fromdate, todate, locationid ?? 0] as const,
|
||||
revenueSummary: (params: Record<string, unknown>) => ['fiesta', 'revenueSummary', params] as const,
|
||||
timeSeries: (params: Record<string, unknown>) => ['fiesta', 'timeSeries', params] as const,
|
||||
locationSummary: (tenantid: number) => ['fiesta', 'locationSummary', tenantid] as const,
|
||||
orderInsight: (tenantid: number) => ['fiesta', 'orderInsight', tenantid] as const,
|
||||
orders: (params: Record<string, unknown>) => ['fiesta', 'orders', params] as const,
|
||||
@@ -102,6 +106,33 @@ export function useFiestaOrderSummary(tenantid: number = FIESTA_TENANT_ID, fromd
|
||||
});
|
||||
}
|
||||
|
||||
export function useFiestaRevenueSummary(opts: {
|
||||
tenantid: number;
|
||||
fromdate: string;
|
||||
todate: string;
|
||||
locationid?: number;
|
||||
}) {
|
||||
return useQuery({
|
||||
queryKey: fiestaKeys.revenueSummary(opts as Record<string, unknown>),
|
||||
queryFn: () => getRevenueSummary(opts),
|
||||
enabled: Boolean(opts.tenantid && opts.fromdate && opts.todate),
|
||||
});
|
||||
}
|
||||
|
||||
export function useFiestaTimeSeries(opts: {
|
||||
tenantid: number;
|
||||
granularity: 'day' | 'month' | 'year';
|
||||
fromdate: string;
|
||||
todate: string;
|
||||
locationid?: number;
|
||||
}) {
|
||||
return useQuery({
|
||||
queryKey: fiestaKeys.timeSeries(opts as Record<string, unknown>),
|
||||
queryFn: () => getTimeSeries(opts),
|
||||
enabled: Boolean(opts.tenantid && opts.fromdate && opts.todate),
|
||||
});
|
||||
}
|
||||
|
||||
export function useFiestaLocationSummary(tenantid: number = FIESTA_TENANT_ID) {
|
||||
return useQuery({
|
||||
queryKey: fiestaKeys.locationSummary(tenantid),
|
||||
@@ -153,10 +184,16 @@ export function useFiestaAllOrders(opts: {
|
||||
return useQuery({
|
||||
queryKey: ['fiesta', 'allOrders', opts],
|
||||
queryFn: async () => {
|
||||
const statuses = ['created', 'pending', 'processing', 'delivered', 'cancelled'];
|
||||
const results = await Promise.all(
|
||||
statuses.map(status =>
|
||||
getOrders({
|
||||
// Include all known statuses from ORDER_STATUS_MAP to ensure we don't miss orders
|
||||
const statuses = [
|
||||
'created', 'pending', 'processing', 'delivered', 'cancelled',
|
||||
'accepted', 'assigned', 'ready', 'picked', 'active', 'arrived'
|
||||
];
|
||||
// Fetch sequentially to avoid rate-limiting or proxy dropping parallel requests
|
||||
const results: Row[][] = [];
|
||||
for (const status of statuses) {
|
||||
try {
|
||||
const res = await getOrders({
|
||||
tenantid: opts.tenantid,
|
||||
status,
|
||||
fromdate: opts.fromdate,
|
||||
@@ -164,10 +201,13 @@ export function useFiestaAllOrders(opts: {
|
||||
locationid: opts.locationid,
|
||||
applocationid: opts.applocationid,
|
||||
keyword: opts.keyword,
|
||||
pagesize: 100,
|
||||
}).catch(() => [] as Row[])
|
||||
)
|
||||
);
|
||||
pagesize: 500,
|
||||
});
|
||||
results.push(res);
|
||||
} catch (e) {
|
||||
results.push([]);
|
||||
}
|
||||
}
|
||||
// Merge and deduplicate by orderid/orderheaderid
|
||||
const merged: Row[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
14
test-api.ts
Normal file
14
test-api.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { getOrderInsight } from './src/services/fiestaApi.ts';
|
||||
import { fiestaGet } from './src/services/fiestaApi.ts';
|
||||
|
||||
async function test() {
|
||||
const res = await fiestaGet('orders/gettimeseries', {
|
||||
tenantid: 1087,
|
||||
granularity: 'day',
|
||||
fromdate: '2026-05-15',
|
||||
todate: '2026-06-15'
|
||||
});
|
||||
console.log(JSON.stringify(res, null, 2));
|
||||
}
|
||||
|
||||
test();
|
||||
14
test.js
Normal file
14
test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const https = require('https');
|
||||
|
||||
const url = 'https://fiesta.nearle.app/live/api/v1/web/orders/gettimeseries?tenantid=1087&granularity=day&fromdate=2026-05-15&todate=2026-06-15';
|
||||
|
||||
https.get(url, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => { data += chunk; });
|
||||
res.on('end', () => {
|
||||
console.log("STATUS:", res.statusCode);
|
||||
console.log("RESPONSE:", data.substring(0, 500));
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
console.error("ERROR:", err.message);
|
||||
});
|
||||
Reference in New Issue
Block a user