like mobile added the functionality

This commit is contained in:
suryac 2026-07-20 13:31:26 +05:30
parent b5e84a34f7
commit 4d7beaef0f
16 changed files with 1115 additions and 135 deletions

246
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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>

View File

@ -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, {

View File

@ -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'
];

View File

@ -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');
}, },
}; };

View File

@ -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;
} }

View File

@ -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?.();
} }

View File

@ -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,105 +72,259 @@ 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;
onChange(next); 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);
} 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
</button>
</div>
<span className="text-xs font-bold text-muted uppercase tracking-wider">Row {i + 1}</span>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{columns.map(col => {
const val = row[col.id];
if (col.data_type === 'select' || col.data_type === 'multiselect') {
// Cascade filter options
const allOpts = col.properties?.options || [];
let filteredOpts = allOpts;
// Specific logic for product_name depending on product_category visibleColumns.forEach(col => {
if (col.id === 'product_name' || col.name === 'Product Name') { const isName = col.id.toLowerCase().includes('name') || col.name.toLowerCase().includes('name') || (col.id.toLowerCase().includes('category') && productName === 'Unknown Product');
const catCol = columns.find(c => c.id === 'product_category' || c.name === 'Product Category'); const isBags = col.id.toLowerCase().includes('bags') || col.name.toLowerCase().includes('bags') || col.id.toLowerCase().includes('quantity');
if (catCol) {
const selectedCategory = row[catCol.id] as string;
if (selectedCategory) {
filteredOpts = allOpts.filter(opt => {
const labelStr = String(opt.label || opt.value || '');
return labelStr.startsWith(selectedCategory);
});
}
}
} else {
// Generic _raw cascading for other fields just in case
filteredOpts = allOpts.filter(opt => {
if (!opt._raw) return true;
for (const [rowKey, rowVal] of Object.entries(row)) {
if (rowKey === col.id || rowVal == null || rowVal === '') continue;
const rawKey = Object.keys(opt._raw).find(rk => rk === rowKey || rk.replace(/_/g, '') === rowKey.replace(/_/g, ''));
if (rawKey && String(opt._raw[rawKey]) !== String(rowVal)) {
return false;
}
}
return true;
});
}
return ( if (isName || isBags) {
<Select const val = row[col.id];
key={col.id} let displayVal = String(val ?? '-');
label={col.name}
required={col.mandatory} if (col.data_type === 'select' || col.data_type === 'multiselect') {
value={(val as string) ?? ''} const opt = col.properties?.options?.find(o => String(o.value) === String(val));
onChange={(e) => updateRow(i, col.id, e.target.value)} if (opt) displayVal = opt.label;
options={[{ value: '', label: 'Select...' }, ...filteredOpts.map((o: any) => ({ value: String(o.value), label: o.label }))]} }
/>
); if (isName && (productName === 'Unknown Product' || col.id.toLowerCase().includes('name'))) {
} productName = displayVal;
return ( }
<Input if (isBags) bags = displayVal;
key={col.id} }
label={col.name} });
required={col.mandatory}
type={col.data_type === 'number' ? 'number' : col.data_type === 'email' ? 'email' : 'text'} return (
value={(val as string) ?? ''} <div key={i} className="bg-slate-50 border border-slate-200 rounded-xl p-3 shadow-sm flex items-center justify-between">
onChange={(e) => updateRow(i, col.id, col.data_type === 'number' ? Number(e.target.value) : e.target.value)} <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>
</div>
</div> </div>
</div> );
))} })}
</div> </div>
)} )}
<Button type="button" variant="secondary" size="sm" onClick={addRow} className="mt-2 self-start">
+ Add Row <div className="flex justify-end mt-2">
</Button> <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') {
const allOpts = col.properties?.options || [];
let filteredOpts = allOpts;
if (col.id === 'product_name' || col.name === 'Product Name' || col.mapped_workflow_field === 'product_name') {
const catCol = columns.find(c => c.id === 'product_category' || c.name === 'Product Category' || c.mapped_workflow_field === 'product_category');
if (catCol) {
const selectedCategory = newRow[catCol.id] as string;
if (selectedCategory) {
filteredOpts = allOpts.filter(opt => {
const labelStr = String(opt.label || opt.value || '');
return labelStr.startsWith(selectedCategory);
});
}
}
} else {
filteredOpts = allOpts.filter(opt => {
if (!opt._raw) return true;
for (const [rowKey, rowVal] of Object.entries(newRow)) {
if (rowKey === col.id || rowVal == null || rowVal === '') continue;
const rawKey = Object.keys(opt._raw).find(rk => rk === rowKey || rk.replace(/_/g, '') === rowKey.replace(/_/g, ''));
if (rawKey && String(opt._raw[rawKey]) !== String(rowVal)) {
return false;
}
}
return true;
});
}
return (
<Select
key={col.id}
label={col.name}
required={col.mandatory}
value={(val as string) ?? ''}
onChange={(e) => updateNewRowField(col.id, e.target.value)}
options={[{ value: '', label: 'Select...' }, ...filteredOpts.map((o: any) => ({ value: String(o.value), label: o.label }))]}
/>
);
}
return (
<Input
key={col.id}
label={col.name}
required={col.mandatory}
type={col.data_type === 'number' ? 'number' : col.data_type === 'email' ? 'email' : 'text'}
value={(val as string) ?? ''}
onChange={(e) => updateNewRowField(col.id, col.data_type === 'number' ? Number(e.target.value) : e.target.value)}
/>
);
})}
<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({}); }}>
Cancel
</Button>
<Button type="button" variant="primary" onClick={submitNewRow}>
{editingIdx !== null ? "Save Item" : "+ Add to Order"}
</Button>
</div>
</div>
</div>
</div>
</div>
</div> </div>
); );
} }

View File

@ -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}
/> />
); );
} }

View File

@ -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);

View File

@ -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 size="sm" onClick={() => setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' })}>
Place Order
</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' })}>
Place Order
</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>

View 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>
);
}

View 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>
</>
);
}

View File

@ -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>
</> </>
); );
} }

View File

@ -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 {