ticket
This commit is contained in:
Generated
+137
-2
@@ -13,6 +13,7 @@
|
|||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@tanstack/react-query": "^5.90.3",
|
"@tanstack/react-query": "^5.90.3",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"formik": "^2.4.6",
|
||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
@@ -23,10 +24,12 @@
|
|||||||
"react-multi-date-picker": "^4.5.2",
|
"react-multi-date-picker": "^4.5.2",
|
||||||
"react-router-dom": "^7.9.4",
|
"react-router-dom": "^7.9.4",
|
||||||
"react-spinners": "^0.17.0",
|
"react-spinners": "^0.17.0",
|
||||||
|
"react-toastify": "^11.0.5",
|
||||||
"swiper": "^12.0.2",
|
"swiper": "^12.0.2",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
"yup": "^1.7.1",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -2102,6 +2105,18 @@
|
|||||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/hoist-non-react-statics": {
|
||||||
|
"version": "3.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz",
|
||||||
|
"integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hoist-non-react-statics": "^3.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
@@ -2133,7 +2148,6 @@
|
|||||||
"version": "19.2.2",
|
"version": "19.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@@ -2768,7 +2782,6 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
@@ -2795,6 +2808,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/deepmerge": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
@@ -3307,6 +3329,31 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/formik": {
|
||||||
|
"version": "2.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz",
|
||||||
|
"integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://opencollective.com/formik"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/hoist-non-react-statics": "^3.3.1",
|
||||||
|
"deepmerge": "^2.1.1",
|
||||||
|
"hoist-non-react-statics": "^3.3.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"react-fast-compare": "^2.0.1",
|
||||||
|
"tiny-warning": "^1.0.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -3483,6 +3530,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hoist-non-react-statics": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"react-is": "^16.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iconsax-react": {
|
"node_modules/iconsax-react": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/iconsax-react/-/iconsax-react-0.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/iconsax-react/-/iconsax-react-0.0.8.tgz",
|
||||||
@@ -3921,6 +3977,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash-es": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@@ -4277,6 +4345,12 @@
|
|||||||
"react-is": "^16.13.1"
|
"react-is": "^16.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/property-expr": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@@ -4368,6 +4442,12 @@
|
|||||||
"react-dom": ">=16.8.0"
|
"react-dom": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-fast-compare": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react-infinite-scroll-component": {
|
"node_modules/react-infinite-scroll-component": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz",
|
||||||
@@ -4467,6 +4547,19 @@
|
|||||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-toastify": {
|
||||||
|
"version": "11.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
|
||||||
|
"integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19",
|
||||||
|
"react-dom": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
@@ -4721,6 +4814,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-case": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tiny-warning": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
@@ -4779,6 +4884,12 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toposort": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||||
@@ -4831,6 +4942,18 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/type-fest": {
|
||||||
|
"version": "2.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
|
||||||
|
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
@@ -5094,6 +5217,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yup": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz",
|
||||||
|
"integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"property-expr": "^2.0.5",
|
||||||
|
"tiny-case": "^1.0.3",
|
||||||
|
"toposort": "^2.0.2",
|
||||||
|
"type-fest": "^2.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zustand": {
|
"node_modules/zustand": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@tanstack/react-query": "^5.90.3",
|
"@tanstack/react-query": "^5.90.3",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"formik": "^2.4.6",
|
||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
@@ -25,10 +26,12 @@
|
|||||||
"react-multi-date-picker": "^4.5.2",
|
"react-multi-date-picker": "^4.5.2",
|
||||||
"react-router-dom": "^7.9.4",
|
"react-router-dom": "^7.9.4",
|
||||||
"react-spinners": "^0.17.0",
|
"react-spinners": "^0.17.0",
|
||||||
|
"react-toastify": "^11.0.5",
|
||||||
"swiper": "^12.0.2",
|
"swiper": "^12.0.2",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
"yup": "^1.7.1",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+31
-4
@@ -2,20 +2,47 @@ import { type FC } from 'react'
|
|||||||
import '@/assets/fonts/irancell/style.css'
|
import '@/assets/fonts/irancell/style.css'
|
||||||
import 'swiper/swiper-bundle.css';
|
import 'swiper/swiper-bundle.css';
|
||||||
import 'react-loading-skeleton/dist/skeleton.css'
|
import 'react-loading-skeleton/dist/skeleton.css'
|
||||||
|
import 'react-toastify/dist/ReactToastify.css'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { ToastContainer } from 'react-toastify'
|
||||||
// import { getToken } from './config/func'
|
// import { getToken } from './config/func'
|
||||||
import MainRouter from './router/MainRouter'
|
import MainRouter from './router/MainRouter'
|
||||||
// import AuthRouter from './router/AuthRouter'
|
// import AuthRouter from './router/AuthRouter'
|
||||||
|
|
||||||
|
// ایجاد QueryClient برای React Query
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: 1,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
|
|
||||||
// const isLoggedIn = getToken()
|
// const isLoggedIn = getToken()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<QueryClientProvider client={queryClient}>
|
||||||
{/* {isLoggedIn ? <MainRouter /> : <AuthRouter />} */}
|
<BrowserRouter>
|
||||||
<MainRouter />
|
{/* {isLoggedIn ? <MainRouter /> : <AuthRouter />} */}
|
||||||
</BrowserRouter>
|
<MainRouter />
|
||||||
|
</BrowserRouter>
|
||||||
|
<ToastContainer
|
||||||
|
position="top-left"
|
||||||
|
autoClose={3000}
|
||||||
|
hideProgressBar={false}
|
||||||
|
newestOnTop={false}
|
||||||
|
closeOnClick
|
||||||
|
rtl
|
||||||
|
pauseOnFocusLoss
|
||||||
|
draggable
|
||||||
|
pauseOnHover
|
||||||
|
/>
|
||||||
|
</QueryClientProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-7
@@ -1,20 +1,25 @@
|
|||||||
import { clx } from '@/helpers/utils'
|
import { clx } from '@/helpers/utils'
|
||||||
import { type FC } from 'react'
|
import { type FC, type ReactNode } from 'react'
|
||||||
|
|
||||||
type TabItem = {
|
type TabItem = {
|
||||||
label: string
|
label: string
|
||||||
value: string
|
value: string
|
||||||
|
icon?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
items: TabItem[]
|
items: TabItem[]
|
||||||
activeTab: string
|
active?: string
|
||||||
onTabChange: (tab: string) => void
|
activeTab?: string
|
||||||
|
onChange?: (tab: string) => void
|
||||||
|
onTabChange?: (tab: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tabs: FC<Props> = (props) => {
|
const Tabs: FC<Props> = (props) => {
|
||||||
|
|
||||||
const { items, activeTab, onTabChange } = props
|
const { items, activeTab, active, onTabChange, onChange } = props
|
||||||
|
const currentActive = active || activeTab || ''
|
||||||
|
const handleChange = onChange || onTabChange || (() => { })
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -23,10 +28,11 @@ const Tabs: FC<Props> = (props) => {
|
|||||||
{
|
{
|
||||||
items.map((item) => {
|
items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<div onClick={() => onTabChange(item.value)} key={item.value} className={clx(
|
<div onClick={() => handleChange(item.value)} key={item.value} className={clx(
|
||||||
'h-[32px] flex items-center justify-center w-[122px] cursor-pointer rounded-full',
|
'h-[32px] flex items-center gap-2 justify-center min-w-[122px] px-3 cursor-pointer rounded-full',
|
||||||
activeTab === item.value && 'bg-primary'
|
currentActive === item.value && 'bg-primary'
|
||||||
)}>
|
)}>
|
||||||
|
{item.icon}
|
||||||
{item.label}
|
{item.label}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,4 +35,10 @@ export const Paths = {
|
|||||||
auth: {
|
auth: {
|
||||||
login: '/auth/login',
|
login: '/auth/login',
|
||||||
},
|
},
|
||||||
|
ticket: {
|
||||||
|
list: "/tickets/list",
|
||||||
|
create: "/tickets/create",
|
||||||
|
detail: "/tickets/messages/",
|
||||||
|
category: "/tickets/category",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import Select from '@/components/Select'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import Td from '@/components/Td'
|
||||||
|
import SwitchComponent from '@/components/Switch'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import type { CategoriesItemType, CreateCategoryType } from './types/TicketTypes'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { useCreateCategory, useGetCategories } from './hooks/useTicketData'
|
||||||
|
import Textarea from '@/components/Textarea'
|
||||||
|
import type { ErrorType } from '@/helpers/types'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
|
const TicketCategory: FC = () => {
|
||||||
|
|
||||||
|
// TODO: این hook باید بعداً وصل شود - مربوط به users است
|
||||||
|
// const getGroups = useGetGroups()
|
||||||
|
const createCategory = useCreateCategory()
|
||||||
|
const getCategories = useGetCategories()
|
||||||
|
|
||||||
|
const formik = useFormik<CreateCategoryType>({
|
||||||
|
initialValues: {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
userGroupId: '',
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
title: Yup.string().required('این فیلد الزامی است'),
|
||||||
|
userGroupId: Yup.string().required('این فیلد الزامی است'),
|
||||||
|
description: Yup.string().required('این فیلد الزامی است'),
|
||||||
|
}),
|
||||||
|
onSubmit: (values: CreateCategoryType) => {
|
||||||
|
createCategory.mutate(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
formik.resetForm()
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4'>
|
||||||
|
<div>
|
||||||
|
دستهبندی تیکتها
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-6 xl:mt-8 mt-4'>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<div className='flex w-full justify-between items-center'>
|
||||||
|
<div className='w-28'>
|
||||||
|
<Select
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
placeholder='وضعیت'
|
||||||
|
className='border'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
variant='search'
|
||||||
|
placeholder='جستجو'
|
||||||
|
className='bg-white border'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='bg-white mt-5 py-3 xl:px-10 px-4 rounded-3xl flex-1'>
|
||||||
|
<div className='relative overflow-x-auto rounded-3xl w-full'>
|
||||||
|
<table className='w-full text-sm '>
|
||||||
|
<thead className='thead'>
|
||||||
|
<tr>
|
||||||
|
<Td text='عنوان' />
|
||||||
|
<Td text='وضعیت' />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
getCategories.data?.data?.ticketCategories?.map((item: CategoriesItemType) => (
|
||||||
|
<tr key={item.id} className='tr'>
|
||||||
|
<Td text={item.title} />
|
||||||
|
<Td text={item.isActive ? 'فعال' : 'غیرفعال'} />
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
||||||
|
<div className='text-sm'>
|
||||||
|
افزودن دستهبندی
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6 flex justify-between items-center'>
|
||||||
|
<div className='text-xs'>
|
||||||
|
وضعیت دستهبندی
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SwitchComponent
|
||||||
|
active={formik.values.isActive}
|
||||||
|
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Input
|
||||||
|
label='عنوان دستهبندی'
|
||||||
|
{...formik.getFieldProps('title')}
|
||||||
|
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Textarea
|
||||||
|
label='توضیحات'
|
||||||
|
{...formik.getFieldProps('description')}
|
||||||
|
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Select
|
||||||
|
label='کاربران'
|
||||||
|
// TODO: این items باید بعداً از getGroups.data گرفته شود
|
||||||
|
items={[
|
||||||
|
// موقت: دادههای mock برای نمایش UI
|
||||||
|
// getGroups.data?.data?.usersGroup?.map((item: GroupItemType) => ({
|
||||||
|
// label: item.name,
|
||||||
|
// value: item.id
|
||||||
|
// }))
|
||||||
|
]}
|
||||||
|
{...formik.getFieldProps('userGroupId')}
|
||||||
|
error_text={formik.touched.userGroupId && formik.errors.userGroupId ? formik.errors.userGroupId : ''}
|
||||||
|
placeholder='انتخاب کنید'
|
||||||
|
className='border'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Button
|
||||||
|
label='ثبت'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={createCategory.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div className='mt-6 border border-dashed border-border rounded-xl p-4 flex gap-2 flex-wrap'>
|
||||||
|
<div className='bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg'>
|
||||||
|
<div className=' '>
|
||||||
|
سیما فرهادی
|
||||||
|
</div>
|
||||||
|
<CloseCircle
|
||||||
|
variant='Bold'
|
||||||
|
className='size-4'
|
||||||
|
color='red'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg'>
|
||||||
|
<div className=' '>
|
||||||
|
سیما فرهادی
|
||||||
|
</div>
|
||||||
|
<CloseCircle
|
||||||
|
variant='Bold'
|
||||||
|
className='size-4'
|
||||||
|
color='red'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg'>
|
||||||
|
<div className=' '>
|
||||||
|
سیما فرهادی
|
||||||
|
</div>
|
||||||
|
<CloseCircle
|
||||||
|
variant='Bold'
|
||||||
|
className='size-4'
|
||||||
|
color='red'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TicketCategory
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import Select from '@/components/Select'
|
||||||
|
import Textarea from '@/components/Textarea'
|
||||||
|
import UploadBox from '@/components/UploadBox'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { TickCircle, TickSquare } from 'iconsax-react'
|
||||||
|
import { useCreateTicket, useGetCategories } from './hooks/useTicketData'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import type { ErrorType } from '@/helpers/types'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import type { CreateTicketAdminType } from './types/TicketTypes'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
|
const CreateTicket: FC = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
// const [files, setFiles] = useState<File[]>([])
|
||||||
|
const getCategories = useGetCategories()
|
||||||
|
const createTicket = useCreateTicket()
|
||||||
|
|
||||||
|
const formik = useFormik<CreateTicketAdminType>({
|
||||||
|
initialValues: {
|
||||||
|
subject: '',
|
||||||
|
message: '',
|
||||||
|
categoryId: '',
|
||||||
|
priority: 'LOW',
|
||||||
|
attachmentUrls: [],
|
||||||
|
userId: ''
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
subject: Yup.string().required('این فیلد الزامی است'),
|
||||||
|
message: Yup.string().required('این فیلد الزامی است'),
|
||||||
|
categoryId: Yup.string().required('این فیلد الزامی است'),
|
||||||
|
priority: Yup.string().required('این فیلد الزامی است'),
|
||||||
|
}),
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
// TODO: آپلود فایل باید بعداً وصل شود - نیاز به multiUpload API دارد
|
||||||
|
// if (files.length > 0) {
|
||||||
|
// const images = new FormData()
|
||||||
|
// files.forEach(file => {
|
||||||
|
// images.append('files', file)
|
||||||
|
// })
|
||||||
|
// await multiUpload.mutateAsync(images, {
|
||||||
|
// onSuccess: async (data) => {
|
||||||
|
// values.attachmentUrls = await data.data.map((item: { url: string }) => item?.url)
|
||||||
|
// },
|
||||||
|
// onError: (error: ErrorType) => {
|
||||||
|
// toast.error(error.response?.data?.error?.message[0])
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// delete values.attachmentUrls
|
||||||
|
// }
|
||||||
|
delete values.attachmentUrls
|
||||||
|
|
||||||
|
if (values.danakServiceId === '') {
|
||||||
|
delete values.danakServiceId
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
createTicket.mutate(values, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast.success('تیکت با موفقیت ثبت شد')
|
||||||
|
formik.resetForm()
|
||||||
|
navigate(`/ticket/detail/${data.data?.ticketId}`)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4 pb-12'>
|
||||||
|
<div>
|
||||||
|
تیکت جدید
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
getCategories.isPending ?
|
||||||
|
<div>در حال بارگذاری...</div>
|
||||||
|
:
|
||||||
|
<div className='flex gap-6 xl:mt-8 mt-4'>
|
||||||
|
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||||
|
<div>
|
||||||
|
پیام خود را ثبت کنید
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-5'>
|
||||||
|
<Select
|
||||||
|
label='مشتری'
|
||||||
|
placeholder='انتخاب کنید'
|
||||||
|
// TODO: این items باید بعداً از getCustomers.data گرفته شود
|
||||||
|
items={[
|
||||||
|
// موقت: دادههای mock برای نمایش UI
|
||||||
|
// getCustomers.data?.data?.customers?.map((item: CustomerItemType) => ({
|
||||||
|
// label: item.firstName + ' ' + item.lastName,
|
||||||
|
// value: item.id
|
||||||
|
// }))
|
||||||
|
]}
|
||||||
|
{...formik.getFieldProps('userId')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className='mt-6 rowTwoInput'>
|
||||||
|
<Input
|
||||||
|
label='موضوع'
|
||||||
|
placeholder='موضوع خود را وارد کنید'
|
||||||
|
name='subject'
|
||||||
|
value={formik.values.subject}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.subject && formik.errors.subject ? formik.errors.subject : ''}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label='اولویت'
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: 'کم',
|
||||||
|
value: 'LOW'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'متوسط',
|
||||||
|
value: 'MEDIUM'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'زیاد',
|
||||||
|
value: 'HIGH'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
placeholder='انتخاب کنید'
|
||||||
|
className='border'
|
||||||
|
name='priority'
|
||||||
|
value={formik.values.priority}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.priority && formik.errors.priority ? formik.errors.priority : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6 rowTwoInput'>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label='دستهبندی'
|
||||||
|
items={getCategories.data?.data?.ticketCategories?.map((item: { id: string, title: string }) => ({
|
||||||
|
label: item.title,
|
||||||
|
value: item.id
|
||||||
|
})
|
||||||
|
) ?? []}
|
||||||
|
placeholder='انتخاب کنید'
|
||||||
|
className='border'
|
||||||
|
name='categoryId'
|
||||||
|
value={formik.values.categoryId}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.categoryId && formik.errors.categoryId ? formik.errors.categoryId : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Textarea
|
||||||
|
label='توضیحات'
|
||||||
|
placeholder='توضیحات خود را وارد کنید'
|
||||||
|
name='message'
|
||||||
|
value={formik.values.message}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.message && formik.errors.message ? formik.errors.message : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<UploadBox
|
||||||
|
label='پیوست'
|
||||||
|
isMultiple
|
||||||
|
onChange={() => { }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-8 flex justify-end'>
|
||||||
|
<Button
|
||||||
|
className='xl:max-w-[100px]'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={createTicket.isPending}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<TickCircle size={20} color='white' />
|
||||||
|
<div>
|
||||||
|
ارسال
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
||||||
|
<div className='text-sm'>
|
||||||
|
راهنمای ارسال تیکت
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<div className='flex items-start gap-2 border-b pb-5'>
|
||||||
|
<div>
|
||||||
|
<TickSquare size={20} color='black' variant='Bold' />
|
||||||
|
</div>
|
||||||
|
<div className='text-description text-xs leading-5'>
|
||||||
|
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
|
||||||
|
<div>
|
||||||
|
<TickSquare size={20} color='black' variant='Bold' />
|
||||||
|
</div>
|
||||||
|
<div className='text-description text-xs leading-5'>
|
||||||
|
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-start gap-2 mt-6'>
|
||||||
|
<div>
|
||||||
|
<TickSquare size={20} color='black' variant='Bold' />
|
||||||
|
</div>
|
||||||
|
<div className='text-description text-xs leading-5'>
|
||||||
|
پاسخگویی 24 ساعته تلفنی را تنها از داناک می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateTicket
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
import { type FC, Fragment, useState } from 'react'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import Textarea from '@/components/Textarea'
|
||||||
|
import UploadBox from '@/components/UploadBox'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { CloseCircle, InfoCircle, Paperclip2 } from 'iconsax-react'
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
import { useAddMessageTicket, useCloseTicket, useGetMessages } from './hooks/useTicketData'
|
||||||
|
import moment from 'moment-jalaali'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import type { AddMessageTicketType, TicketMessageType } from './types/TicketTypes'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import type { ErrorType } from '@/helpers/types'
|
||||||
|
import { clx } from '@/helpers/utils'
|
||||||
|
import { Paths } from '@/config/Paths'
|
||||||
|
import ReferTicket from './components/ReferTicket'
|
||||||
|
|
||||||
|
const TicketDetail: FC = () => {
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { id } = useParams()
|
||||||
|
const getMessages = useGetMessages(id ? id : '')
|
||||||
|
const [files, setFiles] = useState<File[]>([])
|
||||||
|
const addMessage = useAddMessageTicket(id ? id : '')
|
||||||
|
const [isResetFiles, setIsResetFiles] = useState<boolean>(false)
|
||||||
|
const closeTicket = useCloseTicket()
|
||||||
|
|
||||||
|
const formik = useFormik<AddMessageTicketType>({
|
||||||
|
initialValues: {
|
||||||
|
content: '',
|
||||||
|
attachmentUrls: [],
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object().shape({
|
||||||
|
content: Yup.string().required('این فیلد الزامیست'),
|
||||||
|
}),
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
// if (files.length > 0) {
|
||||||
|
// const images = new FormData()
|
||||||
|
// files.forEach(file => {
|
||||||
|
// images.append('files', file)
|
||||||
|
// })
|
||||||
|
// await multiUpload.mutateAsync(images, {
|
||||||
|
// onSuccess: async (data) => {
|
||||||
|
// values.attachmentUrls = await data.data.map((item: { url: string }) => item?.url)
|
||||||
|
// },
|
||||||
|
// onError: (error: ErrorType) => {
|
||||||
|
// toast.error(error.response?.data?.error?.message[0])
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// delete values.attachmentUrls
|
||||||
|
// }
|
||||||
|
delete values.attachmentUrls
|
||||||
|
|
||||||
|
addMessage.mutate(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('پیام با موفقیت ارسال شد')
|
||||||
|
formik.resetForm()
|
||||||
|
setFiles([])
|
||||||
|
setIsResetFiles(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsResetFiles(false)
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const hadleCloseTicket = () => {
|
||||||
|
closeTicket.mutate(id ? id : '', {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('تیکت با موفقیت بسته شد')
|
||||||
|
navigate(Paths.ticket.list)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4'>
|
||||||
|
<div className='flex gap-1'>
|
||||||
|
<div>شماره تیکت</div>
|
||||||
|
<div>{getMessages.data?.data?.ticket?.numericId || '12345'}</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
getMessages.isPending ?
|
||||||
|
<div>در حال بارگذاری...</div>
|
||||||
|
:
|
||||||
|
<div className='flex xl:flex-row flex-col-reverse gap-6 xl:mt-8 mt-6'>
|
||||||
|
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||||
|
<div className='gap-6 rowTwoInput'>
|
||||||
|
<Input
|
||||||
|
label='انتخاب سرویس شما'
|
||||||
|
value={(getMessages.data?.data?.ticket?.danakService as any)?.name || 'طراحی گرافیک'}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label='نام کاربر'
|
||||||
|
value={getMessages.data?.data?.ticket?.user?.firstName + ' ' + getMessages.data?.data?.ticket?.user?.lastName || 'علی احمدی'}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label='پلن کاربر'
|
||||||
|
value={(getMessages.data?.data?.supportPlan as any)?.userSupportPlan?.supportPlan?.name || 'پلن طلایی'}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-8 xl:text-sm text-xs'>
|
||||||
|
{getMessages.data?.data?.ticket?.subject || 'مشکل در دریافت فایل نهایی پروژه'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
getMessages.data?.data?.messages && getMessages.data.data.messages.length > 0 ? (
|
||||||
|
getMessages.data.data.messages.map((item: TicketMessageType) => {
|
||||||
|
if (item?.author?.roles[0]?.name === 'user') {
|
||||||
|
return (
|
||||||
|
<div key={item.id} className='mt-6 xl:text-sm text-xs bg-[#F6F7FA] p-6 rounded-3xl rounded-tr-none xl:max-w-[70%] max-w-[90%]'>
|
||||||
|
<div className='leading-7'>
|
||||||
|
{item.content}
|
||||||
|
</div>
|
||||||
|
<div className='flex dltr end mt-6 text-xs text-description'>
|
||||||
|
{moment(item.createdAt).format('jYYYY-jMM-jDD HH:mm')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-2 flex-wrap text-sm'>
|
||||||
|
{
|
||||||
|
item.attachments && item?.attachments?.map((item, index: number) => {
|
||||||
|
return (
|
||||||
|
<div className='text-[#0047FF] mt-2 flex gap-1 items-center' key={item.id}>
|
||||||
|
<Paperclip2 size={20} color='#0047FF' />
|
||||||
|
<a href={item.attachmentUrl} target='_blank' rel='noreferrer'>
|
||||||
|
{'پیوست' + ' ' + Number(index + 1)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div key={item.id} className='w-full flex justify-end'>
|
||||||
|
<div className='mt-6 xl:text-sm text-xs bg-[#EBEDF5] p-6 rounded-3xl rounded-tl-none xl:max-w-[70%] max-w-[90%]'>
|
||||||
|
<div className='flex gap-1'>
|
||||||
|
<div className='font-bold'>
|
||||||
|
کارشناس
|
||||||
|
</div>
|
||||||
|
<div>{item.author?.firstName + ' ' + item?.author?.lastName}</div>
|
||||||
|
</div>
|
||||||
|
<div className='leading-7 mt-4'>
|
||||||
|
{item.content}
|
||||||
|
</div>
|
||||||
|
<div className='flex dltr mt-6 text-xs text-description'>
|
||||||
|
{moment(item.createdAt).format('jYYYY-jMM-jDD HH:mm')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-2 flex-wrap text-sm'>
|
||||||
|
{
|
||||||
|
item.attachments && item?.attachments?.map((item, index: number) => {
|
||||||
|
return (
|
||||||
|
<div className='text-[#0047FF] mt-2 flex gap-1 items-center' key={item.id}>
|
||||||
|
<Paperclip2 size={20} color='#0047FF' />
|
||||||
|
<a href={item.attachmentUrl} target='_blank' rel='noreferrer'>
|
||||||
|
{'پیوست' + ' ' + Number(index + 1)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-2'></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className='mt-6 xl:text-sm text-xs bg-[#F6F7FA] p-6 rounded-3xl rounded-tr-none xl:max-w-[70%] max-w-[90%]'>
|
||||||
|
<div className='leading-7'>
|
||||||
|
سلام، من فایل نهایی پروژهام رو دریافت نکردم. ممنون میشم پیگیری کنید.
|
||||||
|
</div>
|
||||||
|
<div className='flex dltr end mt-6 text-xs text-description'>
|
||||||
|
1403/08/01 14:30
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='w-full flex justify-end'>
|
||||||
|
<div className='mt-6 xl:text-sm text-xs bg-[#EBEDF5] p-6 rounded-3xl rounded-tl-none xl:max-w-[70%] max-w-[90%]'>
|
||||||
|
<div className='flex gap-1'>
|
||||||
|
<div className='font-bold'>
|
||||||
|
کارشناس
|
||||||
|
</div>
|
||||||
|
<div>رضا محمدی</div>
|
||||||
|
</div>
|
||||||
|
<div className='leading-7 mt-4'>
|
||||||
|
سلام، بله حتما. فایل نهایی پروژه شما در حال بررسی نهایی است و تا پایان امروز برای شما ارسال خواهد شد.
|
||||||
|
</div>
|
||||||
|
<div className='flex dltr mt-6 text-xs text-description'>
|
||||||
|
1403/08/01 15:45
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
getMessages.data?.data?.ticket?.status !== 'CLOSED' &&
|
||||||
|
<Fragment>
|
||||||
|
<div className='mt-9'>
|
||||||
|
<Textarea
|
||||||
|
label='پاسخ'
|
||||||
|
placeholder='توضیحات خود را وارد کنید'
|
||||||
|
value={formik.values.content}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
name='content'
|
||||||
|
error_text={formik.touched.content && formik.errors.content ? formik.errors.content : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-7'>
|
||||||
|
<UploadBox
|
||||||
|
label='پیوست'
|
||||||
|
onChange={setFiles}
|
||||||
|
isMultiple
|
||||||
|
isReset={isResetFiles}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-end mt-6'>
|
||||||
|
<Button
|
||||||
|
label='ارسال'
|
||||||
|
className='max-w-[100px]'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={addMessage.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
<div className='h-8'></div>
|
||||||
|
</div>
|
||||||
|
<div className={`h-fit lg:sticky left-0 lg:top-0 xl:w-[300px] w-full`}>
|
||||||
|
{
|
||||||
|
getMessages.data?.data?.ticket?.status !== 'CLOSED' &&
|
||||||
|
<div className='bg-white xl:p-6 p-4 rounded-3xl flex gap-4'>
|
||||||
|
<Button
|
||||||
|
label='ارسال پاسخ'
|
||||||
|
className='text-xs xl:h-10 h-8'
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className='bg-[#D52903] xl:h-10 h-8'
|
||||||
|
onClick={hadleCloseTicket}
|
||||||
|
isLoading={closeTicket.isPending}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 text-xs items-center'>
|
||||||
|
<CloseCircle
|
||||||
|
color='white'
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
بستن تیکت
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div className={clx(
|
||||||
|
'bg-white p-6 rounded-3xl mt-8',
|
||||||
|
getMessages.data?.data?.ticket?.status === 'CLOSED' && 'mt-0'
|
||||||
|
)}>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<div className='text-sm'>
|
||||||
|
اطلاعات تیکت
|
||||||
|
</div>
|
||||||
|
<div className={clx(
|
||||||
|
'h-7 px-3 rounded-xl flex items-center text-xs bg-green-100 text-green-400',
|
||||||
|
getMessages.data?.data?.ticket?.status === 'PENDING' && 'bg-orange-100 text-orange-700',
|
||||||
|
getMessages.data?.data?.ticket?.status === 'CLOSED' && 'bg-red-100 text-red-400'
|
||||||
|
)}>
|
||||||
|
{
|
||||||
|
getMessages.data?.data?.ticket?.status === 'PENDING' ? 'در انتظار' :
|
||||||
|
getMessages.data?.data?.ticket?.status === 'ANSWERED' ? 'پاسخ داده شده' :
|
||||||
|
'بسته شده'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-14 text-xs w-full'>
|
||||||
|
<div className='flex justify-between text-description'>
|
||||||
|
<div>شماره تیکت</div>
|
||||||
|
<div>
|
||||||
|
{getMessages.data?.data?.ticket?.numericId || '12345'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex mt-6 justify-between text-description'>
|
||||||
|
<div>ارجاع به</div>
|
||||||
|
<div>
|
||||||
|
{getMessages.data?.data?.ticket?.assignedTo ? (getMessages.data?.data?.ticket?.assignedTo as any)?.firstName + ' ' + (getMessages.data?.data?.ticket?.assignedTo as any)?.lastName : 'رضا محمدی'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex mt-6 justify-between text-description'>
|
||||||
|
<div>تاریخ</div>
|
||||||
|
<div className='dltr'>
|
||||||
|
{getMessages.data?.data?.ticket?.createdAt ? moment(getMessages.data?.data?.ticket?.createdAt).format('jYYYY/jMM/jDD HH:mm') : '1403/08/01 14:30'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex mt-6 justify-between text-description'>
|
||||||
|
<div>دستهبندی</div>
|
||||||
|
<div>
|
||||||
|
{getMessages.data?.data?.ticket?.category?.title || 'پشتیبانی فنی'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex mt-6 justify-between text-description'>
|
||||||
|
<div>اولویت</div>
|
||||||
|
<div>
|
||||||
|
{getMessages.data?.data?.ticket?.priority === 'low' ? 'کم' : getMessages.data?.data?.ticket?.priority === 'medium' ? 'متوسط' : getMessages.data?.data?.ticket?.priority === 'high' ? 'زیاد' : 'متوسط'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-10 border-t text-xs pt-5 border-border items-center flex gap-2'>
|
||||||
|
<InfoCircle size={20} color='black' />
|
||||||
|
<div>
|
||||||
|
بهروزرسانی از طریق پیامک
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
<ReferTicket refetch={getMessages.refetch} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TicketDetail
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { type FC, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { Eye, MessageRemove, MessageTick, MessageTime } from 'iconsax-react'
|
||||||
|
import Tabs from '@/components/Tabs'
|
||||||
|
import Table from '@/components/Table'
|
||||||
|
import type { ColumnType } from '@/components/types/TableTypes'
|
||||||
|
import { Link, useSearchParams } from 'react-router-dom'
|
||||||
|
import { useGetTickets } from './hooks/useTicketData'
|
||||||
|
import moment from 'moment-jalaali'
|
||||||
|
import type { TicketItemType } from './types/TicketTypes'
|
||||||
|
import OpenTicket from './components/OpenTicket'
|
||||||
|
|
||||||
|
const TicketList: FC = () => {
|
||||||
|
|
||||||
|
const [page, setPage] = useState<number>(1)
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const [customerId, setCustomerId] = useState<string>('')
|
||||||
|
const [activeTab, setActiveTab] = useState<'ANSWERED' | 'PENDING' | 'CLOSED' | ''>('PENDING')
|
||||||
|
const getTicket = useGetTickets(activeTab, customerId, page)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const urlCustomerId = searchParams.get('user')
|
||||||
|
if (urlCustomerId) {
|
||||||
|
setCustomerId(urlCustomerId)
|
||||||
|
setActiveTab('')
|
||||||
|
}
|
||||||
|
}, [searchParams])
|
||||||
|
|
||||||
|
const columns = useMemo<ColumnType<TicketItemType>[]>(() => [
|
||||||
|
{
|
||||||
|
title: 'شماره',
|
||||||
|
key: 'numericId',
|
||||||
|
render: (item) => item.numericId + ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'عنوان',
|
||||||
|
key: 'subject'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'کاربر',
|
||||||
|
key: 'user',
|
||||||
|
render: (item) => item.user.firstName + ' ' + item.user.lastName
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ارجاع به',
|
||||||
|
key: 'assignedTo',
|
||||||
|
render: (item) => item?.assignedTo?.firstName + ' ' + item?.assignedTo?.lastName
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'دستهبندی',
|
||||||
|
key: 'category',
|
||||||
|
render: (item) => item?.category?.title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'سرویس',
|
||||||
|
key: 'danakService',
|
||||||
|
render: (item) => item?.danakService?.name
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'تاریخ',
|
||||||
|
key: 'updatedAt',
|
||||||
|
render: (item) => (
|
||||||
|
<div className='dltr text-right'>
|
||||||
|
{moment(item.updatedAt).format('jYYYY-jMM-jDD HH:mm')}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'وضعیت',
|
||||||
|
key: 'status',
|
||||||
|
render: (item) =>
|
||||||
|
item.status === 'PENDING' ? 'در حال بررسی' :
|
||||||
|
item.status === 'ANSWERED' ? 'پاسخ داده شده' :
|
||||||
|
item.status === 'CLOSED' ? 'بسته شده' : item.status
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'اولویت',
|
||||||
|
key: 'priority',
|
||||||
|
render: (item) =>
|
||||||
|
item.priority === 'LOW' ? 'کم' :
|
||||||
|
item.priority === 'MEDIUM' ? 'متوسط' :
|
||||||
|
item.priority === 'HIGH' ? 'زیاد' : item.priority
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'جزئیات',
|
||||||
|
key: 'actions',
|
||||||
|
render: (item) => (
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Link to={`/ticket/detail/${item.id}`}>
|
||||||
|
<Eye size={20} color='#8C90A3' />
|
||||||
|
</Link>
|
||||||
|
{item.status === 'CLOSED' && (
|
||||||
|
<OpenTicket refetch={getTicket.refetch} id={item.id} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
], [getTicket.refetch])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4'>
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
|
<div>
|
||||||
|
تیکتها
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-14'>
|
||||||
|
<Tabs
|
||||||
|
active={activeTab}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
icon: <MessageTime color={activeTab === 'PENDING' ? 'black' : '#8C90A3'} size={22} />,
|
||||||
|
label: 'در حال بررسی',
|
||||||
|
value: 'PENDING'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <MessageTick color={activeTab === 'ANSWERED' ? 'black' : '#8C90A3'} size={22} />,
|
||||||
|
label: 'پاسخ داده شده',
|
||||||
|
value: 'ANSWERED'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <MessageRemove color={activeTab === 'CLOSED' ? 'black' : '#8C90A3'} size={22} />,
|
||||||
|
label: 'بسته شده',
|
||||||
|
value: 'CLOSED'
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) => setActiveTab(value as 'ANSWERED' | 'PENDING' | 'CLOSED')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
data={getTicket.data?.data?.tickets}
|
||||||
|
isLoading={getTicket.isLoading}
|
||||||
|
noDataMessage='هیچ تیکتی یافت نشد'
|
||||||
|
pagination={(getTicket.data?.data?.pager?.totalPages ?? 0) > 1 ? {
|
||||||
|
currentPage: page,
|
||||||
|
totalPages: getTicket.data?.data?.pager?.totalPages ?? 1,
|
||||||
|
onPageChange: setPage
|
||||||
|
} : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TicketList
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { useOpenTicket } from '../hooks/useTicketData'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string,
|
||||||
|
refetch: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const OpenTicket: FC<Props> = ({ id, refetch }) => {
|
||||||
|
|
||||||
|
const { mutate: openTicket, isPending } = useOpenTicket()
|
||||||
|
|
||||||
|
const handleOpenTicket = () => {
|
||||||
|
openTicket(id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
label={'باز کردن تیکت'}
|
||||||
|
onClick={handleOpenTicket}
|
||||||
|
className='px-4 h-8 text-xs'
|
||||||
|
isLoading={isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OpenTicket
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { useReferTicket } from '../hooks/useTicketData'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import type { ErrorType } from '@/helpers/types'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
refetch: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReferTicket: FC<Props> = ({ refetch }) => {
|
||||||
|
|
||||||
|
const { id } = useParams()
|
||||||
|
// const [userId, setUserId] = useState<string>('')
|
||||||
|
// TODO: این hook باید بعداً وصل شود - مربوط به users است
|
||||||
|
// const getUsers = useGetUsers('', 'tickets')
|
||||||
|
const referTicket = useReferTicket()
|
||||||
|
|
||||||
|
const handleReferTicket = () => {
|
||||||
|
referTicket.mutate({ id: id as string, userId: '' }, {
|
||||||
|
onSuccess: () => {
|
||||||
|
refetch()
|
||||||
|
toast.success('تیکت با موفقیت ارجاع داده شد')
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'bg-white p-6 rounded-3xl mt-8'}>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<div className='text-sm'>
|
||||||
|
ارجاع تیکت
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
{/* TODO: این بخش باید بعداً وصل شود - نیاز به getUsers.data دارد */}
|
||||||
|
{/* {
|
||||||
|
getUsers.data?.data?.users?.map((item: UserItemType) => (
|
||||||
|
<div key={item.id} className='flex gap-2 items-center'>
|
||||||
|
<div>
|
||||||
|
<CheckBoxComponent
|
||||||
|
checked={userId === item.id}
|
||||||
|
onChange={(e) => setUserId(e.target.checked ? item.id : '')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='text-xs leading-5'>
|
||||||
|
{item.firstName + ' ' + item.lastName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
} */}
|
||||||
|
|
||||||
|
<div className='text-xs text-gray-500 text-center py-4'>
|
||||||
|
لیست کاربران در حال حاضر در دسترس نیست
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-end mt-4'>
|
||||||
|
<Button
|
||||||
|
label='ارجاع تیکت'
|
||||||
|
onClick={handleReferTicket}
|
||||||
|
isLoading={referTicket.isPending}
|
||||||
|
className='w-fit px-7'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReferTicket
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
// import * as api from "../service/TicketServiec";
|
||||||
|
import type {
|
||||||
|
AddMessageTicketType,
|
||||||
|
CreateCategoryType,
|
||||||
|
CreateTicketAdminType,
|
||||||
|
} from "../types/TicketTypes";
|
||||||
|
|
||||||
|
// Mock data برای نمایش UI
|
||||||
|
const mockTicketsData = {
|
||||||
|
data: {
|
||||||
|
tickets: [],
|
||||||
|
pager: {
|
||||||
|
totalPages: 1,
|
||||||
|
currentPage: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockMessagesData = {
|
||||||
|
data: {
|
||||||
|
ticket: {
|
||||||
|
id: "",
|
||||||
|
numericId: 0,
|
||||||
|
subject: "",
|
||||||
|
status: "PENDING",
|
||||||
|
priority: "LOW",
|
||||||
|
user: {
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
},
|
||||||
|
assignedTo: null,
|
||||||
|
category: {
|
||||||
|
title: "",
|
||||||
|
},
|
||||||
|
danakService: null,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
messages: [],
|
||||||
|
supportPlan: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCategoriesData = {
|
||||||
|
data: {
|
||||||
|
ticketCategories: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetTickets = (
|
||||||
|
status: string,
|
||||||
|
customerId?: string,
|
||||||
|
page?: number
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["tickets", status, customerId, page],
|
||||||
|
// TODO: API call کامنت شده - باید بعداً وصل شود
|
||||||
|
// queryFn: () => api.getTickets(status, customerId, page),
|
||||||
|
queryFn: () => Promise.resolve(mockTicketsData),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetMessages = (id: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["ticket-messages", id],
|
||||||
|
// TODO: API call کامنت شده - باید بعداً وصل شود
|
||||||
|
// queryFn: () => api.getMessages(id),
|
||||||
|
queryFn: () => Promise.resolve(mockMessagesData),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAddMessageTicket = (id: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
// TODO: API call کامنت شده - باید بعداً وصل شود
|
||||||
|
// mutationFn: (variables: AddMessageTicketType) =>
|
||||||
|
// api.addMessageTicket(id, variables),
|
||||||
|
mutationFn: (_variables: AddMessageTicketType) =>
|
||||||
|
Promise.resolve({ data: { success: true } }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({
|
||||||
|
queryKey: ["ticket-messages", id],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCloseTicket = () => {
|
||||||
|
return useMutation({
|
||||||
|
// TODO: API call کامنت شده - باید بعداً وصل شود
|
||||||
|
// mutationFn: (id: string) => api.closeTicket(id),
|
||||||
|
mutationFn: (_id: string) => Promise.resolve({ data: { success: true } }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateCategory = () => {
|
||||||
|
return useMutation({
|
||||||
|
// TODO: API call کامنت شده - باید بعداً وصل شود
|
||||||
|
// mutationFn: (variables: CreateCategoryType) =>
|
||||||
|
// api.createCategory(variables),
|
||||||
|
mutationFn: (_variables: CreateCategoryType) =>
|
||||||
|
Promise.resolve({ data: { success: true } }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetCategories = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["categories"],
|
||||||
|
// TODO: API call کامنت شده - باید بعداً وصل شود
|
||||||
|
// queryFn: () => api.getCategories(),
|
||||||
|
queryFn: () => Promise.resolve(mockCategoriesData),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useReferTicket = () => {
|
||||||
|
return useMutation({
|
||||||
|
// TODO: API call کامنت شده - باید بعداً وصل شود
|
||||||
|
// mutationFn: (variables: { id: string; userId: string }) =>
|
||||||
|
// api.referTicket(variables.id, variables.userId),
|
||||||
|
mutationFn: (_variables: { id: string; userId: string }) =>
|
||||||
|
Promise.resolve({ data: { success: true } }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useOpenTicket = () => {
|
||||||
|
return useMutation({
|
||||||
|
// TODO: API call کامنت شده - باید بعداً وصل شود
|
||||||
|
// mutationFn: (id: string) => api.openTicket(id),
|
||||||
|
mutationFn: (_id: string) => Promise.resolve({ data: { success: true } }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateTicket = () => {
|
||||||
|
return useMutation({
|
||||||
|
// TODO: API call کامنت شده - باید بعداً وصل شود
|
||||||
|
// mutationFn: (variables: CreateTicketAdminType) =>
|
||||||
|
// api.createTicket(variables),
|
||||||
|
mutationFn: (_variables: CreateTicketAdminType) =>
|
||||||
|
Promise.resolve({ data: { ticketId: "mock-ticket-id" } }),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// import axios from "@/config/axios";
|
||||||
|
import type {
|
||||||
|
AddMessageTicketType,
|
||||||
|
CreateCategoryType,
|
||||||
|
CreateTicketAdminType,
|
||||||
|
} from "../types/TicketTypes";
|
||||||
|
|
||||||
|
// TODO: تمام API calls کامنت شدهاند - باید بعداً وصل شوند
|
||||||
|
|
||||||
|
export const getTickets = async (
|
||||||
|
_status: string,
|
||||||
|
_customerId?: string,
|
||||||
|
_page?: number
|
||||||
|
) => {
|
||||||
|
// const params = new URLSearchParams();
|
||||||
|
// if (status) params.append("status", status);
|
||||||
|
// if (customerId) params.append("userId", customerId);
|
||||||
|
// if (page) params.append("page", page.toString());
|
||||||
|
// const query = params.toString();
|
||||||
|
// const { data } = await axios.get(`/tickets/admin?${query}`);
|
||||||
|
// return data;
|
||||||
|
return { data: { tickets: [], pager: { totalPages: 1 } } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMessages = async (_id: string) => {
|
||||||
|
// const { data } = await axios.get(`/tickets/${id}/messages`);
|
||||||
|
// return data;
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
ticket: null,
|
||||||
|
messages: [],
|
||||||
|
supportPlan: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addMessageTicket = async (
|
||||||
|
_id: string,
|
||||||
|
_params: AddMessageTicketType
|
||||||
|
) => {
|
||||||
|
// const { data } = await axios.post(`/tickets/${id}/messages`, params);
|
||||||
|
// return data;
|
||||||
|
return { data: { success: true } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const closeTicket = async (_id: string) => {
|
||||||
|
// const { data } = await axios.post(`/tickets/${id}/close`);
|
||||||
|
// return data;
|
||||||
|
return { data: { success: true } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createCategory = async (_params: CreateCategoryType) => {
|
||||||
|
// const { data } = await axios.post(`/tickets/category`, params);
|
||||||
|
// return data;
|
||||||
|
return { data: { success: true } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCategories = async () => {
|
||||||
|
// const { data } = await axios.get(`/tickets/categories`);
|
||||||
|
// return data;
|
||||||
|
return { data: { ticketCategories: [] } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const referTicket = async (_id: string, _userId: string) => {
|
||||||
|
// const { data } = await axios.post(`/tickets/${id}/refer`, {
|
||||||
|
// adminId: userId,
|
||||||
|
// });
|
||||||
|
// return data;
|
||||||
|
return { data: { success: true } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openTicket = async (_id: string) => {
|
||||||
|
// const { data } = await axios.post(`/tickets/${id}/open`);
|
||||||
|
// return data;
|
||||||
|
return { data: { success: true } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTicket = async (_params: CreateTicketAdminType) => {
|
||||||
|
// const { data } = await axios.post(`/tickets/admin`, params);
|
||||||
|
// return data;
|
||||||
|
return { data: { ticketId: "mock-ticket-id" } };
|
||||||
|
};
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
export type TicketItemType = {
|
||||||
|
id: string;
|
||||||
|
numericId: number;
|
||||||
|
subject: string;
|
||||||
|
category: {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
};
|
||||||
|
assignedTo: {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
status: string;
|
||||||
|
priority: string;
|
||||||
|
danakService: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
link: string;
|
||||||
|
isActive: boolean;
|
||||||
|
isDanakSuggest: boolean;
|
||||||
|
metaDescription: string;
|
||||||
|
serviceLanguage: string;
|
||||||
|
softwareLanguage: string;
|
||||||
|
title: string;
|
||||||
|
userCount: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddMessageTicketType = {
|
||||||
|
content: string;
|
||||||
|
attachmentUrls?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TicketMessageType = {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
attachmentUrls: string[];
|
||||||
|
createdAt: string;
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
author: {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
roles: {
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
attachments: {
|
||||||
|
attachmentUrl: string;
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateCategoryType = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
userGroupId: string;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CategoriesItemType = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateTicketAdminType = {
|
||||||
|
subject: string;
|
||||||
|
priority: "LOW" | "MEDIUM" | "HIGH";
|
||||||
|
danakServiceId?: string;
|
||||||
|
categoryId: string;
|
||||||
|
message: string;
|
||||||
|
attachmentUrls?: string[];
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
@@ -13,6 +13,10 @@ import CreateProduct from '@/pages/product/Create'
|
|||||||
import CreateFeature from '@/pages/features/Create'
|
import CreateFeature from '@/pages/features/Create'
|
||||||
import PrintService from '@/pages/service/PrintService'
|
import PrintService from '@/pages/service/PrintService'
|
||||||
import DetailPerfomaInvoice from '@/pages/invoice/DetailPerfomaInvoice'
|
import DetailPerfomaInvoice from '@/pages/invoice/DetailPerfomaInvoice'
|
||||||
|
import TicketList from '@/pages/ticket/TicketList'
|
||||||
|
import CreateTicket from '@/pages/ticket/CreateTicket'
|
||||||
|
import TicketDetail from '@/pages/ticket/Detail'
|
||||||
|
import TicketCategory from '@/pages/ticket/Category'
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className='p-4 overflow-hidden'>
|
<div className='p-4 overflow-hidden'>
|
||||||
@@ -34,6 +38,11 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Paths.features.list} element={<FeaturesList />} />
|
<Route path={Paths.features.list} element={<FeaturesList />} />
|
||||||
<Route path={Paths.features.create} element={<CreateFeature />} />
|
<Route path={Paths.features.create} element={<CreateFeature />} />
|
||||||
<Route path={Paths.service.print} element={<PrintService />} />
|
<Route path={Paths.service.print} element={<PrintService />} />
|
||||||
|
|
||||||
|
<Route path={Paths.ticket.list} element={<TicketList />} />
|
||||||
|
<Route path={Paths.ticket.create} element={<CreateTicket />} />
|
||||||
|
<Route path={Paths.ticket.detail + ':id'} element={<TicketDetail />} />
|
||||||
|
<Route path={Paths.ticket.category} element={<TicketCategory />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user