like mobile added the functionality
This commit is contained in:
parent
b5e84a34f7
commit
4d7beaef0f
246
package-lock.json
generated
246
package-lock.json
generated
@ -8,6 +8,8 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"jspdf": "^4.2.1",
|
||||||
|
"jspdf-autotable": "^5.0.8",
|
||||||
"lucide-react": "^0.400.0",
|
"lucide-react": "^0.400.0",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
@ -223,6 +225,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||||
@ -1257,6 +1268,19 @@
|
|||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pako": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/raf": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.17",
|
"version": "19.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
||||||
@ -1277,6 +1301,13 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@types/use-sync-external-store": {
|
"node_modules/@types/use-sync-external-store": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
@ -1602,6 +1633,16 @@
|
|||||||
"node": "18 || 20 || >=22"
|
"node": "18 || 20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-arraybuffer": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.41",
|
"version": "2.10.41",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.41.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.41.tgz",
|
||||||
@ -1683,6 +1724,26 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/canvg": {
|
||||||
|
"version": "3.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||||
|
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/raf": "^3.4.0",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"raf": "^3.4.1",
|
||||||
|
"regenerator-runtime": "^0.13.7",
|
||||||
|
"rgbcolor": "^1.0.1",
|
||||||
|
"stackblur-canvas": "^2.0.0",
|
||||||
|
"svg-pathdata": "^6.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/clsx": {
|
"node_modules/clsx": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
@ -1712,6 +1773,18 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/core-js": {
|
||||||
|
"version": "3.49.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
|
||||||
|
"integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/core-js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@ -1727,6 +1800,16 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-line-break": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@ -1896,6 +1979,16 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.4.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
|
||||||
|
"integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optional": true,
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.385",
|
"version": "1.5.385",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.385.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.385.tgz",
|
||||||
@ -2162,6 +2255,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-png": {
|
||||||
|
"version": "6.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
|
||||||
|
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/pako": "^2.0.3",
|
||||||
|
"iobuffer": "^5.3.2",
|
||||||
|
"pako": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
@ -2180,6 +2284,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
|
||||||
|
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
@ -2306,6 +2416,20 @@
|
|||||||
"hermes-estree": "0.25.1"
|
"hermes-estree": "0.25.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html2canvas": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"css-line-break": "^2.1.0",
|
||||||
|
"text-segmentation": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@ -2345,6 +2469,12 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/iobuffer": {
|
||||||
|
"version": "5.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||||
|
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@ -2439,6 +2569,32 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jspdf": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"fast-png": "^6.2.0",
|
||||||
|
"fflate": "^0.8.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"canvg": "^3.0.11",
|
||||||
|
"core-js": "^3.6.0",
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
|
"html2canvas": "^1.0.0-rc.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jspdf-autotable": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-Hy05N86yBO7CXBrnSLOge7i1ZYpKH2DjQ94iybaP7vBhSInjvRBgDc99ngKzSbSO8Jc98ZCally8I6n0tj2RJQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"jspdf": "^2 || ^3 || ^4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@ -2878,6 +3034,22 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nodeca"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/path-exists": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@ -2898,6 +3070,13 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/performance-now": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@ -2967,6 +3146,16 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/raf": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"performance-now": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.7",
|
"version": "19.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
||||||
@ -3101,12 +3290,29 @@
|
|||||||
"redux": "^5.0.0"
|
"redux": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.13.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/reselect": {
|
"node_modules/reselect": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz",
|
||||||
"integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==",
|
"integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/rgbcolor": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||||
|
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
|
||||||
@ -3196,6 +3402,26 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stackblur-canvas": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.1.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svg-pathdata": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz",
|
||||||
@ -3217,6 +3443,16 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/text-segmentation": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tiny-invariant": {
|
"node_modules/tiny-invariant": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
@ -3369,6 +3605,16 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/utrie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"base64-arraybuffer": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/victory-vendor": {
|
"node_modules/victory-vendor": {
|
||||||
"version": "37.3.6",
|
"version": "37.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||||
|
|||||||
@ -10,6 +10,8 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"jspdf": "^4.2.1",
|
||||||
|
"jspdf-autotable": "^5.0.8",
|
||||||
"lucide-react": "^0.400.0",
|
"lucide-react": "^0.400.0",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
|
|||||||
@ -3,9 +3,11 @@ import { AuthProvider } from './auth/AuthProvider'
|
|||||||
import { LoginPage } from './screens/LoginPage'
|
import { LoginPage } from './screens/LoginPage'
|
||||||
import { ConsoleLayout } from './screens/ConsoleLayout'
|
import { ConsoleLayout } from './screens/ConsoleLayout'
|
||||||
import { OrdersPage } from './screens/OrdersPage'
|
import { OrdersPage } from './screens/OrdersPage'
|
||||||
|
import { MyOrdersPage } from './screens/MyOrdersPage'
|
||||||
import { CallsPage } from './screens/CallsPage'
|
import { CallsPage } from './screens/CallsPage'
|
||||||
import { StoresPage } from './screens/StoresPage'
|
import { StoresPage } from './screens/StoresPage'
|
||||||
import { DailyLogsPage } from './screens/DailyLogsPage'
|
import { DailyLogsPage } from './screens/DailyLogsPage'
|
||||||
|
import { DailySalesReportPage } from './screens/DailySalesReportPage'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@ -17,6 +19,9 @@ function App() {
|
|||||||
<Route path="orders" element={<OrdersPage />} />
|
<Route path="orders" element={<OrdersPage />} />
|
||||||
<Route path="orders/:instanceId" element={<OrdersPage />} />
|
<Route path="orders/:instanceId" element={<OrdersPage />} />
|
||||||
|
|
||||||
|
<Route path="my-orders" element={<MyOrdersPage />} />
|
||||||
|
<Route path="my-orders/:instanceId" element={<MyOrdersPage />} />
|
||||||
|
|
||||||
<Route path="calls" element={<CallsPage />} />
|
<Route path="calls" element={<CallsPage />} />
|
||||||
<Route path="calls/:instanceId" element={<CallsPage />} />
|
<Route path="calls/:instanceId" element={<CallsPage />} />
|
||||||
|
|
||||||
@ -25,6 +30,8 @@ function App() {
|
|||||||
|
|
||||||
<Route path="daily" element={<DailyLogsPage />} />
|
<Route path="daily" element={<DailyLogsPage />} />
|
||||||
<Route path="daily/:instanceId" element={<DailyLogsPage />} />
|
<Route path="daily/:instanceId" element={<DailyLogsPage />} />
|
||||||
|
|
||||||
|
<Route path="sales-report" element={<DailySalesReportPage />} />
|
||||||
<Route path="*" element={<Navigate to="/orders" replace />} />
|
<Route path="*" element={<Navigate to="/orders" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@ -47,8 +47,8 @@ export class ZinoClient {
|
|||||||
return this.token;
|
return this.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
async request<T>(method: string, path: string, body?: unknown, customHeaders?: Record<string, string>): Promise<T> {
|
||||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
const headers: Record<string, string> = { 'Content-Type': 'application/json', ...customHeaders };
|
||||||
if (this.token) headers['Authorization'] = `Bearer ${this.token}`;
|
if (this.token) headers['Authorization'] = `Bearer ${this.token}`;
|
||||||
|
|
||||||
return this.send<T>(method, path, {
|
return this.send<T>(method, path, {
|
||||||
|
|||||||
@ -30,6 +30,15 @@ export const ORDER_BOOKING = {
|
|||||||
totalKgs: 'total_kgs', // number
|
totalKgs: 'total_kgs', // number
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
EDIT_ORDER: {
|
||||||
|
uid: '296e418e-5b31-43ad-aba4-4e9a1027ccd9',
|
||||||
|
fields: {
|
||||||
|
dateOfOrder: 'date_of_order', // date
|
||||||
|
orderDetails: 'order_details', // grid
|
||||||
|
totalBags: 'total_bags', // number
|
||||||
|
totalKgs: 'total_kgs', // number
|
||||||
|
},
|
||||||
|
},
|
||||||
PRODUCTIVITY_OF_VISIT: {
|
PRODUCTIVITY_OF_VISIT: {
|
||||||
uid: '11bd10f9-a001-470e-867f-33dee8eabe4b',
|
uid: '11bd10f9-a001-470e-867f-33dee8eabe4b',
|
||||||
fields: {
|
fields: {
|
||||||
@ -165,3 +174,31 @@ export const WORKFLOWS = {
|
|||||||
store: STORE,
|
store: STORE,
|
||||||
dailyReports: DAILY_REPORTS,
|
dailyReports: DAILY_REPORTS,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// --- Daily Sales Report Constants ---
|
||||||
|
export const SALES_REPORT_ROUTES = [
|
||||||
|
'Krishna',
|
||||||
|
'East-1',
|
||||||
|
'East-2',
|
||||||
|
'East-3',
|
||||||
|
'East-4',
|
||||||
|
'West-1',
|
||||||
|
'West-2',
|
||||||
|
'West-3',
|
||||||
|
'West-4'
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SALES_REPORT_DISTRIBUTORS = [
|
||||||
|
'Banashree Multi Millet Flour',
|
||||||
|
'Shiva nandi Enterprises',
|
||||||
|
'BPK & Co',
|
||||||
|
'Gaviranga',
|
||||||
|
'Bhagwan Enterprises',
|
||||||
|
'Shreshtaa Trading Co',
|
||||||
|
'Rajarajeshwari Enterprises',
|
||||||
|
'Byraveshwara Trading Co'
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SALES_REPORT_SO_NAMES = [
|
||||||
|
'Surya C'
|
||||||
|
];
|
||||||
|
|||||||
@ -4,16 +4,26 @@ import { AuthCtx, type AuthValue } from './context';
|
|||||||
|
|
||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
const [authed, setAuthed] = useState(() => !!currentToken());
|
const [authed, setAuthed] = useState(() => !!currentToken());
|
||||||
|
const [userEmail, setUserEmail] = useState<string | null>(() => {
|
||||||
|
return typeof window !== 'undefined' ? localStorage.getItem('krishna_sales_user_email') : null;
|
||||||
|
});
|
||||||
|
|
||||||
const value: AuthValue = {
|
const value: AuthValue = {
|
||||||
authed,
|
authed,
|
||||||
|
userEmail,
|
||||||
login: async (email, password, orgId) => {
|
login: async (email, password, orgId) => {
|
||||||
await loginAll(email, password, orgId);
|
const res = await loginAll(email, password, orgId);
|
||||||
setAuthed(true);
|
setAuthed(true);
|
||||||
|
if (res.user?.email) {
|
||||||
|
setUserEmail(res.user.email);
|
||||||
|
localStorage.setItem('krishna_sales_user_email', res.user.email);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
logout: () => {
|
logout: () => {
|
||||||
logoutAll();
|
logoutAll();
|
||||||
setAuthed(false);
|
setAuthed(false);
|
||||||
|
setUserEmail(null);
|
||||||
|
localStorage.removeItem('krishna_sales_user_email');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { createContext, useContext } from 'react';
|
|||||||
|
|
||||||
export interface AuthValue {
|
export interface AuthValue {
|
||||||
authed: boolean;
|
authed: boolean;
|
||||||
|
userEmail: string | null;
|
||||||
login: (email: string, password: string, orgId?: string) => Promise<void>;
|
login: (email: string, password: string, orgId?: string) => Promise<void>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,9 +26,11 @@ export interface DynamicFormProps {
|
|||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
ignorePrefill?: boolean;
|
ignorePrefill?: boolean;
|
||||||
|
customPrefillData?: Record<string, unknown>;
|
||||||
|
onActivityChange?: (name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DynamicForm({ client, activityId: initialActivityId, instanceId: initialInstanceId, onSuccess, onCancel, ignorePrefill }: DynamicFormProps) {
|
export function DynamicForm({ client, activityId: initialActivityId, instanceId: initialInstanceId, onSuccess, onCancel, ignorePrefill, customPrefillData, onActivityChange }: DynamicFormProps) {
|
||||||
const [currentActivityId, setCurrentActivityId] = useState(initialActivityId);
|
const [currentActivityId, setCurrentActivityId] = useState(initialActivityId);
|
||||||
const [currentInstanceId, setCurrentInstanceId] = useState<number | string | undefined>(initialInstanceId);
|
const [currentInstanceId, setCurrentInstanceId] = useState<number | string | undefined>(initialInstanceId);
|
||||||
const [chainQueue, setChainQueue] = useState<Array<{ activity_uid: string; activity_name: string }>>([]);
|
const [chainQueue, setChainQueue] = useState<Array<{ activity_uid: string; activity_name: string }>>([]);
|
||||||
@ -43,6 +45,8 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [chainedPrefillData, setChainedPrefillData] = useState<Record<string, unknown> | undefined>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -85,6 +89,18 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (customPrefillData) {
|
||||||
|
Object.entries(customPrefillData).forEach(([k, v]) => {
|
||||||
|
if (!imageFieldIds.has(k)) defaultValues[k] = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chainedPrefillData) {
|
||||||
|
Object.entries(chainedPrefillData).forEach(([k, v]) => {
|
||||||
|
if (!imageFieldIds.has(k)) defaultValues[k] = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setValues(defaultValues);
|
setValues(defaultValues);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -107,15 +123,37 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
const next = { ...prev, [fieldId]: newVal };
|
const next = { ...prev, [fieldId]: newVal };
|
||||||
|
|
||||||
// Auto-calculate order_details totals
|
// Auto-calculate order_details totals
|
||||||
if (fieldId === 'order_details' && Array.isArray(newVal)) {
|
const getBaseIdForField = (id: string) => id.replace(/_\d+$/, '');
|
||||||
|
const fieldDef = schema?.fields.find(f => f.id === fieldId);
|
||||||
|
const isOrderDetails = fieldId === 'order_details' || getBaseIdForField(fieldId) === 'order_details' || fieldDef?.mapped_workflow_field === 'order_details';
|
||||||
|
|
||||||
|
if (isOrderDetails && Array.isArray(newVal)) {
|
||||||
let totalBags = 0;
|
let totalBags = 0;
|
||||||
let totalKgs = 0;
|
let totalKgs = 0;
|
||||||
|
|
||||||
|
const getBaseId = (id: string) => id.replace(/_\d+$/, '');
|
||||||
|
|
||||||
|
const bagsColId = fieldDef?.columns?.find(c =>
|
||||||
|
c.id === 'bags' || getBaseId(c.id) === 'bags' || c.mapped_workflow_field === 'bags' || c.name.toLowerCase() === 'bags'
|
||||||
|
)?.id || 'bags';
|
||||||
|
|
||||||
|
const rowKgsColId = fieldDef?.columns?.find(c =>
|
||||||
|
c.id === 'row_kgs' || getBaseId(c.id) === 'row_kgs' || c.id === 'rowkgs' || getBaseId(c.id) === 'rowkgs' || c.mapped_workflow_field === 'row_kgs'
|
||||||
|
)?.id || 'row_kgs';
|
||||||
|
|
||||||
newVal.forEach(row => {
|
newVal.forEach(row => {
|
||||||
totalBags += Number(row.bags) || 0;
|
totalBags += Number(row[bagsColId]) || 0;
|
||||||
totalKgs += Number(row.row_kgs) || 0;
|
totalKgs += Number(row[rowKgsColId]) || 0;
|
||||||
});
|
});
|
||||||
next['total_bags'] = totalBags;
|
|
||||||
next['total_kgs'] = totalKgs;
|
const tbField = schema?.fields.find(f => f.id === 'total_bags' || getBaseId(f.id) === 'total_bags' || f.mapped_workflow_field === 'total_bags');
|
||||||
|
const tkField = schema?.fields.find(f => f.id === 'total_kgs' || getBaseId(f.id) === 'total_kgs' || f.mapped_workflow_field === 'total_kgs');
|
||||||
|
|
||||||
|
if (tbField) next[tbField.id] = totalBags;
|
||||||
|
else next['total_bags'] = totalBags;
|
||||||
|
|
||||||
|
if (tkField) next[tkField.id] = totalKgs;
|
||||||
|
else next['total_kgs'] = totalKgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
return next;
|
return next;
|
||||||
@ -199,9 +237,43 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
const nextActivity = pending.shift();
|
const nextActivity = pending.shift();
|
||||||
|
|
||||||
if (nextActivity) {
|
if (nextActivity) {
|
||||||
|
let nextPrefillData = undefined;
|
||||||
|
if (nextActivity.activity_uid === ORDER_BOOKING.activities.POTENTIAL_MINING.uid) {
|
||||||
|
try {
|
||||||
|
const pmRes = await client.request<{ potential: { potential: any[] } }>(
|
||||||
|
'POST',
|
||||||
|
'/api/papi2/potential-mining',
|
||||||
|
{ instance_id: String(res.instance_id ?? currentInstanceId) },
|
||||||
|
{ 'TemplateID': '146' }
|
||||||
|
);
|
||||||
|
const rawPotential = pmRes.potential?.potential || [];
|
||||||
|
const mappedPotential = rawPotential.map((row: any) => {
|
||||||
|
const cat = row.product_category || row.product_category_ || row.category;
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
product_category_: cat,
|
||||||
|
product_category: cat,
|
||||||
|
productcategory: cat,
|
||||||
|
category: cat,
|
||||||
|
product_category_1: cat
|
||||||
|
};
|
||||||
|
});
|
||||||
|
nextPrefillData = { potential: mappedPotential };
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch potential mining for chain", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextPrefillData) {
|
||||||
|
setChainedPrefillData(nextPrefillData);
|
||||||
|
} else {
|
||||||
|
setChainedPrefillData(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
setChainQueue(pending);
|
setChainQueue(pending);
|
||||||
setCurrentActivityId(nextActivity.activity_uid);
|
setCurrentActivityId(nextActivity.activity_uid);
|
||||||
setCurrentInstanceId(res.instance_id ?? currentInstanceId);
|
setCurrentInstanceId(res.instance_id ?? currentInstanceId);
|
||||||
|
onActivityChange?.(nextActivity.activity_name);
|
||||||
} else {
|
} else {
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
|
import { useState, useRef } from 'react';
|
||||||
import { Button } from '../../buttons/Button';
|
import { Button } from '../../buttons/Button';
|
||||||
import { Select } from '../../reusable/Select';
|
import { Select } from '../../reusable/Select';
|
||||||
import { Input } from '../../reusable/Input';
|
import { Input } from '../../reusable/Input';
|
||||||
|
import { Trash2, Pencil } from 'lucide-react';
|
||||||
import type { FormScreenField } from '../../../api/types';
|
import type { FormScreenField } from '../../../api/types';
|
||||||
|
|
||||||
export function SmartGridField({
|
export function SmartGridField({
|
||||||
@ -14,7 +16,22 @@ export function SmartGridField({
|
|||||||
value: Record<string, unknown>[];
|
value: Record<string, unknown>[];
|
||||||
onChange: (val: Record<string, unknown>[]) => void;
|
onChange: (val: Record<string, unknown>[]) => void;
|
||||||
}) {
|
}) {
|
||||||
const addRow = () => onChange([...value, {}]);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const formRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [newRow, setNewRow] = useState<Record<string, unknown>>({});
|
||||||
|
const [editingIdx, setEditingIdx] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const visibleColumns = columns.filter(c => {
|
||||||
|
const isOrderDetails = label.toLowerCase().includes('order');
|
||||||
|
if (isOrderDetails) {
|
||||||
|
const n = (c.name || '').toLowerCase();
|
||||||
|
const id = (c.id || '').toLowerCase();
|
||||||
|
// Show only Category, Name, and Bags
|
||||||
|
return n.includes('category') || n.includes('name') || n.includes('bags') ||
|
||||||
|
id.includes('category') || id.includes('name') || id.includes('bags');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const removeRow = (idx: number) => {
|
const removeRow = (idx: number) => {
|
||||||
const next = [...value];
|
const next = [...value];
|
||||||
@ -22,14 +39,28 @@ export function SmartGridField({
|
|||||||
onChange(next);
|
onChange(next);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateRow = (idx: number, fieldId: string, val: unknown) => {
|
const startEdit = (idx: number) => {
|
||||||
const next = [...value];
|
setEditingIdx(idx);
|
||||||
let row = { ...next[idx], [fieldId]: val };
|
setNewRow({ ...value[idx] });
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startAdd = () => {
|
||||||
|
setEditingIdx(null);
|
||||||
|
setNewRow({});
|
||||||
|
setIsModalOpen(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
}, 150);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateNewRowField = (fieldId: string, val: unknown) => {
|
||||||
|
let row = { ...newRow, [fieldId]: val };
|
||||||
|
|
||||||
const colDef = columns.find(c => c.id === fieldId);
|
const colDef = columns.find(c => c.id === fieldId);
|
||||||
|
|
||||||
// If product category changes, clear out the rest of the row's data
|
// If product category changes, clear out the rest of the row's data
|
||||||
if (colDef && (colDef.id === 'product_category' || colDef.name === 'Product Category')) {
|
if (colDef && (colDef.id === 'product_category' || colDef.name === 'Product Category' || colDef.mapped_workflow_field === 'product_category')) {
|
||||||
Object.keys(row).forEach(k => {
|
Object.keys(row).forEach(k => {
|
||||||
if (k !== fieldId) {
|
if (k !== fieldId) {
|
||||||
row[k] = '';
|
row[k] = '';
|
||||||
@ -41,53 +72,203 @@ export function SmartGridField({
|
|||||||
if (colDef && (colDef.data_type === 'select' || colDef.data_type === 'multiselect')) {
|
if (colDef && (colDef.data_type === 'select' || colDef.data_type === 'multiselect')) {
|
||||||
const option = colDef.properties?.options?.find(o => String(o.value) === String(val));
|
const option = colDef.properties?.options?.find(o => String(o.value) === String(val));
|
||||||
if (option && option._raw) {
|
if (option && option._raw) {
|
||||||
|
const getBaseId = (id: string) => id.replace(/_\d+$/, '');
|
||||||
for (const key of Object.keys(option._raw)) {
|
for (const key of Object.keys(option._raw)) {
|
||||||
// Match column ID with or without underscores (e.g., brcode <-> br_code)
|
const targetCol = columns.find(c => c.id === key || c.mapped_workflow_field === key || getBaseId(c.id) === key || c.name.toLowerCase().replace(/\s+/g, '') === key.toLowerCase().replace(/_/g, ''));
|
||||||
const targetCol = columns.find(c => c.id === key || c.id.replace(/_/g, '') === key.replace(/_/g, ''));
|
|
||||||
if (targetCol && targetCol.id !== fieldId) {
|
if (targetCol && targetCol.id !== fieldId) {
|
||||||
row[targetCol.id] = option._raw[key];
|
row[targetCol.id] = option._raw[key];
|
||||||
|
} else if (!targetCol && key !== fieldId) {
|
||||||
|
row[key] = option._raw[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-calculate row_kgs if sku and bags are present
|
// Auto-calculate row_kgs if sku and bags are present
|
||||||
const skuVal = Number(row.sku) || 0;
|
const getBaseId = (id: string) => id.replace(/_\d+$/, '');
|
||||||
const bagsVal = Number(row.bags) || 0;
|
const getVal = (key: string) => {
|
||||||
row.row_kgs = skuVal * bagsVal;
|
const c = columns.find(c => c.id === key || c.mapped_workflow_field === key || getBaseId(c.id) === key || c.name.toLowerCase().replace(/\s+/g, '') === key.toLowerCase().replace(/_/g, ''));
|
||||||
|
return c ? row[c.id] : row[key];
|
||||||
|
};
|
||||||
|
|
||||||
next[idx] = row;
|
const skuVal = Number(getVal('sku')) || 0;
|
||||||
|
const bagsVal = Number(getVal('bags')) || 0;
|
||||||
|
|
||||||
|
const rowKgsCol = columns.find(c => c.id === 'row_kgs' || c.mapped_workflow_field === 'row_kgs' || getBaseId(c.id) === 'row_kgs' || getBaseId(c.id) === 'rowkgs');
|
||||||
|
if (rowKgsCol) {
|
||||||
|
row[rowKgsCol.id] = skuVal * bagsVal;
|
||||||
|
} else {
|
||||||
|
row['row_kgs'] = skuVal * bagsVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
setNewRow(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitNewRow = () => {
|
||||||
|
if (editingIdx !== null) {
|
||||||
|
const next = [...value];
|
||||||
|
next[editingIdx] = newRow;
|
||||||
onChange(next);
|
onChange(next);
|
||||||
|
} else {
|
||||||
|
const getBaseId = (id: string) => id.replace(/_\d+$/, '');
|
||||||
|
const prodNameCol = columns.find(c => c.id === 'product_name' || getBaseId(c.id) === 'product_name' || c.mapped_workflow_field === 'product_name');
|
||||||
|
|
||||||
|
let foundIdx = -1;
|
||||||
|
if (prodNameCol && newRow[prodNameCol.id]) {
|
||||||
|
foundIdx = value.findIndex(r => r[prodNameCol.id] === newRow[prodNameCol.id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundIdx !== -1) {
|
||||||
|
const next = [...value];
|
||||||
|
const existingRow = { ...next[foundIdx] };
|
||||||
|
|
||||||
|
const bagsCol = columns.find(c => c.id === 'bags' || getBaseId(c.id) === 'bags' || c.mapped_workflow_field === 'bags' || c.name.toLowerCase() === 'bags');
|
||||||
|
const rowKgsCol = columns.find(c => c.id === 'row_kgs' || getBaseId(c.id) === 'row_kgs' || c.mapped_workflow_field === 'row_kgs' || getBaseId(c.id) === 'rowkgs');
|
||||||
|
const skuCol = columns.find(c => c.id === 'sku' || getBaseId(c.id) === 'sku' || c.mapped_workflow_field === 'sku');
|
||||||
|
|
||||||
|
const bagsId = bagsCol ? bagsCol.id : 'bags';
|
||||||
|
const rowKgsId = rowKgsCol ? rowKgsCol.id : 'row_kgs';
|
||||||
|
const skuId = skuCol ? skuCol.id : 'sku';
|
||||||
|
|
||||||
|
const existingBags = Number(existingRow[bagsId]) || 0;
|
||||||
|
const addedBags = Number(newRow[bagsId]) || 0;
|
||||||
|
existingRow[bagsId] = existingBags + addedBags;
|
||||||
|
|
||||||
|
const skuVal = Number(existingRow[skuId]) || 0;
|
||||||
|
existingRow[rowKgsId] = (existingBags + addedBags) * skuVal;
|
||||||
|
|
||||||
|
next[foundIdx] = existingRow;
|
||||||
|
onChange(next);
|
||||||
|
} else {
|
||||||
|
onChange([...value, newRow]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only close form if we were editing an existing row. For new rows, stay open.
|
||||||
|
if (editingIdx !== null) {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
}
|
||||||
|
setNewRow({});
|
||||||
|
setEditingIdx(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 font-sans border border-border-default rounded-md p-4 bg-slate-50">
|
<div className="flex flex-col gap-2 font-sans border border-border-default rounded-md p-4 bg-slate-50">
|
||||||
<span className="text-sm font-semibold text-strong mb-2">{label}</span>
|
<span className="text-sm font-semibold text-strong mb-2">{label}</span>
|
||||||
|
|
||||||
{value.length === 0 ? (
|
{value.length === 0 ? (
|
||||||
<span className="text-sm text-faint italic">No rows added.</span>
|
<span className="text-sm text-faint italic">No rows added.</span>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-3">
|
||||||
{value.map((row, i) => (
|
{value.map((row, i) => {
|
||||||
<div key={i} className="flex flex-col gap-3 p-3 bg-white border border-border-subtle rounded relative shadow-sm">
|
let productName = 'Unknown Product';
|
||||||
<div className="absolute top-2 right-2">
|
let bags = '0';
|
||||||
<button type="button" onClick={() => removeRow(i)} className="text-xs text-ruby-600 font-medium hover:underline">
|
|
||||||
Remove
|
visibleColumns.forEach(col => {
|
||||||
|
const isName = col.id.toLowerCase().includes('name') || col.name.toLowerCase().includes('name') || (col.id.toLowerCase().includes('category') && productName === 'Unknown Product');
|
||||||
|
const isBags = col.id.toLowerCase().includes('bags') || col.name.toLowerCase().includes('bags') || col.id.toLowerCase().includes('quantity');
|
||||||
|
|
||||||
|
if (isName || isBags) {
|
||||||
|
const val = row[col.id];
|
||||||
|
let displayVal = String(val ?? '-');
|
||||||
|
|
||||||
|
if (col.data_type === 'select' || col.data_type === 'multiselect') {
|
||||||
|
const opt = col.properties?.options?.find(o => String(o.value) === String(val));
|
||||||
|
if (opt) displayVal = opt.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isName && (productName === 'Unknown Product' || col.id.toLowerCase().includes('name'))) {
|
||||||
|
productName = displayVal;
|
||||||
|
}
|
||||||
|
if (isBags) bags = displayVal;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={i} className="bg-slate-50 border border-slate-200 rounded-xl p-3 shadow-sm flex items-center justify-between">
|
||||||
|
<div className="flex flex-col min-w-0 pr-4 w-full">
|
||||||
|
<span className="font-bold text-slate-800 text-[14px] truncate">{productName}</span>
|
||||||
|
{label.toLowerCase().includes('potential') ? (
|
||||||
|
<div className="grid grid-cols-2 gap-x-4 gap-y-1 mt-1.5 text-[12px]">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-slate-500">Total Ordered:</span>
|
||||||
|
<span className="font-bold text-slate-700">{String(row.total_ordered ?? row.Total_Ordered ?? '-')}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-slate-500">Potential:</span>
|
||||||
|
<span className="font-bold text-slate-700">{String(row.actual_potential ?? row.Actual_Potential ?? row.store_potential ?? row.Store_Potential ?? '-')}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between col-span-2 border-t border-slate-100 pt-1 mt-0.5">
|
||||||
|
<span className="text-slate-500">Difference:</span>
|
||||||
|
{(() => {
|
||||||
|
const diff = Number(row.difference ?? row.Difference);
|
||||||
|
if (isNaN(diff)) return <span className="font-bold text-slate-700">-</span>;
|
||||||
|
return (
|
||||||
|
<span className={`font-bold ${diff < 0 ? 'text-red-600' : 'text-green-600'}`}>
|
||||||
|
{diff < 0 ? `${Math.abs(diff)} Kgs Less Ordered` : `${diff} Kgs More Ordered`}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="font-extrabold text-indigo-600 text-[13px] mt-0.5">{bags} Bags</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5 shrink-0 pl-2">
|
||||||
|
<button type="button" onClick={() => startEdit(i)} className="text-indigo-600 bg-white border border-indigo-100 hover:bg-indigo-50 p-1.5 rounded-lg transition-colors flex items-center justify-center shadow-sm" title="Edit">
|
||||||
|
<Pencil size={14} />
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => removeRow(i)} className="text-red-600 bg-white border border-red-100 hover:bg-red-50 p-1.5 rounded-lg transition-colors flex items-center justify-center shadow-sm" title="Remove">
|
||||||
|
<Trash2 size={14} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-bold text-muted uppercase tracking-wider">Row {i + 1}</span>
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
);
|
||||||
{columns.map(col => {
|
})}
|
||||||
const val = row[col.id];
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={startAdd}
|
||||||
|
className={`transition-all duration-300 ease-in-out font-bold text-lg leading-none ${isModalOpen || editingIdx !== null ? 'opacity-0 pointer-events-none scale-95 w-0 h-0 p-0 m-0 overflow-hidden' : 'opacity-100 scale-100 rounded-full w-10 h-10 flex items-center justify-center shadow-md'}`}
|
||||||
|
title="Add Row"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={formRef}
|
||||||
|
className={`grid transition-[grid-template-rows,opacity,margin] duration-300 ease-in-out ${
|
||||||
|
isModalOpen || editingIdx !== null ? 'grid-rows-[1fr] opacity-100 mt-4' : 'grid-rows-[0fr] opacity-0 mt-0'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<div className="p-4 border border-border-subtle rounded-xl bg-white shadow-sm">
|
||||||
|
<h4 className="text-sm font-semibold text-strong mb-4 flex items-center gap-2">
|
||||||
|
{editingIdx !== null ? (
|
||||||
|
<><Pencil size={16} className="text-blue-600" /> Edit Row</>
|
||||||
|
) : (
|
||||||
|
<>Add New Item</>
|
||||||
|
)}
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 items-end">
|
||||||
|
{visibleColumns.map(col => {
|
||||||
|
const val = newRow[col.id];
|
||||||
if (col.data_type === 'select' || col.data_type === 'multiselect') {
|
if (col.data_type === 'select' || col.data_type === 'multiselect') {
|
||||||
// Cascade filter options
|
|
||||||
const allOpts = col.properties?.options || [];
|
const allOpts = col.properties?.options || [];
|
||||||
let filteredOpts = allOpts;
|
let filteredOpts = allOpts;
|
||||||
|
|
||||||
// Specific logic for product_name depending on product_category
|
if (col.id === 'product_name' || col.name === 'Product Name' || col.mapped_workflow_field === 'product_name') {
|
||||||
if (col.id === 'product_name' || col.name === 'Product Name') {
|
const catCol = columns.find(c => c.id === 'product_category' || c.name === 'Product Category' || c.mapped_workflow_field === 'product_category');
|
||||||
const catCol = columns.find(c => c.id === 'product_category' || c.name === 'Product Category');
|
|
||||||
if (catCol) {
|
if (catCol) {
|
||||||
const selectedCategory = row[catCol.id] as string;
|
const selectedCategory = newRow[catCol.id] as string;
|
||||||
if (selectedCategory) {
|
if (selectedCategory) {
|
||||||
filteredOpts = allOpts.filter(opt => {
|
filteredOpts = allOpts.filter(opt => {
|
||||||
const labelStr = String(opt.label || opt.value || '');
|
const labelStr = String(opt.label || opt.value || '');
|
||||||
@ -96,10 +277,9 @@ export function SmartGridField({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Generic _raw cascading for other fields just in case
|
|
||||||
filteredOpts = allOpts.filter(opt => {
|
filteredOpts = allOpts.filter(opt => {
|
||||||
if (!opt._raw) return true;
|
if (!opt._raw) return true;
|
||||||
for (const [rowKey, rowVal] of Object.entries(row)) {
|
for (const [rowKey, rowVal] of Object.entries(newRow)) {
|
||||||
if (rowKey === col.id || rowVal == null || rowVal === '') continue;
|
if (rowKey === col.id || rowVal == null || rowVal === '') continue;
|
||||||
const rawKey = Object.keys(opt._raw).find(rk => rk === rowKey || rk.replace(/_/g, '') === rowKey.replace(/_/g, ''));
|
const rawKey = Object.keys(opt._raw).find(rk => rk === rowKey || rk.replace(/_/g, '') === rowKey.replace(/_/g, ''));
|
||||||
if (rawKey && String(opt._raw[rawKey]) !== String(rowVal)) {
|
if (rawKey && String(opt._raw[rawKey]) !== String(rowVal)) {
|
||||||
@ -116,7 +296,7 @@ export function SmartGridField({
|
|||||||
label={col.name}
|
label={col.name}
|
||||||
required={col.mandatory}
|
required={col.mandatory}
|
||||||
value={(val as string) ?? ''}
|
value={(val as string) ?? ''}
|
||||||
onChange={(e) => updateRow(i, col.id, e.target.value)}
|
onChange={(e) => updateNewRowField(col.id, e.target.value)}
|
||||||
options={[{ value: '', label: 'Select...' }, ...filteredOpts.map((o: any) => ({ value: String(o.value), label: o.label }))]}
|
options={[{ value: '', label: 'Select...' }, ...filteredOpts.map((o: any) => ({ value: String(o.value), label: o.label }))]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -128,18 +308,23 @@ export function SmartGridField({
|
|||||||
required={col.mandatory}
|
required={col.mandatory}
|
||||||
type={col.data_type === 'number' ? 'number' : col.data_type === 'email' ? 'email' : 'text'}
|
type={col.data_type === 'number' ? 'number' : col.data_type === 'email' ? 'email' : 'text'}
|
||||||
value={(val as string) ?? ''}
|
value={(val as string) ?? ''}
|
||||||
onChange={(e) => updateRow(i, col.id, col.data_type === 'number' ? Number(e.target.value) : e.target.value)}
|
onChange={(e) => updateNewRowField(col.id, col.data_type === 'number' ? Number(e.target.value) : e.target.value)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
|
||||||
</div>
|
<div className="flex justify-end gap-2 sm:col-span-2 md:col-span-3 mt-2">
|
||||||
))}
|
<Button type="button" variant="ghost" onClick={() => { setIsModalOpen(false); setEditingIdx(null); setNewRow({}); }}>
|
||||||
</div>
|
Cancel
|
||||||
)}
|
|
||||||
<Button type="button" variant="secondary" size="sm" onClick={addRow} className="mt-2 self-start">
|
|
||||||
+ Add Row
|
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button type="button" variant="primary" onClick={submitNewRow}>
|
||||||
|
{editingIdx !== null ? "Save Item" : "+ Add to Order"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,10 +8,11 @@ export interface WiredRecordViewProps {
|
|||||||
headerActions?: React.ReactNode;
|
headerActions?: React.ReactNode;
|
||||||
rowActions?: (row: Record<string, unknown>) => React.ReactNode;
|
rowActions?: (row: Record<string, unknown>) => React.ReactNode;
|
||||||
refreshKey?: number;
|
refreshKey?: number;
|
||||||
|
initialFilters?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Orders record view (Order Booking workflow). */
|
/** Orders record view (Order Booking workflow). */
|
||||||
export function OrdersView({ onRowClick, pageSize, headerActions, rowActions, refreshKey }: WiredRecordViewProps) {
|
export function OrdersView({ onRowClick, pageSize, headerActions, rowActions, refreshKey, initialFilters }: WiredRecordViewProps) {
|
||||||
return (
|
return (
|
||||||
<RecordView
|
<RecordView
|
||||||
client={orderBookingClient}
|
client={orderBookingClient}
|
||||||
@ -25,6 +26,7 @@ export function OrdersView({ onRowClick, pageSize, headerActions, rowActions, re
|
|||||||
sortBy="created_at"
|
sortBy="created_at"
|
||||||
sortDir="desc"
|
sortDir="desc"
|
||||||
omitColumns={['created_at', 'instance_id']}
|
omitColumns={['created_at', 'instance_id']}
|
||||||
|
initialFilters={initialFilters}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,8 @@ export interface RecordViewProps {
|
|||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
/** Sort direction */
|
/** Sort direction */
|
||||||
sortDir?: 'asc' | 'desc';
|
sortDir?: 'asc' | 'desc';
|
||||||
|
/** Default filters to apply initially. */
|
||||||
|
initialFilters?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,11 +62,12 @@ export function RecordView({
|
|||||||
sortBy,
|
sortBy,
|
||||||
sortDir,
|
sortDir,
|
||||||
omitColumns,
|
omitColumns,
|
||||||
|
initialFilters = {},
|
||||||
}: RecordViewProps) {
|
}: RecordViewProps) {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [debounced, setDebounced] = useState('');
|
const [debounced, setDebounced] = useState('');
|
||||||
const [activeFilters, setActiveFilters] = useState<Record<string, string>>({});
|
const [activeFilters, setActiveFilters] = useState<Record<string, string>>(initialFilters);
|
||||||
const [resp, setResp] = useState<RecordViewResponse | null>(null);
|
const [resp, setResp] = useState<RecordViewResponse | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|||||||
import { Modal } from '../components/reusable';
|
import { Modal } from '../components/reusable';
|
||||||
import { CallsView } from '../components/rv';
|
import { CallsView } from '../components/rv';
|
||||||
import { CallDetail } from '../components/dv';
|
import { CallDetail } from '../components/dv';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Button } from '../components/buttons/Button';
|
import { Button } from '../components/buttons/Button';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { DynamicForm } from '../components/forms/DynamicForm';
|
import { DynamicForm } from '../components/forms/DynamicForm';
|
||||||
@ -17,11 +17,74 @@ export function CallsPage() {
|
|||||||
const [refreshKey, setRefreshKey] = useState(0);
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
const [activeActivity, setActiveActivity] = useState<{ id: string; name: string } | null>(null);
|
const [activeActivity, setActiveActivity] = useState<{ id: string; name: string } | null>(null);
|
||||||
|
|
||||||
|
const [selectedRow, setSelectedRow] = useState<Record<string, any> | null>(null);
|
||||||
|
const [miningLoading, setMiningLoading] = useState(false);
|
||||||
|
const [miningPrefill, setMiningPrefill] = useState<Record<string, unknown> | undefined>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (instanceId != null && !selectedRow) {
|
||||||
|
orderBookingClient.recordView(ORDER_BOOKING.recordViews.CALLS, {
|
||||||
|
page: 1,
|
||||||
|
limit: 1,
|
||||||
|
filters: [{ field_key: 'instance_id', value: String(instanceId), data_type: 'number' }]
|
||||||
|
}).then(res => {
|
||||||
|
if (res.data && res.data.length > 0) {
|
||||||
|
setSelectedRow(res.data[0]);
|
||||||
|
}
|
||||||
|
}).catch(err => console.error("Failed to fetch selected row:", err));
|
||||||
|
}
|
||||||
|
}, [instanceId, selectedRow]);
|
||||||
|
|
||||||
|
const handlePotentialMiningClick = async () => {
|
||||||
|
setMiningLoading(true);
|
||||||
|
setMiningPrefill(undefined);
|
||||||
|
try {
|
||||||
|
const storeCode = selectedRow?.store_code || selectedRow?.code || selectedRow?.store?.store_code;
|
||||||
|
|
||||||
|
if (!storeCode) {
|
||||||
|
console.warn("Store code not found on selected row, proceeding anyway.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
store_code: storeCode,
|
||||||
|
instance_id: String(instanceId)
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await orderBookingClient.request<{ potential: { potential: any[] } }>(
|
||||||
|
'POST',
|
||||||
|
'/api/papi2/potential-mining',
|
||||||
|
payload,
|
||||||
|
{ 'TemplateID': '146' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const rawPotential = response.potential?.potential || [];
|
||||||
|
const mappedPotential = rawPotential.map((row: any) => {
|
||||||
|
const cat = row.product_category || row.product_category_ || row.category;
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
product_category_: cat,
|
||||||
|
product_category: cat,
|
||||||
|
productcategory: cat,
|
||||||
|
category: cat,
|
||||||
|
product_category_1: cat
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setMiningPrefill({ potential: mappedPotential });
|
||||||
|
setActiveActivity({ id: ORDER_BOOKING.activities.POTENTIAL_MINING.uid, name: 'Potential Mining' });
|
||||||
|
} catch (e: any) {
|
||||||
|
alert("Failed to fetch potential mining data: " + (e.message || "Unknown error"));
|
||||||
|
} finally {
|
||||||
|
setMiningLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CallsView
|
<CallsView
|
||||||
refreshKey={refreshKey}
|
refreshKey={refreshKey}
|
||||||
onRowClick={(row) => {
|
onRowClick={(row) => {
|
||||||
|
setSelectedRow(row);
|
||||||
const id = row.instance_id as number | string | undefined;
|
const id = row.instance_id as number | string | undefined;
|
||||||
if (id != null) navigate(`/calls/${id}`);
|
if (id != null) navigate(`/calls/${id}`);
|
||||||
}}
|
}}
|
||||||
@ -56,14 +119,33 @@ export function CallsPage() {
|
|||||||
width="lg"
|
width="lg"
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<Button size="sm" variant="secondary" onClick={() => setActiveActivity({ id: ORDER_BOOKING.activities.POTENTIAL_MINING.uid, name: 'Potential Mining' })}>
|
<Button size="sm" variant="secondary" onClick={handlePotentialMiningClick} disabled={miningLoading}>
|
||||||
Potential Mining
|
{miningLoading ? 'Loading...' : 'Potential Mining'}
|
||||||
</Button>
|
</Button>
|
||||||
|
{(() => {
|
||||||
|
const stateName = String(selectedRow?.current_state_name || selectedRow?.current_state_name_ || selectedRow?.current_state || selectedRow?.status || '').toLowerCase();
|
||||||
|
if (stateName.includes('ordered') || stateName === 'ordered' || stateName.includes('order')) {
|
||||||
|
return (
|
||||||
|
<Button size="sm" onClick={() => setActiveActivity({ id: ORDER_BOOKING.activities.EDIT_ORDER.uid, name: 'Edit Order' })}>
|
||||||
|
Edit Order
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
} else if (stateName.includes('productive')) {
|
||||||
|
return (
|
||||||
<Button size="sm" onClick={() => setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' })}>
|
<Button size="sm" onClick={() => setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' })}>
|
||||||
Place Order
|
Place Order
|
||||||
</Button>
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button size="sm" onClick={() => setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' })}>
|
||||||
|
Place Order
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
>
|
>
|
||||||
{instanceId != null && <CallDetail instanceId={instanceId} />}
|
{instanceId != null && <CallDetail instanceId={instanceId} />}
|
||||||
</Modal>
|
</Modal>
|
||||||
@ -80,11 +162,13 @@ export function CallsPage() {
|
|||||||
activityId={activeActivity.id}
|
activityId={activeActivity.id}
|
||||||
instanceId={instanceId}
|
instanceId={instanceId}
|
||||||
ignorePrefill={activeActivity.id === ORDER_BOOKING.activities.PLACE_ORDER.uid}
|
ignorePrefill={activeActivity.id === ORDER_BOOKING.activities.PLACE_ORDER.uid}
|
||||||
|
customPrefillData={activeActivity.id === ORDER_BOOKING.activities.POTENTIAL_MINING.uid ? miningPrefill : undefined}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setActiveActivity(null);
|
setActiveActivity(null);
|
||||||
setRefreshKey(k => k + 1);
|
setRefreshKey(k => k + 1);
|
||||||
}}
|
}}
|
||||||
onCancel={() => setActiveActivity(null)}
|
onCancel={() => setActiveActivity(null)}
|
||||||
|
onActivityChange={(name) => setActiveActivity(prev => prev ? { ...prev, name } : null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
293
src/screens/DailySalesReportPage.tsx
Normal file
293
src/screens/DailySalesReportPage.tsx
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
import { useState, type FormEvent } from 'react';
|
||||||
|
import { Card, Input, Select } from '../components/reusable';
|
||||||
|
import { Button } from '../components/buttons/Button';
|
||||||
|
import {
|
||||||
|
SALES_REPORT_ROUTES,
|
||||||
|
SALES_REPORT_DISTRIBUTORS,
|
||||||
|
SALES_REPORT_SO_NAMES
|
||||||
|
} from '../api/config';
|
||||||
|
import { Download, Printer } from 'lucide-react';
|
||||||
|
import jsPDF from 'jspdf';
|
||||||
|
import autoTable from 'jspdf-autotable';
|
||||||
|
|
||||||
|
export function DailySalesReportPage() {
|
||||||
|
const [date, setDate] = useState('');
|
||||||
|
const [route, setRoute] = useState('');
|
||||||
|
const [distributor, setDistributor] = useState('');
|
||||||
|
const [soName, setSoName] = useState('');
|
||||||
|
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [reportData, setReportData] = useState<any>(null);
|
||||||
|
|
||||||
|
const downloadPDF = () => {
|
||||||
|
const doc = new jsPDF();
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
doc.setFontSize(16);
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.text('Krishna Flour Mills (Bangalore) Pvt. Limited', 105, 15, { align: 'center' });
|
||||||
|
|
||||||
|
doc.setFontSize(11);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.text('19, Platform Road, Bengaluru - 560 020', 105, 22, { align: 'center' });
|
||||||
|
|
||||||
|
doc.setFontSize(12);
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.text('DAILY ORDER REPORT', 105, 30, { align: 'center' });
|
||||||
|
|
||||||
|
// Simple underline for DAILY ORDER REPORT
|
||||||
|
const textWidth = doc.getTextWidth('DAILY ORDER REPORT');
|
||||||
|
doc.setLineWidth(0.5);
|
||||||
|
doc.line(105 - textWidth / 2, 31, 105 + textWidth / 2, 31);
|
||||||
|
|
||||||
|
// Meta Info
|
||||||
|
doc.setFontSize(10);
|
||||||
|
doc.text(`Distributor Name : ${distributor}`, 14, 40);
|
||||||
|
doc.text(`Date : ${date}`, 196, 40, { align: 'right' });
|
||||||
|
doc.text(`SO Name : ${soName}`, 196, 45, { align: 'right' });
|
||||||
|
|
||||||
|
// Table
|
||||||
|
autoTable(doc, {
|
||||||
|
html: '#report-table',
|
||||||
|
startY: 50,
|
||||||
|
theme: 'grid',
|
||||||
|
styles: { fontSize: 8, textColor: [0, 0, 0], lineColor: [0, 0, 0], lineWidth: 0.2 },
|
||||||
|
headStyles: { fillColor: [243, 244, 246], fontStyle: 'bold', halign: 'center' },
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.save(`Daily_Sales_Report_${date || 'Draft'}.pdf`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const printReport = async () => {
|
||||||
|
const doc = new jsPDF();
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
doc.setFontSize(16);
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.text('Krishna Flour Mills (Bangalore) Pvt. Limited', 105, 15, { align: 'center' });
|
||||||
|
|
||||||
|
doc.setFontSize(11);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.text('19, Platform Road, Bengaluru - 560 020', 105, 22, { align: 'center' });
|
||||||
|
|
||||||
|
doc.setFontSize(12);
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.text('DAILY ORDER REPORT', 105, 30, { align: 'center' });
|
||||||
|
|
||||||
|
// Simple underline for DAILY ORDER REPORT
|
||||||
|
const textWidth = doc.getTextWidth('DAILY ORDER REPORT');
|
||||||
|
doc.setLineWidth(0.5);
|
||||||
|
doc.line(105 - textWidth / 2, 31, 105 + textWidth / 2, 31);
|
||||||
|
|
||||||
|
// Meta Info
|
||||||
|
doc.setFontSize(10);
|
||||||
|
doc.text(`Distributor Name : ${distributor}`, 14, 40);
|
||||||
|
doc.text(`Date : ${date}`, 196, 40, { align: 'right' });
|
||||||
|
doc.text(`SO Name : ${soName}`, 196, 45, { align: 'right' });
|
||||||
|
|
||||||
|
// Table
|
||||||
|
autoTable(doc, {
|
||||||
|
html: '#report-table',
|
||||||
|
startY: 50,
|
||||||
|
theme: 'grid',
|
||||||
|
styles: { fontSize: 8, textColor: [0, 0, 0], lineColor: [0, 0, 0], lineWidth: 0.2 },
|
||||||
|
headStyles: { fillColor: [243, 244, 246], fontStyle: 'bold', halign: 'center' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = doc.output('blob');
|
||||||
|
|
||||||
|
// Attempt mobile native share (which includes OS-level Print)
|
||||||
|
if (navigator.share && navigator.canShare) {
|
||||||
|
const file = new File([blob], `Daily_Sales_Report_${date || 'Draft'}.pdf`, { type: 'application/pdf' });
|
||||||
|
try {
|
||||||
|
if (navigator.canShare({ files: [file] })) {
|
||||||
|
await navigator.share({
|
||||||
|
files: [file],
|
||||||
|
title: 'Daily Sales Report',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Share failed or was cancelled', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for PC / unsupported browsers
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.style.display = 'none';
|
||||||
|
iframe.src = blobUrl;
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
|
||||||
|
iframe.onload = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
iframe.contentWindow?.focus();
|
||||||
|
iframe.contentWindow?.print();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
async function submit(e: FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setBusy(true);
|
||||||
|
setError(null);
|
||||||
|
setReportData(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch('https://sandbox.getzino.in/api/papi2/daily-sales-report', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'TemplateID': '157',
|
||||||
|
'Accept': 'application/json, text/plain, */*',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
date,
|
||||||
|
route,
|
||||||
|
distributor,
|
||||||
|
so_name: soName,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Error: ${res.status} ${res.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
setReportData(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError((err as Error).message ?? 'Failed to fetch report');
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-5 p-6 pb-20">
|
||||||
|
<h1 className="m-0 text-xl font-extrabold text-strong tracking-[-0.01em] print:hidden">Daily Sales Report</h1>
|
||||||
|
|
||||||
|
<Card className="print:hidden">
|
||||||
|
<form onSubmit={submit} className="flex flex-col gap-4">
|
||||||
|
<Input
|
||||||
|
label="Date"
|
||||||
|
type="date"
|
||||||
|
value={date}
|
||||||
|
onChange={(e) => setDate(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Route"
|
||||||
|
value={route}
|
||||||
|
onChange={(e) => setRoute(e.target.value)}
|
||||||
|
options={[{ value: '', label: 'Select Route' }, ...SALES_REPORT_ROUTES.map(r => ({ value: r, label: r }))]}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Distributor"
|
||||||
|
value={distributor}
|
||||||
|
onChange={(e) => setDistributor(e.target.value)}
|
||||||
|
options={[{ value: '', label: 'Select Distributor' }, ...SALES_REPORT_DISTRIBUTORS.map(d => ({ value: d, label: d }))]}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="SO Name"
|
||||||
|
value={soName}
|
||||||
|
onChange={(e) => setSoName(e.target.value)}
|
||||||
|
options={[{ value: '', label: 'Select SO Name' }, ...SALES_REPORT_SO_NAMES.map(s => ({ value: s, label: s }))]}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && <div className="text-xs text-ruby-600 font-medium">{error}</div>}
|
||||||
|
|
||||||
|
<Button type="submit" full disabled={busy}>
|
||||||
|
{busy ? 'Fetching...' : 'Get Report'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{reportData?.response && (
|
||||||
|
<Card className="overflow-x-auto p-0 pb-2 border-0 shadow-none print:m-0 print:p-0">
|
||||||
|
<div className="p-4 pb-2 print:hidden">
|
||||||
|
<h2 className="text-lg font-bold text-strong m-0">Report Details</h2>
|
||||||
|
</div>
|
||||||
|
<div className="w-full overflow-x-auto p-4 pt-2">
|
||||||
|
<div className="min-w-max">
|
||||||
|
<div className="flex flex-col items-center text-center gap-1">
|
||||||
|
<h1 className="m-0 text-lg font-bold text-strong">Krishna Flour Mills (Bangalore) Pvt. Limited</h1>
|
||||||
|
<h2 className="m-0 text-sm font-normal text-strong">19, Platform Road, Bengaluru - 560 020</h2>
|
||||||
|
<h3 className="m-0 text-base font-bold text-strong underline mt-1 mb-4">DAILY ORDER REPORT</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-start text-sm font-bold text-strong mb-2">
|
||||||
|
<div>Distributor Name : {distributor}</div>
|
||||||
|
<div className="text-right flex flex-col gap-1.5">
|
||||||
|
<div>Date : {date}</div>
|
||||||
|
<div>SO Name : {soName}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="report-table" className="w-full text-sm text-left border border-black border-collapse mt-2">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-black bg-black/5">
|
||||||
|
<th className="py-2 px-3 font-bold text-black align-bottom border-r border-black" rowSpan={2}>Sl No</th>
|
||||||
|
<th className="py-2 px-3 font-bold text-black align-bottom" rowSpan={2}>Store</th>
|
||||||
|
{reportData.response.headers?.map((h: any) => (
|
||||||
|
<th key={h.br_code} colSpan={h.skus?.length || 1} className="py-1 px-3 font-bold text-black text-center border-l border-black">
|
||||||
|
{h.br_code}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th className="py-2 px-3 font-bold text-black align-bottom border-l border-black" rowSpan={2}>Total</th>
|
||||||
|
</tr>
|
||||||
|
<tr className="border-b border-black bg-black/5">
|
||||||
|
{reportData.response.headers?.flatMap((h: any) => h.skus || []).map((sku: string, idx: number) => (
|
||||||
|
<th key={`${sku}-${idx}`} className="py-1 px-3 font-bold text-black text-center border-l border-black whitespace-nowrap">
|
||||||
|
{sku}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{reportData.response.rows?.map((row: any) => (
|
||||||
|
<tr key={row.sl_no} className="border-b border-black hover:bg-black/5 transition-colors">
|
||||||
|
<td className="py-2 px-3 text-strong border-r border-black">{row.sl_no}</td>
|
||||||
|
<td className="py-2 px-3 text-strong whitespace-nowrap">{row.store}</td>
|
||||||
|
{reportData.response.headers?.flatMap((h: any) => h.skus || []).map((sku: string, idx: number) => (
|
||||||
|
<td key={`${sku}-${idx}`} className="py-2 px-3 text-strong text-center border-l border-black">
|
||||||
|
{row.products?.[sku] ?? 0}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td className="py-2 px-3 text-strong font-bold text-center border-l border-black bg-black/5">{row.total ?? 0}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{reportData.response.total_row && (
|
||||||
|
<tr className="border-b border-black bg-black/10 font-bold">
|
||||||
|
<td className="py-2 px-3 text-strong border-r border-black">{reportData.response.total_row.sl_no}</td>
|
||||||
|
<td className="py-2 px-3 text-strong whitespace-nowrap text-right">{reportData.response.total_row.store}</td>
|
||||||
|
{reportData.response.headers?.flatMap((h: any) => h.skus || []).map((sku: string, idx: number) => (
|
||||||
|
<td key={`total-${sku}-${idx}`} className="py-2 px-3 text-strong text-center border-l border-black">
|
||||||
|
{reportData.response.total_row.products?.[sku] ?? 0}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td className="py-2 px-3 text-strong text-center border-l border-black">{reportData.response.total_row.total ?? 0}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{(!reportData.response.rows || reportData.response.rows.length === 0) && (
|
||||||
|
<div className="py-8 text-center text-faint">No records found.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 pt-4 pb-6 flex gap-4 print:hidden">
|
||||||
|
<Button type="button" onClick={downloadPDF} variant="secondary" iconLeft={<Download size={18} />} full>
|
||||||
|
Download PDF
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={printReport} iconLeft={<Printer size={18} />} full>
|
||||||
|
Print PDF
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
src/screens/MyOrdersPage.tsx
Normal file
67
src/screens/MyOrdersPage.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { Modal } from '../components/reusable';
|
||||||
|
import { OrdersView } from '../components/rv';
|
||||||
|
import { OrderDetail } from '../components/dv';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '../components/buttons/Button';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import { DynamicForm } from '../components/forms/DynamicForm';
|
||||||
|
import { ORDER_BOOKING } from '../api/config';
|
||||||
|
import { orderBookingClient } from '../api/clients';
|
||||||
|
import { useAuth } from '../auth/context';
|
||||||
|
|
||||||
|
export function MyOrdersPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const instanceId = params.instanceId ? Number(params.instanceId) : undefined;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
|
|
||||||
|
const { userEmail } = useAuth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<OrdersView
|
||||||
|
refreshKey={refreshKey}
|
||||||
|
initialFilters={userEmail ? { performed_by_email: userEmail } : undefined}
|
||||||
|
onRowClick={(row) => {
|
||||||
|
const id = row.instance_id as number | string | undefined;
|
||||||
|
if (id != null) navigate(`/my-orders/${id}`);
|
||||||
|
}}
|
||||||
|
headerActions={
|
||||||
|
<Button size="sm" iconLeft={<Plus size={14} />} onClick={() => setIsCreating(true)}>
|
||||||
|
Place Order
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={isCreating}
|
||||||
|
onClose={() => setIsCreating(false)}
|
||||||
|
title="Place Order"
|
||||||
|
width="md"
|
||||||
|
>
|
||||||
|
<DynamicForm
|
||||||
|
client={orderBookingClient}
|
||||||
|
activityId={ORDER_BOOKING.activities.LOG_VISIT.uid}
|
||||||
|
onSuccess={() => {
|
||||||
|
setIsCreating(false);
|
||||||
|
setRefreshKey(k => k + 1);
|
||||||
|
}}
|
||||||
|
onCancel={() => setIsCreating(false)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={instanceId != null}
|
||||||
|
onClose={() => {
|
||||||
|
navigate(`/my-orders`);
|
||||||
|
}}
|
||||||
|
title={instanceId != null ? `Order #${instanceId}` : undefined}
|
||||||
|
width="lg"
|
||||||
|
>
|
||||||
|
{instanceId != null && <OrderDetail instanceId={instanceId} />}
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -15,8 +15,6 @@ export function OrdersPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [refreshKey, setRefreshKey] = useState(0);
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
const [activeActivity, setActiveActivity] = useState<{ id: string; name: string } | null>(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OrdersView
|
<OrdersView
|
||||||
@ -51,43 +49,14 @@ export function OrdersPage() {
|
|||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
open={instanceId != null}
|
open={instanceId != null}
|
||||||
onClose={() => navigate(`/orders`)}
|
onClose={() => {
|
||||||
|
navigate(`/orders`);
|
||||||
|
}}
|
||||||
title={instanceId != null ? `Order #${instanceId}` : undefined}
|
title={instanceId != null ? `Order #${instanceId}` : undefined}
|
||||||
width="lg"
|
width="lg"
|
||||||
actions={
|
|
||||||
<>
|
|
||||||
<Button size="sm" variant="secondary" onClick={() => setActiveActivity({ id: ORDER_BOOKING.activities.POTENTIAL_MINING.uid, name: 'Potential Mining' })}>
|
|
||||||
Potential Mining
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" onClick={() => setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' })}>
|
|
||||||
Place Order
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{instanceId != null && <OrderDetail instanceId={instanceId} />}
|
{instanceId != null && <OrderDetail instanceId={instanceId} />}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
|
||||||
open={activeActivity != null}
|
|
||||||
onClose={() => setActiveActivity(null)}
|
|
||||||
title={activeActivity?.name}
|
|
||||||
width="md"
|
|
||||||
>
|
|
||||||
{activeActivity && instanceId != null && (
|
|
||||||
<DynamicForm
|
|
||||||
client={orderBookingClient}
|
|
||||||
activityId={activeActivity.id}
|
|
||||||
instanceId={instanceId}
|
|
||||||
ignorePrefill={activeActivity.id === ORDER_BOOKING.activities.PLACE_ORDER.uid}
|
|
||||||
onSuccess={() => {
|
|
||||||
setActiveActivity(null);
|
|
||||||
setRefreshKey(k => k + 1);
|
|
||||||
}}
|
|
||||||
onCancel={() => setActiveActivity(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Store, Phone, ShoppingCart, ClipboardList, type LucideIcon } from 'lucide-react';
|
import { Store, Phone, ShoppingCart, ClipboardList, FileText, type LucideIcon } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
OrdersView,
|
OrdersView,
|
||||||
CallsView,
|
CallsView,
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
type WiredDetailViewProps,
|
type WiredDetailViewProps,
|
||||||
} from '../components/dv';
|
} from '../components/dv';
|
||||||
|
|
||||||
export type ScreenKey = 'orders' | 'calls' | 'stores' | 'daily';
|
export type ScreenKey = 'orders' | 'my-orders' | 'calls' | 'stores' | 'daily' | 'sales-report';
|
||||||
|
|
||||||
export interface ScreenDef {
|
export interface ScreenDef {
|
||||||
key: ScreenKey;
|
key: ScreenKey;
|
||||||
@ -28,9 +28,11 @@ export interface ScreenDef {
|
|||||||
|
|
||||||
export const SCREENS: ScreenDef[] = [
|
export const SCREENS: ScreenDef[] = [
|
||||||
{ key: 'orders', label: 'Orders', icon: ShoppingCart, View: OrdersView, Detail: OrderDetail, noun: 'Order' },
|
{ key: 'orders', label: 'Orders', icon: ShoppingCart, View: OrdersView, Detail: OrderDetail, noun: 'Order' },
|
||||||
|
{ key: 'my-orders', label: 'My Orders', icon: ShoppingCart, View: OrdersView, Detail: OrderDetail, noun: 'My Order' },
|
||||||
{ key: 'calls', label: 'Calls', icon: Phone, View: CallsView, Detail: CallDetail, noun: 'Call' },
|
{ key: 'calls', label: 'Calls', icon: Phone, View: CallsView, Detail: CallDetail, noun: 'Call' },
|
||||||
{ key: 'stores', label: 'Stores', icon: Store, View: StoresView, Detail: StoreDetail, noun: 'Store' },
|
{ key: 'stores', label: 'Stores', icon: Store, View: StoresView, Detail: StoreDetail, noun: 'Store' },
|
||||||
{ key: 'daily', label: 'Daily Logs', icon: ClipboardList, View: DailyLogsView, Detail: DailyLogDetail, noun: 'Daily Log' },
|
{ key: 'daily', label: 'Daily Logs', icon: ClipboardList, View: DailyLogsView, Detail: DailyLogDetail, noun: 'Daily Log' },
|
||||||
|
{ key: 'sales-report', label: 'Sales Report', icon: FileText, View: null as any, Detail: null as any, noun: 'Sales Report' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function screenByKey(key: string | undefined): ScreenDef | undefined {
|
export function screenByKey(key: string | undefined): ScreenDef | undefined {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user