From 4410a38353147167c7bc73996b4c78344ac769fd Mon Sep 17 00:00:00 2001 From: suryac Date: Tue, 7 Jul 2026 12:39:58 +0530 Subject: [PATCH] fix:based on the recordview,form-screens response added filters,fields,prefills,charts --- package-lock.json | 403 +++++++++++++++++- package.json | 3 +- response.json | 0 src/api/types.ts | 23 +- src/components/dv/DetailView.tsx | 97 +++-- src/components/forms/DynamicForm.tsx | 281 ++++++++++++ src/components/forms/fields/DateField.tsx | 23 + src/components/forms/fields/EmailField.tsx | 23 + src/components/forms/fields/FileInput.tsx | 89 ++++ .../forms/fields/GeolocationInput.tsx | 61 +++ src/components/forms/fields/GridInput.tsx | 85 ++++ src/components/forms/fields/PhoneInput.tsx | 32 ++ src/components/forms/fields/SelectField.tsx | 25 ++ src/components/forms/fields/TextAreaField.tsx | 22 + src/components/forms/fields/TextField.tsx | 25 ++ src/components/forms/fields/TimeField.tsx | 23 + src/components/forms/fields/index.ts | 10 + src/components/reusable/AnalyticsChart.tsx | 87 ++++ src/components/reusable/Pagination.tsx | 2 +- src/components/reusable/StatsTiles.tsx | 60 +++ src/components/reusable/Textarea.tsx | 50 +++ src/components/reusable/index.ts | 2 + src/components/rv/CallsView.tsx | 7 +- src/components/rv/DailyLogsView.tsx | 5 +- src/components/rv/OrdersView.tsx | 8 +- src/components/rv/RecordView.tsx | 193 ++++++--- src/components/rv/StoresView.tsx | 5 +- src/lib/format.ts | 1 + src/screens/CallsPage.tsx | 41 +- src/screens/DailyLogsPage.tsx | 67 ++- src/screens/OrdersPage.tsx | 41 +- src/screens/StoresPage.tsx | 69 ++- 32 files changed, 1728 insertions(+), 135 deletions(-) create mode 100644 response.json create mode 100644 src/components/forms/DynamicForm.tsx create mode 100644 src/components/forms/fields/DateField.tsx create mode 100644 src/components/forms/fields/EmailField.tsx create mode 100644 src/components/forms/fields/FileInput.tsx create mode 100644 src/components/forms/fields/GeolocationInput.tsx create mode 100644 src/components/forms/fields/GridInput.tsx create mode 100644 src/components/forms/fields/PhoneInput.tsx create mode 100644 src/components/forms/fields/SelectField.tsx create mode 100644 src/components/forms/fields/TextAreaField.tsx create mode 100644 src/components/forms/fields/TextField.tsx create mode 100644 src/components/forms/fields/TimeField.tsx create mode 100644 src/components/forms/fields/index.ts create mode 100644 src/components/reusable/AnalyticsChart.tsx create mode 100644 src/components/reusable/StatsTiles.tsx create mode 100644 src/components/reusable/Textarea.tsx diff --git a/package-lock.json b/package-lock.json index 076f055..a1634e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "lucide-react": "^0.400.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "react-router-dom": "^7.18.1" + "react-router-dom": "^7.18.1", + "recharts": "^3.9.2" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -577,6 +578,32 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", @@ -841,6 +868,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@tailwindcss/node": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", @@ -1124,6 +1163,69 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -1159,7 +1261,7 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1175,6 +1277,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.62.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz", @@ -1575,6 +1683,15 @@ ], "license": "CC-BY-4.0" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1614,9 +1731,130 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1635,6 +1873,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1673,6 +1917,16 @@ "node": ">=10.13.0" } }, + "node_modules/es-toolkit": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz", + "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1881,6 +2135,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2056,6 +2316,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.1.11", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.11.tgz", + "integrity": "sha512-qzXuyXAkPySAGYkfsAwodDPWT8Zm7/Uo5BNt4BjhMhG5WlWyZZ4wQqnWwdS8kjlQ1Cwu6gjw3A6+0gTQwlyYtw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -2066,6 +2336,15 @@ "node": ">=0.8.19" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2709,6 +2988,36 @@ "react": "^19.2.7" } }, + "node_modules/react-is": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.18.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", @@ -2747,6 +3056,57 @@ "react-dom": ">=18" } }, + "node_modules/recharts": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.9.2.tgz", + "integrity": "sha512-G4fy+Pk46RaXgwWMh+Nzhyo/lbFAVqXo9gtetlyehe6Ehge9CsgDuOTwQDD+i1+llaLktNBiNq4bhnGlDRXFtw==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, "node_modules/rolldown": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", @@ -2857,6 +3217,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -2994,6 +3360,37 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", diff --git a/package.json b/package.json index cc1000f..f2fd827 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "lucide-react": "^0.400.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "react-router-dom": "^7.18.1" + "react-router-dom": "^7.18.1", + "recharts": "^3.9.2" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/response.json b/response.json new file mode 100644 index 0000000..e69de29 diff --git a/src/api/types.ts b/src/api/types.ts index 8105dc2..c0ec80c 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -29,10 +29,21 @@ export interface RecordViewField { is_search: boolean; } +export interface TileItem { + tile_uid: string; + key: string; + value: unknown; +} + export interface RecordViewResponse { - config: { fields: RecordViewField[] }; + config: { + fields: RecordViewField[]; + filter_options?: Record; + }; data: Record[]; pagination?: { page: number; limit: number; total_count: number; total_pages: number }; + tile_values?: TileItem[]; + chart_data?: unknown[]; } export interface RecordViewParams { @@ -80,6 +91,8 @@ export interface FormScreenField { mapped_workflow_field?: string; type?: string; // 'mapped' | 'local' properties?: FormScreenFieldProps | null; + /** Present when data_type === 'grid' */ + columns?: FormScreenField[]; } export interface FormScreenGridItem { @@ -121,6 +134,10 @@ export interface FieldDefault { /** Read-only in the form — value is computed (e.g. premium_amount by custom_js). */ disabled?: boolean; value?: unknown; + prefill?: { + type: string; + value: string; + }; } export interface FormScreenResponse { @@ -132,6 +149,10 @@ export interface FormScreenResponse { field_rules?: FieldRule[]; /** Per-field designer defaults (e.g. `{ assign_city: { hidden: true } }`). */ field_defaults?: Record; + /** Existing instance data if instance_id was passed. */ + data?: Record; + /** Existing instance data is sometimes returned as prefill_data. */ + prefill_data?: Record; } // --- Audit (GET /app/{appId}/view/audit) --- diff --git a/src/components/dv/DetailView.tsx b/src/components/dv/DetailView.tsx index 3b7849c..bf66ce5 100644 --- a/src/components/dv/DetailView.tsx +++ b/src/components/dv/DetailView.tsx @@ -2,26 +2,16 @@ import { useEffect, useState } from 'react'; import { cn } from '../../lib/cn'; import { formatValue } from '../../lib/format'; import type { ZinoClient } from '../../api/client'; +import type { AuditEntry } from '../../api/types'; import { Card } from '../reusable/Card'; import { Spinner } from '../reusable/Spinner'; import { EmptyState } from '../reusable/EmptyState'; -interface DetailField { - field_key: string; - output_label: string; - data_type: string; -} - -interface DetailResponse { - config: { fields: DetailField[] }; - data: Record; -} - export interface DetailViewProps { /** Workflow-bound client (see api/clients.ts). */ client: ZinoClient; - /** detailview template uid. */ - dvUid: string; + /** detailview template uid (deprecated, using audit endpoint now). */ + dvUid?: string; /** Instance to render. */ instanceId: number | string; /** Card header title. */ @@ -33,12 +23,11 @@ export interface DetailViewProps { } /** - * Generic Zino detail view. Fetches `GET /app/{id}/view/detailview/{uid}` and - * renders config.fields as a labeled definition grid. Wire it to a specific - * view via the thin wrappers in this folder. + * Generic Zino detail view. Fetches `GET /app/{id}/view/audit` and + * renders the audit trail as a labeled definition grid. */ -export function DetailView({ client, dvUid, instanceId, title, fields, columns = 2 }: DetailViewProps) { - const [resp, setResp] = useState(null); +export function DetailView({ client, instanceId, title, columns = 2 }: DetailViewProps) { + const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -48,8 +37,8 @@ export function DetailView({ client, dvUid, instanceId, title, fields, columns = setLoading(true); setError(null); try { - const r = await client.detailView(dvUid, instanceId); - if (live) setResp(r as DetailResponse); + const r = await client.audit(instanceId); + if (live) setEntries(r); } catch (e) { if (live) setError((e as { message?: string })?.message ?? 'Failed to load'); } finally { @@ -60,38 +49,54 @@ export function DetailView({ client, dvUid, instanceId, title, fields, columns = return () => { live = false; }; - }, [client, dvUid, instanceId]); - - const all = resp?.config.fields ?? []; - const shown = fields - ? fields - .map((k) => all.find((f) => f.field_key === k) ?? { field_key: k, output_label: k, data_type: 'string' }) - : all; + }, [client, instanceId]); const gridCols = { 1: 'grid-cols-1', 2: 'grid-cols-1 sm:grid-cols-2', 3: 'grid-cols-1 sm:grid-cols-3' }[columns]; - return ( - - {error ? ( + if (error) { + return ( + - ) : loading ? ( + + ); + } + + if (loading) { + return ( +
- ) : shown.length === 0 ? ( - - ) : ( -
- {shown.map((f) => ( -
-
{f.output_label}
-
- {formatValue(resp?.data[f.field_key])} -
-
- ))} -
- )} -
+
+ ); + } + + if (entries.length === 0) { + return ( + + + + ); + } + + return ( +
+ {entries.map((entry, i) => ( + +
+ {Object.entries(entry.data).map(([key, value]) => ( +
+
+ {key.replace(/_/g, ' ')} +
+
+ {formatValue(value)} +
+
+ ))} +
+
+ ))} +
); } diff --git a/src/components/forms/DynamicForm.tsx b/src/components/forms/DynamicForm.tsx new file mode 100644 index 0000000..a7b5aae --- /dev/null +++ b/src/components/forms/DynamicForm.tsx @@ -0,0 +1,281 @@ +import { useState, useEffect } from 'react'; +import type { ZinoClient } from '../../api/client'; +import type { FormScreenResponse, FormScreenField } from '../../api/types'; +import { Button } from '../buttons/Button'; +import { Spinner } from '../reusable/Spinner'; +import { + FileInput, + GridInput, + GeolocationInput, + PhoneInput, + SelectField, + TextField, + EmailField, + TextAreaField, + DateField, + TimeField, +} from './fields'; + +export interface DynamicFormProps { + client: ZinoClient; + activityId: string; + instanceId?: number | string; + onSuccess?: () => void; + onCancel?: () => void; +} + +export function DynamicForm({ client, activityId, instanceId, onSuccess, onCancel }: DynamicFormProps) { + const [schema, setSchema] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let mounted = true; + setLoading(true); + client.formSchema(activityId, instanceId) + .then(res => { + if (mounted) { + setSchema(res); + const defaultValues: Record = {}; + if (res.field_defaults) { + Object.entries(res.field_defaults).forEach(([fieldId, def]) => { + if (def.value != null) { + defaultValues[fieldId] = def.value; + } else if (def.prefill) { + if (def.prefill.value === 'current_date') { + defaultValues[fieldId] = new Date().toISOString().split('T')[0]; + } else if (def.prefill.value === 'current_time') { + defaultValues[fieldId] = new Date().toTimeString().split(' ')[0].substring(0, 5); + } else if (def.prefill.value === 'current_user_id') { + const user = client.currentUser(); + defaultValues[fieldId] = user ? Number(user.id) : ''; + } else { + defaultValues[fieldId] = def.prefill.value; + } + } + }); + } + if (res.prefill_data) { + Object.assign(defaultValues, res.prefill_data); + } else if (res.data) { + Object.assign(defaultValues, res.data); + } + setValues(defaultValues); + setLoading(false); + } + }) + .catch((err: any) => { + if (mounted) { + setError(err?.message || 'Failed to load schema'); + setLoading(false); + } + }); + return () => { mounted = false; }; + }, [client, activityId]); + + const [values, setValues] = useState>({}); + const [submitting, setSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + + if (loading) { + return
; + } + if (error || !schema) { + return
Failed to load form: {error}
; + } + + // Filter out disabled fields (usually server-generated IDs) + const fields = schema.fields.filter(f => !f.properties?.disabled); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSubmitting(true); + setSubmitError(null); + try { + const payload: Record = {}; + + for (const f of fields) { + const val = values[f.id]; + if (val == null) continue; + + if (f.data_type === 'phone' && typeof val === 'string') { + const phoneNum = val.replace(/^\+91\s*/, '').trim(); + payload[f.id] = { + dial_code: '+91', + phone: phoneNum, + phone_with_dial_code: `+91${phoneNum}` + }; + } else if ((f.data_type === 'image' || f.data_type === 'file') && Array.isArray(val) && val.length > 0 && val[0] instanceof File) { + const uploadedFiles = []; + for (const file of val) { + const fileMeta = await client.uploadFile(file, { activityId, fieldId: f.id }); + uploadedFiles.push(fileMeta); + } + payload[f.id] = uploadedFiles; + } else if ((f.data_type === 'image' || f.data_type === 'file') && val instanceof File) { + const fileMeta = await client.uploadFile(val, { activityId, fieldId: f.id }); + payload[f.id] = [fileMeta]; + } else { + payload[f.id] = val; + } + } + + if (instanceId != null) { + await client.performActivity(instanceId, activityId, payload); + } else { + await client.startInstance(activityId, payload); + } + onSuccess?.(); + } catch (err: any) { + setSubmitError(err.message || 'Failed to submit form'); + } finally { + setSubmitting(false); + } + }; + + return ( +
+ {fields.map(f => { + const type = f.data_type; + const val = values[f.id]; + const isDisabled = schema.field_defaults?.[f.id]?.disabled; + + const renderField = () => { + if (type.startsWith('select') || type.startsWith('multiselect')) { + return ( + setValues(prev => ({ ...prev, [f.id]: newVal }))} + /> + ); + } + + if (type.startsWith('image') || type.startsWith('file')) { + return ( + setValues(prev => ({ ...prev, [f.id]: newVal }))} + /> + ); + } + + if (type.startsWith('grid')) { + return ( + []) || []} + onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))} + /> + ); + } + + if (type.startsWith('geolocation')) { + return ( + setValues(prev => ({ ...prev, [f.id]: newVal }))} + /> + ); + } + + if (type.startsWith('phone')) { + const phoneStr = typeof val === 'object' && val !== null ? (val as any).phone || '' : (val as string) ?? ''; + return ( + setValues(prev => ({ ...prev, [f.id]: newVal }))} + /> + ); + } + + if (type.startsWith('email')) { + return ( + setValues(prev => ({ ...prev, [f.id]: newVal }))} + /> + ); + } + + if (type.startsWith('textarea') || type.startsWith('text_area') || type.startsWith('longtext')) { + return ( + setValues(prev => ({ ...prev, [f.id]: newVal }))} + /> + ); + } + + if (type.startsWith('date')) { + return ( + setValues(prev => ({ ...prev, [f.id]: newVal }))} + /> + ); + } + + if (type.startsWith('time')) { + return ( + setValues(prev => ({ ...prev, [f.id]: newVal }))} + /> + ); + } + + return ( + setValues(prev => ({ ...prev, [f.id]: newVal }))} + /> + ); + }; + + const content = renderField(); + return isDisabled ? ( +
+ {content} +
+ ) : ( +
{content}
+ ); + })} + + {submitError &&
{submitError}
} + +
+ {onCancel && ( + + )} + +
+ + ); +} diff --git a/src/components/forms/fields/DateField.tsx b/src/components/forms/fields/DateField.tsx new file mode 100644 index 0000000..b8139c0 --- /dev/null +++ b/src/components/forms/fields/DateField.tsx @@ -0,0 +1,23 @@ +import { Input } from '../../reusable/Input'; + +export function DateField({ + label, + required, + value, + onChange, +}: { + label: string; + required?: boolean; + value: string; + onChange: (val: string) => void; +}) { + return ( + onChange(e.target.value)} + /> + ); +} diff --git a/src/components/forms/fields/EmailField.tsx b/src/components/forms/fields/EmailField.tsx new file mode 100644 index 0000000..798f770 --- /dev/null +++ b/src/components/forms/fields/EmailField.tsx @@ -0,0 +1,23 @@ +import { Input } from '../../reusable/Input'; + +export function EmailField({ + label, + required, + value, + onChange, +}: { + label: string; + required?: boolean; + value: string; + onChange: (val: string) => void; +}) { + return ( + onChange(e.target.value)} + /> + ); +} diff --git a/src/components/forms/fields/FileInput.tsx b/src/components/forms/fields/FileInput.tsx new file mode 100644 index 0000000..9a40207 --- /dev/null +++ b/src/components/forms/fields/FileInput.tsx @@ -0,0 +1,89 @@ +import { useState, useEffect } from 'react'; + +export function ImagePreview({ file }: { file: File }) { + const [url, setUrl] = useState(null); + + useEffect(() => { + const objectUrl = URL.createObjectURL(file); + setUrl(objectUrl); + return () => URL.revokeObjectURL(objectUrl); + }, [file]); + + if (!url) return null; + return Preview; +} + +export function FileInput({ + label, + type, + required, + value, + onChange, +}: { + label: string; + type: 'image' | 'file' | string; + required?: boolean; + value: File[] | undefined | null | unknown; + onChange: (files: File[]) => void; +}) { + const files = Array.isArray(value) ? value : (value instanceof File ? [value] : []); + + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files) { + const newFiles = Array.from(e.target.files); + onChange([...files, ...newFiles]); + } + // reset input so the same file can be selected again if needed + e.target.value = ''; + }; + + const removeFile = (index: number) => { + const newFiles = [...files]; + newFiles.splice(index, 1); + onChange(newFiles); + }; + + return ( +
+ + {label} + {required && *} + + +
+ + + {files.length > 0 && ( +
+ {files.map((file, i) => ( +
+ {type === 'image' ? ( + + ) : ( +
+ {file.name} +
+ )} + +
+ ))} +
+ )} +
+
+ ); +} diff --git a/src/components/forms/fields/GeolocationInput.tsx b/src/components/forms/fields/GeolocationInput.tsx new file mode 100644 index 0000000..17e9150 --- /dev/null +++ b/src/components/forms/fields/GeolocationInput.tsx @@ -0,0 +1,61 @@ +import { useState } from 'react'; +import { Button } from '../../buttons/Button'; + +export function GeolocationInput({ + label, + required, + value, + onChange, +}: { + label: string; + required?: boolean; + value: { latitude: number; longitude: number } | null; + onChange: (val: { latitude: number; longitude: number } | null) => void; +}) { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchLocation = () => { + if (!navigator.geolocation) { + setError('Geolocation is not supported by your browser.'); + return; + } + setLoading(true); + setError(null); + navigator.geolocation.getCurrentPosition( + (position) => { + onChange({ + latitude: position.coords.latitude, + longitude: position.coords.longitude, + }); + setLoading(false); + }, + (err) => { + setError(err.message); + setLoading(false); + }, + { enableHighAccuracy: true } + ); + }; + + return ( +
+ + {label} + {required && *} + +
+ + {value && ( + + {value.latitude.toFixed(5)}, {value.longitude.toFixed(5)} + + )} +
+ {error && {error}} + {required && !value && } +
+ ); +} diff --git a/src/components/forms/fields/GridInput.tsx b/src/components/forms/fields/GridInput.tsx new file mode 100644 index 0000000..601c24a --- /dev/null +++ b/src/components/forms/fields/GridInput.tsx @@ -0,0 +1,85 @@ +import { Button } from '../../buttons/Button'; +import { Select } from '../../reusable/Select'; +import { Input } from '../../reusable/Input'; +import type { FormScreenField } from '../../../api/types'; + +export function GridInput({ + label, + columns, + value = [], + onChange, +}: { + label: string; + columns: FormScreenField[]; + value: Record[]; + onChange: (val: Record[]) => void; +}) { + const addRow = () => { + onChange([...value, {}]); + }; + + const removeRow = (idx: number) => { + const next = [...value]; + next.splice(idx, 1); + onChange(next); + }; + + const updateRow = (idx: number, fieldId: string, val: unknown) => { + const next = [...value]; + next[idx] = { ...next[idx], [fieldId]: val }; + onChange(next); + }; + + 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') { + const opts = col.properties?.options || []; + return ( + updateRow(i, col.id, col.data_type === 'number' ? Number(e.target.value) : e.target.value)} + /> + ); + })} +
+
+ ))} +
+ )} + +
+ ); +} diff --git a/src/components/forms/fields/PhoneInput.tsx b/src/components/forms/fields/PhoneInput.tsx new file mode 100644 index 0000000..9672174 --- /dev/null +++ b/src/components/forms/fields/PhoneInput.tsx @@ -0,0 +1,32 @@ +import { Input } from '../../reusable/Input'; + +export function PhoneInput({ + label, + required, + value, + onChange, +}: { + label: string; + required?: boolean; + value: string; + onChange: (val: string) => void; +}) { + return ( + { + const numericOnly = e.target.value.replace(/\D/g, ''); + if (numericOnly.length <= 10) { + onChange(numericOnly); + } + }} + /> + ); +} diff --git a/src/components/forms/fields/SelectField.tsx b/src/components/forms/fields/SelectField.tsx new file mode 100644 index 0000000..4848a4f --- /dev/null +++ b/src/components/forms/fields/SelectField.tsx @@ -0,0 +1,25 @@ +import { Select } from '../../reusable/Select'; + +export function SelectField({ + label, + required, + value, + options, + onChange, +}: { + label: string; + required?: boolean; + value: string; + options: any[]; + onChange: (val: string) => void; +}) { + return ( +