e2e testing

This commit is contained in:
2026-06-15 19:17:13 +05:30
parent 896561245d
commit 01685f14c2
18 changed files with 1102 additions and 198 deletions

419
package-lock.json generated
View File

@@ -22,6 +22,7 @@
"react": "^19.0.1", "react": "^19.0.1",
"react-dom": "^19.0.1", "react-dom": "^19.0.1",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"recharts": "^3.8.1",
"vite": "^6.2.3" "vite": "^6.2.3"
}, },
"devDependencies": { "devDependencies": {
@@ -63,6 +64,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.7", "@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7", "@babel/generator": "^7.29.7",
@@ -857,6 +859,42 @@
"react-dom": "^19.0.0" "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": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.3", "version": "1.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
@@ -1188,6 +1226,18 @@
"win32" "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": { "node_modules/@tailwindcss/node": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
@@ -1533,6 +1583,69 @@
"@types/node": "*" "@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": { "node_modules/@types/estree": {
"version": "1.0.9", "version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@@ -1656,6 +1769,12 @@
"@types/node": "*" "@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": { "node_modules/@vitejs/plugin-react": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
@@ -1840,6 +1959,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.10.12", "baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782", "caniuse-lite": "^1.0.30001782",
@@ -1918,6 +2038,15 @@
], ],
"license": "CC-BY-4.0" "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": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -1960,6 +2089,127 @@
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT" "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": { "node_modules/data-uri-to-buffer": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "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": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -2113,6 +2369,16 @@
"node": ">= 0.4" "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": { "node_modules/esbuild": {
"version": "0.25.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -2178,6 +2444,12 @@
"node": ">= 0.6" "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": { "node_modules/express": {
"version": "4.22.2", "version": "4.22.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
@@ -2599,12 +2871,31 @@
"node": ">=0.10.0" "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": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC" "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": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -2687,7 +2978,8 @@
"version": "1.9.4", "version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause" "license": "BSD-2-Clause",
"peer": true
}, },
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.32.0", "version": "1.32.0",
@@ -3224,6 +3516,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -3250,6 +3543,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.12", "nanoid": "^3.3.12",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -3356,6 +3650,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -3365,6 +3660,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -3372,6 +3668,13 @@
"react": "^19.2.7" "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": { "node_modules/react-leaflet": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
@@ -3386,6 +3689,30 @@
"react-dom": "^19.0.0" "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": { "node_modules/react-refresh": {
"version": "0.18.0", "version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
@@ -3395,6 +3722,58 @@
"node": ">=0.10.0" "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": { "node_modules/retry": {
"version": "0.13.1", "version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
@@ -3658,6 +4037,12 @@
"url": "https://opencollective.com/webpack" "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": { "node_modules/tinyglobby": {
"version": "0.2.17", "version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -4238,6 +4623,15 @@
"browserslist": ">= 4.21.0" "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": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -4256,11 +4650,34 @@
"node": ">= 0.8" "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": { "node_modules/vite": {
"version": "6.4.3", "version": "6.4.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
"integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4", "fdir": "^6.4.4",

View File

@@ -25,6 +25,7 @@
"react": "^19.0.1", "react": "^19.0.1",
"react-dom": "^19.0.1", "react-dom": "^19.0.1",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"recharts": "^3.8.1",
"vite": "^6.2.3" "vite": "^6.2.3"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -541,7 +541,7 @@ export default function App() {
} }
case 'settings': case 'settings':
return <SettingsView tenantId={tenantId} />; return <SettingsView tenantId={tenantId} user={authUser ?? undefined} />;
default: default:
return null; return null;

View File

@@ -35,7 +35,8 @@ const str = (v: unknown): string => (v == null ? '' : String(v));
export default function DashboardView({ searchQuery, tenantId = FIESTA_TENANT_ID }: DashboardViewProps) { export default function DashboardView({ searchQuery, tenantId = FIESTA_TENANT_ID }: DashboardViewProps) {
// Live data — month-to-date order summary + tenant identity + store locations. // Live data — month-to-date order summary + tenant identity + store locations.
const today = new Date(); 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 fromdate = ymd(monthStart);
const todate = ymd(today); 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 // 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 // repeating one. When the tenant has no invoice records we show "—" instead of a
// misleading ₹0. // 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 money = (v: number | null) => (v == null ? '—' : `${Math.round(v).toLocaleString('en-IN')}`);
const monthlyRevenue = insight ? insight.revenue : null; const monthlyRevenue = insight ? Number(insight.grossrevenue || insight.overallrevenue || insight.revenue || 0) : null;
const monthlyProfit = insight ? insight.profit : null; const monthlyProfit = insight ? Number(insight.profit || insight.netrevenue || insight.margin || 0) : null;
const locSummaryQ = useFiestaLocationSummary(tenantId); const locSummaryQ = useFiestaLocationSummary(tenantId);
const summaries = locSummaryQ.data ?? []; const summaries = locSummaryQ.data ?? [];

View File

@@ -15,10 +15,11 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { TrendingUp, Clock, CheckCircle2, IndianRupee, Bike, Truck, Calendar, Store } from 'lucide-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 { FIESTA_TENANT_ID, num as fnum, str as fstr, ymd, type Row } from '../services/fiestaApi';
import { 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, DELIVERY_STATUS, statusColor, BRAND, BRAND_LIGHT, TEXT, TEXT_2, TEXT_3, BORDER, DIVIDER, SURFACE_ALT, tint, soft, edge, ring,
} from './consoleUi'; } from './consoleUi';
@@ -28,9 +29,9 @@ const TABS: Array<{ key: ReportTab; label: string; icon: typeof TrendingUp }> =
{ key: 'riders-summary', label: 'Riders Summary', icon: Bike }, { 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 today = new Date();
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1); const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
const [fromdate, setFromdate] = useState<string>(ymd(monthStart)); 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'; const activePreset = presets.find((p) => p.from === fromdate && p.to === todate)?.key ?? 'custom';
return ( return (
<div className="animate-in fade-in duration-300"> <div className="animate-in fade-in duration-300">
<GradientHeader title="Reports" subtitle="Operational analytics across outlets, riders, and the full order lifecycle." /> <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> </div>
</FilterBar> </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> </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>); 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>
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>
);
}
// ── Orders Summary (per outlet) ────────────────────────────────────────────────── // ── 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 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 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} /> }, // Compute trend data
{ label: 'Pending', value: totals.pending.toLocaleString('en-IN'), color: '#f59e0b', icon: <Clock size={20} /> }, const chartData = useMemo(() => {
{ label: 'Delivered', value: totals.delivered.toLocaleString('en-IN'), color: '#10b981', icon: <CheckCircle2 size={20} /> }, const map = new Map<string, { date: string; orders: number; revenue: number }>();
{ label: 'Outlets', value: rows.length.toLocaleString('en-IN'), color: '#0ea5e9', icon: <Store size={20} /> }, 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 ( return (
<div className="space-y-4"> <div className="space-y-6">
<KpiStrip items={kpis} loading={q.isLoading} /> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<TableShell minWidth={820} head={['#', 'Outlet', 'All', 'Created', 'Pending', 'Processing', 'Delivered', 'Cancelled']} <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'}`}>
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}> <div className={`p-3 rounded-xl ${metric === 'revenue' ? 'bg-indigo-100 text-indigo-600' : 'bg-slate-50 text-slate-400'}`}><IndianRupee size={20} /></div>
{q.isLoading ? <tr><td colSpan={8} className="px-3 py-12 text-center text-xs" style={{ color: TEXT_3 }}>Loading outlet summary</td></tr> <div>
: 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> <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) => ( : 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')}> <tr key={r.locationid || i} className="hover:bg-slate-50 transition-colors">
<td className="px-3 py-2.5 font-mono" style={{ color: TEXT_3 }}>{i + 1}</td> <td className="px-5 py-3 text-slate-400 font-medium">{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-5 py-3 font-semibold text-slate-900">{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-5 py-3 text-right font-bold text-indigo-600">{(locationRevenueMap.get(fnum(r.locationid)) ?? 0).toLocaleString('en-IN')}</td>
<td className="px-3 py-2.5 text-right"><Cnt n={r.created} color="#0ea5e9" /></td> <td className="px-5 py-3 text-right font-bold text-slate-900">{r.total.toLocaleString('en-IN')}</td>
<td className="px-3 py-2.5 text-right"><Cnt n={r.pending} color="#f59e0b" /></td> <td className="px-5 py-3 text-right"><Cnt n={r.created} color="#0ea5e9" /></td>
<td className="px-3 py-2.5 text-right"><Cnt n={r.processing} color="#6366f1" /></td> <td className="px-5 py-3 text-right"><Cnt n={r.pending} color="#f59e0b" /></td>
<td className="px-3 py-2.5 text-right"><Cnt n={r.delivered} color="#10b981" /></td> <td className="px-5 py-3 text-right"><Cnt n={r.processing} color="#6366f1" /></td>
<td className="px-3 py-2.5 text-right"><Cnt n={r.cancelled} color="#ef4444" /></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> </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> </div>
); );
} }
// ── Riders Summary (per rider) ─────────────────────────────────────────────────── // ── Riders Summary (per rider) ───────────────────────────────────────────────────
function RidersSummaryReport({ fromdate, todate, tenantId }: { fromdate: string; todate: string; tenantId: number }) { function RidersSummaryReport({ fromdate, todate, tenantId, locationid }: { fromdate: string; todate: string; tenantId: number; locationid?: number }) {
const q = useFiestaFleetSummary({ tenantid: tenantId, fromdate, todate }); const q = useFiestaFleetSummary({ tenantid: tenantId, fromdate, todate, applocationid: locationid });
const rows = q.data ?? []; const rows = q.data ?? [];
const mapped = rows.map((r) => ({ const mapped = rows.map((r) => ({
name: fstr(r.fullname) || `${fstr(r.firstname)} ${fstr(r.lastname)}`.trim() || fstr(r.username) || `Rider ${fstr(r.userid)}`, name: fstr(r.fullname) || `${fstr(r.firstname)} ${fstr(r.lastname)}`.trim() || fstr(r.username) || `Rider ${fstr(r.userid)}`,

View File

@@ -90,7 +90,6 @@ function pickupLatLon(r: Row): [number, number] | null {
type ViewMode = 'kitchens' | 'zones' | 'riders'; type ViewMode = 'kitchens' | 'zones' | 'riders';
const VIEW_TABS: Array<{ id: ViewMode; label: string; icon: typeof MapIcon }> = [ const VIEW_TABS: Array<{ id: ViewMode; label: string; icon: typeof MapIcon }> = [
{ id: 'kitchens', label: 'By Location', icon: MapPin }, { id: 'kitchens', label: 'By Location', icon: MapPin },
{ id: 'zones', label: 'By Zone', icon: MapIcon },
{ id: 'riders', label: 'By Rider', icon: Bike }, { id: 'riders', label: 'By Rider', icon: Bike },
]; ];
@@ -294,7 +293,6 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
</span> </span>
</div> </div>
</div> </div>
{headerTabs}
</div> </div>
<div className="hdr-stats"> <div className="hdr-stats">
@@ -359,6 +357,11 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
</button> </button>
); );
})} })}
{headerTabs && (
<div style={{ marginLeft: 8 }}>
{headerTabs}
</div>
)}
</div> </div>
{/* ── Body ── */} {/* ── Body ── */}

View File

@@ -58,6 +58,9 @@ export default function OrdersDeliveriesView({
}: OrdersDeliveriesViewProps) { }: OrdersDeliveriesViewProps) {
const todayStr = ymd(new Date()); const todayStr = ymd(new Date());
const [fromdate, setFromdate] = useState(todayStr);
const [todate, setTodate] = useState(todayStr);
const [status, setStatus] = useState<StatusKey>('all'); const [status, setStatus] = useState<StatusKey>('all');
const [pageno, setPageno] = useState(1); const [pageno, setPageno] = useState(1);
const [localSearch, setLocalSearch] = useState(''); const [localSearch, setLocalSearch] = useState('');
@@ -75,8 +78,8 @@ export default function OrdersDeliveriesView({
}, []); }, []);
// ── Queries ────────────────────────────────────────────────────────────────── // ── Queries ──────────────────────────────────────────────────────────────────
const allOrdersQ = useFiestaAllOrders({ tenantid: tenantId, fromdate: todayStr, todate: todayStr, locationid }); const allOrdersQ = useFiestaAllOrders({ tenantid: tenantId, fromdate, todate, locationid });
const summaryQ = useFiestaDeliverySummary({ tenantid: tenantId, fromdate: todayStr, todate: todayStr, locationid }); const summaryQ = useFiestaDeliverySummary({ tenantid: tenantId, fromdate, todate, locationid });
const ridersQ = useFiestaRiders({ tenantid: tenantId }); const ridersQ = useFiestaRiders({ tenantid: tenantId });
const allRows = allOrdersQ.data ?? []; const allRows = allOrdersQ.data ?? [];
@@ -100,7 +103,11 @@ export default function OrdersDeliveriesView({
const rows = useMemo(() => { const rows = useMemo(() => {
const term = (localSearch || searchQuery).toLowerCase(); const term = (localSearch || searchQuery).toLowerCase();
return allRows.filter((r) => { 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 (status !== 'all' && fstr(r.orderstatus).toLowerCase() !== status) return false;
if (!term) return true; if (!term) return true;
return [ 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 blob = new Blob([[headers.join(','), ...lines].join('\n')], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; 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); 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`} /> : <LiveStatus state="live" label={`Live · ${total.toLocaleString('en-IN')} orders in range · ${activeFleet} riders on duty`} />
} }
right={ 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" <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 }}> 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'} <MapPin size={13} /> {locationid ? `Location ${locationid}` : 'All Locations'}
</span> </span>
</div>
} }
/> />

View File

@@ -213,8 +213,10 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI
// ── Selection helpers ─────────────────────────────────────────────────────── // ── Selection helpers ───────────────────────────────────────────────────────
const rowKey = (r: Row) => fstr(r.orderheaderid) || fstr(r.orderid); const rowKey = (r: Row) => fstr(r.orderheaderid) || fstr(r.orderid);
const pageKeys = rows.map(rowKey); const assignableRows = rows.filter((r) => fstr(r.orderstatus).toLowerCase() === 'created');
const allSelected = pageKeys.length > 0 && pageKeys.every((k) => selected.has(k)); const assignableKeys = assignableRows.map(rowKey);
const allSelected = assignableKeys.length > 0 && assignableKeys.every((k) => selected.has(k));
const toggleRow = (k: string) => const toggleRow = (k: string) =>
setSelected((prev) => { setSelected((prev) => {
const n = new Set(prev); const n = new Set(prev);
@@ -222,11 +224,12 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI
else n.add(k); else n.add(k);
return n; return n;
}); });
const toggleAll = () => const toggleAll = () =>
setSelected((prev) => { setSelected((prev) => {
const n = new Set(prev); const n = new Set(prev);
if (allSelected) pageKeys.forEach((k) => n.delete(k)); if (allSelected) assignableKeys.forEach((k) => n.delete(k));
else pageKeys.forEach((k) => n.add(k)); else assignableKeys.forEach((k) => n.add(k));
return n; return n;
}); });
@@ -383,7 +386,7 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI
<thead> <thead>
<tr> <tr>
<th className="px-3 py-2.5 text-left" style={TH_STYLE}> <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> </th>
{['#', 'Order', 'Branch', 'Pickup', 'Drop', 'Qty', 'COD', 'KMs', 'Charges', 'Status', ''].map((h, i) => ( {['#', '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> <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' }} <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'; }}> 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"> <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" 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>
<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 font-mono" style={{ color: TEXT_3 }}>{(pageno - 1) * PAGE_SIZE + i + 1}</td>
<td className="px-3 py-2.5"> <td className="px-3 py-2.5">

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { import {
TrendingUp, TrendingUp,
TrendingDown, TrendingDown,
@@ -29,6 +29,8 @@ import {
useFiestaLocationSummary, useFiestaLocationSummary,
useFiestaOrderInsight, useFiestaOrderInsight,
useFiestaStockStatement, useFiestaStockStatement,
useFiestaRevenueSummary,
useFiestaTimeSeries,
} from '../services/fiestaQueries'; } from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID, num as fnum, str as fstr, ymd } from '../services/fiestaApi'; import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID, num as fnum, str as fstr, ymd } from '../services/fiestaApi';
import { stockRowToProduct } from '../services/fiestaMappers'; 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 [selectedCell, setSelectedCell] = useState<{ day: string; hour: string; val: number } | null>(null);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [chartMetric, setChartMetric] = useState<'orders' | 'revenue' | 'cancelled' | 'skus'>('orders'); 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 [expandedProductId, setExpandedProductId] = useState<string | null>(null);
const [exportingFormat, setExportingFormat] = useState<'PDF' | 'CSV' | null>(null); const [exportingFormat, setExportingFormat] = useState<'PDF' | 'CSV' | null>(null);
const [exportProgress, setExportProgress] = useState(0); 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 prevSummaryQ = useFiestaOrderSummary(tenantId, ymd(prevStart), ymd(prevEnd));
const locSummaryQ = useFiestaLocationSummary(tenantId); const locSummaryQ = useFiestaLocationSummary(tenantId);
const insightQ = useFiestaOrderInsight(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({ const stockQ = useFiestaStockStatement({
tenantid: tenantId, tenantid: tenantId,
locationid: FIESTA_PRIMARY_LOCATION_ID, 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 ordersDelta = s && prevS ? pctChange(s.total, prevS.total) : null;
const cancelledDelta = s && prevS ? pctChange(s.cancelled, prevS.cancelled) : 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)}%`; const fmtDelta = (d: number) => `${d >= 0 ? '+' : ''}${d.toFixed(1)}%`;
// Dynamic sparkline generator helper // Dynamic sparkline generator helper
@@ -185,16 +195,15 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
awaiting: false, awaiting: false,
}, },
{ {
// Revenue: no revenue API ([R1]) — render AwaitingApi instead of a value.
id: 'revenue' as const, id: 'revenue' as const,
title: 'Revenue', title: 'Revenue',
value: '', value: `${(revS?.grossrevenue ?? 0).toLocaleString('en-IN')}`,
trend: null, trend: revenueDelta !== null ? fmtDelta(revenueDelta) : null,
status: '', status: `${(revS?.avgordervalue ?? 0).toLocaleString('en-IN')} avg. order`,
isPositive: true, isPositive: revenueDelta === null ? true : revenueDelta >= 0,
spark: [20, 30, 25, 45, 40, 55, 50, 68], spark: [20, 30, 25, 45, 40, 55, 50, 68],
color: 'emerald', color: 'emerald',
awaiting: true, awaiting: false,
}, },
{ {
id: 'cancelled' as const, id: 'cancelled' as const,
@@ -331,6 +340,55 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
}, 120); }, 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 ( return (
<div className="space-y-lg animate-in fade-in duration-500 font-sans relative"> <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> </div>
<div className="mt-2"> <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> </div>
</div> </div>
@@ -579,7 +640,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
<button <button
key={kpi.id} key={kpi.id}
onClick={() => setChartMetric(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 */} {/* Subtle top glowing line when active */}
{isActive && ( {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"> <span className="text-[10px] font-sans font-bold text-zinc-400 uppercase tracking-widest block">
{kpi.title} {kpi.title}
</span> </span>
<div className={`w-8 h-8 rounded-full flex items-center justify-center transition-transform duration-300 group-hover:scale-110 ${iconBg} ${iconTextColor}`}> <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={14} /> <IconComponent size={13} />
</div> </div>
</div> </div>
{/* Main Metric Value and Trend Badge */} {/* Main Metric Value and Trend Badge */}
{kpi.awaiting ? ( {kpi.awaiting ? (
<div className="mt-3"> <div className="mt-1">
<AwaitingApi label="Revenue" api="[R1]" compact /> <AwaitingApi label="Revenue" api="[R1]" compact />
</div> </div>
) : ( ) : (
<div className="mt-3 flex items-baseline gap-2"> <div className="mt-1.5 flex items-baseline gap-2">
<div className="font-sans font-black text-slate-900 text-3xl tracking-tight leading-none"> <div className="font-sans font-black text-slate-900 text-2xl tracking-tight leading-none">
{kpi.value} {kpi.value}
</div> </div>
{kpi.trend && ( {kpi.trend && (
@@ -617,7 +678,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
)} )}
{/* Bottom Sparkline & Subtext segment */} {/* 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> <span className="text-[10px] text-zinc-550 font-bold uppercase tracking-wider">{kpi.status}</span>
{/* KPI Mini Sparkline Chart */} {/* KPI Mini Sparkline Chart */}
@@ -657,19 +718,94 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
</h3> </h3>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2 items-center">
<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 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 className="w-1.5 h-1.5 rounded-full bg-current animate-pulse" style={{ color: theme.activeLine }} />
<span>Live Sync</span> <span>Live Sync</span>
</span> </span>
</div> </div>
</div> </div>
{/* Plotted Area — no time-series API ([R2]) for orders/revenue/skus. {/* Plotted Area */}
The metric tabs (KPI cards above) still switch the card title; the <div className="relative h-64 select-none w-full flex items-end pt-8 pb-4 px-2">
chart body itself shows the awaiting-backend placeholder. */} <svg className="absolute inset-0 w-full h-full preserve-3d" preserveAspectRatio="none">
<div className="relative h-64 select-none w-full flex items-center justify-center"> <defs>
<AwaitingApi label="Orders & revenue time-series" api="[R2]" className="w-full h-full justify-center" /> <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>
</div> </div>

View File

@@ -23,6 +23,7 @@ import { FIESTA_TENANT_ID, str as fstr, num as fnum, roleName } from '../service
import UsersPanel from './UsersPanel'; import UsersPanel from './UsersPanel';
import AwaitingApi from './AwaitingApi'; import AwaitingApi from './AwaitingApi';
import AdminConsole from './AdminConsole'; import AdminConsole from './AdminConsole';
import type { AuthUser } from '../services/auth';
type TabKey = 'profile' | 'outlets' | 'users'; type TabKey = 'profile' | 'outlets' | 'users';
@@ -122,9 +123,10 @@ function Row({
export interface SettingsViewProps { export interface SettingsViewProps {
tenantId?: number; 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'); const [activeTab, setActiveTab] = useState<TabKey>('profile');
// Live tenant profile + outlets. // 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"> <div className="relative z-10 flex items-center gap-3.5">
{/* Initials avatar badge with glowing ring */} {/* 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"> <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>
<div className="min-w-0"> <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> <p className="text-slate-400 text-[10px] font-mono mt-1 truncate uppercase tracking-wider">Store ID: #{tenantId}</p>
</div> </div>
</div> </div>
@@ -306,7 +308,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
</div> </div>
<div className="min-w-0 flex-1 leading-relaxed"> <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"> <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. Set up your store logo, customer service email, and contact number. Official registration details are synced with your primary credentials.
</span> </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="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"> <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> <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>
<div className="p-4 bg-white rounded-xl border border-slate-200/60 shadow-sm"> <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> <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>
<div className="p-4 bg-white rounded-xl border border-slate-200/60 shadow-sm"> <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> <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"> <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 ${ <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> </p>
</div> </div>
<div className="p-4 bg-white rounded-xl border border-slate-200/60 shadow-sm flex items-center justify-between"> <div className="p-4 bg-white rounded-xl border border-slate-200/60 shadow-sm flex items-center justify-between">
<div> <div>
<span className="text-[10px] text-slate-400 uppercase font-black tracking-wider block">Store Verification</span> <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> </div>
<span className={`px-2.5 py-1 rounded-lg text-[9px] font-black uppercase tracking-wider ${ <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-emerald-700 bg-emerald-50 border border-emerald-100/50'
: 'text-zinc-555 bg-zinc-100' : 'text-zinc-555 bg-zinc-100'
}`}> }`}>
{fnum(tenant?.approved) === 1 ? 'Verified' : 'Pending'} {fnum(tenant?.approved) === 1 || user ? 'Verified' : 'Pending'}
</span> </span>
</div> </div>
</div> </div>
@@ -354,7 +356,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
<div> <div>
<span className="text-[10px] text-slate-400 uppercase font-black tracking-wider block">Registered Address</span> <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"> <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)}` : ''} {tenant?.city ? ` · ${fstr(tenant.city)}, ${fstr(tenant.state)} ${fstr(tenant.postcode)}` : ''}
</p> </p>
</div> </div>
@@ -371,7 +373,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
</label> </label>
<input <input
type="email" type="email"
value={fstr(tenant?.primaryemail) || ''} value={user?.email || fstr(tenant?.primaryemail) || ''}
readOnly 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" 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="—" placeholder="—"
@@ -383,7 +385,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
</label> </label>
<input <input
type="text" type="text"
value={fstr(tenant?.primarycontact) || ''} value={user?.contactno || fstr(tenant?.primarycontact) || ''}
readOnly 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" 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="—" placeholder="—"

View File

@@ -41,10 +41,13 @@ import {
useFiestaTenantCustomers, useFiestaTenantCustomers,
useFiestaCustomerOrders, useFiestaCustomerOrders,
useFiestaRiders, useFiestaRiders,
useFiestaRevenueSummary,
useFiestaAllOrders,
FIESTA_TENANT_ID FIESTA_TENANT_ID
} from '../services/fiestaQueries'; } 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 { 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 ragulStoreCover from '../assets/images/store_front_view_1780299351800.png';
import OrdersDeliveriesView from './OrdersDeliveriesView'; import OrdersDeliveriesView from './OrdersDeliveriesView';
import StoreQRView from './StoreQRView'; import StoreQRView from './StoreQRView';
@@ -163,6 +166,40 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
customerid: selectedCustomer?.id ?? null, customerid: selectedCustomer?.id ?? null,
pagesize: 20 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 customerOrderHistory = (customerOrdersQ.data ?? []).map((row: any) => {
const amount = fnum(row.orderamount) || fnum(row.amount); const amount = fnum(row.orderamount) || fnum(row.amount);
return { 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="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="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"> <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> </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"> <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>
</div> </div>
@@ -547,7 +588,11 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
</div> </div>
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block">Est. Revenue</span> <span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block">Est. Revenue</span>
<div className="mt-xs"> <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>
</div> </div>
@@ -591,9 +636,48 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
</div> </div>
</div> </div>
{/* Intraday dispatch breakdown has no backend yet ([R10]). */} {/* Intraday dispatch breakdown */}
<div className="my-xl"> <div className="my-md h-64">
<AwaitingApi label="Intraday dispatch breakdown" api="[R10]" /> {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>
</div> </div>
@@ -643,65 +727,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
</div> </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> </div>
)} )}

View File

@@ -191,7 +191,7 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
// Logistics console — scoped to this user's store. These views own their // 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. // 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 === '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 // 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 // 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). // gating below — only "My Store Inventory" uses the resolved location id).

View File

@@ -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' }, 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' }, 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' }, 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' }, 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(() => { 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]); }, [users]);
const handleCreateUser = async (e: React.FormEvent) => { const handleCreateUser = async (e: React.FormEvent) => {
@@ -277,32 +280,32 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
</div> </div>
{/* Search & Filter Utility Bar */} {/* 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="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 group"> <div className="relative w-full md:max-w-md shrink-0">
<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"> <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 group-focus-within:text-purple-600 transition-colors"> <span className="pl-4 flex items-center pointer-events-none text-slate-400 transition-colors">
<Search className="w-4.5 h-4.5" /> <Search size={18} strokeWidth={2.5} />
</span> </span>
<input <input
type="text" type="text"
placeholder="Search team members by name, email, phone..." placeholder="Search team members..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} 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 && ( {search && (
<button <button
onClick={() => setSearch('')} 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> </button>
)} )}
</div> </div>
</div> </div>
{/* Role filter capsules */} {/* 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 <button
onClick={() => setUserRoleFilter('ALL')} 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 ${ 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 === 2 && <Shield size={12} />}
{u.roleid === 3 && <SlidersHorizontal size={12} />} {u.roleid === 3 && <SlidersHorizontal size={12} />}
{u.roleid === 4 && <User size={12} />} {u.roleid === 4 && <User size={12} />}
{u.roleid === 5 && <Bike size={12} />}
{u.roleid === 6 && <Coins size={12} />} {u.roleid === 6 && <Coins size={12} />}
{roleInfo.label} {roleInfo.label}
</span> </span>

View File

@@ -131,6 +131,8 @@ export interface KpiItem {
color: string; color: string;
icon: React.ReactNode; icon: React.ReactNode;
badge?: string; badge?: string;
onClick?: () => void;
active?: boolean;
} }
export function KpiStrip({ items, loading }: { items: KpiItem[]; loading?: boolean }) { export function KpiStrip({ items, loading }: { items: KpiItem[]; loading?: boolean }) {
return ( return (
@@ -138,10 +140,11 @@ export function KpiStrip({ items, loading }: { items: KpiItem[]; loading?: boole
{items.map((it) => ( {items.map((it) => (
<div <div
key={it.label} 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" onClick={it.onClick}
style={{ borderColor: BORDER }} 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' : ''}`}
onMouseEnter={(e) => (e.currentTarget.style.boxShadow = SHADOW_MD)} style={{ borderColor: it.active ? it.color : BORDER, boxShadow: it.active ? `0 0 0 1px ${it.color}, ${SHADOW_MD}` : 'none' }}
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = '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="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"> <div className="flex items-start justify-between gap-2">
@@ -303,3 +306,25 @@ export const TH_STYLE: React.CSSProperties = {
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
borderBottom: `1px solid ${BORDER}`, 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>
);
}

View File

@@ -125,6 +125,46 @@ export interface FiestaOrderSummary {
tenantname?: string; 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. */ /** /orders/getordersummary?tenantid=&locationid=&fromdate=&todate= — flat order counts. */
export async function getOrderSummary( export async function getOrderSummary(
tenantid: number, 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. */ /** /orders/getorderdetails?orderheaderid= — line items for a single order. */
export async function getOrderDetails(orderheaderid: number | string): Promise<Row[]> { export async function getOrderDetails(orderheaderid: number | string): Promise<Row[]> {
let cleanId = String(orderheaderid).trim(); let cleanId = String(orderheaderid).trim();

View File

@@ -20,6 +20,8 @@ import {
FIESTA_APPLOCATION_ID, FIESTA_APPLOCATION_ID,
FIESTA_PRIMARY_LOCATION_ID, FIESTA_PRIMARY_LOCATION_ID,
getOrderSummary, getOrderSummary,
getRevenueSummary,
getTimeSeries,
getLocationSummary, getLocationSummary,
getOrderInsight, getOrderInsight,
getOrders, getOrders,
@@ -62,6 +64,8 @@ import {
export const fiestaKeys = { export const fiestaKeys = {
orderSummary: (tenantid: number, fromdate: string, todate: string, locationid?: number) => orderSummary: (tenantid: number, fromdate: string, todate: string, locationid?: number) =>
['fiesta', 'orderSummary', tenantid, fromdate, todate, locationid ?? 0] as const, ['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, locationSummary: (tenantid: number) => ['fiesta', 'locationSummary', tenantid] as const,
orderInsight: (tenantid: number) => ['fiesta', 'orderInsight', tenantid] as const, orderInsight: (tenantid: number) => ['fiesta', 'orderInsight', tenantid] as const,
orders: (params: Record<string, unknown>) => ['fiesta', 'orders', params] 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) { export function useFiestaLocationSummary(tenantid: number = FIESTA_TENANT_ID) {
return useQuery({ return useQuery({
queryKey: fiestaKeys.locationSummary(tenantid), queryKey: fiestaKeys.locationSummary(tenantid),
@@ -153,10 +184,16 @@ export function useFiestaAllOrders(opts: {
return useQuery({ return useQuery({
queryKey: ['fiesta', 'allOrders', opts], queryKey: ['fiesta', 'allOrders', opts],
queryFn: async () => { queryFn: async () => {
const statuses = ['created', 'pending', 'processing', 'delivered', 'cancelled']; // Include all known statuses from ORDER_STATUS_MAP to ensure we don't miss orders
const results = await Promise.all( const statuses = [
statuses.map(status => 'created', 'pending', 'processing', 'delivered', 'cancelled',
getOrders({ '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, tenantid: opts.tenantid,
status, status,
fromdate: opts.fromdate, fromdate: opts.fromdate,
@@ -164,10 +201,13 @@ export function useFiestaAllOrders(opts: {
locationid: opts.locationid, locationid: opts.locationid,
applocationid: opts.applocationid, applocationid: opts.applocationid,
keyword: opts.keyword, keyword: opts.keyword,
pagesize: 100, pagesize: 500,
}).catch(() => [] as Row[]) });
) results.push(res);
); } catch (e) {
results.push([]);
}
}
// Merge and deduplicate by orderid/orderheaderid // Merge and deduplicate by orderid/orderheaderid
const merged: Row[] = []; const merged: Row[] = [];
const seen = new Set<string>(); const seen = new Set<string>();

14
test-api.ts Normal file
View 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
View 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);
});