From 4d7beaef0fca9e42238e35e359e1c4303315327b Mon Sep 17 00:00:00 2001 From: suryac Date: Mon, 20 Jul 2026 13:31:26 +0530 Subject: [PATCH] like mobile added the functionality --- package-lock.json | 246 ++++++++++++ package.json | 2 + src/App.tsx | 7 + src/api/client.ts | 4 +- src/api/config.ts | 37 ++ src/auth/AuthProvider.tsx | 12 +- src/auth/context.ts | 1 + src/components/forms/DynamicForm.tsx | 84 ++++- .../forms/fields/SmartGridField.tsx | 349 ++++++++++++++---- src/components/rv/OrdersView.tsx | 4 +- src/components/rv/RecordView.tsx | 5 +- src/screens/CallsPage.tsx | 96 ++++- src/screens/DailySalesReportPage.tsx | 293 +++++++++++++++ src/screens/MyOrdersPage.tsx | 67 ++++ src/screens/OrdersPage.tsx | 37 +- src/screens/tabs.ts | 6 +- 16 files changed, 1115 insertions(+), 135 deletions(-) create mode 100644 src/screens/DailySalesReportPage.tsx create mode 100644 src/screens/MyOrdersPage.tsx diff --git a/package-lock.json b/package-lock.json index a1634e0..4688ab5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.8", "lucide-react": "^0.400.0", "react": "^19.2.7", "react-dom": "^19.2.7", @@ -223,6 +225,15 @@ "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": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -1257,6 +1268,19 @@ "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": { "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", @@ -1277,6 +1301,13 @@ "@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": { "version": "0.0.6", "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_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": { "version": "2.10.41", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.41.tgz", @@ -1683,6 +1724,26 @@ ], "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": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -1712,6 +1773,18 @@ "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": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1727,6 +1800,16 @@ "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": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1896,6 +1979,16 @@ "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": { "version": "1.5.385", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.385.tgz", @@ -2162,6 +2255,17 @@ "dev": true, "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": { "version": "6.5.0", "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": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2306,6 +2416,20 @@ "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": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2345,6 +2469,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": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2439,6 +2569,32 @@ "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": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2878,6 +3034,22 @@ "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2898,6 +3070,13 @@ "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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2967,6 +3146,16 @@ "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": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", @@ -3101,12 +3290,29 @@ "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": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", "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": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", @@ -3196,6 +3402,26 @@ "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": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", @@ -3217,6 +3443,16 @@ "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": { "version": "1.3.3", "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" } }, + "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": { "version": "37.3.6", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", diff --git a/package.json b/package.json index f2fd827..af4561e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.8", "lucide-react": "^0.400.0", "react": "^19.2.7", "react-dom": "^19.2.7", diff --git a/src/App.tsx b/src/App.tsx index 3a78089..990bb18 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,9 +3,11 @@ import { AuthProvider } from './auth/AuthProvider' import { LoginPage } from './screens/LoginPage' import { ConsoleLayout } from './screens/ConsoleLayout' import { OrdersPage } from './screens/OrdersPage' +import { MyOrdersPage } from './screens/MyOrdersPage' import { CallsPage } from './screens/CallsPage' import { StoresPage } from './screens/StoresPage' import { DailyLogsPage } from './screens/DailyLogsPage' +import { DailySalesReportPage } from './screens/DailySalesReportPage' function App() { return ( @@ -17,6 +19,9 @@ function App() { } /> } /> + } /> + } /> + } /> } /> @@ -25,6 +30,8 @@ function App() { } /> } /> + + } /> } /> diff --git a/src/api/client.ts b/src/api/client.ts index 3f28527..c0b9f7f 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -47,8 +47,8 @@ export class ZinoClient { return this.token; } - private async request(method: string, path: string, body?: unknown): Promise { - const headers: Record = { 'Content-Type': 'application/json' }; + async request(method: string, path: string, body?: unknown, customHeaders?: Record): Promise { + const headers: Record = { 'Content-Type': 'application/json', ...customHeaders }; if (this.token) headers['Authorization'] = `Bearer ${this.token}`; return this.send(method, path, { diff --git a/src/api/config.ts b/src/api/config.ts index c7d21e1..799a3ea 100644 --- a/src/api/config.ts +++ b/src/api/config.ts @@ -30,6 +30,15 @@ export const ORDER_BOOKING = { 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: { uid: '11bd10f9-a001-470e-867f-33dee8eabe4b', fields: { @@ -165,3 +174,31 @@ export const WORKFLOWS = { store: STORE, dailyReports: DAILY_REPORTS, } 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' +]; diff --git a/src/auth/AuthProvider.tsx b/src/auth/AuthProvider.tsx index 5c59eab..429005e 100644 --- a/src/auth/AuthProvider.tsx +++ b/src/auth/AuthProvider.tsx @@ -4,16 +4,26 @@ import { AuthCtx, type AuthValue } from './context'; export function AuthProvider({ children }: { children: ReactNode }) { const [authed, setAuthed] = useState(() => !!currentToken()); + const [userEmail, setUserEmail] = useState(() => { + return typeof window !== 'undefined' ? localStorage.getItem('krishna_sales_user_email') : null; + }); const value: AuthValue = { authed, + userEmail, login: async (email, password, orgId) => { - await loginAll(email, password, orgId); + const res = await loginAll(email, password, orgId); setAuthed(true); + if (res.user?.email) { + setUserEmail(res.user.email); + localStorage.setItem('krishna_sales_user_email', res.user.email); + } }, logout: () => { logoutAll(); setAuthed(false); + setUserEmail(null); + localStorage.removeItem('krishna_sales_user_email'); }, }; diff --git a/src/auth/context.ts b/src/auth/context.ts index 5dc72b7..04b94a8 100644 --- a/src/auth/context.ts +++ b/src/auth/context.ts @@ -2,6 +2,7 @@ import { createContext, useContext } from 'react'; export interface AuthValue { authed: boolean; + userEmail: string | null; login: (email: string, password: string, orgId?: string) => Promise; logout: () => void; } diff --git a/src/components/forms/DynamicForm.tsx b/src/components/forms/DynamicForm.tsx index 4af9d3f..bca422c 100644 --- a/src/components/forms/DynamicForm.tsx +++ b/src/components/forms/DynamicForm.tsx @@ -26,9 +26,11 @@ export interface DynamicFormProps { onSuccess?: () => void; onCancel?: () => void; ignorePrefill?: boolean; + customPrefillData?: Record; + 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 [currentInstanceId, setCurrentInstanceId] = useState(initialInstanceId); const [chainQueue, setChainQueue] = useState>([]); @@ -43,6 +45,8 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [chainedPrefillData, setChainedPrefillData] = useState | undefined>(); + useEffect(() => { let mounted = true; setLoading(true); @@ -84,6 +88,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); setLoading(false); @@ -107,15 +123,37 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: const next = { ...prev, [fieldId]: newVal }; // 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 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 => { - totalBags += Number(row.bags) || 0; - totalKgs += Number(row.row_kgs) || 0; + totalBags += Number(row[bagsColId]) || 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; @@ -199,9 +237,43 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: const nextActivity = pending.shift(); 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); setCurrentActivityId(nextActivity.activity_uid); setCurrentInstanceId(res.instance_id ?? currentInstanceId); + onActivityChange?.(nextActivity.activity_name); } else { onSuccess?.(); } diff --git a/src/components/forms/fields/SmartGridField.tsx b/src/components/forms/fields/SmartGridField.tsx index 8c45803..d59749b 100644 --- a/src/components/forms/fields/SmartGridField.tsx +++ b/src/components/forms/fields/SmartGridField.tsx @@ -1,6 +1,8 @@ +import { useState, useRef } from 'react'; import { Button } from '../../buttons/Button'; import { Select } from '../../reusable/Select'; import { Input } from '../../reusable/Input'; +import { Trash2, Pencil } from 'lucide-react'; import type { FormScreenField } from '../../../api/types'; export function SmartGridField({ @@ -14,7 +16,22 @@ export function SmartGridField({ value: Record[]; onChange: (val: Record[]) => void; }) { - const addRow = () => onChange([...value, {}]); + const [isModalOpen, setIsModalOpen] = useState(false); + const formRef = useRef(null); + const [newRow, setNewRow] = useState>({}); + const [editingIdx, setEditingIdx] = useState(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 next = [...value]; @@ -22,14 +39,28 @@ export function SmartGridField({ onChange(next); }; - const updateRow = (idx: number, fieldId: string, val: unknown) => { - const next = [...value]; - let row = { ...next[idx], [fieldId]: val }; + const startEdit = (idx: number) => { + setEditingIdx(idx); + 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); // 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 => { if (k !== fieldId) { row[k] = ''; @@ -41,105 +72,259 @@ export function SmartGridField({ if (colDef && (colDef.data_type === 'select' || colDef.data_type === 'multiselect')) { const option = colDef.properties?.options?.find(o => String(o.value) === String(val)); if (option && option._raw) { + const getBaseId = (id: string) => id.replace(/_\d+$/, ''); 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.id.replace(/_/g, '') === key.replace(/_/g, '')); + 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, '')); if (targetCol && targetCol.id !== fieldId) { 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 - const skuVal = Number(row.sku) || 0; - const bagsVal = Number(row.bags) || 0; - row.row_kgs = skuVal * bagsVal; + const getBaseId = (id: string) => id.replace(/_\d+$/, ''); + const getVal = (key: string) => { + 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]; + }; + + 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; + } - next[idx] = row; - onChange(next); + 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 (
{label} + {value.length === 0 ? ( No rows added. ) : ( -
- {value.map((row, i) => ( -
-
- -
- Row {i + 1} -
- {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 - if (col.id === 'product_name' || col.name === 'Product Name') { - const catCol = columns.find(c => c.id === 'product_category' || c.name === 'Product Category'); - 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; - }); - } +
+ {value.map((row, i) => { + let productName = 'Unknown Product'; + let bags = '0'; - return ( - updateRow(i, col.id, col.data_type === 'number' ? Number(e.target.value) : e.target.value)} - /> - ); - })} + 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 ( +
+
+ {productName} + {label.toLowerCase().includes('potential') ? ( +
+
+ Total Ordered: + {String(row.total_ordered ?? row.Total_Ordered ?? '-')} +
+
+ Potential: + {String(row.actual_potential ?? row.Actual_Potential ?? row.store_potential ?? row.Store_Potential ?? '-')} +
+
+ Difference: + {(() => { + const diff = Number(row.difference ?? row.Difference); + if (isNaN(diff)) return -; + return ( + + {diff < 0 ? `${Math.abs(diff)} Kgs Less Ordered` : `${diff} Kgs More Ordered`} + + ); + })()} +
+
+ ) : ( + {bags} Bags + )} +
+ +
+ + +
-
- ))} + ); + })}
)} - + +
+ +
+ +
+
+
+

+ {editingIdx !== null ? ( + <> Edit Row + ) : ( + <>Add New Item + )} +

+
+ {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 ( + updateNewRowField(col.id, col.data_type === 'number' ? Number(e.target.value) : e.target.value)} + /> + ); + })} + +
+ + +
+
+
+
+
); } diff --git a/src/components/rv/OrdersView.tsx b/src/components/rv/OrdersView.tsx index 8e08dc9..8fab1b2 100644 --- a/src/components/rv/OrdersView.tsx +++ b/src/components/rv/OrdersView.tsx @@ -8,10 +8,11 @@ export interface WiredRecordViewProps { headerActions?: React.ReactNode; rowActions?: (row: Record) => React.ReactNode; refreshKey?: number; + initialFilters?: Record; } /** 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 ( ); } diff --git a/src/components/rv/RecordView.tsx b/src/components/rv/RecordView.tsx index ee9e76c..9c94469 100644 --- a/src/components/rv/RecordView.tsx +++ b/src/components/rv/RecordView.tsx @@ -39,6 +39,8 @@ export interface RecordViewProps { sortBy?: string; /** Sort direction */ sortDir?: 'asc' | 'desc'; + /** Default filters to apply initially. */ + initialFilters?: Record; } /** @@ -60,11 +62,12 @@ export function RecordView({ sortBy, sortDir, omitColumns, + initialFilters = {}, }: RecordViewProps) { const [page, setPage] = useState(1); const [search, setSearch] = useState(''); const [debounced, setDebounced] = useState(''); - const [activeFilters, setActiveFilters] = useState>({}); + const [activeFilters, setActiveFilters] = useState>(initialFilters); const [resp, setResp] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); diff --git a/src/screens/CallsPage.tsx b/src/screens/CallsPage.tsx index 0bd6938..5488583 100644 --- a/src/screens/CallsPage.tsx +++ b/src/screens/CallsPage.tsx @@ -2,7 +2,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import { Modal } from '../components/reusable'; import { CallsView } from '../components/rv'; import { CallDetail } from '../components/dv'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Button } from '../components/buttons/Button'; import { Plus } from 'lucide-react'; import { DynamicForm } from '../components/forms/DynamicForm'; @@ -17,11 +17,74 @@ export function CallsPage() { const [refreshKey, setRefreshKey] = useState(0); const [activeActivity, setActiveActivity] = useState<{ id: string; name: string } | null>(null); + const [selectedRow, setSelectedRow] = useState | null>(null); + const [miningLoading, setMiningLoading] = useState(false); + const [miningPrefill, setMiningPrefill] = useState | 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 ( <> { + setSelectedRow(row); const id = row.instance_id as number | string | undefined; if (id != null) navigate(`/calls/${id}`); }} @@ -56,14 +119,33 @@ export function CallsPage() { width="lg" actions={ <> - - + {(() => { + 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 ( + + ); + } else if (stateName.includes('productive')) { + return ( + + ); + } + return ( + + ); + })()} } + > {instanceId != null && } @@ -80,11 +162,13 @@ export function CallsPage() { activityId={activeActivity.id} instanceId={instanceId} ignorePrefill={activeActivity.id === ORDER_BOOKING.activities.PLACE_ORDER.uid} + customPrefillData={activeActivity.id === ORDER_BOOKING.activities.POTENTIAL_MINING.uid ? miningPrefill : undefined} onSuccess={() => { setActiveActivity(null); setRefreshKey(k => k + 1); }} onCancel={() => setActiveActivity(null)} + onActivityChange={(name) => setActiveActivity(prev => prev ? { ...prev, name } : null)} /> )} diff --git a/src/screens/DailySalesReportPage.tsx b/src/screens/DailySalesReportPage.tsx new file mode 100644 index 0000000..f1107a5 --- /dev/null +++ b/src/screens/DailySalesReportPage.tsx @@ -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(null); + const [reportData, setReportData] = useState(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 ( +
+

Daily Sales Report

+ + +
+ setDate(e.target.value)} + required + /> + setDistributor(e.target.value)} + options={[{ value: '', label: 'Select Distributor' }, ...SALES_REPORT_DISTRIBUTORS.map(d => ({ value: d, label: d }))]} + required + /> +