fix:based on the recordview,form-screens response added filters,fields,prefills,charts

This commit is contained in:
suryac 2026-07-07 12:39:58 +05:30
parent 4990eceada
commit 4410a38353
32 changed files with 1728 additions and 135 deletions

403
package-lock.json generated
View File

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

View File

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

0
response.json Normal file
View File

View File

@ -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<string, string[]>;
};
data: Record<string, unknown>[];
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<string, FieldDefault>;
/** Existing instance data if instance_id was passed. */
data?: Record<string, unknown>;
/** Existing instance data is sometimes returned as prefill_data. */
prefill_data?: Record<string, unknown>;
}
// --- Audit (GET /app/{appId}/view/audit) ---

View File

@ -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<string, unknown>;
}
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<DetailResponse | null>(null);
export function DetailView({ client, instanceId, title, columns = 2 }: DetailViewProps) {
const [entries, setEntries] = useState<AuditEntry[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 (
<Card title={title}>
{error ? (
if (error) {
return (
<Card title={title}>
<EmptyState title="Couldnt load record" hint={error} />
) : loading ? (
</Card>
);
}
if (loading) {
return (
<Card title={title}>
<div className="py-8 flex justify-center">
<Spinner label="Loading…" />
</div>
) : shown.length === 0 ? (
<EmptyState title="No fields to display" />
) : (
<dl className={cn('grid gap-x-6 gap-y-4', gridCols)}>
{shown.map((f) => (
<div key={f.field_key} className="flex flex-col gap-1 min-w-0">
<dt className="text-[10px] font-bold uppercase tracking-[0.06em] text-faint">{f.output_label}</dt>
<dd className="m-0 text-sm text-strong font-medium break-words">
{formatValue(resp?.data[f.field_key])}
</dd>
</div>
))}
</dl>
)}
</Card>
</Card>
);
}
if (entries.length === 0) {
return (
<Card title={title}>
<EmptyState title="No history found" />
</Card>
);
}
return (
<div className="flex flex-col gap-4">
{entries.map((entry, i) => (
<Card key={entry.id} title={i === 0 && title ? title : `Update on ${new Date(entry.created_at).toLocaleString()}`}>
<dl className={cn('grid gap-x-6 gap-y-4', gridCols)}>
{Object.entries(entry.data).map(([key, value]) => (
<div key={key} className="flex flex-col gap-1 min-w-0">
<dt className="text-[10px] font-bold uppercase tracking-[0.06em] text-faint">
{key.replace(/_/g, ' ')}
</dt>
<dd className="m-0 text-sm text-strong font-medium break-words">
{formatValue(value)}
</dd>
</div>
))}
</dl>
</Card>
))}
</div>
);
}

View File

@ -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<FormScreenResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let mounted = true;
setLoading(true);
client.formSchema(activityId, instanceId)
.then(res => {
if (mounted) {
setSchema(res);
const defaultValues: Record<string, unknown> = {};
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<Record<string, unknown>>({});
const [submitting, setSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
if (loading) {
return <div className="p-8 flex justify-center"><Spinner label="Loading form..." /></div>;
}
if (error || !schema) {
return <div className="p-4 text-ruby-600">Failed to load form: {error}</div>;
}
// 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<string, unknown> = {};
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 (
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
{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 (
<SelectField
label={f.name}
required={f.mandatory}
value={(val as string) ?? ''}
options={f.properties?.options || []}
onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))}
/>
);
}
if (type.startsWith('image') || type.startsWith('file')) {
return (
<FileInput
label={f.name}
type={type}
required={f.mandatory}
value={val}
onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))}
/>
);
}
if (type.startsWith('grid')) {
return (
<GridInput
label={f.name}
columns={f.columns || []}
value={(val as Record<string, unknown>[]) || []}
onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))}
/>
);
}
if (type.startsWith('geolocation')) {
return (
<GeolocationInput
label={f.name}
required={f.mandatory}
value={(val as { latitude: number; longitude: number }) || null}
onChange={(newVal) => 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 (
<PhoneInput
label={f.name}
required={f.mandatory}
value={phoneStr}
onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))}
/>
);
}
if (type.startsWith('email')) {
return (
<EmailField
label={f.name}
required={f.mandatory}
value={(val as string) ?? ''}
onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))}
/>
);
}
if (type.startsWith('textarea') || type.startsWith('text_area') || type.startsWith('longtext')) {
return (
<TextAreaField
label={f.name}
required={f.mandatory}
value={(val as string) ?? ''}
onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))}
/>
);
}
if (type.startsWith('date')) {
return (
<DateField
label={f.name}
required={f.mandatory}
value={(val as string) ?? ''}
onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))}
/>
);
}
if (type.startsWith('time')) {
return (
<TimeField
label={f.name}
required={f.mandatory}
value={(val as string) ?? ''}
onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))}
/>
);
}
return (
<TextField
label={f.name}
required={f.mandatory}
type={type}
value={(val as string) ?? ''}
onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))}
/>
);
};
const content = renderField();
return isDisabled ? (
<fieldset key={f.id} disabled className="opacity-60 pointer-events-none">
{content}
</fieldset>
) : (
<div key={f.id}>{content}</div>
);
})}
{submitError && <div className="text-sm text-ruby-600 mt-2">{submitError}</div>}
<div className="flex items-center justify-end gap-3 mt-4 pt-4 border-t border-border-subtle">
{onCancel && (
<Button type="button" variant="secondary" onClick={onCancel} disabled={submitting}>
Cancel
</Button>
)}
<Button type="submit" variant="primary" disabled={submitting}>
{submitting ? 'Submitting...' : 'Submit'}
</Button>
</div>
</form>
);
}

View File

@ -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 (
<Input
label={label}
required={required}
type="date"
value={value ?? ''}
onChange={(e) => onChange(e.target.value)}
/>
);
}

View File

@ -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 (
<Input
label={label}
required={required}
type="email"
value={value ?? ''}
onChange={(e) => onChange(e.target.value)}
/>
);
}

View File

@ -0,0 +1,89 @@
import { useState, useEffect } from 'react';
export function ImagePreview({ file }: { file: File }) {
const [url, setUrl] = useState<string | null>(null);
useEffect(() => {
const objectUrl = URL.createObjectURL(file);
setUrl(objectUrl);
return () => URL.revokeObjectURL(objectUrl);
}, [file]);
if (!url) return null;
return <img src={url} alt="Preview" className="object-cover w-full h-full" />;
}
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<HTMLInputElement>) => {
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 (
<div className="flex flex-col gap-1.5 font-sans">
<span className="text-sm font-medium text-muted">
{label}
{required && <span className="text-ruby-600"> *</span>}
</span>
<div className="flex flex-col gap-3">
<input
type="file"
multiple
required={required && files.length === 0}
accept={type === 'image' ? 'image/*' : undefined}
onChange={handleFileChange}
className="w-full text-sm text-strong file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-slate-100 file:text-navy-700 hover:file:bg-slate-200"
/>
{files.length > 0 && (
<div className="flex flex-wrap gap-3">
{files.map((file, i) => (
<div key={i} className="relative w-24 h-24 rounded-md overflow-hidden border border-border-default shadow-sm bg-slate-50 flex-shrink-0 group">
{type === 'image' ? (
<ImagePreview file={file} />
) : (
<div className="w-full h-full flex items-center justify-center p-2 text-xs text-center break-all overflow-hidden text-muted">
{file.name}
</div>
)}
<button
type="button"
onClick={() => removeFile(i)}
className="absolute top-1 right-1 bg-ruby-600/90 hover:bg-ruby-700 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition-opacity"
title="Remove file"
>
</button>
</div>
))}
</div>
)}
</div>
</div>
);
}

View File

@ -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<string | null>(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 (
<div className="flex flex-col gap-1.5 font-sans">
<span className="text-sm font-medium text-muted">
{label}
{required && <span className="text-ruby-600"> *</span>}
</span>
<div className="flex items-center gap-3">
<Button type="button" variant="secondary" size="sm" onClick={fetchLocation} disabled={loading}>
{loading ? 'Fetching...' : 'Get Location'}
</Button>
{value && (
<span className="text-sm text-strong">
{value.latitude.toFixed(5)}, {value.longitude.toFixed(5)}
</span>
)}
</div>
{error && <span className="text-xs text-ruby-600">{error}</span>}
{required && !value && <input type="text" className="sr-only" required />}
</div>
);
}

View File

@ -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<string, unknown>[];
onChange: (val: Record<string, unknown>[]) => 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 (
<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>
{value.length === 0 ? (
<span className="text-sm text-faint italic">No rows added.</span>
) : (
<div className="flex flex-col gap-4">
{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">
<div className="absolute top-2 right-2">
<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') {
const opts = col.properties?.options || [];
return (
<Select
key={col.id}
label={col.name}
required={col.mandatory}
value={(val as string) ?? ''}
onChange={(e) => updateRow(i, col.id, e.target.value)}
options={[{ value: '', label: 'Select...' }, ...opts.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) => updateRow(i, col.id, col.data_type === 'number' ? Number(e.target.value) : e.target.value)}
/>
);
})}
</div>
</div>
))}
</div>
)}
<Button type="button" variant="secondary" size="sm" onClick={addRow} className="mt-2 self-start">
+ Add Row
</Button>
</div>
);
}

View File

@ -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 (
<Input
label={label}
required={required}
type="text"
inputMode="numeric"
prefix="+91"
maxLength={10}
placeholder="10-digit number"
value={value ?? ''}
onChange={(e) => {
const numericOnly = e.target.value.replace(/\D/g, '');
if (numericOnly.length <= 10) {
onChange(numericOnly);
}
}}
/>
);
}

View File

@ -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 (
<Select
label={label}
required={required}
value={value ?? ''}
onChange={(e) => onChange(e.target.value)}
options={[{ value: '', label: 'Select...' }, ...options.map((o: any) => ({ value: String(o.value), label: o.label }))]}
/>
);
}

View File

@ -0,0 +1,22 @@
import { Textarea } from '../../reusable/Textarea';
export function TextAreaField({
label,
required,
value,
onChange,
}: {
label: string;
required?: boolean;
value: string;
onChange: (val: string) => void;
}) {
return (
<Textarea
label={label}
required={required}
value={value ?? ''}
onChange={(e) => onChange(e.target.value)}
/>
);
}

View File

@ -0,0 +1,25 @@
import { Input } from '../../reusable/Input';
export function TextField({
label,
type,
required,
value,
onChange,
}: {
label: string;
type: string;
required?: boolean;
value: string;
onChange: (val: string) => void;
}) {
return (
<Input
label={label}
required={required}
type={type === 'number' ? 'number' : type === 'email' ? 'email' : 'text'}
value={value ?? ''}
onChange={(e) => onChange(e.target.value)}
/>
);
}

View File

@ -0,0 +1,23 @@
import { Input } from '../../reusable/Input';
export function TimeField({
label,
required,
value,
onChange,
}: {
label: string;
required?: boolean;
value: string;
onChange: (val: string) => void;
}) {
return (
<Input
label={label}
required={required}
type="time"
value={value ?? ''}
onChange={(e) => onChange(e.target.value)}
/>
);
}

View File

@ -0,0 +1,10 @@
export * from './FileInput';
export * from './GridInput';
export * from './GeolocationInput';
export * from './PhoneInput';
export * from './SelectField';
export * from './TextField';
export * from './EmailField';
export * from './TextAreaField';
export * from './DateField';
export * from './TimeField';

View File

@ -0,0 +1,87 @@
import { Card } from './Card';
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer
} from 'recharts';
export interface ChartRow {
dimension: string;
value: number;
}
export interface ChartConfig {
chart_uid: string;
key: string;
rows: ChartRow[];
}
export interface AnalyticsChartProps {
data?: unknown[];
}
export function AnalyticsChart({ data }: AnalyticsChartProps) {
if (!data || !Array.isArray(data) || data.length === 0) return null;
const charts = data as ChartConfig[];
const colors = ['#3B82F6', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899', '#14B8A6'];
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
{charts.map((chart, idx) => {
const title = (chart.key || `Chart ${idx + 1}`)
.replace(/_/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase());
// Safely format dimension string (fallbacks for empty strings)
const formattedRows = (chart.rows || []).map(r => ({
...r,
dimension: String(r.dimension || '').trim() || 'Unknown'
}));
if (formattedRows.length === 0) return null;
return (
<Card key={chart.chart_uid || idx} title={title} className="shadow-sm border-t-4 border-t-indigo-500">
<div className="h-[320px] w-full mt-4">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={formattedRows} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#E2E8F0" />
<XAxis
dataKey="dimension"
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: '#64748B' }}
dy={10}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: '#64748B' }}
allowDecimals={false}
/>
<Tooltip
cursor={{ fill: '#F8FAFC' }}
contentStyle={{ borderRadius: '8px', border: '1px solid #E2E8F0', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)', fontSize: '14px', fontFamily: 'inherit' }}
labelStyle={{ fontWeight: 'bold', color: '#0F172A', marginBottom: '4px' }}
/>
<Bar
dataKey="value"
name="Value"
fill={colors[idx % colors.length]}
radius={[4, 4, 0, 0]}
maxBarSize={40}
/>
</BarChart>
</ResponsiveContainer>
</div>
</Card>
);
})}
</div>
);
}

View File

@ -13,7 +13,7 @@ export interface PaginationProps {
/** Pager: "Showing ab of N" + Prev/Next. Renders nothing when the set fits on
* one page. Shared by the record-view tables. */
export function Pagination({ page, pageSize, total, onPage }: PaginationProps) {
if (total <= pageSize) return null;
if (total <= 0) return null;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const safePage = Math.min(Math.max(page, 1), totalPages);
const start = (safePage - 1) * pageSize;

View File

@ -0,0 +1,60 @@
import { Card } from "./Card";
import type { TileItem } from "../../api/types";
export interface StatsTilesProps {
tiles?: TileItem[];
}
const colors = [
"from-blue-500 to-cyan-500",
"from-emerald-500 to-green-500",
"from-orange-500 to-amber-500",
"from-violet-500 to-fuchsia-500",
"from-pink-500 to-rose-500",
"from-indigo-500 to-blue-500",
];
export function StatsTiles({ tiles }: StatsTilesProps) {
if (!tiles?.length) return null;
return (
<div className="grid grid-cols-2 gap-5 md:grid-cols-4">
{tiles.map((tile, idx) => {
const displayLabel = (tile.key || `tile_${idx}`)
.replace(/_/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase());
const gradient = colors[idx % colors.length];
return (
<Card
key={tile.tile_uid || idx}
className="group relative overflow-hidden rounded-2xl border border-gray-200 bg-white p-6 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-xl"
>
{/* Top Gradient */}
<div
className={`absolute left-0 top-0 h-1.5 w-full bg-gradient-to-r ${gradient}`}
/>
{/* Background Decoration */}
<div
className={`absolute -right-6 -top-6 h-24 w-24 rounded-full bg-gradient-to-br ${gradient} opacity-10 transition-all duration-300 group-hover:scale-125`}
/>
<div className="relative flex flex-col gap-3">
<p className="text-xs font-semibold uppercase tracking-[0.15em] text-gray-500">
{displayLabel}
</p>
<h2 className="text-4xl font-bold tracking-tight text-gray-900">
{(tile.value as React.ReactNode) ?? "-"}
</h2>
<div className="h-1 w-12 rounded-full bg-gray-200 transition-all duration-300 group-hover:w-20 group-hover:bg-blue-500" />
</div>
</Card>
);
})}
</div>
);
}

View File

@ -0,0 +1,50 @@
import type { TextareaHTMLAttributes } from 'react';
import { cn } from '../../lib/cn';
export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
label?: string;
hint?: string;
error?: string;
className?: string;
}
export function Textarea({
label,
hint,
error,
required,
className,
disabled,
...rest
}: TextareaProps) {
return (
<label className={cn('flex flex-col gap-1.5 font-sans', disabled && 'cursor-not-allowed', className)}>
{label && (
<span className="text-sm font-medium text-muted">
{label}
{required && <span className="text-ruby-600"> *</span>}
</span>
)}
<div
className={cn(
'flex rounded-md border transition-[border-color,box-shadow] duration-150 overflow-hidden',
disabled ? 'bg-sunk border-border-subtle' : 'bg-card',
error ? 'border-ruby-600' : !disabled && 'border-border-default focus-within:ring-2 focus-within:ring-navy-600/20 focus-within:border-navy-600',
)}
>
<textarea
required={required}
disabled={disabled}
className={cn(
'flex-1 w-full min-h-[80px] p-3 border-none outline-none bg-transparent font-sans text-base resize-y',
disabled ? 'text-muted cursor-not-allowed' : 'text-strong',
)}
{...rest}
/>
</div>
{(hint || error) && (
<span className={cn('text-xs', error ? 'text-ruby-600' : 'text-faint')}>{error || hint}</span>
)}
</label>
);
}

View File

@ -2,6 +2,8 @@ export { Card } from './Card';
export type { CardProps, Elevation } from './Card';
export { Badge } from './Badge';
export type { BadgeProps, BadgeTone } from './Badge';
export { Textarea } from './Textarea';
export type { TextareaProps } from './Textarea';
export { Input } from './Input';
export type { InputProps } from './Input';
export { Select } from './Select';

View File

@ -3,8 +3,8 @@ import { ORDER_BOOKING } from '../../api/config';
import { RecordView } from './RecordView';
import type { WiredRecordViewProps } from './OrdersView';
/** Calls record view (Order Booking workflow). */
export function CallsView({ onRowClick, pageSize }: WiredRecordViewProps) {
/** Calls record view (Calls/Visits workflow). */
export function CallsView({ onRowClick, pageSize, headerActions, rowActions, refreshKey }: WiredRecordViewProps) {
return (
<RecordView
client={orderBookingClient}
@ -12,6 +12,9 @@ export function CallsView({ onRowClick, pageSize }: WiredRecordViewProps) {
title="Calls"
onRowClick={onRowClick}
pageSize={pageSize}
headerActions={headerActions}
rowActions={rowActions}
refreshKey={refreshKey}
/>
);
}

View File

@ -4,7 +4,7 @@ import { RecordView } from './RecordView';
import type { WiredRecordViewProps } from './OrdersView';
/** Daily Logs record view (Daily Reports workflow). */
export function DailyLogsView({ onRowClick, pageSize }: WiredRecordViewProps) {
export function DailyLogsView({ onRowClick, pageSize, headerActions, rowActions, refreshKey }: WiredRecordViewProps) {
return (
<RecordView
client={dailyReportsClient}
@ -12,6 +12,9 @@ export function DailyLogsView({ onRowClick, pageSize }: WiredRecordViewProps) {
title="Daily Logs"
onRowClick={onRowClick}
pageSize={pageSize}
headerActions={headerActions}
rowActions={rowActions}
refreshKey={refreshKey}
/>
);
}

View File

@ -5,10 +5,13 @@ import { RecordView } from './RecordView';
export interface WiredRecordViewProps {
onRowClick?: (row: Record<string, unknown>, index: number) => void;
pageSize?: number;
headerActions?: React.ReactNode;
rowActions?: (row: Record<string, unknown>) => React.ReactNode;
refreshKey?: number;
}
/** Orders record view (Order Booking workflow). */
export function OrdersView({ onRowClick, pageSize }: WiredRecordViewProps) {
export function OrdersView({ onRowClick, pageSize, headerActions, rowActions, refreshKey }: WiredRecordViewProps) {
return (
<RecordView
client={orderBookingClient}
@ -16,6 +19,9 @@ export function OrdersView({ onRowClick, pageSize }: WiredRecordViewProps) {
title="Orders"
onRowClick={onRowClick}
pageSize={pageSize}
headerActions={headerActions}
rowActions={rowActions}
refreshKey={refreshKey}
/>
);
}

View File

@ -8,6 +8,9 @@ import { Card } from '../reusable/Card';
import { Pagination } from '../reusable/Pagination';
import { Spinner } from '../reusable/Spinner';
import { EmptyState } from '../reusable/EmptyState';
import { StatsTiles } from '../reusable/StatsTiles';
import { AnalyticsChart } from '../reusable/AnalyticsChart';
import { Select } from '../reusable/Select';
export interface RecordViewProps {
/** Workflow-bound client (see api/clients.ts). */
@ -24,6 +27,12 @@ export interface RecordViewProps {
onRowClick?: (row: Record<string, unknown>, index: number) => void;
/** Extract a stable key for a row. @default row.instance_id ?? index */
rowKey?: (row: Record<string, unknown>, index: number) => string;
/** Additional actions to render in the header next to search box */
headerActions?: React.ReactNode;
/** Custom actions to render at the end of each row. */
rowActions?: (row: Record<string, unknown>) => React.ReactNode;
/** Pass a new value to trigger a refresh. */
refreshKey?: number;
}
/**
@ -34,15 +43,19 @@ export interface RecordViewProps {
export function RecordView({
client,
rvUid,
title,
title = 'Records',
columns,
pageSize = 25,
pageSize = 50,
onRowClick,
rowKey,
headerActions,
rowActions,
refreshKey,
}: RecordViewProps) {
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const [debounced, setDebounced] = useState('');
const [activeFilters, setActiveFilters] = useState<Record<string, string>>({});
const [resp, setResp] = useState<RecordViewResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@ -60,13 +73,24 @@ export function RecordView({
};
}, [search]);
const filtersParam = useMemo(() => {
return Object.entries(activeFilters)
.filter(([_, val]) => val)
.map(([key, val]) => ({
field_key: key,
value: val,
data_type: 'string',
}));
}, [activeFilters]);
useEffect(() => {
let live = true;
async function run() {
setLoading(true);
setError(null);
try {
const r = await client.recordView(rvUid, { page, limit: pageSize, search: debounced });
const r = await client.recordView(rvUid, { page, limit: pageSize, search: debounced, filters: filtersParam });
console.log("RECORD VIEW RESP:", r);
if (live) setResp(r);
} catch (e) {
if (live) setError((e as { message?: string })?.message ?? 'Failed to load');
@ -78,7 +102,7 @@ export function RecordView({
return () => {
live = false;
};
}, [client, rvUid, page, pageSize, debounced]);
}, [client, rvUid, page, pageSize, debounced, filtersParam, refreshKey]);
const fields: RecordViewField[] = useMemo(() => {
const all = resp?.config.fields ?? [];
@ -92,67 +116,114 @@ export function RecordView({
const rows = resp?.data ?? [];
const total = resp?.pagination?.total_count ?? rows.length;
const tileValues = resp?.tile_values;
const chartData = resp?.chart_data;
return (
<Card
title={title}
pad={false}
action={
<div className="relative w-[240px] max-w-full">
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-faint pointer-events-none" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search…"
className="w-full h-9 rounded-md border border-border-default bg-card pl-9 pr-3 font-sans text-sm text-strong outline-none focus-ring"
/>
<div className="flex flex-col gap-6 w-full">
<StatsTiles tiles={tileValues} />
<AnalyticsChart data={chartData} />
<Card
title={title}
action={headerActions}
pad={false}
>
<div className="flex flex-wrap items-center justify-between gap-4 p-4 border-b border-border-subtle bg-slate-50/50">
<div className="flex flex-wrap items-center gap-3">
{resp?.config?.filter_options && Object.entries(resp.config.filter_options).map(([key, options]) => {
if (!options || options.length === 0) return null;
// Find the field in config to get its proper label
const fieldDef = resp.config.fields.find(f => f.field_key === key);
const label = fieldDef?.output_label || key;
const opts = options.map(o => ({ value: o, label: o }));
return (
<Select
key={key}
value={activeFilters[key] || ''}
onChange={(e) => {
setActiveFilters(prev => ({ ...prev, [key]: e.target.value }));
setPage(1);
}}
options={[{ value: '', label: `All ${label}` }, ...opts]}
className="w-40 shrink-0"
/>
);
})}
</div>
<div className="relative w-full sm:w-[240px] shrink-0">
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-faint pointer-events-none" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search…"
className="w-full h-9 rounded-md border border-border-default bg-card pl-9 pr-3 font-sans text-sm text-strong outline-none focus-ring"
/>
</div>
</div>
}
>
{error ? (
<EmptyState title="Couldnt load records" hint={error} />
) : loading && !resp ? (
<div className="p-10 flex justify-center">
<Spinner label="Loading records…" />
</div>
) : rows.length === 0 ? (
<EmptyState title="No records" hint={debounced ? 'Nothing matches your search.' : undefined} />
) : (
<div className="overflow-x-auto scrollbar-slim">
<table className="w-full border-collapse min-w-[640px]">
<thead>
<tr className="bg-slate-50 border-b border-border-subtle">
{fields.map((f) => (
<th
key={f.field_key}
className="text-left px-[18px] py-[11px] text-[10px] font-bold uppercase tracking-[0.06em] text-faint whitespace-nowrap"
>
{f.output_label}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr
key={rowKey ? rowKey(row, i) : String(row.instance_id ?? i)}
onClick={onRowClick ? () => onRowClick(row, i) : undefined}
className={cn(
'border-b border-border-subtle transition-colors duration-150',
onRowClick && 'cursor-pointer hover:bg-slate-50',
)}
>
{error ? (
<EmptyState title="Couldnt load records" hint={error} />
) : loading && !resp ? (
<div className="p-10 flex justify-center">
<Spinner label="Loading records…" />
</div>
) : rows.length === 0 ? (
<EmptyState title="No records" hint={debounced ? 'Nothing matches your search.' : undefined} />
) : (
<div className="overflow-x-auto scrollbar-slim">
<table className="w-full border-collapse min-w-[640px]">
<thead>
<tr className="bg-slate-50 border-b border-border-subtle">
{fields.map((f) => (
<td key={f.field_key} className="px-[18px] py-3 text-sm text-body whitespace-nowrap">
{formatValue(row[f.field_key])}
</td>
<th
key={f.field_key}
className="text-left px-[18px] py-[11px] text-[10px] font-bold uppercase tracking-[0.06em] text-faint whitespace-nowrap"
>
{f.output_label}
</th>
))}
{rowActions && (
<th className="sticky right-0 bg-inherit px-4 py-3 text-left text-[10px] font-bold uppercase tracking-[0.06em] text-faint border-b border-border-subtle z-10 w-24">
Actions
</th>
)}
</tr>
))}
</tbody>
</table>
</div>
)}
<Pagination page={page} pageSize={pageSize} total={total} onPage={setPage} />
</Card>
</thead>
<tbody>
{rows.map((row, i) => (
<tr
key={rowKey ? rowKey(row, i) : String(row.instance_id ?? i)}
onClick={onRowClick ? () => onRowClick(row, i) : undefined}
className={cn(
'bg-surface border-b border-border-subtle transition-colors duration-150',
onRowClick && 'cursor-pointer hover:bg-slate-50',
)}
>
{fields.map((f) => (
<td key={f.field_key} className="px-[18px] py-3 text-sm text-body whitespace-nowrap">
{formatValue(row[f.field_key])}
</td>
))}
{rowActions && (
<td className="sticky right-0 bg-inherit px-4 py-3 whitespace-nowrap border-b border-border-subtle z-10">
<div onClick={(e) => e.stopPropagation()}>
{rowActions(row)}
</div>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
)}
<Pagination page={page} pageSize={pageSize} total={total} onPage={setPage} />
</Card>
</div>
);
}

View File

@ -4,7 +4,7 @@ import { RecordView } from './RecordView';
import type { WiredRecordViewProps } from './OrdersView';
/** Store record view (Store workflow). */
export function StoresView({ onRowClick, pageSize }: WiredRecordViewProps) {
export function StoresView({ onRowClick, pageSize, headerActions, rowActions, refreshKey }: WiredRecordViewProps & { headerActions?: React.ReactNode }) {
return (
<RecordView
client={storeClient}
@ -12,6 +12,9 @@ export function StoresView({ onRowClick, pageSize }: WiredRecordViewProps) {
title="Stores"
onRowClick={onRowClick}
pageSize={pageSize}
headerActions={headerActions}
rowActions={rowActions}
refreshKey={refreshKey}
/>
);
}

View File

@ -33,6 +33,7 @@ export function formatValue(value: unknown): string {
// File / image blob
if ('original_name' in o && o.original_name != null) return String(o.original_name);
if ('url' in o && o.url != null) return String(o.url);
if ('name' in o && o.name != null) return String(o.name);
if ('value' in o && o.value != null) return String(o.value);
}

View File

@ -2,17 +2,50 @@ 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 { 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';
export function CallsPage() {
const { instanceId } = useParams();
const navigate = useNavigate();
const [isCreating, setIsCreating] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
return (
<>
<CallsView onRowClick={(row) => {
const id = row.instance_id as number | string | undefined;
if (id != null) navigate(`/calls/${id}`);
}} />
<CallsView
refreshKey={refreshKey}
onRowClick={(row) => {
const id = row.instance_id as number | string | undefined;
if (id != null) navigate(`/calls/${id}`);
}}
headerActions={
<Button size="sm" iconLeft={<Plus size={14} />} onClick={() => setIsCreating(true)}>
Log Visit
</Button>
}
/>
<Modal
open={isCreating}
onClose={() => setIsCreating(false)}
title="Log Visit"
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}

View File

@ -2,17 +2,76 @@ import { useNavigate, useParams } from 'react-router-dom';
import { Modal } from '../components/reusable';
import { DailyLogsView } from '../components/rv';
import { DailyLogDetail } 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 { DAILY_REPORTS } from '../api/config';
import { dailyReportsClient } from '../api/clients';
export function DailyLogsPage() {
const { instanceId } = useParams();
const navigate = useNavigate();
const [isCreating, setIsCreating] = useState(false);
const [punchOutInstanceId, setPunchOutInstanceId] = useState<number | string | null>(null);
const [refreshKey, setRefreshKey] = useState(0);
return (
<>
<DailyLogsView onRowClick={(row) => {
const id = row.instance_id as number | string | undefined;
if (id != null) navigate(`/daily/${id}`);
}} />
<DailyLogsView
refreshKey={refreshKey}
onRowClick={(row) => {
const id = row.instance_id as number | string | undefined;
if (id != null) navigate(`/daily/${id}`);
}}
headerActions={
<Button size="sm" iconLeft={<Plus size={14} />} onClick={() => setIsCreating(true)}>
Punch In
</Button>
}
rowActions={(row) => (
<Button size="sm" variant="secondary" onClick={() => setPunchOutInstanceId(row.instance_id as string | number)}>
Punch Out
</Button>
)}
/>
<Modal
open={isCreating}
onClose={() => setIsCreating(false)}
title="Punch In"
width="md"
>
<DynamicForm
client={dailyReportsClient}
activityId={DAILY_REPORTS.activities.INIT.uid}
onSuccess={() => {
setIsCreating(false);
setRefreshKey(k => k + 1);
}}
onCancel={() => setIsCreating(false)}
/>
</Modal>
<Modal
open={punchOutInstanceId != null}
onClose={() => setPunchOutInstanceId(null)}
title="Punch Out"
width="md"
>
{punchOutInstanceId != null && (
<DynamicForm
client={dailyReportsClient}
activityId={DAILY_REPORTS.activities.PUNCH_OUT.uid}
instanceId={punchOutInstanceId}
onSuccess={() => {
setPunchOutInstanceId(null);
setRefreshKey(k => k + 1);
}}
onCancel={() => setPunchOutInstanceId(null)}
/>
)}
</Modal>
<Modal
open={instanceId != null}

View File

@ -2,17 +2,50 @@ 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';
export function OrdersPage() {
const { instanceId } = useParams();
const navigate = useNavigate();
const [isCreating, setIsCreating] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
return (
<>
<OrdersView onRowClick={(row) => {
const id = row.instance_id as number | string | undefined;
if (id != null) navigate(`/orders/${id}`);
}} />
<OrdersView
refreshKey={refreshKey}
onRowClick={(row) => {
const id = row.instance_id as number | string | undefined;
if (id != null) navigate(`/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.PLACE_ORDER.uid}
onSuccess={() => {
setIsCreating(false);
setRefreshKey(k => k + 1);
}}
onCancel={() => setIsCreating(false)}
/>
</Modal>
<Modal
open={instanceId != null}

View File

@ -1,21 +1,80 @@
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Modal } from '../components/reusable';
import { StoresView } from '../components/rv';
import { StoreDetail } from '../components/dv';
import { Button } from '../components/buttons/Button';
import { Plus } from 'lucide-react';
import { DynamicForm } from '../components/forms/DynamicForm';
import { STORE } from '../api/config';
import { storeClient } from '../api/clients';
export function StoresPage() {
const { instanceId } = useParams();
const navigate = useNavigate();
const [isCreating, setIsCreating] = useState(false);
const [editingInstanceId, setEditingInstanceId] = useState<number | string | null>(null);
const [refreshKey, setRefreshKey] = useState(0);
return (
<>
<StoresView onRowClick={(row) => {
const id = row.instance_id as number | string | undefined;
if (id != null) navigate(`/stores/${id}`);
}} />
<StoresView
refreshKey={refreshKey}
onRowClick={(row) => {
const id = row.instance_id as number | string | undefined;
if (id != null) navigate(`/stores/${id}`);
}}
headerActions={
<Button size="sm" iconLeft={<Plus size={14} />} onClick={() => setIsCreating(true)}>
Create Store
</Button>
}
rowActions={(row) => (
<Button size="sm" variant="secondary" onClick={() => setEditingInstanceId(row.instance_id as string | number)}>
Edit
</Button>
)}
/>
<Modal
open={instanceId != null}
open={isCreating}
onClose={() => setIsCreating(false)}
title="Create Store"
width="md"
>
<DynamicForm
client={storeClient}
activityId={STORE.activities.INIT.uid}
onSuccess={() => {
setIsCreating(false);
setRefreshKey(k => k + 1);
}}
onCancel={() => setIsCreating(false)}
/>
</Modal>
<Modal
open={editingInstanceId != null}
onClose={() => setEditingInstanceId(null)}
title="Edit Store"
width="md"
>
{editingInstanceId != null && (
<DynamicForm
client={storeClient}
activityId={STORE.activities.EDIT_STORE.uid}
instanceId={editingInstanceId}
onSuccess={() => {
setEditingInstanceId(null);
setRefreshKey(k => k + 1);
}}
onCancel={() => setEditingInstanceId(null)}
/>
)}
</Modal>
<Modal
open={instanceId != null && !isCreating}
onClose={() => navigate(`/stores`)}
title={instanceId != null ? `Store #${instanceId}` : undefined}
width="lg"