Compare commits
40 Commits
dd9bc0ddca
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b96ecf9d9 | |||
| 4bb164be0f | |||
| 2f71b4c96c | |||
| cabf248c0e | |||
| 1990e73368 | |||
| 185961c162 | |||
| b60a38e21d | |||
| f53b5d8fe1 | |||
| f5da44d9cf | |||
| 77475f09cd | |||
| 9413cf03c9 | |||
| b2f9166c22 | |||
| 7fedfdfbfd | |||
| 71e7d4eac2 | |||
| c9713a93ac | |||
| 42528f4d5c | |||
| 52bb36ac3c | |||
| f7871db066 | |||
| 153547d098 | |||
| 09feb73331 | |||
| 9a7858a4ec | |||
| df2f800b54 | |||
| 3537094e7c | |||
| fa55fd0f02 | |||
| d43774a8a1 | |||
| 13781df281 | |||
| 086db34c12 | |||
| b5b8344888 | |||
| 76a68adc1c | |||
| 24171dbaf8 | |||
| 9aadb8e6ad | |||
| 96d750fc88 | |||
| f1173adc6d | |||
| a2d2234a28 | |||
| 81353fc281 | |||
| 402354e122 | |||
| 9004413a33 | |||
| 6b5ac30f32 | |||
| 09adc8bdd0 | |||
| bddf675d66 |
Binary file not shown.
Generated
+23
-1
@@ -113,6 +113,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
|
||||||
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
|
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.26.0",
|
"@babel/code-frame": "^7.26.0",
|
||||||
@@ -2412,6 +2413,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||||
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -2424,6 +2426,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||||
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
|
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.0"
|
"scheduler": "^0.23.0"
|
||||||
@@ -3072,6 +3075,7 @@
|
|||||||
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.20.7",
|
"@babel/parser": "^7.20.7",
|
||||||
"@babel/types": "^7.20.7",
|
"@babel/types": "^7.20.7",
|
||||||
@@ -3173,6 +3177,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
|
||||||
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
|
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@@ -3249,6 +3254,7 @@
|
|||||||
"integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==",
|
"integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.18.1",
|
"@typescript-eslint/scope-manager": "8.18.1",
|
||||||
"@typescript-eslint/types": "8.18.1",
|
"@typescript-eslint/types": "8.18.1",
|
||||||
@@ -3457,6 +3463,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -3812,6 +3819,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001688",
|
"caniuse-lite": "^1.0.30001688",
|
||||||
"electron-to-chromium": "^1.5.73",
|
"electron-to-chromium": "^1.5.73",
|
||||||
@@ -4542,6 +4550,7 @@
|
|||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -5456,6 +5465,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.23.2"
|
"@babel/runtime": "^7.23.2"
|
||||||
},
|
},
|
||||||
@@ -6786,6 +6796,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.8",
|
"nanoid": "^3.3.8",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -6952,6 +6963,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.4.0",
|
"loose-envify": "^1.4.0",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
@@ -7087,6 +7099,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
||||||
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
|
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -7102,6 +7115,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
||||||
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
|
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.25.0"
|
"scheduler": "^0.25.0"
|
||||||
},
|
},
|
||||||
@@ -7324,7 +7338,8 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
@@ -7503,6 +7518,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
|
||||||
"integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
|
"integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.7"
|
"@types/estree": "1.0.7"
|
||||||
},
|
},
|
||||||
@@ -8406,6 +8422,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -8569,6 +8586,7 @@
|
|||||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -8776,6 +8794,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
@@ -8897,6 +8916,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -9187,6 +9207,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@@ -9248,6 +9269,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
||||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FC, Fragment, ReactNode, useEffect } from 'react'
|
import { FC, Fragment, ReactNode, useEffect } from 'react'
|
||||||
|
import { clx } from '../helpers/utils'
|
||||||
import HeaderModal from './HeaderModal'
|
import HeaderModal from './HeaderModal'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -8,6 +9,8 @@ interface Props {
|
|||||||
isHeader?: boolean,
|
isHeader?: boolean,
|
||||||
title_header?: string,
|
title_header?: string,
|
||||||
width?: number
|
width?: number
|
||||||
|
/** Raise above popovers (z-80) when confirm opens from nested panels */
|
||||||
|
elevated?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaulModal: FC<Props> = (props: Props) => {
|
const DefaulModal: FC<Props> = (props: Props) => {
|
||||||
@@ -26,7 +29,14 @@ const DefaulModal: FC<Props> = (props: Props) => {
|
|||||||
{
|
{
|
||||||
props.open && (
|
props.open && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div style={{ maxWidth: props.width }} className='xl:justify-center xl:items-center items-end flex overflow-hidden fixed inset-0 z-[60] h-auto top-0 bottom-0 m-auto outline-none focus:outline-none xl:max-w-xl mx-auto'>
|
<div
|
||||||
|
style={{ maxWidth: props.width }}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
className={clx(
|
||||||
|
'xl:justify-center xl:items-center items-end flex overflow-hidden fixed inset-0 h-auto top-0 bottom-0 m-auto outline-none focus:outline-none xl:max-w-xl mx-auto',
|
||||||
|
props.elevated ? 'z-[100]' : 'z-[60]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className='relative max-h-[85vh] bottom-0 left-0 flex xl:items-center items-end w-full xl:my-6 xl:p-2'>
|
<div className='relative max-h-[85vh] bottom-0 left-0 flex xl:items-center items-end w-full xl:my-6 xl:p-2'>
|
||||||
<div className='border-0 max-h-[85vh] p-5 lg:min-w-full overflow-y-auto rounded-3xl rounded-b-none xl:rounded-b-3xl relative flex flex-col w-full modalGlass2 outline-none focus:outline-none'>
|
<div className='border-0 max-h-[85vh] p-5 lg:min-w-full overflow-y-auto rounded-3xl rounded-b-none xl:rounded-b-3xl relative flex flex-col w-full modalGlass2 outline-none focus:outline-none'>
|
||||||
|
|
||||||
@@ -46,7 +56,14 @@ const DefaulModal: FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div onClick={props.close} className='fixed size-full top-0 bottom-0 right-0 modalGlass inset-0 z-50 '></div>
|
<div
|
||||||
|
onClick={props.close}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
className={clx(
|
||||||
|
'fixed size-full top-0 bottom-0 right-0 modalGlass inset-0',
|
||||||
|
props.elevated ? 'z-[90]' : 'z-50',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SearchNormal } from "iconsax-react";
|
import { SearchNormal } from "iconsax-react";
|
||||||
import { FC, InputHTMLAttributes, useEffect, useState } from "react";
|
import { FC, InputHTMLAttributes, useEffect, useRef, useState } from "react";
|
||||||
import EyeIcon from "../assets/images/eye.svg";
|
import EyeIcon from "../assets/images/eye.svg";
|
||||||
import { clx } from "../helpers/utils";
|
import { clx } from "../helpers/utils";
|
||||||
import Error from "./Error";
|
import Error from "./Error";
|
||||||
@@ -30,6 +30,8 @@ const Input: FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||||
const [search, setSearch] = useState<string>("");
|
const [search, setSearch] = useState<string>("");
|
||||||
|
const onChangeSearchFinalRef = useRef(props.onChangeSearchFinal);
|
||||||
|
onChangeSearchFinalRef.current = props.onChangeSearchFinal;
|
||||||
|
|
||||||
const inputClass = clx(
|
const inputClass = clx(
|
||||||
"w-full h-10 text-black block px-4 text-xs rounded-xl border border-border",
|
"w-full h-10 text-black block px-4 text-xs rounded-xl border border-border",
|
||||||
@@ -68,13 +70,13 @@ const Input: FC<Props> = (props: Props) => {
|
|||||||
}, [props.value]);
|
}, [props.value]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.variant === "search" && props.onChangeSearchFinal) {
|
if (props.variant !== "search" || !onChangeSearchFinalRef.current) return;
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
props.onChangeSearchFinal?.(search);
|
onChangeSearchFinalRef.current?.(search);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}
|
}, [search, props.variant]);
|
||||||
}, [search, props]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import DefaulModal from './DefaulModal'
|
import { createPortal } from 'react-dom'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
|
import DefaulModal from './DefaulModal'
|
||||||
import Textarea from './Textarea'
|
import Textarea from './Textarea'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -18,12 +19,13 @@ const ModalConfrim: FC<Props> = (props: Props) => {
|
|||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [description, setDescription] = useState<string>('')
|
const [description, setDescription] = useState<string>('')
|
||||||
|
|
||||||
return (
|
return createPortal(
|
||||||
<DefaulModal
|
<DefaulModal
|
||||||
open={props.isOpen}
|
open={props.isOpen}
|
||||||
close={props.close}
|
close={props.close}
|
||||||
title_header={t('confrim.subject')}
|
title_header={t('confrim.subject')}
|
||||||
isHeader
|
isHeader
|
||||||
|
elevated
|
||||||
>
|
>
|
||||||
<div className='mt-6'>
|
<div className='mt-6'>
|
||||||
<div className='text-sm text-center'>
|
<div className='text-sm text-center'>
|
||||||
@@ -60,7 +62,8 @@ const ModalConfrim: FC<Props> = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DefaulModal>
|
</DefaulModal>,
|
||||||
|
document.body,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ interface PaginationProps {
|
|||||||
currentPage: number;
|
currentPage: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
onPageChange: (page: number) => void;
|
onPageChange: (page: number) => void;
|
||||||
|
limit?: number;
|
||||||
|
onLimitChange?: (limit: number) => void;
|
||||||
|
limitOptions?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Pagination: React.FC<PaginationProps> = ({
|
const Pagination: React.FC<PaginationProps> = ({ currentPage, totalPages, onPageChange, limit = 10, onLimitChange, limitOptions = [10, 20, 50] }) => {
|
||||||
currentPage,
|
const handleLimitChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
totalPages,
|
onLimitChange?.(Number(event.target.value));
|
||||||
onPageChange,
|
onPageChange(1);
|
||||||
}) => {
|
};
|
||||||
|
|
||||||
const getPageNumbers = () => {
|
const getPageNumbers = () => {
|
||||||
const pageNumbers: (number | string)[] = [];
|
const pageNumbers: (number | string)[] = [];
|
||||||
const maxVisiblePages = 5;
|
const maxVisiblePages = 5;
|
||||||
@@ -56,7 +60,29 @@ const Pagination: React.FC<PaginationProps> = ({
|
|||||||
const pageNumbers = getPageNumbers();
|
const pageNumbers = getPageNumbers();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 text-xs justify-center items-center mt-4">
|
<div className="flex flex-wrap gap-4 text-xs justify-center items-center mt-4">
|
||||||
|
{onLimitChange && (
|
||||||
|
<label className="flex items-center gap-2 text-gray-600">
|
||||||
|
<span>تعداد در صفحه</span>
|
||||||
|
<select
|
||||||
|
aria-label="تعداد آیتم در صفحه"
|
||||||
|
className="h-8 rounded-md border border-[#EAECF5] bg-white px-2 outline-none focus:border-primary"
|
||||||
|
value={limit}
|
||||||
|
onChange={handleLimitChange}
|
||||||
|
>
|
||||||
|
{limitOptions.map((option) => (
|
||||||
|
<option
|
||||||
|
key={option}
|
||||||
|
value={option}
|
||||||
|
>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
{/* دکمه قبلی */}
|
{/* دکمه قبلی */}
|
||||||
{/* <button
|
{/* <button
|
||||||
className={`px-3 py-1 rounded-md ${currentPage === 1
|
className={`px-3 py-1 rounded-md ${currentPage === 1
|
||||||
@@ -74,19 +100,19 @@ const Pagination: React.FC<PaginationProps> = ({
|
|||||||
typeof page === "number" ? (
|
typeof page === "number" ? (
|
||||||
<button
|
<button
|
||||||
key={`page-${page}`}
|
key={`page-${page}`}
|
||||||
className={`size-8 rounded-md ${currentPage === page
|
className={`size-8 rounded-md ${currentPage === page ? "bg-primary text-white" : "text-primary bg-[#EAECF5] hover:bg-gray-100"}`}
|
||||||
? "bg-primary text-white"
|
|
||||||
: "text-primary bg-[#EAECF5] hover:bg-gray-100"
|
|
||||||
}`}
|
|
||||||
onClick={() => onPageChange(page)}
|
onClick={() => onPageChange(page)}
|
||||||
>
|
>
|
||||||
{page}
|
{page}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<span key={`ellipsis-${index}`} className="px-3 py-1 text-gray-500">
|
<span
|
||||||
|
key={`ellipsis-${index}`}
|
||||||
|
className="px-3 py-1 text-gray-500"
|
||||||
|
>
|
||||||
{page}
|
{page}
|
||||||
</span>
|
</span>
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* دکمه بعدی */}
|
{/* دکمه بعدی */}
|
||||||
@@ -101,6 +127,7 @@ const Pagination: React.FC<PaginationProps> = ({
|
|||||||
بعدی
|
بعدی
|
||||||
</button> */}
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ export const Pages = {
|
|||||||
create: "/tickets/create",
|
create: "/tickets/create",
|
||||||
detail: "/tickets/messages/",
|
detail: "/tickets/messages/",
|
||||||
category: "/tickets/category",
|
category: "/tickets/category",
|
||||||
|
masters: "/tickets/masters",
|
||||||
|
mastersCreate: "/tickets/masters/create",
|
||||||
|
mastersUpdate: "/tickets/masters/update/",
|
||||||
},
|
},
|
||||||
announcement: {
|
announcement: {
|
||||||
list: "/announcement",
|
list: "/announcement",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export const SideBarItemHasSubMenu = [
|
|||||||
"customers",
|
"customers",
|
||||||
"representatives",
|
"representatives",
|
||||||
"users",
|
"users",
|
||||||
|
"taskmanager",
|
||||||
"content",
|
"content",
|
||||||
"financial",
|
"financial",
|
||||||
"support",
|
"support",
|
||||||
@@ -44,7 +45,7 @@ export const getSubMenuNameFromPath = (pathname: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.startsWith("/workspace") || pathname.startsWith("/project") || pathname.startsWith("/taskmanager")) {
|
if (pathname.startsWith("/workspace") || pathname.startsWith("/project") || pathname.startsWith("/taskmanager")) {
|
||||||
return "users";
|
return "taskmanager";
|
||||||
}
|
}
|
||||||
|
|
||||||
const segment = pathname.split("/")[1] ?? "";
|
const segment = pathname.split("/")[1] ?? "";
|
||||||
|
|||||||
@@ -95,6 +95,21 @@ export enum PermissionEnum {
|
|||||||
BLOGS_UPDATE = "blogs_update",
|
BLOGS_UPDATE = "blogs_update",
|
||||||
BLOGS_DELETE = "blogs_delete",
|
BLOGS_DELETE = "blogs_delete",
|
||||||
|
|
||||||
|
BLOG_CATEGORIES_CREATE = "blog_categories_create",
|
||||||
|
BLOG_CATEGORIES_READ = "blog_categories_read",
|
||||||
|
BLOG_CATEGORIES_UPDATE = "blog_categories_update",
|
||||||
|
BLOG_CATEGORIES_DELETE = "blog_categories_delete",
|
||||||
|
|
||||||
|
BLOG_COMMENTS_CREATE = "blog_comments_create",
|
||||||
|
BLOG_COMMENTS_READ = "blog_comments_read",
|
||||||
|
BLOG_COMMENTS_UPDATE = "blog_comments_update",
|
||||||
|
BLOG_COMMENTS_DELETE = "blog_comments_delete",
|
||||||
|
|
||||||
|
SLIDERS_CREATE = "sliders_create",
|
||||||
|
SLIDERS_READ = "sliders_read",
|
||||||
|
SLIDERS_UPDATE = "sliders_update",
|
||||||
|
SLIDERS_DELETE = "sliders_delete",
|
||||||
|
|
||||||
LEARNINGS_CREATE = "learnings_create",
|
LEARNINGS_CREATE = "learnings_create",
|
||||||
LEARNINGS_READ = "learnings_read",
|
LEARNINGS_READ = "learnings_read",
|
||||||
LEARNINGS_UPDATE = "learnings_update",
|
LEARNINGS_UPDATE = "learnings_update",
|
||||||
@@ -145,6 +160,19 @@ export enum PermissionEnum {
|
|||||||
TASK_PHASE_UPDATE = "task_phase_update",
|
TASK_PHASE_UPDATE = "task_phase_update",
|
||||||
TASK_PHASE_DELETE = "task_phase_delete",
|
TASK_PHASE_DELETE = "task_phase_delete",
|
||||||
|
|
||||||
|
TICKET_CATEGORIES_CREATE = "ticket_categories_create",
|
||||||
|
TICKET_CATEGORIES_READ = "ticket_categories_read",
|
||||||
|
TICKET_CATEGORIES_UPDATE = "ticket_categories_update",
|
||||||
|
TICKET_CATEGORIES_DELETE = "ticket_categories_delete",
|
||||||
|
|
||||||
|
TICKET_MASTERS_CREATE = "ticket_masters_create",
|
||||||
|
TICKET_MASTERS_READ = "ticket_masters_read",
|
||||||
|
TICKET_MASTERS_UPDATE = "ticket_masters_update",
|
||||||
|
TICKET_MASTERS_DELETE = "ticket_masters_delete",
|
||||||
|
|
||||||
|
VIEW_ALL_WORKSPACES = "view_all_workspaces",
|
||||||
|
VIEW_ALL_PROJECTS = "view_all_projects",
|
||||||
|
|
||||||
LOGS = "logs",
|
LOGS = "logs",
|
||||||
MANAGE_SSO_CLIENTS = "manage_sso_clients",
|
MANAGE_SSO_CLIENTS = "manage_sso_clients",
|
||||||
DMENU = "dmenu",
|
DMENU = "dmenu",
|
||||||
@@ -153,12 +181,35 @@ export enum PermissionEnum {
|
|||||||
DMAIL = "dmail",
|
DMAIL = "dmail",
|
||||||
}
|
}
|
||||||
|
|
||||||
const CRUD_ACTIONS = ["create", "read", "update", "delete"] as const;
|
export type CrudAction = "create" | "read" | "update" | "delete";
|
||||||
|
|
||||||
export const CONTENT_PERMISSION_RESOURCES = ["blogs", "learnings", "advertisements", "announcements"] as const;
|
const CRUD_ACTIONS: readonly CrudAction[] = ["create", "read", "update", "delete"];
|
||||||
|
|
||||||
|
export const CONTENT_PERMISSION_RESOURCES = [
|
||||||
|
"blogs",
|
||||||
|
"blog_categories",
|
||||||
|
"blog_comments",
|
||||||
|
"sliders",
|
||||||
|
"learnings",
|
||||||
|
"advertisements",
|
||||||
|
"announcements",
|
||||||
|
] as const;
|
||||||
export const FINANCIAL_PERMISSION_RESOURCES = ["payments", "invoices", "transactions", "discounts", "bank_accounts"] as const;
|
export const FINANCIAL_PERMISSION_RESOURCES = ["payments", "invoices", "transactions", "discounts", "bank_accounts"] as const;
|
||||||
export const SUPPORT_PERMISSION_RESOURCES = ["support_plan", "tickets"] as const;
|
export const SUPPORT_PERMISSION_RESOURCES = [
|
||||||
export const TASK_MANAGER_PERMISSION_RESOURCES = ["workspace", "project", "task", "task_phase"] as const;
|
"support_plan",
|
||||||
|
"tickets",
|
||||||
|
"ticket_categories",
|
||||||
|
"ticket_masters",
|
||||||
|
] as const;
|
||||||
|
export const TASK_MANAGER_PERMISSION_RESOURCES = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
"task",
|
||||||
|
"task_phase",
|
||||||
|
"view_all_workspaces",
|
||||||
|
"view_all_projects",
|
||||||
|
] as const;
|
||||||
|
export const USERS_SIDEBAR_PERMISSION_RESOURCES = ["admins"] as const;
|
||||||
export const PRODUCT_PERMISSION_RESOURCES = ["dmenu", "dkala", "dpage", "dmail"] as const;
|
export const PRODUCT_PERMISSION_RESOURCES = ["dmenu", "dkala", "dpage", "dmail"] as const;
|
||||||
|
|
||||||
type PermissionEntry = { name: string };
|
type PermissionEntry = { name: string };
|
||||||
@@ -179,6 +230,32 @@ export const hasPermission = (
|
|||||||
return CRUD_ACTIONS.some((action) => names.includes(`${resourceOrPermission}_${action}`));
|
return CRUD_ACTIONS.some((action) => names.includes(`${resourceOrPermission}_${action}`));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const hasActionPermission = (
|
||||||
|
permissions: PermissionEntry[] | undefined,
|
||||||
|
resource: string,
|
||||||
|
action: CrudAction,
|
||||||
|
): boolean => {
|
||||||
|
const names = getPermissionNames(permissions);
|
||||||
|
|
||||||
|
if (names.includes(resource)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return names.includes(`${resource}_${action}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const canCreate = (permissions: PermissionEntry[] | undefined, resource: string) =>
|
||||||
|
hasActionPermission(permissions, resource, "create");
|
||||||
|
|
||||||
|
export const canRead = (permissions: PermissionEntry[] | undefined, resource: string) =>
|
||||||
|
hasActionPermission(permissions, resource, "read");
|
||||||
|
|
||||||
|
export const canUpdate = (permissions: PermissionEntry[] | undefined, resource: string) =>
|
||||||
|
hasActionPermission(permissions, resource, "update");
|
||||||
|
|
||||||
|
export const canDelete = (permissions: PermissionEntry[] | undefined, resource: string) =>
|
||||||
|
hasActionPermission(permissions, resource, "delete");
|
||||||
|
|
||||||
export const hasAnyPermission = (
|
export const hasAnyPermission = (
|
||||||
permissions: PermissionEntry[] | undefined,
|
permissions: PermissionEntry[] | undefined,
|
||||||
resources: readonly string[],
|
resources: readonly string[],
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
canCreate,
|
||||||
|
canDelete,
|
||||||
|
canRead,
|
||||||
|
canUpdate,
|
||||||
|
CrudAction,
|
||||||
|
hasActionPermission,
|
||||||
|
hasAnyPermission,
|
||||||
|
hasPermission,
|
||||||
|
} from "../helpers/permissions";
|
||||||
|
import { useGetAdminPermissions } from "../pages/users/hooks/useUserData";
|
||||||
|
|
||||||
|
export const usePermissions = () => {
|
||||||
|
const { data, isPending, isLoading } = useGetAdminPermissions();
|
||||||
|
const permissions = data?.data?.permissions;
|
||||||
|
|
||||||
|
return {
|
||||||
|
permissions,
|
||||||
|
isPending: isPending || isLoading,
|
||||||
|
can: (resource: string, action: CrudAction) => hasActionPermission(permissions, resource, action),
|
||||||
|
canCreate: (resource: string) => canCreate(permissions, resource),
|
||||||
|
canRead: (resource: string) => canRead(permissions, resource),
|
||||||
|
canUpdate: (resource: string) => canUpdate(permissions, resource),
|
||||||
|
canDelete: (resource: string) => canDelete(permissions, resource),
|
||||||
|
has: (resourceOrPermission: string) => hasPermission(permissions, resourceOrPermission),
|
||||||
|
hasAny: (resources: readonly string[]) => hasAnyPermission(permissions, resources),
|
||||||
|
};
|
||||||
|
};
|
||||||
+20
-3
@@ -35,7 +35,16 @@
|
|||||||
"enter_password_step3": "رمز عبور را وارد کنید",
|
"enter_password_step3": "رمز عبور را وارد کنید",
|
||||||
"enter_your_password": "رمز عبور خود را وارد کنید.",
|
"enter_your_password": "رمز عبور خود را وارد کنید.",
|
||||||
"forgot_password": "فراموشی رمز عبور",
|
"forgot_password": "فراموشی رمز عبور",
|
||||||
"login_with_otp": "ورود با رمز عبور یکبار مصرف"
|
"forgot_password_description": "ایمیل خود را وارد کنید تا کد بازیابی برای شما ارسال شود.",
|
||||||
|
"reset_password": "بازیابی رمز عبور",
|
||||||
|
"new_password": "رمز عبور جدید",
|
||||||
|
"confirm_password": "تکرار رمز عبور",
|
||||||
|
"password_mismatch": "رمز عبور و تکرار آن یکسان نیستند",
|
||||||
|
"change_email": "تغییر ایمیل",
|
||||||
|
"resend_code": "ارسال مجدد کد",
|
||||||
|
"back_to_login": "بازگشت به صفحه ورود",
|
||||||
|
"login_with_otp": "ورود با رمز عبور یکبار مصرف",
|
||||||
|
"otp_sent": "رمز یک بار مصرف برای شما ارسال شد"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"required": "این فیلد اجباری می باشد",
|
"required": "این فیلد اجباری می باشد",
|
||||||
@@ -134,6 +143,7 @@
|
|||||||
"representative_list": "لیست نمایندگان",
|
"representative_list": "لیست نمایندگان",
|
||||||
"ticket_list": "لیست تیکت ها",
|
"ticket_list": "لیست تیکت ها",
|
||||||
"ticket_category": "دسته بندی تیکت ها",
|
"ticket_category": "دسته بندی تیکت ها",
|
||||||
|
"ticket_masters": "مستر",
|
||||||
"send_ticket": "ارسال تیکت",
|
"send_ticket": "ارسال تیکت",
|
||||||
"category": "دسته بندی",
|
"category": "دسته بندی",
|
||||||
"user_list": "لیست کاربران",
|
"user_list": "لیست کاربران",
|
||||||
@@ -368,7 +378,10 @@
|
|||||||
"column": "ستون",
|
"column": "ستون",
|
||||||
"select_workspace_required": "فضای کاری را انتخاب کنید",
|
"select_workspace_required": "فضای کاری را انتخاب کنید",
|
||||||
"select_project_required": "پروژه را انتخاب کنید",
|
"select_project_required": "پروژه را انتخاب کنید",
|
||||||
"select_column_required": "ستون را انتخاب کنید"
|
"select_column_required": "ستون را انتخاب کنید",
|
||||||
|
"masters": "مستر",
|
||||||
|
"add_master": "افزودن مستر",
|
||||||
|
"edit_master": "ویرایش مستر"
|
||||||
},
|
},
|
||||||
"active": "فعال",
|
"active": "فعال",
|
||||||
"inactive": "غیرفعال",
|
"inactive": "غیرفعال",
|
||||||
@@ -748,6 +761,7 @@
|
|||||||
"group_add": "ساخت گروه",
|
"group_add": "ساخت گروه",
|
||||||
"submit_group": "ثبت گروه",
|
"submit_group": "ثبت گروه",
|
||||||
"error_count_users": "حداقل یک کاربر باید انتخاب شود",
|
"error_count_users": "حداقل یک کاربر باید انتخاب شود",
|
||||||
|
"error_count_roles": "حداقل یک نقش باید انتخاب شود",
|
||||||
"list_user_group": "لیست گروه های کاربری",
|
"list_user_group": "لیست گروه های کاربری",
|
||||||
"groups": "گروه ها",
|
"groups": "گروه ها",
|
||||||
"group_name": "نام گروه",
|
"group_name": "نام گروه",
|
||||||
@@ -784,7 +798,10 @@
|
|||||||
"confrim_email": "تایید ایمیل",
|
"confrim_email": "تایید ایمیل",
|
||||||
"confrim": "تایید صورتحساب ",
|
"confrim": "تایید صورتحساب ",
|
||||||
"link_email": "یک لینک برای تایید ایمیل برای شما ارسال شد.",
|
"link_email": "یک لینک برای تایید ایمیل برای شما ارسال شد.",
|
||||||
"email_not_verified": "ایمیل تایید نشده"
|
"email_not_verified": "ایمیل تایید نشده",
|
||||||
|
"resend_email": "ارسال مجدد تایید",
|
||||||
|
"first_name": "نام",
|
||||||
|
"last_name": "نام خانوادگی"
|
||||||
},
|
},
|
||||||
"email": "ایمیل",
|
"email": "ایمیل",
|
||||||
"customer": {
|
"customer": {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import moment from 'moment-jalaali'
|
|||||||
import ToggleStatusAds from './components/ToggleStatus'
|
import ToggleStatusAds from './components/ToggleStatus'
|
||||||
import PageLoading from '../../components/PageLoading'
|
import PageLoading from '../../components/PageLoading'
|
||||||
import Pagination from '../../components/Pagination'
|
import Pagination from '../../components/Pagination'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const AddList: FC = () => {
|
const AddList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
@@ -23,6 +24,7 @@ const AddList: FC = () => {
|
|||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const getAds = useGetAdsList(page, search, isActive, since)
|
const getAds = useGetAdsList(page, search, isActive, since)
|
||||||
const deleteAds = useDeleteAds()
|
const deleteAds = useDeleteAds()
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
deleteAds.mutate(id, {
|
deleteAds.mutate(id, {
|
||||||
@@ -39,6 +41,7 @@ const AddList: FC = () => {
|
|||||||
{t('ads.ads')}
|
{t('ads.ads')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{canCreate('advertisements') && (
|
||||||
<Link to={Pages.ads.create}>
|
<Link to={Pages.ads.create}>
|
||||||
<Button
|
<Button
|
||||||
className='w-[172px]'
|
className='w-[172px]'
|
||||||
@@ -51,6 +54,7 @@ const AddList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
<div className='flex flex-col xl:flex-row justify-between items-center xl:items-end'>
|
<div className='flex flex-col xl:flex-row justify-between items-center xl:items-end'>
|
||||||
@@ -130,10 +134,14 @@ const AddList: FC = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex gap-3'>
|
<div className='flex gap-3'>
|
||||||
|
{canUpdate('advertisements') && (
|
||||||
<Link to={Pages.ads.update + item.id}>
|
<Link to={Pages.ads.update + item.id}>
|
||||||
<Eye size={20} color='#8C90A3' />
|
<Eye size={20} color='#8C90A3' />
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
|
{canDelete('advertisements') && (
|
||||||
<Trash onClick={() => handleDelete(item.id)} size={20} color='#8C90A3' />
|
<Trash onClick={() => handleDelete(item.id)} size={20} color='#8C90A3' />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import moment from 'moment-jalaali'
|
|||||||
import Pagination from '../../components/Pagination'
|
import Pagination from '../../components/Pagination'
|
||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const AnnouncementtList: FC = () => {
|
const AnnouncementtList: FC = () => {
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ const AnnouncementtList: FC = () => {
|
|||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const getAnnoncements = useGetAnnoncements(page, '', '')
|
const getAnnoncements = useGetAnnoncements(page, '', '')
|
||||||
const deleteAnnoncement = useDeleteAnnoncement()
|
const deleteAnnoncement = useDeleteAnnoncement()
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
deleteAnnoncement.mutate(id, {
|
deleteAnnoncement.mutate(id, {
|
||||||
@@ -38,6 +40,7 @@ const AnnouncementtList: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('announcement.announcements')}
|
{t('announcement.announcements')}
|
||||||
</div>
|
</div>
|
||||||
|
{canCreate('announcements') && (
|
||||||
<div>
|
<div>
|
||||||
<Link to={Pages.announcement.create}>
|
<Link to={Pages.announcement.create}>
|
||||||
<Button
|
<Button
|
||||||
@@ -53,6 +56,7 @@ const AnnouncementtList: FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -88,17 +92,21 @@ const AnnouncementtList: FC = () => {
|
|||||||
<Td text={moment(item.publishAt).format('jYYYY-jMM-jDD')} />
|
<Td text={moment(item.publishAt).format('jYYYY-jMM-jDD')} />
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
|
{canUpdate('announcements') && (
|
||||||
<Link to={Pages.announcement.detail + item.id}>
|
<Link to={Pages.announcement.detail + item.id}>
|
||||||
<Edit
|
<Edit
|
||||||
className='size-5'
|
className='size-5'
|
||||||
color='#888'
|
color='#888'
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
|
{canDelete('announcements') && (
|
||||||
<Trash
|
<Trash
|
||||||
className='size-5'
|
className='size-5'
|
||||||
color='#888'
|
color='#888'
|
||||||
onClick={() => handleDelete(item.id)}
|
onClick={() => handleDelete(item.id)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import LogoImage from '../../assets/images/logo.svg'
|
||||||
|
import LogoSmallImage from '../../assets/images/logo-small.svg'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { Pages } from '../../config/Pages'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import ForgotPasswordStep1 from './components/ForgotPasswordStep1'
|
||||||
|
import ForgotPasswordStep2 from './components/ForgotPasswordStep2'
|
||||||
|
|
||||||
|
const ForgotPassword: FC = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const [step, setStep] = useState(1)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full h-full flex justify-center lg:py-[75px] py-4 lg:items-center lg:px-10 px-4'>
|
||||||
|
<div className='flex w-full max-h-[812px] max-w-[1200px] bg-secondary h-full rounded-3xl overflow-hidden'>
|
||||||
|
<div className='flex-1 min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7'>
|
||||||
|
<div className='flex-1 h-full flex flex-col'>
|
||||||
|
<img src={LogoSmallImage} className='w-8' />
|
||||||
|
|
||||||
|
<div className='flex flex-1 flex-col h-full justify-center'>
|
||||||
|
{
|
||||||
|
step === 1 ?
|
||||||
|
<ForgotPasswordStep1 onSuccess={() => setStep(2)} />
|
||||||
|
:
|
||||||
|
<ForgotPasswordStep2 onBack={() => setStep(1)} />
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className='mt-6 flex justify-center gap-2 items-center text-sm'>
|
||||||
|
<p className='text-description'>
|
||||||
|
{t('auth.back_to_login')}
|
||||||
|
</p>
|
||||||
|
<Link to={Pages.auth.login}>
|
||||||
|
<div>{t('auth.login')}</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex-1 min-w-[50%] lg:flex hidden justify-center items-center'>
|
||||||
|
<img src={LogoImage} className='h-20' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ForgotPassword
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { ForgotPasswordType } from '../types/AuthTypes'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { useAuthStore } from '../store/AuthStore'
|
||||||
|
import Error from '../../../components/Error'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import { useForgotPassword } from '../hooks/useAuthData'
|
||||||
|
import { toast } from '../../../components/Toast';
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
import { isEmail } from '../../../config/func'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSuccess: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ForgotPasswordStep1: FC<Props> = ({ onSuccess }) => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const { email, setEmail } = useAuthStore()
|
||||||
|
const forgotPassword = useForgotPassword()
|
||||||
|
|
||||||
|
const formik = useFormik<ForgotPasswordType>({
|
||||||
|
initialValues: {
|
||||||
|
email: email,
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
email: Yup.string()
|
||||||
|
.required(t('errors.required'))
|
||||||
|
.test('is-email', t('errors.required'), (value) => !!value && isEmail(value)),
|
||||||
|
}),
|
||||||
|
onSubmit(values) {
|
||||||
|
setEmail(values.email)
|
||||||
|
forgotPassword.mutate({ email: values.email }, {
|
||||||
|
onSuccess() {
|
||||||
|
toast(t('auth.otp_sent'), 'success')
|
||||||
|
onSuccess()
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast(error?.response?.data?.error?.message[0], 'error')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='mt-5'>
|
||||||
|
<h2 className='text-2xl font-bold'>
|
||||||
|
{t('auth.forgot_password')}
|
||||||
|
</h2>
|
||||||
|
<p className='text-description text-sm mt-2'>
|
||||||
|
{t('auth.forgot_password_description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-16'>
|
||||||
|
<Input
|
||||||
|
label={t('auth.email')}
|
||||||
|
placeholder={t('auth.enter_email')}
|
||||||
|
type='text'
|
||||||
|
dir='ltr'
|
||||||
|
name='email'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.email}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
formik.touched.email && formik.errors.email &&
|
||||||
|
<Error
|
||||||
|
errorText={formik.errors.email}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={t('auth.next')}
|
||||||
|
className='mt-8'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={forgotPassword.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ForgotPasswordStep1
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { ResetPasswordType } from '../types/AuthTypes'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { useAuthStore } from '../store/AuthStore'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import Error from '../../../components/Error'
|
||||||
|
import OTPInput from 'react-otp-input'
|
||||||
|
import { useCountDown } from '../../../hooks/useCountDown'
|
||||||
|
import { Pages } from '../../../config/Pages'
|
||||||
|
import { useForgotPassword, useResetPassword } from '../hooks/useAuthData'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
import { toast } from '../../../components/Toast';
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onBack: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ForgotPasswordStep2: FC<Props> = ({ onBack }) => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { email } = useAuthStore()
|
||||||
|
const { value, reset } = useCountDown(2, true)
|
||||||
|
const resetPassword = useResetPassword()
|
||||||
|
const forgotPassword = useForgotPassword()
|
||||||
|
|
||||||
|
const formik = useFormik<ResetPasswordType & { confirmPassword: string }>({
|
||||||
|
initialValues: {
|
||||||
|
email: email,
|
||||||
|
code: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
code: Yup.string()
|
||||||
|
.required(t('errors.required')),
|
||||||
|
newPassword: Yup.string()
|
||||||
|
.required(t('errors.required')),
|
||||||
|
confirmPassword: Yup.string()
|
||||||
|
.required(t('errors.required'))
|
||||||
|
.oneOf([Yup.ref('newPassword')], t('auth.password_mismatch')),
|
||||||
|
}),
|
||||||
|
onSubmit(values) {
|
||||||
|
resetPassword.mutate({
|
||||||
|
email,
|
||||||
|
code: values.code,
|
||||||
|
newPassword: values.newPassword,
|
||||||
|
}, {
|
||||||
|
onSuccess() {
|
||||||
|
toast(t('success'), 'success')
|
||||||
|
navigate(Pages.auth.login)
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast(error?.response?.data?.error?.message[0], 'error')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleResend = () => {
|
||||||
|
forgotPassword.mutate({ email }, {
|
||||||
|
onSuccess() {
|
||||||
|
reset()
|
||||||
|
toast(t('auth.otp_sent'), 'success')
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast(error?.response?.data?.error?.message[0], 'error')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='mt-5'>
|
||||||
|
<h2 className='lg:text-2xl font-bold'>
|
||||||
|
{t('auth.reset_password')}
|
||||||
|
</h2>
|
||||||
|
<p className='text-description flex lg:flex-row flex-col lg:gap-1 gap-2 text-sm lg:mt-2 mt-3'>
|
||||||
|
<div className='flex gap-1 flex-wrap'>
|
||||||
|
<div>{t('auth.verfify_code_text_1')}</div>
|
||||||
|
<div>{email}</div>
|
||||||
|
<div>{t('auth.verfify_code_text_2')}</div>
|
||||||
|
</div>
|
||||||
|
<div onClick={onBack} className='text-black cursor-pointer'>
|
||||||
|
{t('auth.change_email')}
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-16'>
|
||||||
|
<div className='text-sm'>
|
||||||
|
{t('auth.verify_code')}
|
||||||
|
</div>
|
||||||
|
<div className='mt-2 w-full flex justify-center dltr otp'>
|
||||||
|
<OTPInput
|
||||||
|
shouldAutoFocus
|
||||||
|
value={formik.values.code}
|
||||||
|
numInputs={5}
|
||||||
|
inputType='tel'
|
||||||
|
onChange={(otp: string) => formik.setFieldValue('code', otp)}
|
||||||
|
renderInput={(props) => <input {...props} className='w-full h-[50px] flex-1 mx-2 bg-white border rounded-2.5' />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
formik.touched.code && formik.errors.code &&
|
||||||
|
<div className='text-xs mt-2 text-red-500'>{formik.errors.code}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className='flex mt-4 justify-between items-center'>
|
||||||
|
<div
|
||||||
|
onClick={handleResend}
|
||||||
|
className='text-xs text-black cursor-pointer'
|
||||||
|
>
|
||||||
|
{t('auth.resend_code')}
|
||||||
|
</div>
|
||||||
|
<div className='flex gap-1 text-description text-xs'>
|
||||||
|
<div>{value}</div>
|
||||||
|
<div>{t('auth.counter_otp')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Input
|
||||||
|
label={t('auth.new_password')}
|
||||||
|
type='password'
|
||||||
|
className='text-right'
|
||||||
|
name='newPassword'
|
||||||
|
dir='ltr'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.newPassword}
|
||||||
|
error_text={formik.touched.newPassword && formik.errors.newPassword ? formik.errors.newPassword : ''}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
formik.touched.newPassword && formik.errors.newPassword &&
|
||||||
|
<Error errorText={formik.errors.newPassword} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-4'>
|
||||||
|
<Input
|
||||||
|
label={t('auth.confirm_password')}
|
||||||
|
type='password'
|
||||||
|
className='text-right'
|
||||||
|
name='confirmPassword'
|
||||||
|
dir='ltr'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.confirmPassword}
|
||||||
|
error_text={formik.touched.confirmPassword && formik.errors.confirmPassword ? formik.errors.confirmPassword : ''}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
formik.touched.confirmPassword && formik.errors.confirmPassword &&
|
||||||
|
<Error errorText={formik.errors.confirmPassword} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={t('auth.reset_password')}
|
||||||
|
className='mt-8'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={resetPassword.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ForgotPasswordStep2
|
||||||
@@ -69,7 +69,7 @@ const LoginStep1: FC = () => {
|
|||||||
label={t('auth.mobile_or_email')}
|
label={t('auth.mobile_or_email')}
|
||||||
placeholder={t('auth.enter_mobile_or_email')}
|
placeholder={t('auth.enter_mobile_or_email')}
|
||||||
type='text'
|
type='text'
|
||||||
className='text-right'
|
dir='ltr'
|
||||||
name='phone_email'
|
name='phone_email'
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
value={formik.values.phone_email}
|
value={formik.values.phone_email}
|
||||||
|
|||||||
@@ -78,7 +78,13 @@ const LoginStep2: FC = () => {
|
|||||||
value={formik.values.code}
|
value={formik.values.code}
|
||||||
numInputs={5}
|
numInputs={5}
|
||||||
inputType='tel'
|
inputType='tel'
|
||||||
onChange={(otp: string) => formik.setFieldValue('code', otp)}
|
onChange={(otp: string) => {
|
||||||
|
formik.setFieldValue('code', otp).then(() => {
|
||||||
|
if (otp.length === 5 && !otpVerify.isPending) {
|
||||||
|
formik.submitForm()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
renderInput={(props) => <input {...props} className='w-full h-[50px] flex-1 mx-2 bg-white border rounded-2.5' />}
|
renderInput={(props) => <input {...props} className='w-full h-[50px] flex-1 mx-2 bg-white border rounded-2.5' />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ const LoginStep3: FC = () => {
|
|||||||
type='password'
|
type='password'
|
||||||
className='text-right'
|
className='text-right'
|
||||||
name='password'
|
name='password'
|
||||||
|
dir='ltr'
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
value={formik.values.password}
|
value={formik.values.password}
|
||||||
error_text={formik.touched.password && formik.errors.password ? formik.errors.password : ''}
|
error_text={formik.touched.password && formik.errors.password ? formik.errors.password : ''}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import * as api from '../service/AuthService'
|
import * as api from '../service/AuthService'
|
||||||
import { CheckHasAccountPhoneType, CheckHasAccountType, LoginWithOtpType, LoginWithPasswordType, OtpVerifyType, RegisterType } from '../types/AuthTypes';
|
import { CheckHasAccountPhoneType, CheckHasAccountType, ForgotPasswordType, LoginWithOtpType, LoginWithPasswordType, OtpVerifyType, RegisterType, ResetPasswordType } from '../types/AuthTypes';
|
||||||
|
|
||||||
export const useLoginWithPassword = () => {
|
export const useLoginWithPassword = () => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
@@ -37,3 +37,15 @@ export const useRegister = () => {
|
|||||||
mutationFn: (variables: RegisterType) => api.register(variables),
|
mutationFn: (variables: RegisterType) => api.register(variables),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useForgotPassword = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: ForgotPasswordType) => api.forgotPassword(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useResetPassword = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: ResetPasswordType) => api.resetPassword(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -2,11 +2,13 @@ import axios from "../../../config/axios";
|
|||||||
import {
|
import {
|
||||||
CheckHasAccountPhoneType,
|
CheckHasAccountPhoneType,
|
||||||
CheckHasAccountType,
|
CheckHasAccountType,
|
||||||
|
ForgotPasswordType,
|
||||||
LoginWithOtpType,
|
LoginWithOtpType,
|
||||||
LoginWithPasswordType,
|
LoginWithPasswordType,
|
||||||
OtpVerifyType,
|
OtpVerifyType,
|
||||||
RefreshTokenType,
|
RefreshTokenType,
|
||||||
RegisterType,
|
RegisterType,
|
||||||
|
ResetPasswordType,
|
||||||
} from "../types/AuthTypes";
|
} from "../types/AuthTypes";
|
||||||
|
|
||||||
export const loginWithPassword = async (params: LoginWithPasswordType) => {
|
export const loginWithPassword = async (params: LoginWithPasswordType) => {
|
||||||
@@ -43,3 +45,13 @@ export const refreshToken = async (params: RefreshTokenType) => {
|
|||||||
const { data } = await axios.post(`/auth/refresh`, params);
|
const { data } = await axios.post(`/auth/refresh`, params);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const forgotPassword = async (params: ForgotPasswordType) => {
|
||||||
|
const { data } = await axios.post(`/auth/forgot-password`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetPassword = async (params: ResetPasswordType) => {
|
||||||
|
const { data } = await axios.post(`/auth/reset-password`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -52,3 +52,13 @@ export type RegisterType = {
|
|||||||
export type RefreshTokenType = {
|
export type RefreshTokenType = {
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ForgotPasswordType = {
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResetPasswordType = {
|
||||||
|
email: string;
|
||||||
|
code: string;
|
||||||
|
newPassword: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import UploadBoxDraggble from '../../components/UploadBoxDraggble'
|
|||||||
import { useSingleUpload } from '../service/hooks/useServiceData'
|
import { useSingleUpload } from '../service/hooks/useServiceData'
|
||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
import UpdateCategory from './components/UpdateCategory'
|
import UpdateCategory from './components/UpdateCategory'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const BlogCategory: FC = () => {
|
const BlogCategory: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
@@ -25,6 +26,7 @@ const BlogCategory: FC = () => {
|
|||||||
const singleUpload = useSingleUpload()
|
const singleUpload = useSingleUpload()
|
||||||
const createBlogCategory = useCreateBlogCategory()
|
const createBlogCategory = useCreateBlogCategory()
|
||||||
const deleteBlogCategory = useDeleteBlogCategory()
|
const deleteBlogCategory = useDeleteBlogCategory()
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
const formik = useFormik<CreateBlogCategoryType>({
|
const formik = useFormik<CreateBlogCategoryType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
title: '',
|
title: '',
|
||||||
@@ -102,7 +104,10 @@ const BlogCategory: FC = () => {
|
|||||||
<Td text={moment(item.createdAt).format('jYYYY/jMM/jDD')} />
|
<Td text={moment(item.createdAt).format('jYYYY/jMM/jDD')} />
|
||||||
<Td text={t('')}>
|
<Td text={t('')}>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className='flex gap-2 items-center'>
|
||||||
|
{canUpdate('blog_categories') && (
|
||||||
<UpdateCategory id={item.id} />
|
<UpdateCategory id={item.id} />
|
||||||
|
)}
|
||||||
|
{canDelete('blog_categories') && (
|
||||||
<Trash size={20} color='red' onClick={() => {
|
<Trash size={20} color='red' onClick={() => {
|
||||||
deleteBlogCategory.mutateAsync(item.id, {
|
deleteBlogCategory.mutateAsync(item.id, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -111,6 +116,7 @@ const BlogCategory: FC = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}} />
|
}} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -123,6 +129,7 @@ const BlogCategory: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{canCreate('blog_categories') && (
|
||||||
<div className='relative bg-white min-h-[calc(100vh-185px)] w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
<div className='relative bg-white min-h-[calc(100vh-185px)] w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
||||||
<p className='text-md'>{t('blog.add_category')}</p>
|
<p className='text-md'>{t('blog.add_category')}</p>
|
||||||
<div className='text-sm flex items-center justify-between mt-4'>
|
<div className='text-sm flex items-center justify-between mt-4'>
|
||||||
@@ -176,6 +183,7 @@ const BlogCategory: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { BlogItemType } from './types/BlogTypes'
|
|||||||
import moment from 'moment-jalaali'
|
import moment from 'moment-jalaali'
|
||||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||||
import Pagination from '../../components/Pagination'
|
import Pagination from '../../components/Pagination'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const BlogList: FC = () => {
|
const BlogList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
@@ -18,6 +19,7 @@ const BlogList: FC = () => {
|
|||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
const getBlogs = useGetBlogs(search, page);
|
const getBlogs = useGetBlogs(search, page);
|
||||||
const deleteBlog = useDeleteBlog()
|
const deleteBlog = useDeleteBlog()
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4 min-h-[500px]'>
|
<div className='mt-4 min-h-[500px]'>
|
||||||
@@ -26,6 +28,7 @@ const BlogList: FC = () => {
|
|||||||
{t('blog.blog')}
|
{t('blog.blog')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{canCreate('blogs') && (
|
||||||
<Link to={Pages.blog.create}>
|
<Link to={Pages.blog.create}>
|
||||||
<Button
|
<Button
|
||||||
className='w-[172px]'
|
className='w-[172px]'
|
||||||
@@ -38,6 +41,7 @@ const BlogList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -139,9 +143,12 @@ const BlogList: FC = () => {
|
|||||||
</Td> */}
|
</Td> */}
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
|
{canUpdate('blogs') && (
|
||||||
<Link to={Pages.blog.detail + item.id}>
|
<Link to={Pages.blog.detail + item.id}>
|
||||||
<Eye size={20} color='#8C90A3' />
|
<Eye size={20} color='#8C90A3' />
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
|
{canDelete('blogs') && (
|
||||||
<TrashWithConfrim
|
<TrashWithConfrim
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
deleteBlog.mutate(item.id, {
|
deleteBlog.mutate(item.id, {
|
||||||
@@ -152,6 +159,7 @@ const BlogList: FC = () => {
|
|||||||
}}
|
}}
|
||||||
isLoading={deleteBlog.isPending}
|
isLoading={deleteBlog.isPending}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import moment from 'moment-jalaali'
|
|||||||
import { CloseCircle, TickCircle } from 'iconsax-react'
|
import { CloseCircle, TickCircle } from 'iconsax-react'
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const Comments: FC = () => {
|
const Comments: FC = () => {
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canUpdate } = usePermissions()
|
||||||
const getBlogComments = useGetBlogComments()
|
const getBlogComments = useGetBlogComments()
|
||||||
const changeStatusComment = useChangeStatusComment()
|
const changeStatusComment = useChangeStatusComment()
|
||||||
const handleChangeStatus = (id: string, status: string) => {
|
const handleChangeStatus = (id: string, status: string) => {
|
||||||
@@ -49,7 +51,7 @@ const Comments: FC = () => {
|
|||||||
<Td text={moment(item.createdAt).format('jYYYY/jMM/jDD')} />
|
<Td text={moment(item.createdAt).format('jYYYY/jMM/jDD')} />
|
||||||
<Td text=''>
|
<Td text=''>
|
||||||
{
|
{
|
||||||
item.status === 'PENDING' &&
|
item.status === 'PENDING' && canUpdate('blog_comments') &&
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<TickCircle onClick={() => handleChangeStatus(item.id, 'APPROVED')} color='green' size={20} />
|
<TickCircle onClick={() => handleChangeStatus(item.id, 'APPROVED')} color='green' size={20} />
|
||||||
<CloseCircle onClick={() => handleChangeStatus(item.id, 'REJECTED')} color='red' size={20} />
|
<CloseCircle onClick={() => handleChangeStatus(item.id, 'REJECTED')} color='red' size={20} />
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import { Link } from 'react-router-dom'
|
|||||||
import { Pages } from '../../config/Pages'
|
import { Pages } from '../../config/Pages'
|
||||||
import Button from '../../components/Button'
|
import Button from '../../components/Button'
|
||||||
import { Add, Eye } from 'iconsax-react'
|
import { Add, Eye } from 'iconsax-react'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const CardBankList: FC = () => {
|
const CardBankList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate } = usePermissions()
|
||||||
const getCardBanks = useGetCardBanks()
|
const getCardBanks = useGetCardBanks()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -23,6 +25,7 @@ const CardBankList: FC = () => {
|
|||||||
{t('cardBank.manage_banks')}
|
{t('cardBank.manage_banks')}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
{canCreate('bank_accounts') &&
|
||||||
<Link to={Pages.cardBank.create}>
|
<Link to={Pages.cardBank.create}>
|
||||||
<Button
|
<Button
|
||||||
className='px-5'
|
className='px-5'
|
||||||
@@ -36,6 +39,7 @@ const CardBankList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
@@ -63,9 +67,11 @@ const CardBankList: FC = () => {
|
|||||||
<Td text={item.cardNumber} />
|
<Td text={item.cardNumber} />
|
||||||
<Td text={item.IBan} />
|
<Td text={item.IBan} />
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
|
{canUpdate('bank_accounts') &&
|
||||||
<Link to={Pages.cardBank.detail + item.id}>
|
<Link to={Pages.cardBank.detail + item.id}>
|
||||||
<Eye size={20} color='#8C90A3' />
|
<Eye size={20} color='#8C90A3' />
|
||||||
</Link>
|
</Link>
|
||||||
|
}
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import { CriticismsItemTypes } from './types/CriticismsTypes'
|
|||||||
import moment from 'moment-jalaali'
|
import moment from 'moment-jalaali'
|
||||||
import ModalConfrim from '../../components/ModalConfrim'
|
import ModalConfrim from '../../components/ModalConfrim'
|
||||||
import Pagination from '../../components/Pagination'
|
import Pagination from '../../components/Pagination'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const CriticismsList: FC = () => {
|
const CriticismsList: FC = () => {
|
||||||
|
|
||||||
|
const { canDelete } = usePermissions()
|
||||||
const [itemId, setItemId] = useState<string>('')
|
const [itemId, setItemId] = useState<string>('')
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const [openConfrim, setOpenConfrim] = useState(false)
|
const [openConfrim, setOpenConfrim] = useState(false)
|
||||||
@@ -69,7 +71,9 @@ const CriticismsList: FC = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</Td>
|
</Td>
|
||||||
<Td text=''>
|
<Td text=''>
|
||||||
|
{canDelete('criticisms') &&
|
||||||
<Trash onClick={() => handleDelete(item.id)} size={20} color='#8C90A3' className='cursor-pointer' />
|
<Trash onClick={() => handleDelete(item.id)} size={20} color='#8C90A3' className='cursor-pointer' />
|
||||||
|
}
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|||||||
+80
-86
@@ -1,122 +1,114 @@
|
|||||||
import { FC, useState } from 'react'
|
import { Add, Eye } from "iconsax-react";
|
||||||
import { useTranslation } from 'react-i18next'
|
import { FC, useState } from "react";
|
||||||
import Button from '../../components/Button'
|
import { useTranslation } from "react-i18next";
|
||||||
import { Add, Eye } from 'iconsax-react'
|
import { Link } from "react-router-dom";
|
||||||
import Select from '../../components/Select'
|
import Button from "../../components/Button";
|
||||||
import Input from '../../components/Input'
|
import Input from "../../components/Input";
|
||||||
import Td from '../../components/Td'
|
import PageLoading from "../../components/PageLoading";
|
||||||
import { Link } from 'react-router-dom'
|
import Pagination from "../../components/Pagination";
|
||||||
import { Pages } from '../../config/Pages'
|
import Td from "../../components/Td";
|
||||||
import { useGetCustomers } from './hooks/useCustomerData'
|
import { Pages } from "../../config/Pages";
|
||||||
import PageLoading from '../../components/PageLoading'
|
import { usePermissions } from "../../hooks/usePermissions";
|
||||||
import { CustomerItemType } from './types/CustomerTypes'
|
import { useGetCustomers } from "./hooks/useCustomerData";
|
||||||
import Pagination from '../../components/Pagination'
|
import { CustomerItemType } from "./types/CustomerTypes";
|
||||||
|
|
||||||
const CustomerList: FC = () => {
|
const CustomerList: FC = () => {
|
||||||
|
const { t } = useTranslation("global");
|
||||||
|
const { canCreate, canUpdate } = usePermissions();
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [limit, setLimit] = useState(10);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const getCustomers = useGetCustomers(page, limit, false, search);
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const handleSearch = (value: string) => {
|
||||||
const [page, setPage] = useState(1)
|
if (value !== search) {
|
||||||
const getCustomers = useGetCustomers(page)
|
setPage(1);
|
||||||
|
}
|
||||||
|
setSearch(value);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4'>
|
<div className="mt-4">
|
||||||
<div className='flex w-full justify-between items-center'>
|
<div className="flex w-full justify-between items-center">
|
||||||
<div>
|
<div>{t("customer.customer_list")}</div>
|
||||||
{t('customer.customer_list')}
|
{canCreate("customers") && (
|
||||||
</div>
|
|
||||||
<Link to={Pages.customer.create}>
|
<Link to={Pages.customer.create}>
|
||||||
<Button
|
<Button className="px-5">
|
||||||
className='px-5'
|
<div className="flex gap-2">
|
||||||
>
|
|
||||||
<div className='flex gap-2'>
|
|
||||||
<Add
|
<Add
|
||||||
className='size-5'
|
className="size-5"
|
||||||
color='#fff'
|
color="#fff"
|
||||||
/>
|
/>
|
||||||
<div>{t('customer.add_cutomer')}</div>
|
<div>{t("customer.add_cutomer")}</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-between items-center mt-12'>
|
<div className="flex justify-end items-center mt-12">
|
||||||
<div className='flex gap-4'>
|
|
||||||
<Select
|
|
||||||
className='w-36'
|
|
||||||
items={[
|
|
||||||
{ label: 'All', value: 'all' },
|
|
||||||
{ label: 'Active', value: 'active' },
|
|
||||||
{ label: 'Inactive', value: 'inactive' },
|
|
||||||
]}
|
|
||||||
placeholder={t('service.category')}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
className='w-36'
|
|
||||||
items={[
|
|
||||||
{ label: 'All', value: 'all' },
|
|
||||||
{ label: 'Active', value: 'active' },
|
|
||||||
{ label: 'Inactive', value: 'inactive' },
|
|
||||||
]}
|
|
||||||
placeholder={t('service.status')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
variant='search'
|
variant="search"
|
||||||
placeholder={t('service.search')}
|
placeholder={t("search")}
|
||||||
className='bg-white border border-border'
|
className="bg-white border border-border"
|
||||||
|
onChangeSearchFinal={handleSearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{getCustomers.isPending ? (
|
||||||
getCustomers.isPending ?
|
|
||||||
<PageLoading />
|
<PageLoading />
|
||||||
:
|
) : (
|
||||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
<div className="relative overflow-x-auto rounded-3xl mt-9 w-full">
|
||||||
<table className='w-full text-sm '>
|
<table className="w-full text-sm ">
|
||||||
<thead className='thead'>
|
<thead className="thead">
|
||||||
<tr>
|
<tr>
|
||||||
<Td text={t('customer.name')} />
|
<Td text={t("customer.name")} />
|
||||||
<Td text={t('customer.company')} />
|
<Td text={t("customer.company")} />
|
||||||
<Td text={t('customer.email')} />
|
<Td text={t("customer.email")} />
|
||||||
<Td text={t('customer.active_services')} />
|
<Td text={t("customer.active_services")} />
|
||||||
<Td text={t('customer.factor')} />
|
<Td text={t("customer.factor")} />
|
||||||
<Td text={t('customer.tickets')} />
|
<Td text={t("customer.tickets")} />
|
||||||
<Td text={t('customer.detail')} />
|
<Td text={t("customer.detail")} />
|
||||||
<Td text={''} />
|
<Td text={""} />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{
|
{getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
|
||||||
getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
|
|
||||||
return (
|
return (
|
||||||
<tr key={item.id} className='tr'>
|
<tr
|
||||||
<Td text={item.firstName + ' ' + item.lastName} />
|
key={item.id}
|
||||||
|
className="tr"
|
||||||
|
>
|
||||||
|
<Td text={item.firstName + " " + item.lastName} />
|
||||||
<Td text={item?.legalUser?.registrationName} />
|
<Td text={item?.legalUser?.registrationName} />
|
||||||
<Td text={item.email ? item.email : ''} />
|
<Td text={item.email ? item.email : ""} />
|
||||||
<Td text={item.subscriptionsCount} />
|
<Td text={item.subscriptionsCount} />
|
||||||
<Td text={''}>
|
<Td text={""}>
|
||||||
<Link to={Pages.receipts.index + `?user=${item.id}`}>
|
<Link to={Pages.receipts.index + `?user=${item.id}`}>
|
||||||
<div className='text-[#0047FF]'>{item.invoicesCount} صورت حساب</div>
|
<div className="text-[#0047FF]">{item.invoicesCount} صورت حساب</div>
|
||||||
</Link>
|
</Link>
|
||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={""}>
|
||||||
<Link to={Pages.ticket.list + `?user=${item.id}`}>
|
<Link to={Pages.ticket.list + `?user=${item.id}`}>
|
||||||
<div className='text-[#0047FF]'>{item.ticketsCount} تیکت</div>
|
<div className="text-[#0047FF]">{item.ticketsCount} تیکت</div>
|
||||||
</Link>
|
</Link>
|
||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={""}>
|
||||||
|
{canUpdate("customers") && (
|
||||||
<Link to={Pages.customer.update + item.id}>
|
<Link to={Pages.customer.update + item.id}>
|
||||||
<Eye size={20} color='#8C90A3' />
|
<Eye
|
||||||
|
size={20}
|
||||||
|
color="#8C90A3"
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
<Td text={''} />
|
<Td text={""} />
|
||||||
</tr>
|
</tr>
|
||||||
)
|
);
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -124,11 +116,13 @@ const CustomerList: FC = () => {
|
|||||||
totalPages={getCustomers.data?.data?.pager?.totalPages}
|
totalPages={getCustomers.data?.data?.pager?.totalPages}
|
||||||
currentPage={page}
|
currentPage={page}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
|
limit={limit}
|
||||||
|
onLimitChange={setLimit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default CustomerList
|
export default CustomerList;
|
||||||
|
|||||||
@@ -62,40 +62,33 @@ const UpdateCustomer: FC = () => {
|
|||||||
profilePic: ''
|
profilePic: ''
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
phone: Yup.string().required(t('errors.required')),
|
email: Yup.string().test(
|
||||||
firstName: Yup.string().required(t('errors.required')),
|
'email',
|
||||||
lastName: Yup.string().required(t('errors.required')),
|
t('errors.email'),
|
||||||
email: Yup.string().email(t('errors.email')).required(t('errors.required')),
|
(value) => !value || Yup.string().email().isValidSync(value)
|
||||||
birthDate: Yup.string().required(t('errors.required')),
|
),
|
||||||
nationalCode: Yup.string().required(t('errors.required')),
|
|
||||||
economicCode: Yup.string().required(t('errors.required')),
|
|
||||||
registrationName: Yup.string().required(t('errors.required')),
|
|
||||||
registrationCode: Yup.string().required(t('errors.required')),
|
|
||||||
nationalIdentity: Yup.string().required(t('errors.required')),
|
|
||||||
postalCode: Yup.string().required(t('errors.required')),
|
|
||||||
address: Yup.string().required(t('errors.required')),
|
|
||||||
cityId: Yup.string().required(t('errors.required')),
|
|
||||||
}),
|
}),
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
values.cityId = +values.cityId
|
const payload: Partial<CreateCustomerType> = Object.fromEntries(
|
||||||
if (values.password === '') values.password = undefined
|
Object.entries(values).filter(([, value]) => value !== '' && value !== undefined && value !== null)
|
||||||
|
) as Partial<CreateCustomerType>
|
||||||
|
|
||||||
|
if (payload.cityId !== undefined) payload.cityId = +payload.cityId
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
await singleUpload.mutateAsync(formData, {
|
await singleUpload.mutateAsync(formData, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
values.profilePic = data?.data?.url
|
payload.profilePic = data?.data?.url
|
||||||
},
|
},
|
||||||
onError: (error: ErrorType) => {
|
onError: (error: ErrorType) => {
|
||||||
toast(error.response?.data?.error?.message[0], 'error')
|
toast(error.response?.data?.error?.message[0], 'error')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
handleUpdateCustomer(values)
|
|
||||||
} else {
|
|
||||||
values.profilePic = undefined
|
|
||||||
handleUpdateCustomer(values)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleUpdateCustomer(payload as CreateCustomerType)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as api from "../service/CustomerService";
|
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import * as api from "../service/CustomerService";
|
||||||
import { CreateCustomerType } from "../types/CustomerTypes";
|
import { CreateCustomerType } from "../types/CustomerTypes";
|
||||||
|
|
||||||
export const useGetCustomers = (page: number, withoutPaginate?: boolean) => {
|
export const useGetCustomers = (page: number, limit: number, withoutPaginate?: boolean, search?: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["customers", page],
|
queryKey: ["customers", page, limit, withoutPaginate, search],
|
||||||
queryFn: () => api.getCustomers(page, withoutPaginate),
|
queryFn: () => api.getCustomers(page, limit, withoutPaginate, search),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,8 +27,7 @@ export const useGetCities = (provincesId: string) => {
|
|||||||
export const useCreateCustomer = () => {
|
export const useCreateCustomer = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (variables: CreateCustomerType) =>
|
mutationFn: (variables: CreateCustomerType) => api.createCustomer(variables),
|
||||||
api.createCustomer(variables),
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["customers"],
|
queryKey: ["customers"],
|
||||||
@@ -48,8 +47,7 @@ export const useGetCustomerDetail = (id?: string) => {
|
|||||||
export const useUpdateCustomer = (id: string) => {
|
export const useUpdateCustomer = (id: string) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (variables: CreateCustomerType) =>
|
mutationFn: (variables: CreateCustomerType) => api.updateCustomer(id, variables),
|
||||||
api.updateCustomer(id, variables),
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["customers"],
|
queryKey: ["customers"],
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import axios from "../../../config/axios";
|
import axios from "../../../config/axios";
|
||||||
import { CreateCustomerType } from "../types/CustomerTypes";
|
import { CreateCustomerType } from "../types/CustomerTypes";
|
||||||
|
|
||||||
export const getCustomers = async (page: number, withoutPaginate?: boolean) => {
|
export const getCustomers = async (page: number, limit: number, withoutPaginate?: boolean, search?: string) => {
|
||||||
const { data } = await axios.get(
|
const { data } = await axios.get(`/users/customers?page=${page}&limit=${limit}&paginate=${withoutPaginate ? "0" : "1"}&q=${encodeURIComponent(search ?? "")}`);
|
||||||
`/users/customers?page=${page}&paginate=${withoutPaginate ? "0" : "1"}`
|
|
||||||
);
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,10 +26,7 @@ export const getDetailCustomer = async (id: string) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateCustomer = async (
|
export const updateCustomer = async (id: string, params: CreateCustomerType) => {
|
||||||
id: string,
|
|
||||||
params: CreateCustomerType
|
|
||||||
) => {
|
|
||||||
const { data } = await axios.patch(`/users/customers/${id}`, params);
|
const { data } = await axios.patch(`/users/customers/${id}`, params);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ import PageLoading from '../../components/PageLoading'
|
|||||||
import { NumberFormat } from '../../config/func'
|
import { NumberFormat } from '../../config/func'
|
||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const DiscountList: FC = () => {
|
const DiscountList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
const getDiscounts = useGetDiscounts(search)
|
const getDiscounts = useGetDiscounts(search)
|
||||||
const deleteDiscount = useDeleteDiscount()
|
const deleteDiscount = useDeleteDiscount()
|
||||||
@@ -40,6 +42,7 @@ const DiscountList: FC = () => {
|
|||||||
{t('discount.list')}
|
{t('discount.list')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{canCreate('discounts') &&
|
||||||
<Link to={Pages.discount.create}>
|
<Link to={Pages.discount.create}>
|
||||||
<Button
|
<Button
|
||||||
className='w-[172px]'
|
className='w-[172px]'
|
||||||
@@ -52,6 +55,7 @@ const DiscountList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -114,17 +118,21 @@ const DiscountList: FC = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td text={t('')}>
|
<Td text={t('')}>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className='flex gap-2 items-center'>
|
||||||
|
{canUpdate('discounts') &&
|
||||||
<Link
|
<Link
|
||||||
to={Pages.discount.detail + item.id}
|
to={Pages.discount.detail + item.id}
|
||||||
>
|
>
|
||||||
<Edit size={20} color='#888888' />
|
<Edit size={20} color='#888888' />
|
||||||
</Link>
|
</Link>
|
||||||
|
}
|
||||||
|
|
||||||
|
{canDelete('discounts') &&
|
||||||
<Trash
|
<Trash
|
||||||
size={20}
|
size={20}
|
||||||
color='#888888'
|
color='#888888'
|
||||||
onClick={() => handleDelete(item.id)}
|
onClick={() => handleDelete(item.id)}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Td from '../../components/Td'
|
|||||||
import StatusWithText from '../../components/StatusWithText'
|
import StatusWithText from '../../components/StatusWithText'
|
||||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||||
import { useDeleteGuide } from './hooks/useGuideData'
|
import { useDeleteGuide } from './hooks/useGuideData'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
type GuideItem = {
|
type GuideItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -24,6 +25,7 @@ type GuideItem = {
|
|||||||
|
|
||||||
const List: FC = () => {
|
const List: FC = () => {
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const getGuides = useGetGuides(id || '')
|
const getGuides = useGetGuides(id || '')
|
||||||
const deleteGuide = useDeleteGuide(id || '')
|
const deleteGuide = useDeleteGuide(id || '')
|
||||||
@@ -36,6 +38,7 @@ const List: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('guide.list_guide')}
|
{t('guide.list_guide')}
|
||||||
</div>
|
</div>
|
||||||
|
{canCreate('services') && (
|
||||||
<div>
|
<div>
|
||||||
<Link to={Pages.services.guide + id + '/create'}>
|
<Link to={Pages.services.guide + id + '/create'}>
|
||||||
<Button
|
<Button
|
||||||
@@ -51,6 +54,7 @@ const List: FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -85,13 +89,17 @@ const List: FC = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td text=''>
|
<Td text=''>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
|
{canUpdate('services') && (
|
||||||
<Link to={Pages.services.guide + id + '/' + item.id}>
|
<Link to={Pages.services.guide + id + '/' + item.id}>
|
||||||
<Eye size={20} color='#8C90A3' />
|
<Eye size={20} color='#8C90A3' />
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
|
{canDelete('services') && (
|
||||||
<TrashWithConfrim
|
<TrashWithConfrim
|
||||||
onDelete={() => deleteGuide.mutate(item.id)}
|
onDelete={() => deleteGuide.mutate(item.id)}
|
||||||
isLoading={deleteGuide.isPending}
|
isLoading={deleteGuide.isPending}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,37 +1,32 @@
|
|||||||
import { FC, ReactNode } from 'react'
|
import { FC, ReactNode } from "react";
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string
|
title: string;
|
||||||
icon: ReactNode,
|
icon: ReactNode;
|
||||||
color: string,
|
color: string;
|
||||||
count: number,
|
count: number;
|
||||||
description: string,
|
description: string;
|
||||||
to?: string
|
to?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const ItemDashboard: FC<Props> = (props: Props) => {
|
const ItemDashboard: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={props.to ? props.to : ''} className='p-6 min-w-[40%] xl:min-w-[15%] flex flex-col items-center xl:items-start flex-1 h-[178px] bg-white rounded-3xl'>
|
<div className="p-6 min-w-[40%] xl:min-w-[15%] flex flex-col items-center xl:items-start flex-1 h-[178px] bg-white rounded-3xl">
|
||||||
<div className='size-10 rounded-full bg-[#EEF0F7] flex justify-center items-center'>
|
<div className="size-10 rounded-full bg-[#EEF0F7] flex justify-center items-center">{props.icon}</div>
|
||||||
{props.icon}
|
<div className="mt-5">{props.title}</div>
|
||||||
</div>
|
|
||||||
<div className='mt-5'>
|
|
||||||
{props.title}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5 text-xs text-description flex gap-1.5 items-center'>
|
<div className="mt-5 text-xs text-description flex gap-1.5 items-center">
|
||||||
<div style={{ background: props.color }} className='size-1.5 rounded-full'></div>
|
<div
|
||||||
<div className='flex gap-0.5'>
|
style={{ background: props.color }}
|
||||||
|
className="size-1.5 rounded-full"
|
||||||
|
></div>
|
||||||
|
<div className="flex gap-0.5">
|
||||||
<div>{props.count}</div>
|
<div>{props.count}</div>
|
||||||
<div className='whitespace-nowrap'>
|
<div className="whitespace-nowrap">{props.description}</div>
|
||||||
{props.description}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
);
|
||||||
)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default ItemDashboard
|
export default ItemDashboard;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Pagination from '../../components/Pagination'
|
|||||||
import { useGetCategory } from './hooks/useLearningData'
|
import { useGetCategory } from './hooks/useLearningData'
|
||||||
import { CategoryLearningType } from './types/LearningTypes'
|
import { CategoryLearningType } from './types/LearningTypes'
|
||||||
import CreateCategory from './components/CreateCategory'
|
import CreateCategory from './components/CreateCategory'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
|
|
||||||
const LearningCategory: FC = () => {
|
const LearningCategory: FC = () => {
|
||||||
@@ -13,6 +14,7 @@ const LearningCategory: FC = () => {
|
|||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const getCategory = useGetCategory()
|
const getCategory = useGetCategory()
|
||||||
|
const { canCreate } = usePermissions()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
@@ -65,7 +67,9 @@ const LearningCategory: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{canCreate('learnings') && (
|
||||||
<CreateCategory />
|
<CreateCategory />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import PageLoading from '../../components/PageLoading'
|
|||||||
import Td from '../../components/Td'
|
import Td from '../../components/Td'
|
||||||
import { LearningItemType } from './types/LearningTypes'
|
import { LearningItemType } from './types/LearningTypes'
|
||||||
import Pagination from '../../components/Pagination'
|
import Pagination from '../../components/Pagination'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const LearningList: FC = () => {
|
const LearningList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const getLearning = useGetLearning(page)
|
const getLearning = useGetLearning(page)
|
||||||
|
const { canCreate } = usePermissions()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
@@ -21,6 +23,7 @@ const LearningList: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('learning.list_learning')}
|
{t('learning.list_learning')}
|
||||||
</div>
|
</div>
|
||||||
|
{canCreate('learnings') && (
|
||||||
<div>
|
<div>
|
||||||
<Link to={Pages.learning.create}>
|
<Link to={Pages.learning.create}>
|
||||||
<Button
|
<Button
|
||||||
@@ -36,6 +39,7 @@ const LearningList: FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ import Pagination from '../../components/Pagination'
|
|||||||
import moment from 'moment-jalaali'
|
import moment from 'moment-jalaali'
|
||||||
import Accept from './components/Accept'
|
import Accept from './components/Accept'
|
||||||
import Reject from './components/Reject'
|
import Reject from './components/Reject'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const PaymentList: FC = () => {
|
const PaymentList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canUpdate } = usePermissions()
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const [pageGateWay, setPageGateWay] = useState<number>(1)
|
const [pageGateWay, setPageGateWay] = useState<number>(1)
|
||||||
const [activeTab, setActiveTab] = useState<PaymentStatusType>('GATEWAY')
|
const [activeTab, setActiveTab] = useState<PaymentStatusType>('GATEWAY')
|
||||||
@@ -140,7 +142,7 @@ const PaymentList: FC = () => {
|
|||||||
<Td text={t(`payment.${item.status}`)} />
|
<Td text={t(`payment.${item.status}`)} />
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
{
|
{
|
||||||
item.status === 'PENDING' &&
|
item.status === 'PENDING' && canUpdate('payments') &&
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Accept
|
<Accept
|
||||||
|
|||||||
+14
-125
@@ -1,70 +1,31 @@
|
|||||||
import { FC, useCallback, useEffect, useState } from 'react'
|
import { FC, useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import AvatarImage from '../../assets/images/avatar_image.png'
|
import AvatarImage from '../../assets/images/avatar_image.png'
|
||||||
import Button from '../../components/Button'
|
import Button from '../../components/Button'
|
||||||
import { DocumentUpload } from 'iconsax-react'
|
import { DocumentUpload } from 'iconsax-react'
|
||||||
import Input from '../../components/Input'
|
|
||||||
import DatePickerComponent from '../../components/DatePicker'
|
|
||||||
import { useGetProfile, useUpdateProfile } from './hooks/useProfileData'
|
import { useGetProfile, useUpdateProfile } from './hooks/useProfileData'
|
||||||
import Username from './components/Username'
|
import Username from './components/Username'
|
||||||
import { useDropzone } from 'react-dropzone'
|
import { useDropzone } from 'react-dropzone'
|
||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast'
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
import { UpdateProfileType } from './types/ProfileTypes'
|
import { UpdateProfileType } from './types/ProfileTypes'
|
||||||
import PageLoading from '../../components/PageLoading'
|
import PageLoading from '../../components/PageLoading'
|
||||||
import { useFormik } from 'formik'
|
|
||||||
import * as Yup from 'yup'
|
|
||||||
import Email from './components/Email'
|
import Email from './components/Email'
|
||||||
import Phone from './components/Phone'
|
import Phone from './components/Phone'
|
||||||
import { useGetProvines } from '../customer/hooks/useCustomerData'
|
import FirstName from './components/FirstName'
|
||||||
|
import LastName from './components/LastName'
|
||||||
|
import BirthDate from './components/BirthDate'
|
||||||
|
import NationalCode from './components/NationalCode'
|
||||||
import { useSingleUpload } from '../service/hooks/useServiceData'
|
import { useSingleUpload } from '../service/hooks/useServiceData'
|
||||||
|
|
||||||
const Profile: FC = () => {
|
const Profile: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [file, setFile] = useState<File>()
|
const [file, setFile] = useState<File>()
|
||||||
const getProvines = useGetProvines()
|
|
||||||
const getProfile = useGetProfile()
|
const getProfile = useGetProfile()
|
||||||
const singleUpload = useSingleUpload()
|
const singleUpload = useSingleUpload()
|
||||||
const updateProfile = useUpdateProfile()
|
const updateProfile = useUpdateProfile()
|
||||||
|
|
||||||
const formik = useFormik<UpdateProfileType>({
|
|
||||||
initialValues: {
|
|
||||||
cityId: '',
|
|
||||||
userAddress: '',
|
|
||||||
postalCode: '',
|
|
||||||
},
|
|
||||||
validationSchema: Yup.object({
|
|
||||||
cityId: Yup.string().required(t('errors.required')),
|
|
||||||
userAddress: Yup.string().required(t('errors.required')),
|
|
||||||
postalCode: Yup.string().required(t('errors.required')),
|
|
||||||
}),
|
|
||||||
onSubmit: (values) => {
|
|
||||||
updateProfile.mutate(values, {
|
|
||||||
onSuccess: () => {
|
|
||||||
toast(t('success'), 'success')
|
|
||||||
},
|
|
||||||
onError: (error: ErrorType) => {
|
|
||||||
toast(error.response?.data?.error?.message[0], 'error')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
|
|
||||||
if (getProfile.data) {
|
|
||||||
formik.setValues({
|
|
||||||
cityId: getProfile.data.data.user.city?.id || '',
|
|
||||||
userAddress: getProfile.data.data.user.userAddress || '',
|
|
||||||
postalCode: getProfile.data.data.user.postalCode || '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [getProfile.data])
|
|
||||||
|
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
|
|
||||||
if (acceptedFiles.length > 0) {
|
if (acceptedFiles.length > 0) {
|
||||||
@@ -83,12 +44,12 @@ const Profile: FC = () => {
|
|||||||
toast(t('profile.image_uploaded_successfully'), 'success')
|
toast(t('profile.image_uploaded_successfully'), 'success')
|
||||||
},
|
},
|
||||||
onError: (error: ErrorType) => {
|
onError: (error: ErrorType) => {
|
||||||
toast(error.response?.data?.error?.message[0], 'error')
|
toast(error.response?.data?.error?.message?.[0], 'error')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onError: (error: ErrorType) => {
|
onError: (error: ErrorType) => {
|
||||||
toast(error.response?.data?.error?.message[0], 'error')
|
toast(error.response?.data?.error?.message?.[0], 'error')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -107,7 +68,7 @@ const Profile: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
getProfile.isPending || getProvines.isPending ?
|
getProfile.isPending ?
|
||||||
<PageLoading />
|
<PageLoading />
|
||||||
:
|
:
|
||||||
<div className='bg-white rounded-3xl xl:p-6 p-4 mt-8 text-sm'>
|
<div className='bg-white rounded-3xl xl:p-6 p-4 mt-8 text-sm'>
|
||||||
@@ -175,86 +136,14 @@ const Profile: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-1'>
|
<div className='flex-1'>
|
||||||
<DatePickerComponent
|
<FirstName firstName={getProfile.data?.data?.user?.firstName} />
|
||||||
label={t('profile.date_of_birth')}
|
<LastName lastName={getProfile.data?.data?.user?.lastName} />
|
||||||
onChange={() => { }}
|
|
||||||
placeholder=''
|
|
||||||
defaulValue={getProfile.data?.data?.user?.birthDate}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='xl:mt-7 mt-4'>
|
<div className='xl:mt-7 mt-4'>
|
||||||
<Input
|
<BirthDate birthDate={getProfile.data?.data?.user?.birthDate} />
|
||||||
label={t('profile.national_code')}
|
</div>
|
||||||
value={getProfile.data?.data?.user?.nationalCode}
|
<NationalCode nationalCode={getProfile.data?.data?.user?.nationalCode} />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* <div className='mt-8 flex xl:flex-row flex-col xl:gap-0 gap-7'>
|
|
||||||
<div className='flex-1'>
|
|
||||||
<div>
|
|
||||||
{t('profile.address')}
|
|
||||||
</div>
|
|
||||||
<div className='text-description text-xs mt-2'>
|
|
||||||
{t('profile.address_live')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex-1'>
|
|
||||||
<div className='rowTwoInput'>
|
|
||||||
<Select
|
|
||||||
label={t('profile.province')}
|
|
||||||
items={getProvines.data?.data?.provinces.map((item: ProvinesItemType) => ({ label: item.name, value: item.id }))}
|
|
||||||
onChange={(e) => setProvinesId(e.target.value)}
|
|
||||||
className='bg-white border'
|
|
||||||
placeholder={t('select')}
|
|
||||||
value={provinesId}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label={t('profile.city')}
|
|
||||||
items={getCities.data?.data?.cities ? getCities.data?.data?.cities.map((item: ProvinesItemType) => ({ label: item.name, value: item.id })) : []}
|
|
||||||
placeholder={t('select')}
|
|
||||||
{...formik.getFieldProps('cityId')}
|
|
||||||
error_text={formik.touched.cityId && formik.errors.cityId ? formik.errors.cityId : ''}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='xl:mt-7 mt-4'>
|
|
||||||
<Input
|
|
||||||
label={t('profile.postal_code')}
|
|
||||||
{...formik.getFieldProps('postalCode')}
|
|
||||||
error_text={formik.touched.postalCode && formik.errors.postalCode ? formik.errors.postalCode : ''}
|
|
||||||
value={formik.values.postalCode}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='xl:mt-7 mt-4'>
|
|
||||||
<Textarea
|
|
||||||
label={t('profile.address')}
|
|
||||||
defaultValue={formik.values.userAddress}
|
|
||||||
{...formik.getFieldProps('userAddress')}
|
|
||||||
error_text={formik.touched.userAddress && formik.errors.userAddress ? formik.errors.userAddress : ''}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex justify-end mt-5'>
|
|
||||||
<Button
|
|
||||||
className='xl:w-fit px-10'
|
|
||||||
onClick={() => formik.handleSubmit()}
|
|
||||||
isLoading={updateProfile.isPending}
|
|
||||||
>
|
|
||||||
<div className='flex gap-2 items-center'>
|
|
||||||
<TickCircle size={20} color='white' />
|
|
||||||
<div>
|
|
||||||
{t('save')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
<div className='h-20 xl:hidden'></div>
|
<div className='h-20 xl:hidden'></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { FC, useEffect, useState } from 'react'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import DatePickerComponent from '../../../components/DatePicker'
|
||||||
|
import { Edit } from 'iconsax-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useUpdateProfile } from '../hooks/useProfileData'
|
||||||
|
import { UpdateProfileType } from '../types/ProfileTypes'
|
||||||
|
import { toast } from '../../../components/Toast'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
birthDate: string | null | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const BirthDate: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const [value, setValue] = useState<string>(props.birthDate || '')
|
||||||
|
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
|
||||||
|
const updateProfile = useUpdateProfile()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(props.birthDate || '')
|
||||||
|
}, [props.birthDate])
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const params: UpdateProfileType = { birthDate: value }
|
||||||
|
updateProfile.mutate(params, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast(t('success'), 'success')
|
||||||
|
setIsReadOnly(true)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error?.message?.[0], 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDisabled = !value || value === (props.birthDate || '')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-end xl:gap-6 gap-3'>
|
||||||
|
{
|
||||||
|
isReadOnly ? (
|
||||||
|
<Input
|
||||||
|
label={t('profile.date_of_birth')}
|
||||||
|
value={value}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DatePickerComponent
|
||||||
|
label={t('profile.date_of_birth')}
|
||||||
|
onChange={setValue}
|
||||||
|
placeholder=''
|
||||||
|
defaulValue={value}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
isReadOnly ? (
|
||||||
|
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
|
||||||
|
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
|
||||||
|
<div>{t('edit')}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
label={t('save')}
|
||||||
|
className='px-4 w-fit'
|
||||||
|
disabled={isDisabled}
|
||||||
|
onClick={handleSave}
|
||||||
|
isLoading={updateProfile.isPending}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BirthDate
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
import { FC, Fragment, useState } from 'react'
|
import { FC, Fragment, useEffect, useState } from 'react'
|
||||||
import Input from '../../../components/Input'
|
import Input from '../../../components/Input'
|
||||||
import { Edit, TickCircle } from 'iconsax-react'
|
import { Edit } from 'iconsax-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Button from '../../../components/Button'
|
import Button from '../../../components/Button'
|
||||||
import { isEmail } from '../../../config/func'
|
import { isEmail } from '../../../config/func'
|
||||||
import DefaulModal from '../../../components/DefaulModal'
|
|
||||||
import OTPInput from 'react-otp-input'
|
|
||||||
import { useUpdateEmail } from '../hooks/useProfileData'
|
import { useUpdateEmail } from '../hooks/useProfileData'
|
||||||
import { ErrorType } from '../../../helpers/types'
|
import { ErrorType } from '../../../helpers/types'
|
||||||
import { toast } from '../../../components/Toast';
|
import { toast } from '../../../components/Toast'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
email: string | null,
|
email: string | null,
|
||||||
@@ -20,17 +18,20 @@ const Email: FC<Props> = (props: Props) => {
|
|||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [email, setEmail] = useState<string>(props.email ? props.email : '')
|
const [email, setEmail] = useState<string>(props.email ? props.email : '')
|
||||||
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
|
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
|
||||||
const [showModal, setShowModal] = useState<boolean>(false)
|
|
||||||
const [code, setCode] = useState<string>('')
|
|
||||||
const updateEmail = useUpdateEmail()
|
const updateEmail = useUpdateEmail()
|
||||||
|
|
||||||
const handleShowModal = () => {
|
useEffect(() => {
|
||||||
updateEmail.mutate({ email: email }, {
|
setEmail(props.email ? props.email : '')
|
||||||
|
}, [props.email])
|
||||||
|
|
||||||
|
const handleSendVerification = () => {
|
||||||
|
updateEmail.mutate({ email }, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast(t('profile.link_email'), 'success')
|
toast(t('profile.link_email'), 'success')
|
||||||
|
setIsReadOnly(true)
|
||||||
},
|
},
|
||||||
onError: (error: ErrorType) => {
|
onError: (error: ErrorType) => {
|
||||||
toast(error.response?.data?.error.message[0], 'error')
|
toast(error.response?.data?.error?.message?.[0], 'error')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -45,81 +46,42 @@ const Email: FC<Props> = (props: Props) => {
|
|||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
!props.isVerified &&
|
!props.isVerified && isReadOnly &&
|
||||||
<div className='text-[10px] absolute left-24 text-red-400 top-[34px]'>
|
<div className='text-[10px] absolute left-24 text-red-400 top-[34px]'>
|
||||||
{t('profile.email_not_verified')}
|
{t('profile.email_not_verified')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
isReadOnly ?
|
isReadOnly ?
|
||||||
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
|
<div className='flex items-center gap-3 relative -top-3'>
|
||||||
|
{
|
||||||
|
!props.isVerified && !!email &&
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={handleSendVerification}
|
||||||
|
disabled={updateEmail.isPending}
|
||||||
|
className='text-[#0047FF] text-xs whitespace-nowrap disabled:opacity-50'
|
||||||
|
>
|
||||||
|
{t('profile.resend_email')}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer gap-1 text-[#0047FF] text-xs items-center'>
|
||||||
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
|
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
|
||||||
<div>
|
<div>
|
||||||
{t('edit')}
|
{t('edit')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
:
|
:
|
||||||
<Button
|
<Button
|
||||||
label={t('save')}
|
label={t('save')}
|
||||||
className='px-4 w-fit'
|
className='px-4 w-fit'
|
||||||
disabled={!isEmail(email)}
|
disabled={!isEmail(email)}
|
||||||
onClick={handleShowModal}
|
onClick={handleSendVerification}
|
||||||
isLoading={updateEmail.isPending}
|
isLoading={updateEmail.isPending}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DefaulModal
|
|
||||||
open={showModal}
|
|
||||||
close={() => setShowModal(false)}
|
|
||||||
isHeader
|
|
||||||
title_header={t('profile.confrim_email')}
|
|
||||||
>
|
|
||||||
<div className='mt-7'>
|
|
||||||
<div className='text-xs text-center'>
|
|
||||||
کد تایید به ایمیل {email} ارسال شده است.برای تایید کد مربوطه را وارد کنید.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-10'>کد تایید</div>
|
|
||||||
<div className='mt-2 w-full flex justify-center dltr otp'>
|
|
||||||
<OTPInput
|
|
||||||
value={code}
|
|
||||||
onChange={setCode}
|
|
||||||
shouldAutoFocus
|
|
||||||
renderInput={(props) =>
|
|
||||||
<input
|
|
||||||
{...props}
|
|
||||||
type='tel'
|
|
||||||
autoComplete="one-time-code"
|
|
||||||
inputMode="numeric"
|
|
||||||
className='w-full h-[50px] flex-1 mx-2 bg-white bg-opacity-30 border rounded-2.5' />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-14 flex justify-end border-t border-border pt-8'>
|
|
||||||
<div className='flex gap-5'>
|
|
||||||
<Button
|
|
||||||
className='bg-white bg-opacity-40 text-description w-[150px] text-xs'
|
|
||||||
label='لغو'
|
|
||||||
onClick={() => setShowModal(false)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
className='w-[150px] text-xs'
|
|
||||||
>
|
|
||||||
<div className='flex gap-2 items-center'>
|
|
||||||
<TickCircle
|
|
||||||
size={20}
|
|
||||||
color='white'
|
|
||||||
/>
|
|
||||||
<div>{t('wallet.submit_slip')}</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</DefaulModal>
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { FC, useEffect, useState } from 'react'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import { Edit } from 'iconsax-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useUpdateProfile } from '../hooks/useProfileData'
|
||||||
|
import { UpdateProfileType } from '../types/ProfileTypes'
|
||||||
|
import { toast } from '../../../components/Toast'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
firstName: string | null | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const FirstName: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const [value, setValue] = useState<string>(props.firstName || '')
|
||||||
|
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
|
||||||
|
const updateProfile = useUpdateProfile()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(props.firstName || '')
|
||||||
|
}, [props.firstName])
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const params: UpdateProfileType = { firstName: value.trim() }
|
||||||
|
updateProfile.mutate(params, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast(t('success'), 'success')
|
||||||
|
setIsReadOnly(true)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error?.message?.[0], 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDisabled = value.trim().length < 2 || value.trim() === (props.firstName || '')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-end xl:gap-6 gap-3 xl:mt-7 mt-4'>
|
||||||
|
<Input
|
||||||
|
label={t('profile.first_name')}
|
||||||
|
value={value}
|
||||||
|
readOnly={isReadOnly}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
isReadOnly ? (
|
||||||
|
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
|
||||||
|
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
|
||||||
|
<div>{t('edit')}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
label={t('save')}
|
||||||
|
className='px-4 w-fit'
|
||||||
|
disabled={isDisabled}
|
||||||
|
onClick={handleSave}
|
||||||
|
isLoading={updateProfile.isPending}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FirstName
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { FC, useEffect, useState } from 'react'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import { Edit } from 'iconsax-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useUpdateProfile } from '../hooks/useProfileData'
|
||||||
|
import { UpdateProfileType } from '../types/ProfileTypes'
|
||||||
|
import { toast } from '../../../components/Toast'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
lastName: string | null | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const LastName: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const [value, setValue] = useState<string>(props.lastName || '')
|
||||||
|
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
|
||||||
|
const updateProfile = useUpdateProfile()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(props.lastName || '')
|
||||||
|
}, [props.lastName])
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const params: UpdateProfileType = { lastName: value.trim() }
|
||||||
|
updateProfile.mutate(params, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast(t('success'), 'success')
|
||||||
|
setIsReadOnly(true)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error?.message?.[0], 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDisabled = value.trim().length < 2 || value.trim() === (props.lastName || '')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-end xl:gap-6 gap-3 xl:mt-7 mt-4'>
|
||||||
|
<Input
|
||||||
|
label={t('profile.last_name')}
|
||||||
|
value={value}
|
||||||
|
readOnly={isReadOnly}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
isReadOnly ? (
|
||||||
|
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
|
||||||
|
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
|
||||||
|
<div>{t('edit')}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
label={t('save')}
|
||||||
|
className='px-4 w-fit'
|
||||||
|
disabled={isDisabled}
|
||||||
|
onClick={handleSave}
|
||||||
|
isLoading={updateProfile.isPending}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LastName
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { FC, useEffect, useState } from 'react'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import { Edit } from 'iconsax-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useUpdateProfile } from '../hooks/useProfileData'
|
||||||
|
import { UpdateProfileType } from '../types/ProfileTypes'
|
||||||
|
import { toast } from '../../../components/Toast'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
nationalCode: string | null | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const NationalCode: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const [value, setValue] = useState<string>(props.nationalCode || '')
|
||||||
|
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
|
||||||
|
const updateProfile = useUpdateProfile()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(props.nationalCode || '')
|
||||||
|
}, [props.nationalCode])
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const params: UpdateProfileType = { nationalCode: value }
|
||||||
|
updateProfile.mutate(params, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast(t('success'), 'success')
|
||||||
|
setIsReadOnly(true)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error?.message?.[0], 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDisabled = value.length !== 10 || value === (props.nationalCode || '')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-end xl:gap-6 gap-3 xl:mt-7 mt-4'>
|
||||||
|
<Input
|
||||||
|
label={t('profile.national_code')}
|
||||||
|
value={value}
|
||||||
|
readOnly={isReadOnly}
|
||||||
|
onChange={(e) => {
|
||||||
|
const next = e.target.value.replace(/\D/g, '').slice(0, 10)
|
||||||
|
setValue(next)
|
||||||
|
}}
|
||||||
|
inputMode='numeric'
|
||||||
|
maxLength={10}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
isReadOnly ? (
|
||||||
|
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
|
||||||
|
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
|
||||||
|
<div>{t('edit')}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
label={t('save')}
|
||||||
|
className='px-4 w-fit'
|
||||||
|
disabled={isDisabled}
|
||||||
|
onClick={handleSave}
|
||||||
|
isLoading={updateProfile.isPending}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NationalCode
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import * as api from '../service/ProfileService'
|
import * as api from '../service/ProfileService'
|
||||||
import { CheckUserNameType, UpdateEmailType, UpdatePhoneType, UpdateProfileType, VerifyPhoneType } from '../types/ProfileTypes';
|
import { CheckUserNameType, UpdateEmailType, UpdatePhoneType, UpdateProfileType, VerifyPhoneType } from '../types/ProfileTypes';
|
||||||
|
|
||||||
@@ -16,14 +16,22 @@ export const useCheckUserName = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateProfile = () => {
|
export const useUpdateProfile = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (variables: UpdateProfileType) => api.updateProfile(variables),
|
mutationFn: (variables: UpdateProfileType) => api.updateProfile(variables),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateEmail = () => {
|
export const useUpdateEmail = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (variables: UpdateEmailType) => api.updateEmail(variables),
|
mutationFn: (variables: UpdateEmailType) => api.updateEmail(variables),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,7 +42,11 @@ export const useUpdatePhone = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useVerifyPhone = () => {
|
export const useVerifyPhone = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (variables: VerifyPhoneType) => api.verifyPhone(variables),
|
mutationFn: (variables: VerifyPhoneType) => api.verifyPhone(variables),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -7,6 +7,7 @@ export type UpdateProfileType = {
|
|||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
birthDate?: string;
|
birthDate?: string;
|
||||||
|
nationalCode?: string;
|
||||||
userName?: string;
|
userName?: string;
|
||||||
profilePic?: string;
|
profilePic?: string;
|
||||||
cityId?: string;
|
cityId?: string;
|
||||||
|
|||||||
+248
-283
@@ -1,62 +1,64 @@
|
|||||||
import { ChangeEvent, FC, Fragment, useState } from 'react'
|
import { useFormik } from "formik";
|
||||||
import { useTranslation } from 'react-i18next'
|
import { Add, TickCircle, TickSquare, Trash } from "iconsax-react";
|
||||||
import Button from '../../components/Button'
|
import { ChangeEvent, FC, Fragment, useState } from "react";
|
||||||
import { Add, TickCircle, TickSquare, Trash } from 'iconsax-react'
|
import { useTranslation } from "react-i18next";
|
||||||
import Select from '../../components/Select'
|
import { useNavigate } from "react-router-dom";
|
||||||
import Input from '../../components/Input'
|
import * as Yup from "yup";
|
||||||
import { CreateReceiptType, ReceiptCreateItemsType } from './types/ReceiptTypes'
|
import Button from "../../components/Button";
|
||||||
import { useFormik } from 'formik'
|
import Input from "../../components/Input";
|
||||||
import * as Yup from 'yup'
|
import PageLoading from "../../components/PageLoading";
|
||||||
import { toast } from '../../components/Toast';
|
import Select from "../../components/Select";
|
||||||
import { useGetCustomers } from '../customer/hooks/useCustomerData'
|
import SwitchComponent from "../../components/Switch";
|
||||||
import { CustomerItemType } from '../customer/types/CustomerTypes'
|
import { toast } from "../../components/Toast";
|
||||||
import PageLoading from '../../components/PageLoading'
|
import { Pages } from "../../config/Pages";
|
||||||
import { useCreateInvoice } from './hooks/useReceiptData'
|
import { ErrorType } from "../../helpers/types";
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useGetCustomers } from "../customer/hooks/useCustomerData";
|
||||||
import { Pages } from '../../config/Pages'
|
import { CustomerItemType } from "../customer/types/CustomerTypes";
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { RecurringPeriodEnum } from "./enum/ReceipEnum";
|
||||||
import { RecurringPeriodEnum } from './enum/ReceipEnum'
|
import { useCreateInvoice } from "./hooks/useReceiptData";
|
||||||
import SwitchComponent from '../../components/Switch'
|
import { CreateReceiptType, ReceiptCreateItemsType } from "./types/ReceiptTypes";
|
||||||
const CreateReceipt: FC = () => {
|
const CreateReceipt: FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const navigate = useNavigate()
|
const { t } = useTranslation("global");
|
||||||
const { t } = useTranslation('global')
|
const [items, setItems] = useState<ReceiptCreateItemsType[]>([]);
|
||||||
const [items, setItems] = useState<ReceiptCreateItemsType[]>([])
|
const [customer, setCustomer] = useState<CustomerItemType>();
|
||||||
const [customer, setCustomer] = useState<CustomerItemType>()
|
const [isRecurring, setIsRecurring] = useState<boolean>(false);
|
||||||
const [isRecurring, setIsRecurring] = useState<boolean>(false)
|
const [type, setType] = useState<RecurringPeriodEnum>(RecurringPeriodEnum.WEEKLY);
|
||||||
const [type, setType] = useState<RecurringPeriodEnum>(RecurringPeriodEnum.WEEKLY)
|
const [maxRecurringCycles, setMaxRecurringCycles] = useState<number>(0);
|
||||||
const [maxRecurringCycles, setMaxRecurringCycles] = useState<number>(0)
|
const getCustomers = useGetCustomers(1, 50, true);
|
||||||
const getCustomers = useGetCustomers(1, true)
|
const createInvoice = useCreateInvoice();
|
||||||
const createInvoice = useCreateInvoice()
|
|
||||||
|
|
||||||
const formik = useFormik<ReceiptCreateItemsType>({
|
const formik = useFormik<ReceiptCreateItemsType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: '',
|
name: "",
|
||||||
count: '',
|
count: "",
|
||||||
unitPrice: '',
|
unitPrice: "",
|
||||||
discount: ''
|
discount: "",
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
name: Yup.string().required(t('errors.required')),
|
name: Yup.string().required(t("errors.required")),
|
||||||
count: Yup.string().required(t('errors.required')),
|
count: Yup.string().required(t("errors.required")),
|
||||||
unitPrice: Yup.string().required(t('errors.required')),
|
unitPrice: Yup.string().required(t("errors.required")),
|
||||||
}),
|
}),
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
setItems((prev) => [...prev, {
|
setItems((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
count: +values.count,
|
count: +values.count,
|
||||||
discount: +values.discount,
|
discount: +values.discount,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
unitPrice: +values.unitPrice
|
unitPrice: +values.unitPrice,
|
||||||
}])
|
},
|
||||||
formik.resetForm()
|
]);
|
||||||
}
|
formik.resetForm();
|
||||||
})
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleChangeCustomer = (e: ChangeEvent<HTMLSelectElement>) => {
|
const handleChangeCustomer = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
const value = e.target.value
|
const value = e.target.value;
|
||||||
const customer = getCustomers.data?.data?.customers?.find((item: CustomerItemType) => item.id === value)
|
const customer = getCustomers.data?.data?.customers?.find((item: CustomerItemType) => item.id === value);
|
||||||
setCustomer(customer)
|
setCustomer(customer);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (customer) {
|
if (customer) {
|
||||||
@@ -65,406 +67,369 @@ const CreateReceipt: FC = () => {
|
|||||||
items: items,
|
items: items,
|
||||||
isRecurring: isRecurring,
|
isRecurring: isRecurring,
|
||||||
recurringPeriod: isRecurring ? +type : undefined,
|
recurringPeriod: isRecurring ? +type : undefined,
|
||||||
maxRecurringCycles: isRecurring ? maxRecurringCycles : undefined
|
maxRecurringCycles: isRecurring ? maxRecurringCycles : undefined,
|
||||||
}
|
};
|
||||||
|
|
||||||
createInvoice.mutate(params, {
|
createInvoice.mutate(params, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast(t('success'), 'success')
|
toast(t("success"), "success");
|
||||||
navigate(Pages.receipts.index)
|
navigate(Pages.receipts.index);
|
||||||
},
|
},
|
||||||
onError(error: ErrorType) {
|
onError(error: ErrorType) {
|
||||||
toast(error.response?.data?.error.message[0], 'error')
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleRemove = (index: number) => {
|
const handleRemove = (index: number) => {
|
||||||
setItems((prev) => prev.filter((_, i) => i !== index))
|
setItems((prev) => prev.filter((_, i) => i !== index));
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4'>
|
<div className="mt-4">
|
||||||
{
|
{getCustomers.isPending ? (
|
||||||
getCustomers.isPending ?
|
|
||||||
<PageLoading />
|
<PageLoading />
|
||||||
:
|
) : (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className='flex justify-between items-center'>
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>{t("receip.add_receip")}</div>
|
||||||
{t('receip.add_receip')}
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className='px-5'
|
className="px-5"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
isLoading={createInvoice.isPending}
|
isLoading={createInvoice.isPending}
|
||||||
>
|
>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className="flex gap-2 items-center">
|
||||||
<TickCircle size={20} color='white' />
|
<TickCircle
|
||||||
<div>
|
size={20}
|
||||||
{t('receip.submit_receip')}
|
color="white"
|
||||||
</div>
|
/>
|
||||||
|
<div>{t("receip.submit_receip")}</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex gap-6 xl:mt-8 mt-4'>
|
<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 className="flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl">
|
||||||
<div>
|
<div>{t("receip.customer_information")}</div>
|
||||||
{t('receip.customer_information')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className='w-full xl:w-1/2'>
|
<div className="w-full xl:w-1/2">
|
||||||
<Select
|
<Select
|
||||||
label={t('receip.customer')}
|
label={t("receip.customer")}
|
||||||
placeholder={t('select')}
|
placeholder={t("select")}
|
||||||
items={getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
|
items={getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
|
||||||
return {
|
return {
|
||||||
label: item.firstName + ' ' + item.lastName,
|
label: item.firstName + " " + item.lastName,
|
||||||
value: item.id
|
value: item.id,
|
||||||
}
|
};
|
||||||
})}
|
})}
|
||||||
onChange={handleChangeCustomer}
|
onChange={handleChangeCustomer}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{customer && (
|
||||||
customer &&
|
<div className="mt-10 bg-[#F6F7FB] p-8 rounded-3xl text-sm">
|
||||||
<div className='mt-10 bg-[#F6F7FB] p-8 rounded-3xl text-sm'>
|
<div className="flex justify-between items-center border-b border-border border-dashed pb-7">
|
||||||
<div className='flex justify-between items-center border-b border-border border-dashed pb-7'>
|
<div className="flex items-center gap-2">
|
||||||
<div className='flex items-center gap-2'>
|
<div className="text-description">{t("receip.type_person")}</div>
|
||||||
<div className='text-description'>{t('receip.type_person')}</div>
|
<div>{customer.legalUser ? "حقوقی" : "حقیقی"}</div>
|
||||||
<div>{customer.legalUser ? 'حقوقی' : 'حقیقی'}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className="flex items-center gap-2">
|
||||||
<div className='text-description'>{t('receip.representativeـname')}</div>
|
<div className="text-description">{t("receip.representativeـname")}</div>
|
||||||
<div>{customer.firstName + ' ' + customer.lastName}</div>
|
<div>{customer.firstName + " " + customer.lastName}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className="flex items-center gap-2">
|
||||||
<div className='text-description'>{t('receip.company_name')}</div>
|
<div className="text-description">{t("receip.company_name")}</div>
|
||||||
<div>{customer?.legalUser?.registrationName}</div>
|
<div>{customer?.legalUser?.registrationName}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className="flex items-center gap-2">
|
||||||
<div className='text-description'>{t('receip.registration_number')}</div>
|
<div className="text-description">{t("receip.registration_number")}</div>
|
||||||
<div>{customer?.legalUser?.registrationCode}</div>
|
<div>{customer?.legalUser?.registrationCode}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-between items-center mt-7'>
|
<div className="flex justify-between items-center mt-7">
|
||||||
<div className='flex items-center gap-2 flex-1'>
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<div className='text-description'>{t('receip.tel_company')}</div>
|
<div className="text-description">{t("receip.tel_company")}</div>
|
||||||
<div>{customer?.realUser?.phone || customer?.legalUser?.phone}</div>
|
<div>{customer?.realUser?.phone || customer?.legalUser?.phone}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2 flex-1'>
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<div className='text-description'>{t('receip.national_code')}</div>
|
<div className="text-description">{t("receip.national_code")}</div>
|
||||||
<div>{customer.nationalCode}</div>
|
<div>{customer.nationalCode}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2 flex-1'>
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<div className='text-description'>{t('receip.postal_code')}</div>
|
<div className="text-description">{t("receip.postal_code")}</div>
|
||||||
<div>{customer?.realUser?.address?.postalCode || customer?.legalUser?.address?.postalCode}</div>
|
<div>{customer?.realUser?.address?.postalCode || customer?.legalUser?.address?.postalCode}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-between items-center mt-7'>
|
<div className="flex justify-between items-center mt-7">
|
||||||
<div className='flex items-center gap-2 flex-1'>
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<div className='text-description'>{t('receip.state')}</div>
|
<div className="text-description">{t("receip.state")}</div>
|
||||||
<div>{customer?.realUser?.address?.city?.province?.name || customer?.legalUser?.address?.city?.province?.name}</div>
|
<div>{customer?.realUser?.address?.city?.province?.name || customer?.legalUser?.address?.city?.province?.name}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2 flex-1'>
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<div className='text-description'>{t('receip.city')}</div>
|
<div className="text-description">{t("receip.city")}</div>
|
||||||
<div>{customer?.realUser?.address?.city?.name || customer?.legalUser?.address?.city?.name}</div>
|
<div>{customer?.realUser?.address?.city?.name || customer?.legalUser?.address?.city?.name}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2 flex-1'>
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<div className='text-description'>{t('receip.address')}</div>
|
<div className="text-description">{t("receip.address")}</div>
|
||||||
<div>{customer?.realUser?.address?.fullAddress || customer?.legalUser?.address?.fullAddress}</div>
|
<div>{customer?.realUser?.address?.fullAddress || customer?.legalUser?.address?.fullAddress}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<div className='mt-10'>
|
<div className="mt-10">
|
||||||
<SwitchComponent
|
<SwitchComponent
|
||||||
label={t('receip.recurring')}
|
label={t("receip.recurring")}
|
||||||
active={isRecurring}
|
active={isRecurring}
|
||||||
onChange={(value) => setIsRecurring(value)}
|
onChange={(value) => setIsRecurring(value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{isRecurring && (
|
||||||
isRecurring &&
|
<div className="mt-10">
|
||||||
<div className='mt-10'>
|
<div>{t("receip.select_type")}</div>
|
||||||
<div>
|
|
||||||
{t('receip.select_type')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-4 rowTwoInput'>
|
<div className="mt-4 rowTwoInput">
|
||||||
<Select
|
<Select
|
||||||
label={t('select')}
|
label={t("select")}
|
||||||
items={[
|
items={[
|
||||||
{ label: t('receip.WEEKLY'), value: RecurringPeriodEnum.WEEKLY.toString() },
|
{ label: t("receip.WEEKLY"), value: RecurringPeriodEnum.WEEKLY.toString() },
|
||||||
{ label: t('receip.MONTHLY'), value: RecurringPeriodEnum.MONTHLY.toString() },
|
{ label: t("receip.MONTHLY"), value: RecurringPeriodEnum.MONTHLY.toString() },
|
||||||
{ label: t('receip.QUARTERLY'), value: RecurringPeriodEnum.QUARTERLY.toString() },
|
{ label: t("receip.QUARTERLY"), value: RecurringPeriodEnum.QUARTERLY.toString() },
|
||||||
{ label: t('receip.SEMIANNUALLY'), value: RecurringPeriodEnum.SEMIANNUALLY.toString() },
|
{ label: t("receip.SEMIANNUALLY"), value: RecurringPeriodEnum.SEMIANNUALLY.toString() },
|
||||||
{ label: t('receip.ANNUALLY'), value: RecurringPeriodEnum.ANNUALLY.toString() }
|
{ label: t("receip.ANNUALLY"), value: RecurringPeriodEnum.ANNUALLY.toString() },
|
||||||
]}
|
]}
|
||||||
onChange={(e) => setType(e.target.value as unknown as RecurringPeriodEnum)}
|
onChange={(e) => setType(e.target.value as unknown as RecurringPeriodEnum)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label={t('receip.maxRecurringCycles')}
|
label={t("receip.maxRecurringCycles")}
|
||||||
name='maxRecurringCycles'
|
name="maxRecurringCycles"
|
||||||
value={maxRecurringCycles}
|
value={maxRecurringCycles}
|
||||||
onChange={(e) => setMaxRecurringCycles(+e.target.value)}
|
onChange={(e) => setMaxRecurringCycles(+e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<div className='mt-10'>
|
<div className="mt-10">
|
||||||
<div>{t('receip.receipt_information')}</div>
|
<div>{t("receip.receipt_information")}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-8 border-b border-dashed border-border pb-7 flex items-end justify-between text-xs text-description'>
|
<div className="mt-8 border-b border-dashed border-border pb-7 flex items-end justify-between text-xs text-description">
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<div>
|
<div>{t("receip.number")}</div>
|
||||||
{t('receip.number')}
|
<div className="size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center">{items.length + 1}</div>
|
||||||
</div>
|
|
||||||
<div className='size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center'>
|
|
||||||
{items.length + 1}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<div>
|
<div>{t("receip.product_name")}</div>
|
||||||
{t('receip.product_name')}
|
|
||||||
</div>
|
|
||||||
<Input
|
<Input
|
||||||
className='text-center'
|
className="text-center"
|
||||||
name='name'
|
name="name"
|
||||||
value={formik.values.name}
|
value={formik.values.name}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<div>
|
<div>{t("receip.count")}</div>
|
||||||
{t('receip.count')}
|
|
||||||
</div>
|
|
||||||
<Input
|
<Input
|
||||||
type='number'
|
type="number"
|
||||||
className='w-16 text-center'
|
className="w-16 text-center"
|
||||||
name='count'
|
name="count"
|
||||||
value={formik.values.count}
|
value={formik.values.count}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<div>
|
<div>{t("receip.unit_amount")}</div>
|
||||||
{t('receip.unit_amount')}
|
|
||||||
</div>
|
|
||||||
<Input
|
<Input
|
||||||
className='text-center'
|
className="text-center"
|
||||||
name='unitPrice'
|
name="unitPrice"
|
||||||
value={formik.values.unitPrice}
|
value={formik.values.unitPrice}
|
||||||
onChange={(e) => formik.setFieldValue('unitPrice', e.target.value)}
|
onChange={(e) => formik.setFieldValue("unitPrice", e.target.value)}
|
||||||
seprator
|
seprator
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<div>
|
<div>{t("receip.discount")}</div>
|
||||||
{t('receip.discount')}
|
|
||||||
</div>
|
|
||||||
<Input
|
<Input
|
||||||
type='number'
|
type="number"
|
||||||
className='w-16 text-center'
|
className="w-16 text-center"
|
||||||
name='discount'
|
name="discount"
|
||||||
value={formik.values.discount}
|
value={formik.values.discount}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<div>
|
<div>{t("receip.total_amount")}</div>
|
||||||
{t('receip.total_amount')}
|
|
||||||
</div>
|
|
||||||
<Input
|
<Input
|
||||||
className='text-center bg-[#EBEDF5]'
|
className="text-center bg-[#EBEDF5]"
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div onClick={() => {
|
<div
|
||||||
|
onClick={() => {
|
||||||
if (formik.errors.name || formik.errors.count || formik.errors.unitPrice || formik.errors.discount) {
|
if (formik.errors.name || formik.errors.count || formik.errors.unitPrice || formik.errors.discount) {
|
||||||
toast(t('receip.error_empty'), 'error')
|
toast(t("receip.error_empty"), "error");
|
||||||
} else {
|
} else {
|
||||||
formik.handleSubmit()
|
formik.handleSubmit();
|
||||||
}
|
}
|
||||||
}} className='size-10 border border-border rounded-xl flex justify-center items-center'>
|
}}
|
||||||
<Add size={20} color='black' />
|
className="size-10 border border-border rounded-xl flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<Add
|
||||||
|
size={20}
|
||||||
|
color="black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{items.map((item, index: number) => {
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
items.map((item, index: number) => {
|
|
||||||
return (
|
return (
|
||||||
<div key={item.name} className='mt-6 flex items-end justify-between text-xs text-description'>
|
<div
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
key={item.name}
|
||||||
{
|
className="mt-6 flex items-end justify-between text-xs text-description"
|
||||||
index === 0 &&
|
>
|
||||||
<div>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
{t('receip.number')}
|
{index === 0 && <div>{t("receip.number")}</div>}
|
||||||
|
<div className="size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center">{index + 1}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<div className='size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center'>
|
{index === 0 && <div>{t("receip.product_name")}</div>}
|
||||||
{index + 1}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
|
||||||
{
|
|
||||||
index === 0 &&
|
|
||||||
<div>
|
|
||||||
{t('receip.product_name')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Input
|
<Input
|
||||||
className='text-center'
|
className="text-center"
|
||||||
name='name'
|
name="name"
|
||||||
readOnly
|
readOnly
|
||||||
value={item.name}
|
value={item.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
{
|
{index === 0 && <div>{t("receip.count")}</div>}
|
||||||
index === 0 &&
|
|
||||||
<div>
|
|
||||||
{t('receip.count')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Input
|
<Input
|
||||||
type='number'
|
type="number"
|
||||||
className='w-16 text-center'
|
className="w-16 text-center"
|
||||||
name='count'
|
name="count"
|
||||||
readOnly
|
readOnly
|
||||||
value={item.count}
|
value={item.count}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
{
|
{index === 0 && <div>{t("receip.unit_amount")}</div>}
|
||||||
index === 0 &&
|
|
||||||
<div>
|
|
||||||
{t('receip.unit_amount')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Input
|
<Input
|
||||||
className='text-center'
|
className="text-center"
|
||||||
name='unitPrice'
|
name="unitPrice"
|
||||||
readOnly
|
readOnly
|
||||||
value={item.unitPrice}
|
value={item.unitPrice}
|
||||||
seprator
|
seprator
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
{
|
{index === 0 && <div>{t("receip.discount")}</div>}
|
||||||
index === 0 &&
|
|
||||||
<div>
|
|
||||||
{t('receip.discount')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Input
|
<Input
|
||||||
type='number'
|
type="number"
|
||||||
className='w-16 text-center'
|
className="w-16 text-center"
|
||||||
name='discount'
|
name="discount"
|
||||||
readOnly
|
readOnly
|
||||||
value={item.discount}
|
value={item.discount}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
{
|
{index === 0 && <div>{t("receip.total_amount")}</div>}
|
||||||
index === 0 &&
|
|
||||||
<div>
|
|
||||||
{t('receip.total_amount')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Input
|
<Input
|
||||||
className='text-center bg-[#EBEDF5]'
|
className="text-center bg-[#EBEDF5]"
|
||||||
readOnly
|
readOnly
|
||||||
value={((Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0)).toFixed(1)}
|
value={((Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0)).toFixed(1)}
|
||||||
seprator
|
seprator
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div onClick={() => handleRemove(index)} className='size-10 border border-border rounded-xl flex justify-center items-center'>
|
<div
|
||||||
<Trash size={20} color='black' />
|
onClick={() => handleRemove(index)}
|
||||||
|
className="size-10 border border-border rounded-xl flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<Trash
|
||||||
|
size={20}
|
||||||
|
color="black"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-col gap-4 items-end">
|
||||||
|
<div className="xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center">
|
||||||
<div className='mt-8 flex flex-col gap-4 items-end'>
|
<div>{t("receip.tax")}</div>
|
||||||
<div className='xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center'>
|
<div className="text-black">
|
||||||
<div>
|
|
||||||
{t('receip.tax')}
|
|
||||||
</div>
|
|
||||||
<div className='text-black'>
|
|
||||||
{((items.reduce((acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0), 0)) * 0.10).toFixed(1)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center'>
|
|
||||||
<div>
|
|
||||||
{t('receip.total')}
|
|
||||||
</div>
|
|
||||||
<div className='text-black'>
|
|
||||||
{(
|
{(
|
||||||
items.reduce((acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0), 0) * 1.10
|
items.reduce(
|
||||||
|
(acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0),
|
||||||
|
0,
|
||||||
|
) * 0.1
|
||||||
|
).toFixed(1)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center">
|
||||||
|
<div>{t("receip.total")}</div>
|
||||||
|
<div className="text-black">
|
||||||
|
{(
|
||||||
|
items.reduce(
|
||||||
|
(acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0),
|
||||||
|
0,
|
||||||
|
) * 1.1
|
||||||
).toFixed(1)}
|
).toFixed(1)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='bg-white w-sidebar 3xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
<div className="bg-white w-sidebar 3xl:block hidden py-10 px-5 h-fit rounded-3xl">
|
||||||
<div className='text-sm'>
|
<div className="text-sm">{t("ticket.title_hint")}</div>
|
||||||
{t('ticket.title_hint')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className="mt-6">
|
||||||
<div className='flex items-start gap-2 border-b pb-5'>
|
<div className="flex items-start gap-2 border-b pb-5">
|
||||||
<div>
|
<div>
|
||||||
<TickSquare size={20} color='black' variant='Bold' />
|
<TickSquare
|
||||||
|
size={20}
|
||||||
|
color="black"
|
||||||
|
variant="Bold"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-description text-xs leading-5'>
|
<div className="text-description text-xs leading-5">سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.</div>
|
||||||
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-start gap-2 mt-6 border-b pb-5">
|
||||||
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
|
|
||||||
<div>
|
<div>
|
||||||
<TickSquare size={20} color='black' variant='Bold' />
|
<TickSquare
|
||||||
|
size={20}
|
||||||
|
color="black"
|
||||||
|
variant="Bold"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-description text-xs leading-5'>
|
<div className="text-description text-xs leading-5">لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.</div>
|
||||||
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-start gap-2 mt-6">
|
||||||
<div className='flex items-start gap-2 mt-6'>
|
|
||||||
<div>
|
<div>
|
||||||
<TickSquare size={20} color='black' variant='Bold' />
|
<TickSquare
|
||||||
|
size={20}
|
||||||
|
color="black"
|
||||||
|
variant="Bold"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-description text-xs leading-5'>
|
<div className="text-description text-xs leading-5">
|
||||||
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
|
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -472,9 +437,9 @@ const CreateReceipt: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default CreateReceipt
|
export default CreateReceipt;
|
||||||
|
|||||||
+263
-296
@@ -1,22 +1,22 @@
|
|||||||
import { ChangeEvent, FC, Fragment, useState, useEffect } from 'react'
|
import { useFormik } from "formik";
|
||||||
import { useTranslation } from 'react-i18next'
|
import { Add, TickCircle, TickSquare, Trash } from "iconsax-react";
|
||||||
import Button from '../../components/Button'
|
import { ChangeEvent, FC, Fragment, useEffect, useState } from "react";
|
||||||
import { Add, TickCircle, TickSquare, Trash } from 'iconsax-react'
|
import { useTranslation } from "react-i18next";
|
||||||
import Select from '../../components/Select'
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import Input from '../../components/Input'
|
import * as Yup from "yup";
|
||||||
import { CreateReceiptType, ReceiptCreateItemsType } from './types/ReceiptTypes'
|
import Button from "../../components/Button";
|
||||||
import { useFormik } from 'formik'
|
import Input from "../../components/Input";
|
||||||
import * as Yup from 'yup'
|
import PageLoading from "../../components/PageLoading";
|
||||||
import { toast } from '../../components/Toast';
|
import Select from "../../components/Select";
|
||||||
import { useGetCustomers } from '../customer/hooks/useCustomerData'
|
import SwitchComponent from "../../components/Switch";
|
||||||
import { CustomerItemType } from '../customer/types/CustomerTypes'
|
import { toast } from "../../components/Toast";
|
||||||
import PageLoading from '../../components/PageLoading'
|
import { Pages } from "../../config/Pages";
|
||||||
import { useGetInvoiceById, useUpdateInvoice } from './hooks/useReceiptData'
|
import { ErrorType } from "../../helpers/types";
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useGetCustomers } from "../customer/hooks/useCustomerData";
|
||||||
import { Pages } from '../../config/Pages'
|
import { CustomerItemType } from "../customer/types/CustomerTypes";
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { RecurringPeriodEnum } from "./enum/ReceipEnum";
|
||||||
import { RecurringPeriodEnum } from './enum/ReceipEnum'
|
import { useGetInvoiceById, useUpdateInvoice } from "./hooks/useReceiptData";
|
||||||
import SwitchComponent from '../../components/Switch'
|
import { CreateReceiptType, ReceiptCreateItemsType } from "./types/ReceiptTypes";
|
||||||
|
|
||||||
// Add interface for invoice item from API
|
// Add interface for invoice item from API
|
||||||
interface InvoiceItemType {
|
interface InvoiceItemType {
|
||||||
@@ -32,47 +32,49 @@ interface InvoiceItemType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CreateReceipt: FC = () => {
|
const CreateReceipt: FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const navigate = useNavigate()
|
const { id } = useParams();
|
||||||
const { id } = useParams()
|
const { t } = useTranslation("global");
|
||||||
const { t } = useTranslation('global')
|
const { data: invoice, isPending: isLoading } = useGetInvoiceById(id as string);
|
||||||
const { data: invoice, isPending: isLoading } = useGetInvoiceById(id as string)
|
const updateInvoice = useUpdateInvoice();
|
||||||
const updateInvoice = useUpdateInvoice()
|
const [items, setItems] = useState<ReceiptCreateItemsType[]>([]);
|
||||||
const [items, setItems] = useState<ReceiptCreateItemsType[]>([])
|
const [customer, setCustomer] = useState<CustomerItemType>();
|
||||||
const [customer, setCustomer] = useState<CustomerItemType>()
|
const [isRecurring, setIsRecurring] = useState<boolean>(false);
|
||||||
const [isRecurring, setIsRecurring] = useState<boolean>(false)
|
const [type, setType] = useState<RecurringPeriodEnum>(RecurringPeriodEnum.WEEKLY);
|
||||||
const [type, setType] = useState<RecurringPeriodEnum>(RecurringPeriodEnum.WEEKLY)
|
const [maxRecurringCycles, setMaxRecurringCycles] = useState<number>(0);
|
||||||
const [maxRecurringCycles, setMaxRecurringCycles] = useState<number>(0)
|
const getCustomers = useGetCustomers(1, 50, true);
|
||||||
const getCustomers = useGetCustomers(1, true)
|
|
||||||
|
|
||||||
const formik = useFormik<ReceiptCreateItemsType>({
|
const formik = useFormik<ReceiptCreateItemsType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: '',
|
name: "",
|
||||||
count: '',
|
count: "",
|
||||||
unitPrice: '',
|
unitPrice: "",
|
||||||
discount: ''
|
discount: "",
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
name: Yup.string().required(t('errors.required')),
|
name: Yup.string().required(t("errors.required")),
|
||||||
count: Yup.string().required(t('errors.required')),
|
count: Yup.string().required(t("errors.required")),
|
||||||
unitPrice: Yup.string().required(t('errors.required')),
|
unitPrice: Yup.string().required(t("errors.required")),
|
||||||
}),
|
}),
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
setItems((prev) => [...prev, {
|
setItems((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
count: +values.count,
|
count: +values.count,
|
||||||
discount: +values.discount,
|
discount: +values.discount,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
unitPrice: +values.unitPrice
|
unitPrice: +values.unitPrice,
|
||||||
}])
|
},
|
||||||
formik.resetForm()
|
]);
|
||||||
}
|
formik.resetForm();
|
||||||
})
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleChangeCustomer = (e: ChangeEvent<HTMLSelectElement>) => {
|
const handleChangeCustomer = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
const value = e.target.value
|
const value = e.target.value;
|
||||||
const customer = getCustomers.data?.data?.customers?.find((item: CustomerItemType) => item.id === value)
|
const customer = getCustomers.data?.data?.customers?.find((item: CustomerItemType) => item.id === value);
|
||||||
setCustomer(customer)
|
setCustomer(customer);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (customer) {
|
if (customer) {
|
||||||
@@ -81,28 +83,30 @@ const CreateReceipt: FC = () => {
|
|||||||
items: items,
|
items: items,
|
||||||
isRecurring: isRecurring,
|
isRecurring: isRecurring,
|
||||||
recurringPeriod: isRecurring ? +type : undefined,
|
recurringPeriod: isRecurring ? +type : undefined,
|
||||||
maxRecurringCycles: isRecurring ? maxRecurringCycles : undefined
|
maxRecurringCycles: isRecurring ? maxRecurringCycles : undefined,
|
||||||
}
|
};
|
||||||
|
|
||||||
updateInvoice.mutate({ id: id as string, params }, {
|
updateInvoice.mutate(
|
||||||
|
{ id: id as string, params },
|
||||||
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast(t('success'), 'success')
|
toast(t("success"), "success");
|
||||||
navigate(Pages.receipts.index)
|
navigate(Pages.receipts.index);
|
||||||
},
|
},
|
||||||
onError(error: ErrorType) {
|
onError(error: ErrorType) {
|
||||||
toast(error.response?.data?.error.message[0], 'error')
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleRemove = (index: number) => {
|
const handleRemove = (index: number) => {
|
||||||
setItems((prev) => prev.filter((_, i) => i !== index))
|
setItems((prev) => prev.filter((_, i) => i !== index));
|
||||||
}
|
};
|
||||||
|
|
||||||
// Prefill form when editing an invoice
|
// Prefill form when editing an invoice
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
// Check if we need to access invoice.data or directly invoice
|
// Check if we need to access invoice.data or directly invoice
|
||||||
const invoiceData = invoice?.data?.invoice || invoice;
|
const invoiceData = invoice?.data?.invoice || invoice;
|
||||||
|
|
||||||
@@ -111,9 +115,7 @@ const CreateReceipt: FC = () => {
|
|||||||
|
|
||||||
// 1. If there's a user, find matching customer
|
// 1. If there's a user, find matching customer
|
||||||
if (invoiceData.user && getCustomers.data?.data?.customers) {
|
if (invoiceData.user && getCustomers.data?.data?.customers) {
|
||||||
const foundCustomer = getCustomers.data.data.customers.find(
|
const foundCustomer = getCustomers.data.data.customers.find((item: CustomerItemType) => item.id === invoiceData.user.id);
|
||||||
(item: CustomerItemType) => item.id === invoiceData.user.id
|
|
||||||
);
|
|
||||||
setCustomer(foundCustomer);
|
setCustomer(foundCustomer);
|
||||||
console.log("Found customer:", foundCustomer);
|
console.log("Found customer:", foundCustomer);
|
||||||
setCustomer(foundCustomer);
|
setCustomer(foundCustomer);
|
||||||
@@ -123,20 +125,21 @@ const CreateReceipt: FC = () => {
|
|||||||
if (invoiceData.items && invoiceData.items.length > 0) {
|
if (invoiceData.items && invoiceData.items.length > 0) {
|
||||||
console.log("Setting items:", invoiceData.items);
|
console.log("Setting items:", invoiceData.items);
|
||||||
// Map API items to the format expected by our component
|
// Map API items to the format expected by our component
|
||||||
setItems(invoiceData.items.map((item: InvoiceItemType) => ({
|
setItems(
|
||||||
|
invoiceData.items.map((item: InvoiceItemType) => ({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
count: item.count,
|
count: item.count,
|
||||||
unitPrice: item.unitPrice,
|
unitPrice: item.unitPrice,
|
||||||
discount: item.discount || 0
|
discount: item.discount || 0,
|
||||||
})));
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
// 3. Set formik values from first item
|
// 3. Set formik values from first item
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Set recurring settings
|
// 4. Set recurring settings
|
||||||
setIsRecurring(Boolean(invoiceData.isRecurring));
|
setIsRecurring(Boolean(invoiceData.isRecurring));
|
||||||
if (typeof invoiceData.recurringPeriod === 'number') {
|
if (typeof invoiceData.recurringPeriod === "number") {
|
||||||
setType(invoiceData.recurringPeriod.toString());
|
setType(invoiceData.recurringPeriod.toString());
|
||||||
}
|
}
|
||||||
setMaxRecurringCycles(invoiceData.maxRecurringCycles || 0);
|
setMaxRecurringCycles(invoiceData.maxRecurringCycles || 0);
|
||||||
@@ -149,8 +152,8 @@ const CreateReceipt: FC = () => {
|
|||||||
// Trigger the onChange manually with a synthetic event
|
// Trigger the onChange manually with a synthetic event
|
||||||
const event = {
|
const event = {
|
||||||
target: {
|
target: {
|
||||||
value: invoice.user.id
|
value: invoice.user.id,
|
||||||
}
|
},
|
||||||
} as ChangeEvent<HTMLSelectElement>;
|
} as ChangeEvent<HTMLSelectElement>;
|
||||||
|
|
||||||
handleChangeCustomer(event);
|
handleChangeCustomer(event);
|
||||||
@@ -166,387 +169,351 @@ const CreateReceipt: FC = () => {
|
|||||||
}, [customer, invoice]);
|
}, [customer, invoice]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4'>
|
<div className="mt-4">
|
||||||
{
|
{getCustomers.isPending || isLoading ? (
|
||||||
getCustomers.isPending || isLoading ?
|
|
||||||
<PageLoading />
|
<PageLoading />
|
||||||
:
|
) : (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className='flex justify-between items-center'>
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>{t("receip.add_receip")}</div>
|
||||||
{t('receip.add_receip')}
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className='px-5'
|
className="px-5"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
isLoading={updateInvoice.isPending}
|
isLoading={updateInvoice.isPending}
|
||||||
>
|
>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className="flex gap-2 items-center">
|
||||||
<TickCircle size={20} color='white' />
|
<TickCircle
|
||||||
<div>
|
size={20}
|
||||||
{t('receip.submit_receip')}
|
color="white"
|
||||||
</div>
|
/>
|
||||||
|
<div>{t("receip.submit_receip")}</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex gap-6 xl:mt-8 mt-4'>
|
<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 className="flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl">
|
||||||
<div>
|
<div>{t("receip.customer_information")}</div>
|
||||||
{t('receip.customer_information')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className="mt-8">
|
||||||
<div className='w-full xl:w-1/2'>
|
<div className="w-full xl:w-1/2">
|
||||||
<Select
|
<Select
|
||||||
label={t('receip.customer')}
|
label={t("receip.customer")}
|
||||||
placeholder={t('select')}
|
placeholder={t("select")}
|
||||||
value={(invoice?.data?.invoice || invoice)?.user?.id}
|
value={(invoice?.data?.invoice || invoice)?.user?.id}
|
||||||
items={getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
|
items={getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
|
||||||
return {
|
return {
|
||||||
label: item.firstName + ' ' + item.lastName,
|
label: item.firstName + " " + item.lastName,
|
||||||
value: item.id
|
value: item.id,
|
||||||
}
|
};
|
||||||
})}
|
})}
|
||||||
onChange={handleChangeCustomer}
|
onChange={handleChangeCustomer}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{customer && (
|
||||||
customer &&
|
<div className="mt-10 bg-[#F6F7FB] p-8 rounded-3xl text-sm">
|
||||||
<div className='mt-10 bg-[#F6F7FB] p-8 rounded-3xl text-sm'>
|
<div className="flex justify-between items-center border-b border-border border-dashed pb-7">
|
||||||
<div className='flex justify-between items-center border-b border-border border-dashed pb-7'>
|
<div className="flex items-center gap-2">
|
||||||
<div className='flex items-center gap-2'>
|
<div className="text-description">{t("receip.type_person")}</div>
|
||||||
<div className='text-description'>{t('receip.type_person')}</div>
|
<div>{customer.legalUser ? "حقوقی" : "حقیقی"}</div>
|
||||||
<div>{customer.legalUser ? 'حقوقی' : 'حقیقی'}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className="flex items-center gap-2">
|
||||||
<div className='text-description'>{t('receip.representativeـname')}</div>
|
<div className="text-description">{t("receip.representativeـname")}</div>
|
||||||
<div>{customer.firstName + ' ' + customer.lastName}</div>
|
<div>{customer.firstName + " " + customer.lastName}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className="flex items-center gap-2">
|
||||||
<div className='text-description'>{t('receip.company_name')}</div>
|
<div className="text-description">{t("receip.company_name")}</div>
|
||||||
<div>{customer?.legalUser?.registrationName}</div>
|
<div>{customer?.legalUser?.registrationName}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className="flex items-center gap-2">
|
||||||
<div className='text-description'>{t('receip.registration_number')}</div>
|
<div className="text-description">{t("receip.registration_number")}</div>
|
||||||
<div>{customer?.legalUser?.registrationCode}</div>
|
<div>{customer?.legalUser?.registrationCode}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-between items-center mt-7'>
|
<div className="flex justify-between items-center mt-7">
|
||||||
<div className='flex items-center gap-2 flex-1'>
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<div className='text-description'>{t('receip.tel_company')}</div>
|
<div className="text-description">{t("receip.tel_company")}</div>
|
||||||
<div>{customer?.realUser?.phone || customer?.legalUser?.phone}</div>
|
<div>{customer?.realUser?.phone || customer?.legalUser?.phone}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2 flex-1'>
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<div className='text-description'>{t('receip.national_code')}</div>
|
<div className="text-description">{t("receip.national_code")}</div>
|
||||||
<div>{customer.nationalCode}</div>
|
<div>{customer.nationalCode}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2 flex-1'>
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<div className='text-description'>{t('receip.postal_code')}</div>
|
<div className="text-description">{t("receip.postal_code")}</div>
|
||||||
<div>{customer?.realUser?.address?.postalCode || customer?.legalUser?.address?.postalCode}</div>
|
<div>{customer?.realUser?.address?.postalCode || customer?.legalUser?.address?.postalCode}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-between items-center mt-7'>
|
<div className="flex justify-between items-center mt-7">
|
||||||
<div className='flex items-center gap-2 flex-1'>
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<div className='text-description'>{t('receip.state')}</div>
|
<div className="text-description">{t("receip.state")}</div>
|
||||||
<div>{customer?.realUser?.address?.city?.province?.name || customer?.legalUser?.address?.city?.province?.name}</div>
|
<div>{customer?.realUser?.address?.city?.province?.name || customer?.legalUser?.address?.city?.province?.name}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2 flex-1'>
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<div className='text-description'>{t('receip.city')}</div>
|
<div className="text-description">{t("receip.city")}</div>
|
||||||
<div>{customer?.realUser?.address?.city?.name || customer?.legalUser?.address?.city?.name}</div>
|
<div>{customer?.realUser?.address?.city?.name || customer?.legalUser?.address?.city?.name}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2 flex-1'>
|
<div className="flex items-center gap-2 flex-1">
|
||||||
<div className='text-description'>{t('receip.address')}</div>
|
<div className="text-description">{t("receip.address")}</div>
|
||||||
<div>{customer?.realUser?.address?.fullAddress || customer?.legalUser?.address?.fullAddress}</div>
|
<div>{customer?.realUser?.address?.fullAddress || customer?.legalUser?.address?.fullAddress}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<div className='mt-10'>
|
<div className="mt-10">
|
||||||
<SwitchComponent
|
<SwitchComponent
|
||||||
label={t('receip.recurring')}
|
label={t("receip.recurring")}
|
||||||
active={isRecurring}
|
active={isRecurring}
|
||||||
onChange={(value) => setIsRecurring(value)}
|
onChange={(value) => setIsRecurring(value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{isRecurring && (
|
||||||
isRecurring &&
|
<div className="mt-10">
|
||||||
<div className='mt-10'>
|
<div>{t("receip.select_type")}</div>
|
||||||
<div>
|
|
||||||
{t('receip.select_type')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-4 rowTwoInput'>
|
<div className="mt-4 rowTwoInput">
|
||||||
<Select
|
<Select
|
||||||
label={t('select')}
|
label={t("select")}
|
||||||
items={[
|
items={[
|
||||||
{ label: t('receip.WEEKLY'), value: RecurringPeriodEnum.WEEKLY.toString() },
|
{ label: t("receip.WEEKLY"), value: RecurringPeriodEnum.WEEKLY.toString() },
|
||||||
{ label: t('receip.MONTHLY'), value: RecurringPeriodEnum.MONTHLY.toString() },
|
{ label: t("receip.MONTHLY"), value: RecurringPeriodEnum.MONTHLY.toString() },
|
||||||
{ label: t('receip.QUARTERLY'), value: RecurringPeriodEnum.QUARTERLY.toString() },
|
{ label: t("receip.QUARTERLY"), value: RecurringPeriodEnum.QUARTERLY.toString() },
|
||||||
{ label: t('receip.SEMIANNUALLY'), value: RecurringPeriodEnum.SEMIANNUALLY.toString() },
|
{ label: t("receip.SEMIANNUALLY"), value: RecurringPeriodEnum.SEMIANNUALLY.toString() },
|
||||||
{ label: t('receip.ANNUALLY'), value: RecurringPeriodEnum.ANNUALLY.toString() }
|
{ label: t("receip.ANNUALLY"), value: RecurringPeriodEnum.ANNUALLY.toString() },
|
||||||
]}
|
]}
|
||||||
value={type}
|
value={type}
|
||||||
onChange={(e) => setType(e.target.value as unknown as RecurringPeriodEnum)}
|
onChange={(e) => setType(e.target.value as unknown as RecurringPeriodEnum)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label={t('receip.maxRecurringCycles')}
|
label={t("receip.maxRecurringCycles")}
|
||||||
name='maxRecurringCycles'
|
name="maxRecurringCycles"
|
||||||
value={maxRecurringCycles}
|
value={maxRecurringCycles}
|
||||||
onChange={(e) => setMaxRecurringCycles(+e.target.value)}
|
onChange={(e) => setMaxRecurringCycles(+e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<div className='mt-10'>
|
<div className="mt-10">
|
||||||
<div>{t('receip.receipt_information')}</div>
|
<div>{t("receip.receipt_information")}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-8 border-b border-dashed border-border pb-7 flex items-end justify-between text-xs text-description'>
|
<div className="mt-8 border-b border-dashed border-border pb-7 flex items-end justify-between text-xs text-description">
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<div>
|
<div>{t("receip.number")}</div>
|
||||||
{t('receip.number')}
|
<div className="size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center">{items.length + 1}</div>
|
||||||
</div>
|
|
||||||
<div className='size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center'>
|
|
||||||
{items.length + 1}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<div>
|
<div>{t("receip.product_name")}</div>
|
||||||
{t('receip.product_name')}
|
|
||||||
</div>
|
|
||||||
<Input
|
<Input
|
||||||
className='text-center'
|
className="text-center"
|
||||||
name='name'
|
name="name"
|
||||||
value={formik.values.name}
|
value={formik.values.name}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<div>
|
<div>{t("receip.count")}</div>
|
||||||
{t('receip.count')}
|
|
||||||
</div>
|
|
||||||
<Input
|
<Input
|
||||||
type='number'
|
type="number"
|
||||||
className='w-16 text-center'
|
className="w-16 text-center"
|
||||||
name='count'
|
name="count"
|
||||||
value={formik.values.count}
|
value={formik.values.count}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<div>
|
<div>{t("receip.unit_amount")}</div>
|
||||||
{t('receip.unit_amount')}
|
|
||||||
</div>
|
|
||||||
<Input
|
<Input
|
||||||
className='text-center'
|
className="text-center"
|
||||||
name='unitPrice'
|
name="unitPrice"
|
||||||
value={formik.values.unitPrice}
|
value={formik.values.unitPrice}
|
||||||
onChange={(e) => formik.setFieldValue('unitPrice', e.target.value)}
|
onChange={(e) => formik.setFieldValue("unitPrice", e.target.value)}
|
||||||
seprator
|
seprator
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<div>
|
<div>{t("receip.discount")}</div>
|
||||||
{t('receip.discount')}
|
|
||||||
</div>
|
|
||||||
<Input
|
<Input
|
||||||
type='number'
|
type="number"
|
||||||
className='w-16 text-center'
|
className="w-16 text-center"
|
||||||
name='discount'
|
name="discount"
|
||||||
value={formik.values.discount}
|
value={formik.values.discount}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<div>
|
<div>{t("receip.total_amount")}</div>
|
||||||
{t('receip.total_amount')}
|
|
||||||
</div>
|
|
||||||
<Input
|
<Input
|
||||||
className='text-center bg-[#EBEDF5]'
|
className="text-center bg-[#EBEDF5]"
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div onClick={() => {
|
<div
|
||||||
|
onClick={() => {
|
||||||
if (formik.errors.name || formik.errors.count || formik.errors.unitPrice || formik.errors.discount) {
|
if (formik.errors.name || formik.errors.count || formik.errors.unitPrice || formik.errors.discount) {
|
||||||
toast(t('receip.error_empty'), 'error')
|
toast(t("receip.error_empty"), "error");
|
||||||
} else {
|
} else {
|
||||||
formik.handleSubmit()
|
formik.handleSubmit();
|
||||||
}
|
}
|
||||||
}} className='size-10 border border-border rounded-xl flex justify-center items-center'>
|
}}
|
||||||
<Add size={20} color='black' />
|
className="size-10 border border-border rounded-xl flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<Add
|
||||||
|
size={20}
|
||||||
|
color="black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{items.map((item, index: number) => {
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
items.map((item, index: number) => {
|
|
||||||
return (
|
return (
|
||||||
<div key={item.name} className='mt-6 flex items-end justify-between text-xs text-description'>
|
<div
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
key={item.name}
|
||||||
{
|
className="mt-6 flex items-end justify-between text-xs text-description"
|
||||||
index === 0 &&
|
>
|
||||||
<div>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
{t('receip.number')}
|
{index === 0 && <div>{t("receip.number")}</div>}
|
||||||
|
<div className="size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center">{index + 1}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<div className='size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center'>
|
{index === 0 && <div>{t("receip.product_name")}</div>}
|
||||||
{index + 1}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
|
||||||
{
|
|
||||||
index === 0 &&
|
|
||||||
<div>
|
|
||||||
{t('receip.product_name')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Input
|
<Input
|
||||||
className='text-center'
|
className="text-center"
|
||||||
name='name'
|
name="name"
|
||||||
readOnly
|
readOnly
|
||||||
value={item.name}
|
value={item.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
{
|
{index === 0 && <div>{t("receip.count")}</div>}
|
||||||
index === 0 &&
|
|
||||||
<div>
|
|
||||||
{t('receip.count')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Input
|
<Input
|
||||||
type='number'
|
type="number"
|
||||||
className='w-16 text-center'
|
className="w-16 text-center"
|
||||||
name='count'
|
name="count"
|
||||||
readOnly
|
readOnly
|
||||||
value={item.count}
|
value={item.count}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
{
|
{index === 0 && <div>{t("receip.unit_amount")}</div>}
|
||||||
index === 0 &&
|
|
||||||
<div>
|
|
||||||
{t('receip.unit_amount')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Input
|
<Input
|
||||||
className='text-center'
|
className="text-center"
|
||||||
name='unitPrice'
|
name="unitPrice"
|
||||||
readOnly
|
readOnly
|
||||||
value={item.unitPrice}
|
value={item.unitPrice}
|
||||||
seprator
|
seprator
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
{
|
{index === 0 && <div>{t("receip.discount")}</div>}
|
||||||
index === 0 &&
|
|
||||||
<div>
|
|
||||||
{t('receip.discount')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Input
|
<Input
|
||||||
type='number'
|
type="number"
|
||||||
className='w-16 text-center'
|
className="w-16 text-center"
|
||||||
name='discount'
|
name="discount"
|
||||||
readOnly
|
readOnly
|
||||||
value={item.discount}
|
value={item.discount}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-2 items-center'>
|
<div className="flex flex-col gap-2 items-center">
|
||||||
{
|
{index === 0 && <div>{t("receip.total_amount")}</div>}
|
||||||
index === 0 &&
|
|
||||||
<div>
|
|
||||||
{t('receip.total_amount')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Input
|
<Input
|
||||||
className='text-center bg-[#EBEDF5]'
|
className="text-center bg-[#EBEDF5]"
|
||||||
readOnly
|
readOnly
|
||||||
value={((Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0)).toFixed(1)}
|
value={((Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0)).toFixed(1)}
|
||||||
seprator
|
seprator
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div onClick={() => handleRemove(index)} className='size-10 border border-border rounded-xl flex justify-center items-center'>
|
<div
|
||||||
<Trash size={20} color='black' />
|
onClick={() => handleRemove(index)}
|
||||||
|
className="size-10 border border-border rounded-xl flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<Trash
|
||||||
|
size={20}
|
||||||
|
color="black"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-col gap-4 items-end">
|
||||||
|
<div className="xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center">
|
||||||
<div className='mt-8 flex flex-col gap-4 items-end'>
|
<div>{t("receip.tax")}</div>
|
||||||
<div className='xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center'>
|
<div className="text-black">
|
||||||
<div>
|
|
||||||
{t('receip.tax')}
|
|
||||||
</div>
|
|
||||||
<div className='text-black'>
|
|
||||||
{(items.reduce((acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0), 0) * 0.10).toFixed(1)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center'>
|
|
||||||
<div>
|
|
||||||
{t('receip.total')}
|
|
||||||
</div>
|
|
||||||
<div className='text-black'>
|
|
||||||
{(
|
{(
|
||||||
items.reduce((acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0), 0) * 1.10
|
items.reduce(
|
||||||
|
(acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0),
|
||||||
|
0,
|
||||||
|
) * 0.1
|
||||||
|
).toFixed(1)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center">
|
||||||
|
<div>{t("receip.total")}</div>
|
||||||
|
<div className="text-black">
|
||||||
|
{(
|
||||||
|
items.reduce(
|
||||||
|
(acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0),
|
||||||
|
0,
|
||||||
|
) * 1.1
|
||||||
).toFixed(1)}
|
).toFixed(1)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='bg-white w-sidebar 3xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
<div className="bg-white w-sidebar 3xl:block hidden py-10 px-5 h-fit rounded-3xl">
|
||||||
<div className='text-sm'>
|
<div className="text-sm">{t("ticket.title_hint")}</div>
|
||||||
{t('ticket.title_hint')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className="mt-6">
|
||||||
<div className='flex items-start gap-2 border-b pb-5'>
|
<div className="flex items-start gap-2 border-b pb-5">
|
||||||
<div>
|
<div>
|
||||||
<TickSquare size={20} color='black' variant='Bold' />
|
<TickSquare
|
||||||
|
size={20}
|
||||||
|
color="black"
|
||||||
|
variant="Bold"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-description text-xs leading-5'>
|
<div className="text-description text-xs leading-5">سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.</div>
|
||||||
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-start gap-2 mt-6 border-b pb-5">
|
||||||
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
|
|
||||||
<div>
|
<div>
|
||||||
<TickSquare size={20} color='black' variant='Bold' />
|
<TickSquare
|
||||||
|
size={20}
|
||||||
|
color="black"
|
||||||
|
variant="Bold"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-description text-xs leading-5'>
|
<div className="text-description text-xs leading-5">لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.</div>
|
||||||
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-start gap-2 mt-6">
|
||||||
<div className='flex items-start gap-2 mt-6'>
|
|
||||||
<div>
|
<div>
|
||||||
<TickSquare size={20} color='black' variant='Bold' />
|
<TickSquare
|
||||||
|
size={20}
|
||||||
|
color="black"
|
||||||
|
variant="Bold"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-description text-xs leading-5'>
|
<div className="text-description text-xs leading-5">
|
||||||
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
|
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -554,9 +521,9 @@ const CreateReceipt: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default CreateReceipt
|
export default CreateReceipt;
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ import { Pages } from '../../config/Pages'
|
|||||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const ReceiptsList: FC = () => {
|
const ReceiptsList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canUpdate, canDelete } = usePermissions()
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const [activeTab, setActiveTab] = useState<InvoiceStatus>('PENDING')
|
const [activeTab, setActiveTab] = useState<InvoiceStatus>('PENDING')
|
||||||
@@ -177,13 +179,17 @@ const ReceiptsList: FC = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
|
{canUpdate('invoices') &&
|
||||||
<Link to={Pages.receipts.detail + item.id}>
|
<Link to={Pages.receipts.detail + item.id}>
|
||||||
<Edit size={20} color='#888' />
|
<Edit size={20} color='#888' />
|
||||||
</Link>
|
</Link>
|
||||||
|
}
|
||||||
|
{canDelete('invoices') &&
|
||||||
<TrashWithConfrim
|
<TrashWithConfrim
|
||||||
onDelete={() => handleDeleteInvoice(item.id)}
|
onDelete={() => handleDeleteInvoice(item.id)}
|
||||||
isLoading={deleteInvoice.isPending}
|
isLoading={deleteInvoice.isPending}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import { useGetResellers } from './hooks/useResellerData'
|
|||||||
import PageLoading from '../../components/PageLoading'
|
import PageLoading from '../../components/PageLoading'
|
||||||
import Td from '../../components/Td'
|
import Td from '../../components/Td'
|
||||||
import { ResellerItemType } from './types/Types'
|
import { ResellerItemType } from './types/Types'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const ResellerList: FC = () => {
|
const ResellerList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate } = usePermissions()
|
||||||
const getReseller = useGetResellers()
|
const getReseller = useGetResellers()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -21,6 +23,7 @@ const ResellerList: FC = () => {
|
|||||||
{t('reseller.list')}
|
{t('reseller.list')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{canCreate('reseller') && (
|
||||||
<Link to={Pages.reseller.create}>
|
<Link to={Pages.reseller.create}>
|
||||||
<Button
|
<Button
|
||||||
className='w-[172px]'
|
className='w-[172px]'
|
||||||
@@ -33,6 +36,7 @@ const ResellerList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ import StatusCategory from './components/StatusCategory'
|
|||||||
import Pagination from '../../components/Pagination'
|
import Pagination from '../../components/Pagination'
|
||||||
import UpdateCategory from './components/UpdateCategory'
|
import UpdateCategory from './components/UpdateCategory'
|
||||||
import { Trash } from 'iconsax-react'
|
import { Trash } from 'iconsax-react'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
|
|
||||||
const Category: FC = () => {
|
const Category: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const [isActive, setIsActive] = useState<'active' | 'deactive' | ''>('')
|
const [isActive, setIsActive] = useState<'active' | 'deactive' | ''>('')
|
||||||
@@ -102,12 +104,16 @@ const Category: FC = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
|
{canUpdate('services') && (
|
||||||
<UpdateCategory refetch={() => getCategory.refetch()} id={item.id} />
|
<UpdateCategory refetch={() => getCategory.refetch()} id={item.id} />
|
||||||
|
)}
|
||||||
|
{canDelete('services') && (
|
||||||
<Trash
|
<Trash
|
||||||
size={20}
|
size={20}
|
||||||
color='#888888'
|
color='#888888'
|
||||||
onClick={() => handleDelete(item.id)}
|
onClick={() => handleDelete(item.id)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -130,7 +136,7 @@ const Category: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CreateCategory />
|
{canCreate('services') && <CreateCategory />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,10 +15,12 @@ import Pagination from '../../components/Pagination'
|
|||||||
import ToggleStatusService from './components/ToggleStatusService'
|
import ToggleStatusService from './components/ToggleStatusService'
|
||||||
import CheckBoxComponent from '../../components/CheckBoxComponent'
|
import CheckBoxComponent from '../../components/CheckBoxComponent'
|
||||||
import ToggleSliderService from './components/ToggleSliderService'
|
import ToggleSliderService from './components/ToggleSliderService'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const ListService: FC = () => {
|
const ListService: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate } = usePermissions()
|
||||||
const [category, setCategory] = useState<string>('')
|
const [category, setCategory] = useState<string>('')
|
||||||
const [status, setstatus] = useState<'ACTIVE' | 'IN_ACTIVE' | ''>('')
|
const [status, setstatus] = useState<'ACTIVE' | 'IN_ACTIVE' | ''>('')
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
@@ -33,6 +35,7 @@ const ListService: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('service.list_service')}
|
{t('service.list_service')}
|
||||||
</div>
|
</div>
|
||||||
|
{canCreate('services') && (
|
||||||
<div>
|
<div>
|
||||||
<Link to={Pages.services.add}>
|
<Link to={Pages.services.add}>
|
||||||
<Button
|
<Button
|
||||||
@@ -48,6 +51,7 @@ const ListService: FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-between items-center mt-12'>
|
<div className='flex justify-between items-center mt-12'>
|
||||||
@@ -134,6 +138,7 @@ const ListService: FC = () => {
|
|||||||
<Td text=''>
|
<Td text=''>
|
||||||
{
|
{
|
||||||
item.subscriptionCount === 0 ?
|
item.subscriptionCount === 0 ?
|
||||||
|
(canCreate('services') &&
|
||||||
<Link to={Pages.services.plan + item.id}>
|
<Link to={Pages.services.plan + item.id}>
|
||||||
<Button
|
<Button
|
||||||
className='h-8 bg-transparent border border-black text-black text-xs'
|
className='h-8 bg-transparent border border-black text-black text-xs'
|
||||||
@@ -144,6 +149,7 @@ const ListService: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
)
|
||||||
:
|
:
|
||||||
<Link to={Pages.services.plan + item.id} className='flex gap-1 text-sm text-[#0047FF]'>
|
<Link to={Pages.services.plan + item.id} className='flex gap-1 text-sm text-[#0047FF]'>
|
||||||
<div>{item.subscriptionCount}</div>
|
<div>{item.subscriptionCount}</div>
|
||||||
@@ -164,9 +170,11 @@ const ListService: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
|
{canUpdate('services') && (
|
||||||
<Link to={Pages.services.update + item.id}>
|
<Link to={Pages.services.update + item.id}>
|
||||||
<Eye size={20} color='#8C90A3' />
|
<Eye size={20} color='#8C90A3' />
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<Link to={Pages.services.guide + item.id}>
|
<Link to={Pages.services.guide + item.id}>
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import { PlanItemType } from './types/ServiceTypes'
|
|||||||
import ToggleStatusPlan from './components/ToggleStatusPlan'
|
import ToggleStatusPlan from './components/ToggleStatusPlan'
|
||||||
import EditPlan from './components/EditPlan'
|
import EditPlan from './components/EditPlan'
|
||||||
import { NumberFormat } from '../../config/func'
|
import { NumberFormat } from '../../config/func'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const Plans: FC = () => {
|
const Plans: FC = () => {
|
||||||
|
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate } = usePermissions()
|
||||||
const getPlans = useGetPlans(id)
|
const getPlans = useGetPlans(id)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -27,7 +29,7 @@ const Plans: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('plan.plans_list') + ' ' + getPlans.data?.data?.service?.name}
|
{t('plan.plans_list') + ' ' + getPlans.data?.data?.service?.name}
|
||||||
</div>
|
</div>
|
||||||
<CreatePlan />
|
{canCreate('services') && <CreatePlan />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||||
@@ -54,9 +56,11 @@ const Plans: FC = () => {
|
|||||||
<ToggleStatusPlan defaultActive={item.isActive} id={item.id} />
|
<ToggleStatusPlan defaultActive={item.isActive} id={item.id} />
|
||||||
</Td>
|
</Td>
|
||||||
<Td text=''>
|
<Td text=''>
|
||||||
|
{canUpdate('services') && (
|
||||||
<EditPlan
|
<EditPlan
|
||||||
id={item.id}
|
id={item.id}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ import PageLoading from '../../components/PageLoading'
|
|||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const SliderList: FC = () => {
|
const SliderList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const { data: sliders, isPending, refetch } = useGetSliders()
|
const { data: sliders, isPending, refetch } = useGetSliders()
|
||||||
const deleteSlider = useDeleteSlider()
|
const deleteSlider = useDeleteSlider()
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
deleteSlider.mutate(id, {
|
deleteSlider.mutate(id, {
|
||||||
@@ -35,6 +37,7 @@ const SliderList: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('slider.list_slider')}
|
{t('slider.list_slider')}
|
||||||
</div>
|
</div>
|
||||||
|
{canCreate('sliders') && (
|
||||||
<div>
|
<div>
|
||||||
<Link to={Pages.sliders.create}>
|
<Link to={Pages.sliders.create}>
|
||||||
<Button
|
<Button
|
||||||
@@ -50,6 +53,7 @@ const SliderList: FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -80,16 +84,20 @@ const SliderList: FC = () => {
|
|||||||
<Td text={item.isActive ? t('slider.active') : t('slider.inactive')} />
|
<Td text={item.isActive ? t('slider.active') : t('slider.inactive')} />
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
|
{canUpdate('sliders') && (
|
||||||
<Link to={Pages.sliders.detail + item.id}>
|
<Link to={Pages.sliders.detail + item.id}>
|
||||||
<Edit
|
<Edit
|
||||||
className='size-5'
|
className='size-5'
|
||||||
color='#888'
|
color='#888'
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
|
{canDelete('sliders') && (
|
||||||
<TrashWithConfrim
|
<TrashWithConfrim
|
||||||
onDelete={() => handleDelete(item.id)}
|
onDelete={() => handleDelete(item.id)}
|
||||||
isLoading={deleteSlider.isPending}
|
isLoading={deleteSlider.isPending}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ import { ServiceItemType } from '../service/types/ServiceTypes'
|
|||||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const SubscriptionsList: FC = () => {
|
const SubscriptionsList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canDelete } = usePermissions()
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const [status, setStatus] = useState<string>('')
|
const [status, setStatus] = useState<string>('')
|
||||||
const [serviceId, setServiceId] = useState<string>('')
|
const [serviceId, setServiceId] = useState<string>('')
|
||||||
@@ -199,10 +201,12 @@ const SubscriptionsList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td text=''>
|
<Td text=''>
|
||||||
|
{canDelete('customers') && (
|
||||||
<TrashWithConfrim
|
<TrashWithConfrim
|
||||||
onDelete={() => handleDelete(item.id)}
|
onDelete={() => handleDelete(item.id)}
|
||||||
isLoading={isPending}
|
isLoading={isPending}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import { Link } from 'react-router-dom'
|
|||||||
import { Pages } from '../../config/Pages'
|
import { Pages } from '../../config/Pages'
|
||||||
import Delete from './components/Delete'
|
import Delete from './components/Delete'
|
||||||
import Button from '../../components/Button'
|
import Button from '../../components/Button'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const SupportList: FC = () => {
|
const SupportList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
const { data, refetch } = useGetPlans()
|
const { data, refetch } = useGetPlans()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -20,6 +22,7 @@ const SupportList: FC = () => {
|
|||||||
{t('support.plans')}
|
{t('support.plans')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{canCreate('support_plan') &&
|
||||||
<Link to={Pages.support.create}>
|
<Link to={Pages.support.create}>
|
||||||
<Button
|
<Button
|
||||||
className='w-fit px-6'
|
className='w-fit px-6'
|
||||||
@@ -32,6 +35,7 @@ const SupportList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||||
@@ -65,10 +69,14 @@ const SupportList: FC = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
|
{canUpdate('support_plan') &&
|
||||||
<Link to={Pages.support.detail + item.id}>
|
<Link to={Pages.support.detail + item.id}>
|
||||||
<Edit size={20} color='#8C90A3' />
|
<Edit size={20} color='#8C90A3' />
|
||||||
</Link>
|
</Link>
|
||||||
|
}
|
||||||
|
{canDelete('support_plan') &&
|
||||||
<Delete id={item.id} refetch={refetch} />
|
<Delete id={item.id} refetch={refetch} />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { clx } from "../../../helpers/utils";
|
|||||||
import { useCreateTaskPhase } from "../task-phase/hooks/useTaskPhaseData";
|
import { useCreateTaskPhase } from "../task-phase/hooks/useTaskPhaseData";
|
||||||
import type { TaskPhaseItemType } from "../task-phase/types/TaskPhaseTypes";
|
import type { TaskPhaseItemType } from "../task-phase/types/TaskPhaseTypes";
|
||||||
import { LABEL_COLOR_OPTIONS } from "./task-detail/labels/constants";
|
import { LABEL_COLOR_OPTIONS } from "./task-detail/labels/constants";
|
||||||
|
import { usePermissions } from "../../../hooks/usePermissions";
|
||||||
|
|
||||||
const DEFAULT_COLUMN_COLOR = "#4F46E5";
|
const DEFAULT_COLUMN_COLOR = "#4F46E5";
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ type Props = {
|
|||||||
|
|
||||||
const AddNewColumn: FC<Props> = ({ projectId, nextOrder, onColumnCreated }) => {
|
const AddNewColumn: FC<Props> = ({ projectId, nextOrder, onColumnCreated }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const { canCreate } = usePermissions();
|
||||||
const colorInputRef = useRef<HTMLInputElement>(null);
|
const colorInputRef = useRef<HTMLInputElement>(null);
|
||||||
const createTaskPhase = useCreateTaskPhase();
|
const createTaskPhase = useCreateTaskPhase();
|
||||||
|
|
||||||
@@ -57,6 +59,10 @@ const AddNewColumn: FC<Props> = ({ projectId, nextOrder, onColumnCreated }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!canCreate("task_phase")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAdding) {
|
if (!isAdding) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { toast } from '../../../components/Toast';
|
import { toast } from '../../../components/Toast';
|
||||||
import ModalConfrim from "../../../components/ModalConfrim";
|
import ModalConfrim from "../../../components/ModalConfrim";
|
||||||
import { ErrorType } from "../../../helpers/types";
|
import { ErrorType } from "../../../helpers/types";
|
||||||
|
import { usePermissions } from "../../../hooks/usePermissions";
|
||||||
import { useCreateTask, useGetTasksByTaskPhase } from "../task/hooks/useTaskData";
|
import { useCreateTask, useGetTasksByTaskPhase } from "../task/hooks/useTaskData";
|
||||||
import type { TaskItemType } from "../task/types/TaskTypes";
|
import type { TaskItemType } from "../task/types/TaskTypes";
|
||||||
import { mapTaskItemToTask } from "../task/utils/mapTaskItemToTask";
|
import { mapTaskItemToTask } from "../task/utils/mapTaskItemToTask";
|
||||||
@@ -37,6 +38,7 @@ const Column: FC<Props> = ({
|
|||||||
onColumnTitleUpdated,
|
onColumnTitleUpdated,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const { canCreate } = usePermissions();
|
||||||
const deleteTaskPhase = useDeleteTaskPhase();
|
const deleteTaskPhase = useDeleteTaskPhase();
|
||||||
const updateTaskPhase = useUpdateTaskPhase();
|
const updateTaskPhase = useUpdateTaskPhase();
|
||||||
const createTask = useCreateTask();
|
const createTask = useCreateTask();
|
||||||
@@ -52,7 +54,7 @@ const Column: FC<Props> = ({
|
|||||||
const items = tasksData?.pages.flatMap((page) => page.data?.items ?? []) ?? [];
|
const items = tasksData?.pages.flatMap((page) => page.data?.items ?? []) ?? [];
|
||||||
|
|
||||||
return [...items]
|
return [...items]
|
||||||
.sort((a, b) => (a.order ?? a.priority ?? 0) - (b.order ?? b.priority ?? 0))
|
.sort((a, b) => (a.priority ?? a.order ?? 0) - (b.priority ?? b.order ?? 0))
|
||||||
.map((task, index) => mapTaskItemToTask(task as TaskItemType, column.id, index));
|
.map((task, index) => mapTaskItemToTask(task as TaskItemType, column.id, index));
|
||||||
}, [tasksData?.pages, column.id]);
|
}, [tasksData?.pages, column.id]);
|
||||||
|
|
||||||
@@ -220,8 +222,8 @@ const Column: FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={columnRef}
|
ref={columnRef}
|
||||||
className={`bg-[#F0F3F7] rounded-xl p-3 sm:p-4 xl:p-6 w-[272px] sm:w-[288px] xl:w-[310px] shrink-0 flex flex-col h-full min-h-0 transition-colors ${
|
className={`bg-[#F0F3F7] rounded-xl p-3 sm:p-4 xl:p-6 w-[272px] sm:w-[288px] xl:w-[310px] shrink-0 flex flex-col h-full min-h-0 min-w-0 overflow-hidden transition-colors ${
|
||||||
isColumnDragOver ? "ring-2 ring-black/30" : ""
|
isColumnDragOver ? "ring-2 ring-black/30 ring-inset" : ""
|
||||||
} ${isDraggingColumn ? "opacity-50" : ""}`}
|
} ${isDraggingColumn ? "opacity-50" : ""}`}
|
||||||
onDragOver={handleColumnDragOver}
|
onDragOver={handleColumnDragOver}
|
||||||
onDragLeave={handleColumnDragLeave}
|
onDragLeave={handleColumnDragLeave}
|
||||||
@@ -276,8 +278,8 @@ const Column: FC<Props> = ({
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
onScroll={handleTasksScroll}
|
onScroll={handleTasksScroll}
|
||||||
className={`mt-3 sm:mt-4 xl:mt-5 flex-1 min-h-[80px] overflow-y-auto overscroll-y-contain rounded-lg transition-colors ${
|
className={`mt-3 sm:mt-4 xl:mt-5 flex-1 min-h-[80px] min-w-0 overflow-y-auto overflow-x-hidden overscroll-y-contain rounded-lg transition-colors ${
|
||||||
snapshot.isDraggingOver ? "bg-primary/5 ring-2 ring-primary/30" : ""
|
snapshot.isDraggingOver ? "bg-primary/5 ring-2 ring-primary/30 ring-inset" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isTasksPending
|
{isTasksPending
|
||||||
@@ -294,7 +296,7 @@ const Column: FC<Props> = ({
|
|||||||
ref={dragProvided.innerRef}
|
ref={dragProvided.innerRef}
|
||||||
{...dragProvided.draggableProps}
|
{...dragProvided.draggableProps}
|
||||||
{...dragProvided.dragHandleProps}
|
{...dragProvided.dragHandleProps}
|
||||||
className="mb-3 sm:mb-4"
|
className="mb-3 sm:mb-4 w-full min-w-0 max-w-full"
|
||||||
style={dragProvided.draggableProps.style}
|
style={dragProvided.draggableProps.style}
|
||||||
>
|
>
|
||||||
<Task
|
<Task
|
||||||
@@ -314,7 +316,8 @@ const Column: FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
|
|
||||||
{isAdding ? (
|
{canCreate("task") && (
|
||||||
|
isAdding ? (
|
||||||
<div className="mt-3 sm:mt-4 xl:mt-5 shrink-0 flex flex-col gap-3">
|
<div className="mt-3 sm:mt-4 xl:mt-5 shrink-0 flex flex-col gap-3">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -356,6 +359,7 @@ const Column: FC<Props> = ({
|
|||||||
<AddCircle size={18} color="#888888" />
|
<AddCircle size={18} color="#888888" />
|
||||||
<span className="text-description text-[13px] mt-0.5">{t("taskmanager.add_new_task")}</span>
|
<span className="text-description text-[13px] mt-0.5">{t("taskmanager.add_new_task")}</span>
|
||||||
</button>
|
</button>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
|||||||
import { More } from "iconsax-react";
|
import { More } from "iconsax-react";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { usePermissions } from "../../../hooks/usePermissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
@@ -10,6 +11,7 @@ type Props = {
|
|||||||
|
|
||||||
const ColumnMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
const ColumnMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const { canUpdate, canDelete } = usePermissions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
@@ -26,6 +28,7 @@ const ColumnMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
|||||||
anchor="bottom start"
|
anchor="bottom start"
|
||||||
className="z-20 mt-1 min-w-[120px] rounded-xl bg-white shadow-md border border-border py-1 text-sm"
|
className="z-20 mt-1 min-w-[120px] rounded-xl bg-white shadow-md border border-border py-1 text-sm"
|
||||||
>
|
>
|
||||||
|
{canUpdate("task_phase") && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -36,6 +39,8 @@ const ColumnMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
|||||||
>
|
>
|
||||||
{t("taskmanager.edit_column")}
|
{t("taskmanager.edit_column")}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
{canDelete("task_phase") && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -46,6 +51,7 @@ const ColumnMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
|||||||
>
|
>
|
||||||
{t("taskmanager.delete_column")}
|
{t("taskmanager.delete_column")}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</PopoverPanel>
|
</PopoverPanel>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Link } from "react-router-dom";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Pages } from "../../../config/Pages";
|
import { Pages } from "../../../config/Pages";
|
||||||
import type { Task as TaskType } from "../types";
|
import type { Task as TaskType } from "../types";
|
||||||
|
import UserAvatar from "./UserAvatar";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
task: TaskType;
|
task: TaskType;
|
||||||
@@ -32,15 +33,17 @@ const Task: FC<Props> = ({ task, isDragging, onClick }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={`bg-white rounded-lg p-2 cursor-grab active:cursor-grabbing transition-opacity touch-manipulation ${
|
className={`bg-white rounded-lg p-2 w-full min-w-0 max-w-full overflow-hidden cursor-grab active:cursor-grabbing transition-opacity touch-manipulation ${
|
||||||
isDragging ? "opacity-40 shadow-lg" : ""
|
isDragging ? "opacity-40 shadow-lg" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{task.color ? <div className="h-10 rounded-lg" style={{ backgroundColor: task.color }} /> : null}
|
{task.color ? <div className="h-10 rounded-lg" style={{ backgroundColor: task.color }} /> : null}
|
||||||
<div className="bg-[#FF76C2] h-5 px-3 flex items-center mt-2 text-[10px] text-white rounded-md w-fit">
|
{task.tag ? (
|
||||||
|
<div className="bg-[#FF76C2] h-5 px-3 flex items-center mt-2 text-[10px] text-white rounded-md w-fit max-w-full truncate">
|
||||||
{task.tag}
|
{task.tag}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-bold text-xs mt-2">{task.title}</div>
|
) : null}
|
||||||
|
<div className="font-bold text-xs mt-2 break-words">{task.title}</div>
|
||||||
{task.ticketId ? (
|
{task.ticketId ? (
|
||||||
<Link
|
<Link
|
||||||
to={Pages.ticket.detail + task.ticketId}
|
to={Pages.ticket.detail + task.ticketId}
|
||||||
@@ -68,10 +71,21 @@ const Task: FC<Props> = ({ task, isDragging, onClick }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 flex justify-end">
|
{task.users?.length ? (
|
||||||
<div className="size-6 bg-gray-200 rounded-full"></div>
|
<div className="mt-2 flex justify-end pl-1.5">
|
||||||
|
<div className="flex items-center -space-x-1.5 rtl:space-x-reverse">
|
||||||
|
{task.users.map((user) => (
|
||||||
|
<UserAvatar
|
||||||
|
key={user.id}
|
||||||
|
src={user.profilePic}
|
||||||
|
alt={`${user.firstName} ${user.lastName}`}
|
||||||
|
className="size-6 ring-2 ring-white"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,23 @@ type Props = {
|
|||||||
src?: string | null;
|
src?: string | null;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
showTooltip?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserAvatar: FC<Props> = ({ src, alt = "", className = "size-6" }) => (
|
const UserAvatar: FC<Props> = ({ src, alt = "", className = "size-6", showTooltip = true }) => (
|
||||||
<div className={`${className} rounded-full overflow-hidden shrink-0 bg-[#D0D0D0]`}>
|
<div className={`relative group/avatar ${className} rounded-full overflow-visible shrink-0`}>
|
||||||
|
<div className="size-full rounded-full overflow-hidden bg-[#D0D0D0]">
|
||||||
<img src={src || AvatarImage} alt={alt} className="size-full object-cover" />
|
<img src={src || AvatarImage} alt={alt} className="size-full object-cover" />
|
||||||
</div>
|
</div>
|
||||||
|
{showTooltip && alt ? (
|
||||||
|
<span
|
||||||
|
role="tooltip"
|
||||||
|
className="pointer-events-none absolute bottom-full left-1/2 z-20 mb-1.5 -translate-x-1/2 whitespace-nowrap rounded-md bg-[#292D32] px-2 py-1 text-[10px] text-white opacity-0 transition-opacity group-hover/avatar:opacity-100"
|
||||||
|
>
|
||||||
|
{alt}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default UserAvatar;
|
export default UserAvatar;
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const TaskDetailActionBar: FC<Props> = ({ activeTab, onTabChange, labelsPopover,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center justify-center sm:justify-start gap-2">
|
||||||
{TABS.map(({ id, icon: Icon, labelKey }) => {
|
{TABS.map(({ id, icon: Icon, labelKey }) => {
|
||||||
const isActive = activeTab === id;
|
const isActive = activeTab === id;
|
||||||
const hasPopover = POPOVER_TABS.has(id);
|
const hasPopover = POPOVER_TABS.has(id);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const TaskDetailDescription: FC<Props> = ({ value = "", onChange, onSave, isSavi
|
|||||||
onChange={(e) => onChange?.(e.target.value)}
|
onChange={(e) => onChange?.(e.target.value)}
|
||||||
placeholder={t("taskmanager.task_detail.description_placeholder")}
|
placeholder={t("taskmanager.task_detail.description_placeholder")}
|
||||||
rows={5}
|
rows={5}
|
||||||
className="w-full bg-transparent rounded-xl px-4 pt-4 pb-3 text-xs outline-none resize-none placeholder:text-description"
|
className="w-full bg-transparent rounded-xl px-4 pt-4 pb-3 pl-20 text-xs outline-none resize-none placeholder:text-description"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ArrowDown2, CloseCircle, Paintbucket } from "iconsax-react";
|
|||||||
import { type FC } from "react";
|
import { type FC } from "react";
|
||||||
import TrashWithConfrim from "../../../../components/TrashWithConfrim";
|
import TrashWithConfrim from "../../../../components/TrashWithConfrim";
|
||||||
import { clx } from "../../../../helpers/utils";
|
import { clx } from "../../../../helpers/utils";
|
||||||
|
import { usePermissions } from "../../../../hooks/usePermissions";
|
||||||
import type { Column } from "../../types";
|
import type { Column } from "../../types";
|
||||||
import TaskDetailCoverColorPopover from "./cover/TaskDetailCoverColorPopover";
|
import TaskDetailCoverColorPopover from "./cover/TaskDetailCoverColorPopover";
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ const TaskDetailHeader: FC<Props> = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { canDelete } = usePermissions();
|
||||||
const selectedColumn = columns.find((column) => column.id === selectedPhaseId);
|
const selectedColumn = columns.find((column) => column.id === selectedPhaseId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -99,7 +101,7 @@ const TaskDetailHeader: FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
{onDelete ? (
|
{onDelete && canDelete("task") ? (
|
||||||
<div className="size-8 rounded-full bg-white/60 flex items-center justify-center">
|
<div className="size-8 rounded-full bg-white/60 flex items-center justify-center">
|
||||||
<TrashWithConfrim onDelete={onDelete} isLoading={isDeleting} color="#FF0000" />
|
<TrashWithConfrim onDelete={onDelete} isLoading={isDeleting} color="#FF0000" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, o
|
|||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
taskDetail={taskDetail}
|
taskDetail={taskDetail}
|
||||||
taskId={task.id}
|
taskId={task.id}
|
||||||
|
projectId={taskDetail?.projectId ?? projectId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -209,7 +210,7 @@ const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, o
|
|||||||
isSaving={updateTask.isPending}
|
isSaving={updateTask.isPending}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AttachmentList attachments={taskDetail?.attachments} />
|
<AttachmentList taskId={task.id} attachments={taskDetail?.attachments} />
|
||||||
|
|
||||||
<CheckLists
|
<CheckLists
|
||||||
taskId={task.id}
|
taskId={task.id}
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ type Props = {
|
|||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MOBILE_BREAKPOINT = 640;
|
||||||
|
|
||||||
const TaskDetailPopoverPanel: FC<Props> = ({ anchorRef, children, className, width = 300, onClose }) => {
|
const TaskDetailPopoverPanel: FC<Props> = ({ anchorRef, children, className, width = 300, onClose }) => {
|
||||||
const [position, setPosition] = useState<{ top: number; left: number } | null>(null);
|
const [position, setPosition] = useState<{ top: number; left: number; width: number } | null>(null);
|
||||||
const panelRef = useRef<HTMLDivElement>(null);
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -21,12 +23,21 @@ const TaskDetailPopoverPanel: FC<Props> = ({ anchorRef, children, className, wid
|
|||||||
|
|
||||||
const rect = anchor.getBoundingClientRect();
|
const rect = anchor.getBoundingClientRect();
|
||||||
const padding = 8;
|
const padding = 8;
|
||||||
let left = rect.right - width;
|
const isMobile = window.innerWidth < MOBILE_BREAKPOINT;
|
||||||
left = Math.max(padding, Math.min(left, window.innerWidth - width - padding));
|
const panelWidth = Math.min(width, window.innerWidth - padding * 2);
|
||||||
|
|
||||||
|
let left: number;
|
||||||
|
if (isMobile) {
|
||||||
|
left = (window.innerWidth - panelWidth) / 2;
|
||||||
|
} else {
|
||||||
|
left = rect.right - panelWidth;
|
||||||
|
left = Math.max(padding, Math.min(left, window.innerWidth - panelWidth - padding));
|
||||||
|
}
|
||||||
|
|
||||||
setPosition({
|
setPosition({
|
||||||
top: rect.bottom + 8,
|
top: rect.bottom + 8,
|
||||||
left,
|
left,
|
||||||
|
width: panelWidth,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,8 +61,10 @@ const TaskDetailPopoverPanel: FC<Props> = ({ anchorRef, children, className, wid
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("pointerdown", handlePointerDown);
|
// Capture: DefaulModal stops pointerdown propagation on bubble, so outside-click
|
||||||
return () => document.removeEventListener("pointerdown", handlePointerDown);
|
// would never reach a document bubble listener while the modal is open.
|
||||||
|
document.addEventListener("pointerdown", handlePointerDown, true);
|
||||||
|
return () => document.removeEventListener("pointerdown", handlePointerDown, true);
|
||||||
}, [anchorRef, onClose]);
|
}, [anchorRef, onClose]);
|
||||||
|
|
||||||
if (!position) return null;
|
if (!position) return null;
|
||||||
@@ -59,7 +72,7 @@ const TaskDetailPopoverPanel: FC<Props> = ({ anchorRef, children, className, wid
|
|||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
ref={panelRef}
|
ref={panelRef}
|
||||||
style={{ top: position.top, left: position.left, width }}
|
style={{ top: position.top, left: position.left, width: position.width }}
|
||||||
className={clx("fixed z-[80] bg-white/90 backdrop-blur-md rounded-2xl p-4 shadow-lg border border-white/80", className)}
|
className={clx("fixed z-[80] bg-white/90 backdrop-blur-md rounded-2xl p-4 shadow-lg border border-white/80", className)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { type FC, useEffect, useMemo, useState } from "react";
|
import { type FC, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from '../../../../components/Toast';
|
import { toast } from '../../../../components/Toast';
|
||||||
import { useGetUsersInfinite } from "../../../users/hooks/useUserData";
|
|
||||||
import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover";
|
import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover";
|
||||||
import TaskDetailDatePopover from "./date/TaskDetailDatePopover";
|
import TaskDetailDatePopover from "./date/TaskDetailDatePopover";
|
||||||
import { parseApiDate, toApiDate } from "./date/utils";
|
import { parseApiDate, toApiDate } from "./date/utils";
|
||||||
@@ -15,9 +14,16 @@ import type { TaskDetailTab } from "./types";
|
|||||||
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
|
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
|
||||||
import TaskDetailTimePopover from "./time/TaskDetailTimePopover";
|
import TaskDetailTimePopover from "./time/TaskDetailTimePopover";
|
||||||
import type { TaskUser } from "./users/types";
|
import type { TaskUser } from "./users/types";
|
||||||
import type { UserItemType } from "../../../users/types/UserTypes";
|
|
||||||
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
||||||
import { useCreateCheckListItem, useCreateTaskAttachment, useCreateTaskRemark, useUpdateTask } from "../../task/hooks/useTaskData";
|
import {
|
||||||
|
useCreateCheckListItem,
|
||||||
|
useCreateTaskAttachment,
|
||||||
|
useCreateTaskRemark,
|
||||||
|
useDeleteTaskRemark,
|
||||||
|
useUpdateTask,
|
||||||
|
} from "../../task/hooks/useTaskData";
|
||||||
|
import { useGetProjectUsers } from "../../project/hooks/useProjectData";
|
||||||
|
import type { ProjectUserType } from "../../project/types/ProjectTypes";
|
||||||
import { useSingleUpload } from "../../../service/hooks/useServiceData";
|
import { useSingleUpload } from "../../../service/hooks/useServiceData";
|
||||||
import { ErrorType } from "../../../../helpers/types";
|
import { ErrorType } from "../../../../helpers/types";
|
||||||
|
|
||||||
@@ -26,25 +32,20 @@ type Props = {
|
|||||||
onTabChange: (tab: TaskDetailTab) => void;
|
onTabChange: (tab: TaskDetailTab) => void;
|
||||||
taskDetail?: TaskDetailType;
|
taskDetail?: TaskDetailType;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
|
projectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId }) => {
|
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId, projectId }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
const createTaskRemark = useCreateTaskRemark();
|
const createTaskRemark = useCreateTaskRemark();
|
||||||
|
const deleteTaskRemark = useDeleteTaskRemark();
|
||||||
const createCheckListItem = useCreateCheckListItem();
|
const createCheckListItem = useCreateCheckListItem();
|
||||||
const createTaskAttachment = useCreateTaskAttachment();
|
const createTaskAttachment = useCreateTaskAttachment();
|
||||||
const singleUpload = useSingleUpload();
|
const singleUpload = useSingleUpload();
|
||||||
const updateTask = useUpdateTask();
|
const updateTask = useUpdateTask();
|
||||||
const isUsersOpen = activeTab === "users";
|
const isUsersOpen = activeTab === "users";
|
||||||
const [userSearch, setUserSearch] = useState("");
|
const [userSearch, setUserSearch] = useState("");
|
||||||
const [debouncedUserSearch, setDebouncedUserSearch] = useState("");
|
const { data: usersData, isPending: isUsersPending } = useGetProjectUsers(projectId, isUsersOpen);
|
||||||
const {
|
|
||||||
data: usersData,
|
|
||||||
isPending: isUsersPending,
|
|
||||||
isFetchingNextPage: isFetchingNextUsersPage,
|
|
||||||
fetchNextPage: fetchNextUsersPage,
|
|
||||||
hasNextPage: hasNextUsersPage,
|
|
||||||
} = useGetUsersInfinite(debouncedUserSearch, undefined, isUsersOpen);
|
|
||||||
const [labelsView, setLabelsView] = useState<LabelsView>("list");
|
const [labelsView, setLabelsView] = useState<LabelsView>("list");
|
||||||
const [labels, setLabels] = useState<TaskLabel[]>([]);
|
const [labels, setLabels] = useState<TaskLabel[]>([]);
|
||||||
const [selectedLabelIds, setSelectedLabelIds] = useState<Set<string>>(new Set());
|
const [selectedLabelIds, setSelectedLabelIds] = useState<Set<string>>(new Set());
|
||||||
@@ -52,14 +53,6 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
const [startDate, setStartDate] = useState<DateObject | null>(null);
|
const [startDate, setStartDate] = useState<DateObject | null>(null);
|
||||||
const [endDate, setEndDate] = useState<DateObject | null>(null);
|
const [endDate, setEndDate] = useState<DateObject | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timeoutId = window.setTimeout(() => {
|
|
||||||
setDebouncedUserSearch(userSearch.trim());
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
return () => window.clearTimeout(timeoutId);
|
|
||||||
}, [userSearch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!taskDetail) return;
|
if (!taskDetail) return;
|
||||||
|
|
||||||
@@ -84,16 +77,13 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
}, [taskDetail]);
|
}, [taskDetail]);
|
||||||
|
|
||||||
const selectedLabels = labels.filter((label) => selectedLabelIds.has(label.id));
|
const selectedLabels = labels.filter((label) => selectedLabelIds.has(label.id));
|
||||||
const users = useMemo((): TaskUser[] => {
|
const projectUsers = useMemo((): TaskUser[] => {
|
||||||
const apiUsers: TaskUser[] =
|
const apiUsers: TaskUser[] =
|
||||||
usersData?.pages.flatMap(
|
usersData?.data?.map((user: ProjectUserType) => ({
|
||||||
(page) =>
|
|
||||||
page.data?.users?.map((user: UserItemType) => ({
|
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: `${user.firstName} ${user.lastName}`.trim(),
|
name: `${user.firstName} ${user.lastName}`.trim(),
|
||||||
avatar: user.profilePic,
|
avatar: user.profilePic,
|
||||||
})) ?? [],
|
})) ?? [];
|
||||||
) ?? [];
|
|
||||||
const taskUsers: TaskUser[] =
|
const taskUsers: TaskUser[] =
|
||||||
taskDetail?.users?.map((user) => ({
|
taskDetail?.users?.map((user) => ({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@@ -116,20 +106,21 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(userMap.values());
|
return Array.from(userMap.values());
|
||||||
}, [usersData?.pages, taskDetail?.users]);
|
}, [usersData?.data, taskDetail?.users]);
|
||||||
const listUsers = useMemo((): TaskUser[] => {
|
const listUsers = useMemo((): TaskUser[] => {
|
||||||
return (
|
const query = userSearch.trim().toLowerCase();
|
||||||
usersData?.pages.flatMap(
|
const apiUsers: TaskUser[] =
|
||||||
(page) =>
|
usersData?.data?.map((user: ProjectUserType) => ({
|
||||||
page.data?.users?.map((user: UserItemType) => ({
|
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: `${user.firstName} ${user.lastName}`.trim(),
|
name: `${user.firstName} ${user.lastName}`.trim(),
|
||||||
avatar: user.profilePic,
|
avatar: user.profilePic,
|
||||||
})) ?? [],
|
})) ?? [];
|
||||||
) ?? []
|
|
||||||
);
|
if (!query) return apiUsers;
|
||||||
}, [usersData?.pages]);
|
|
||||||
const selectedUsers = users.filter((user) => selectedUserIds.has(user.id));
|
return apiUsers.filter((user) => user.name.toLowerCase().includes(query));
|
||||||
|
}, [usersData?.data, userSearch]);
|
||||||
|
const selectedUsers = projectUsers.filter((user) => selectedUserIds.has(user.id));
|
||||||
const isLabelsOpen = activeTab === "labels";
|
const isLabelsOpen = activeTab === "labels";
|
||||||
const isChecklistOpen = activeTab === "checklist";
|
const isChecklistOpen = activeTab === "checklist";
|
||||||
const isAttachmentOpen = activeTab === "attachment";
|
const isAttachmentOpen = activeTab === "attachment";
|
||||||
@@ -224,6 +215,29 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteLabel = (id: string) => {
|
||||||
|
const effectiveTaskId = taskId || taskDetail?.id;
|
||||||
|
if (!effectiveTaskId) return;
|
||||||
|
|
||||||
|
deleteTaskRemark.mutate(
|
||||||
|
{ id, taskId: effectiveTaskId },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setLabels((prev) => prev.filter((label) => label.id !== id));
|
||||||
|
setSelectedLabelIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
toast(t("success"), "success");
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleChecklistClose = () => {
|
const handleChecklistClose = () => {
|
||||||
if (isChecklistOpen) onTabChange("checklist");
|
if (isChecklistOpen) onTabChange("checklist");
|
||||||
};
|
};
|
||||||
@@ -338,7 +352,9 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
onViewChange={setLabelsView}
|
onViewChange={setLabelsView}
|
||||||
onToggle={handleLabelToggle}
|
onToggle={handleLabelToggle}
|
||||||
onCreate={handleCreate}
|
onCreate={handleCreate}
|
||||||
|
onDelete={handleDeleteLabel}
|
||||||
isSubmitting={createTaskRemark.isPending}
|
isSubmitting={createTaskRemark.isPending}
|
||||||
|
isDeleting={deleteTaskRemark.isPending}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
@@ -350,11 +366,6 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
onToggle={handleUserToggle}
|
onToggle={handleUserToggle}
|
||||||
isLoading={isUsersPending}
|
isLoading={isUsersPending}
|
||||||
isSubmitting={updateTask.isPending}
|
isSubmitting={updateTask.isPending}
|
||||||
hasNextPage={hasNextUsersPage}
|
|
||||||
isFetchingNextPage={isFetchingNextUsersPage}
|
|
||||||
onLoadMore={() => {
|
|
||||||
void fetchNextUsersPage();
|
|
||||||
}}
|
|
||||||
search={userSearch}
|
search={userSearch}
|
||||||
onSearchChange={setUserSearch}
|
onSearchChange={setUserSearch}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||||
import { Document, Link2, More } from "iconsax-react";
|
import { CloseCircle, Document, Link2, More } from "iconsax-react";
|
||||||
import { type FC } from "react";
|
import { useEffect, useState, type FC } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import { timeAgo } from "../../../../../config/func";
|
import { timeAgo } from "../../../../../config/func";
|
||||||
|
import { isImageAttachment } from "./isImageAttachment";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -13,15 +15,48 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AttachmentItem: FC<Props> = ({ title, createdAt, type, url, onEdit, onDelete }) => {
|
const AttachmentItem: FC<Props> = ({ title, createdAt, type, url, onEdit, onDelete }) => {
|
||||||
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||||
|
const isImage = type === "file" && isImageAttachment(url);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPreviewOpen) return;
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") setIsPreviewOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [isPreviewOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
|
{isImage && url ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsPreviewOpen(true)}
|
||||||
|
className="shrink-0 size-14 rounded-[10px] overflow-hidden bg-white outline-none"
|
||||||
|
>
|
||||||
|
<img src={url} alt={title} className="size-full object-cover" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
<div className="shrink-0 size-9 bg-white rounded-[10px] flex items-center justify-center">
|
<div className="shrink-0 size-9 bg-white rounded-[10px] flex items-center justify-center">
|
||||||
{type === "file" ? <Document size={20} color="#292D32" /> : <Link2 size={20} color="#292D32" />}
|
{type === "file" ? <Document size={20} color="#292D32" /> : <Link2 size={20} color="#292D32" />}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
{url ? (
|
{isImage && url ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsPreviewOpen(true)}
|
||||||
|
className="text-xs font-bold text-[#0047FF] underline truncate block max-w-full text-right outline-none"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</button>
|
||||||
|
) : url ? (
|
||||||
<a href={url} target="_blank" rel="noopener noreferrer" className="text-xs font-bold text-[#0047FF] underline truncate block">
|
<a href={url} target="_blank" rel="noopener noreferrer" className="text-xs font-bold text-[#0047FF] underline truncate block">
|
||||||
{title}
|
{title}
|
||||||
</a>
|
</a>
|
||||||
@@ -38,16 +73,62 @@ const AttachmentItem: FC<Props> = ({ title, createdAt, type, url, onEdit, onDele
|
|||||||
</PopoverButton>
|
</PopoverButton>
|
||||||
|
|
||||||
<PopoverPanel anchor="bottom end" className="z-[80] mt-1 rounded-[10px] bg-white shadow-md text-right p-3">
|
<PopoverPanel anchor="bottom end" className="z-[80] mt-1 rounded-[10px] bg-white shadow-md text-right p-3">
|
||||||
<button type="button" onClick={onEdit} className="w-full text-sm text-right">
|
{({ close }) => (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onEdit();
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
className="w-full text-sm text-right"
|
||||||
|
>
|
||||||
ویرایش
|
ویرایش
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button" onClick={onDelete} className="w-full mt-3 text-sm text-right text-[#FF0000]">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onDelete();
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
className="w-full mt-3 text-sm text-right text-[#FF0000]"
|
||||||
|
>
|
||||||
پاک کردن
|
پاک کردن
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</PopoverPanel>
|
</PopoverPanel>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isPreviewOpen &&
|
||||||
|
url &&
|
||||||
|
createPortal(
|
||||||
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4" role="dialog" aria-modal="true">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="بستن"
|
||||||
|
className="absolute inset-0 bg-black/70 outline-none"
|
||||||
|
onClick={() => setIsPreviewOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative z-10 max-w-[90vw] max-h-[90vh]">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="بستن"
|
||||||
|
onClick={() => setIsPreviewOpen(false)}
|
||||||
|
className="absolute -top-3 -left-3 size-9 rounded-full bg-white flex items-center justify-center shadow outline-none"
|
||||||
|
>
|
||||||
|
<CloseCircle size={22} color="#292D32" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<img src={url} alt={title} className="max-w-[90vw] max-h-[90vh] rounded-2xl object-contain bg-white" />
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
import { type FC } from "react";
|
import { useState, type FC } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import AttachmentItem from "./AttachmentItem";
|
import DefaulModal from "../../../../../components/DefaulModal";
|
||||||
import type { TaskAttachment } from "./types";
|
import ModalConfrim from "../../../../../components/ModalConfrim";
|
||||||
|
import { toast } from "../../../../../components/Toast";
|
||||||
|
import { ErrorType } from "../../../../../helpers/types";
|
||||||
|
import { useSingleUpload } from "../../../../service/hooks/useServiceData";
|
||||||
|
import { useDeleteTaskAttachment, useUpdateTaskAttachment } from "../../../task/hooks/useTaskData";
|
||||||
import type { TaskAttachmentType } from "../../../task/types/TaskTypes";
|
import type { TaskAttachmentType } from "../../../task/types/TaskTypes";
|
||||||
|
import AttachmentItem from "./AttachmentItem";
|
||||||
|
import TaskDetailAttachmentForm from "./TaskDetailAttachmentForm";
|
||||||
|
import type { TaskAttachment } from "./types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
taskId: string;
|
||||||
attachments?: TaskAttachmentType[];
|
attachments?: TaskAttachmentType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const AttachmentList: FC<Props> = ({ attachments = [] }) => {
|
const AttachmentList: FC<Props> = ({ taskId, attachments = [] }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const singleUpload = useSingleUpload();
|
||||||
|
const updateTaskAttachment = useUpdateTaskAttachment();
|
||||||
|
const deleteTaskAttachment = useDeleteTaskAttachment();
|
||||||
|
const [deleteItemId, setDeleteItemId] = useState<string | null>(null);
|
||||||
|
const [editItem, setEditItem] = useState<TaskAttachment | null>(null);
|
||||||
|
|
||||||
const mappedAttachments: TaskAttachment[] = attachments.map((item) => ({
|
const mappedAttachments: TaskAttachment[] = attachments.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
@@ -22,6 +35,61 @@ const AttachmentList: FC<Props> = ({ attachments = [] }) => {
|
|||||||
const files = mappedAttachments.filter((item) => item.type === "file");
|
const files = mappedAttachments.filter((item) => item.type === "file");
|
||||||
const links = mappedAttachments.filter((item) => item.type === "link");
|
const links = mappedAttachments.filter((item) => item.type === "link");
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (!deleteItemId) return;
|
||||||
|
|
||||||
|
deleteTaskAttachment.mutate(
|
||||||
|
{ id: deleteItemId, taskId },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setDeleteItemId(null);
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = async (data: { file?: File; title: string; linkId: string }) => {
|
||||||
|
if (!editItem) return;
|
||||||
|
|
||||||
|
let fileUrl = data.linkId.trim() || editItem.file || "";
|
||||||
|
|
||||||
|
if (data.file) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", data.file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadResult = await singleUpload.mutateAsync(formData);
|
||||||
|
fileUrl = uploadResult?.data?.url ?? "";
|
||||||
|
} catch (error) {
|
||||||
|
toast((error as ErrorType).response?.data?.error.message[0], "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileUrl) return;
|
||||||
|
|
||||||
|
const title = data.title.trim() || data.file?.name || editItem.title;
|
||||||
|
const type = data.file ? "file" : editItem.type;
|
||||||
|
|
||||||
|
updateTaskAttachment.mutate(
|
||||||
|
{ id: editItem.id, params: { file: fileUrl, title, type, taskId } },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setEditItem(null);
|
||||||
|
toast(t("success"), "success");
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isUpdateSubmitting = singleUpload.isPending || updateTaskAttachment.isPending;
|
||||||
|
|
||||||
if (mappedAttachments.length === 0) return null;
|
if (mappedAttachments.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -40,8 +108,8 @@ const AttachmentList: FC<Props> = ({ attachments = [] }) => {
|
|||||||
createdAt={item.createdAt}
|
createdAt={item.createdAt}
|
||||||
type="file"
|
type="file"
|
||||||
url={item.file}
|
url={item.file}
|
||||||
onEdit={() => {}}
|
onEdit={() => setEditItem(item)}
|
||||||
onDelete={() => {}}
|
onDelete={() => setDeleteItemId(item.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -60,13 +128,38 @@ const AttachmentList: FC<Props> = ({ attachments = [] }) => {
|
|||||||
createdAt={item.createdAt}
|
createdAt={item.createdAt}
|
||||||
type="link"
|
type="link"
|
||||||
url={item.file}
|
url={item.file}
|
||||||
onEdit={() => {}}
|
onEdit={() => setEditItem(item)}
|
||||||
onDelete={() => {}}
|
onDelete={() => setDeleteItemId(item.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ModalConfrim
|
||||||
|
isOpen={deleteItemId !== null}
|
||||||
|
close={() => setDeleteItemId(null)}
|
||||||
|
onConfrim={handleDelete}
|
||||||
|
isLoading={deleteTaskAttachment.isPending}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DefaulModal
|
||||||
|
open={editItem !== null}
|
||||||
|
close={() => setEditItem(null)}
|
||||||
|
isHeader
|
||||||
|
title_header={t("edit")}
|
||||||
|
width={420}
|
||||||
|
>
|
||||||
|
{editItem ? (
|
||||||
|
<TaskDetailAttachmentForm
|
||||||
|
initialValues={{ title: editItem.title, fileUrl: editItem.file, type: editItem.type }}
|
||||||
|
onClose={() => setEditItem(null)}
|
||||||
|
onSubmit={handleUpdate}
|
||||||
|
isSubmitting={isUpdateSubmitting}
|
||||||
|
submitLabel={t("save")}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DefaulModal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+29
-6
@@ -4,22 +4,39 @@ import Button from "../../../../../components/Button";
|
|||||||
import Input from "../../../../../components/Input";
|
import Input from "../../../../../components/Input";
|
||||||
import Select from "../../../../../components/Select";
|
import Select from "../../../../../components/Select";
|
||||||
|
|
||||||
|
type InitialValues = {
|
||||||
|
title: string;
|
||||||
|
fileUrl?: string;
|
||||||
|
type: "file" | "link";
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (data: { file?: File; title: string; linkId: string }) => void;
|
onSubmit: (data: { file?: File; title: string; linkId: string }) => void;
|
||||||
isSubmitting?: boolean;
|
isSubmitting?: boolean;
|
||||||
|
initialValues?: InitialValues;
|
||||||
|
submitLabel?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskDetailAttachmentForm: FC<Props> = ({ onClose, onSubmit, isSubmitting = false }) => {
|
const TaskDetailAttachmentForm: FC<Props> = ({
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
isSubmitting = false,
|
||||||
|
initialValues,
|
||||||
|
submitLabel,
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState(initialValues?.title ?? "");
|
||||||
const [linkId, setLinkId] = useState("");
|
const [linkId, setLinkId] = useState(initialValues?.type === "link" ? (initialValues.fileUrl ?? "") : "");
|
||||||
|
const isEdit = Boolean(initialValues);
|
||||||
|
|
||||||
const linkItems = [{ value: "", label: t("taskmanager.task_detail.none") }];
|
const linkItems = [{ value: "", label: t("taskmanager.task_detail.none") }];
|
||||||
|
|
||||||
const canSubmit = Boolean(file) || Boolean(title.trim());
|
const canSubmit = isEdit
|
||||||
|
? Boolean(title.trim()) || Boolean(file)
|
||||||
|
: Boolean(file) || Boolean(title.trim());
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const selected = e.target.files?.[0];
|
const selected = e.target.files?.[0];
|
||||||
@@ -35,6 +52,12 @@ const TaskDetailAttachmentForm: FC<Props> = ({ onClose, onSubmit, isSubmitting =
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentFileLabel = file
|
||||||
|
? file.name
|
||||||
|
: initialValues?.fileUrl
|
||||||
|
? initialValues.fileUrl
|
||||||
|
: t("no_select_file");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<h3 className="text-xs font-bold text-center">{t("taskmanager.task_detail.attachment")}</h3>
|
<h3 className="text-xs font-bold text-center">{t("taskmanager.task_detail.attachment")}</h3>
|
||||||
@@ -43,7 +66,7 @@ const TaskDetailAttachmentForm: FC<Props> = ({ onClose, onSubmit, isSubmitting =
|
|||||||
<button type="button" onClick={() => fileInputRef.current?.click()} className="shrink-0 h-6 rounded-lg text-[11px] px-3 bg-secondary">
|
<button type="button" onClick={() => fileInputRef.current?.click()} className="shrink-0 h-6 rounded-lg text-[11px] px-3 bg-secondary">
|
||||||
{t("select_file")}
|
{t("select_file")}
|
||||||
</button>
|
</button>
|
||||||
<span className="text-[11px] text-description truncate">{file ? file.name : t("no_select_file")}</span>
|
<span className="text-[11px] text-description truncate">{currentFileLabel}</span>
|
||||||
<input ref={fileInputRef} type="file" className="hidden" onChange={handleFileChange} />
|
<input ref={fileInputRef} type="file" className="hidden" onChange={handleFileChange} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -69,7 +92,7 @@ const TaskDetailAttachmentForm: FC<Props> = ({ onClose, onSubmit, isSubmitting =
|
|||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" onClick={handleSubmit} disabled={!canSubmit || isSubmitting} isLoading={isSubmitting} className="h-8 bg-[#292D32] text-white rounded-xl text-xs disabled:opacity-50 w-[95px]">
|
<Button type="button" onClick={handleSubmit} disabled={!canSubmit || isSubmitting} isLoading={isSubmitting} className="h-8 bg-[#292D32] text-white rounded-xl text-xs disabled:opacity-50 w-[95px]">
|
||||||
{t("taskmanager.task_detail.insert")}
|
{submitLabel ?? t("taskmanager.task_detail.insert")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const IMAGE_EXTENSIONS = new Set(["jpg", "jpeg", "png", "gif", "webp", "bmp", "svg", "avif"]);
|
||||||
|
|
||||||
|
export const isImageAttachment = (url?: string): boolean => {
|
||||||
|
if (!url) return false;
|
||||||
|
|
||||||
|
const path = url.split("?")[0].split("#")[0];
|
||||||
|
const extension = path.split(".").pop()?.toLowerCase();
|
||||||
|
|
||||||
|
return !!extension && IMAGE_EXTENSIONS.has(extension);
|
||||||
|
};
|
||||||
@@ -1,17 +1,83 @@
|
|||||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||||
import { More } from "iconsax-react";
|
import { More } from "iconsax-react";
|
||||||
import { type FC } from "react";
|
import { type FC, useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import TaskDetailLabelCheckbox from "../labels/TaskDetailLabelCheckbox";
|
import TaskDetailLabelCheckbox from "../labels/TaskDetailLabelCheckbox";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
onEdit: () => void;
|
onSave: (title: string) => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
|
isSaving?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChecklistItem: FC<Props> = ({ title, checked, onToggle, onEdit, onDelete }) => {
|
const ChecklistItem: FC<Props> = ({ title, checked, onToggle, onSave, onDelete, isSaving = false }) => {
|
||||||
|
const { t } = useTranslation("global");
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editTitle, setEditTitle] = useState(title);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEditing) {
|
||||||
|
setEditTitle(title);
|
||||||
|
}
|
||||||
|
}, [title, isEditing]);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const trimmed = editTitle.trim();
|
||||||
|
if (!trimmed || trimmed === title) {
|
||||||
|
setIsEditing(false);
|
||||||
|
setEditTitle(title);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSave(trimmed);
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsEditing(false);
|
||||||
|
setEditTitle(title);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1 items-center">
|
||||||
|
<TaskDetailLabelCheckbox checked={checked} onToggle={onToggle} />
|
||||||
|
|
||||||
|
<div className="flex-1 flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
value={editTitle}
|
||||||
|
onChange={(e) => setEditTitle(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") handleSave();
|
||||||
|
if (e.key === "Escape") handleCancel();
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
disabled={isSaving}
|
||||||
|
className="flex-1 h-[27px] px-2 text-xs bg-white/50 rounded outline-none"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!editTitle.trim() || isSaving}
|
||||||
|
className="h-7 px-3 rounded-lg bg-[#292D32] text-white text-[11px] disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{t("save")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCancel}
|
||||||
|
disabled={isSaving}
|
||||||
|
className="h-7 px-3 rounded-lg bg-white/50 text-[#292D32] text-[11px] border border-white disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{t("cancel")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-1 h-[27px] items-center">
|
<div className="flex gap-1 h-[27px] items-center">
|
||||||
<TaskDetailLabelCheckbox checked={checked} onToggle={onToggle} />
|
<TaskDetailLabelCheckbox checked={checked} onToggle={onToggle} />
|
||||||
@@ -25,13 +91,31 @@ const ChecklistItem: FC<Props> = ({ title, checked, onToggle, onEdit, onDelete }
|
|||||||
</PopoverButton>
|
</PopoverButton>
|
||||||
|
|
||||||
<PopoverPanel anchor="bottom end" className="z-[80] mt-1 rounded-[10px] bg-white shadow-md text-right p-3">
|
<PopoverPanel anchor="bottom end" className="z-[80] mt-1 rounded-[10px] bg-white shadow-md text-right p-3">
|
||||||
<button type="button" onClick={onEdit} className="w-full text-sm text-right">
|
{({ close }) => (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
close();
|
||||||
|
setIsEditing(true);
|
||||||
|
}}
|
||||||
|
className="w-full text-sm text-right"
|
||||||
|
>
|
||||||
ویرایش
|
ویرایش
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button" onClick={onDelete} className="w-full mt-3 text-sm text-right text-[#FF0000]">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
close();
|
||||||
|
onDelete();
|
||||||
|
}}
|
||||||
|
className="w-full mt-3 text-sm text-right text-[#FF0000]"
|
||||||
|
>
|
||||||
پاک کردن
|
پاک کردن
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</PopoverPanel>
|
</PopoverPanel>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Edit } from "iconsax-react";
|
import { Edit } from "iconsax-react";
|
||||||
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from '../../../../../components/Toast';
|
import ModalConfrim from "../../../../../components/ModalConfrim";
|
||||||
|
import { toast } from "../../../../../components/Toast";
|
||||||
import ProgressBar from "../../../../../components/ProgressBar";
|
import ProgressBar from "../../../../../components/ProgressBar";
|
||||||
import TrashWithConfrim from "../../../../../components/TrashWithConfrim";
|
|
||||||
import { ErrorType } from "../../../../../helpers/types";
|
import { ErrorType } from "../../../../../helpers/types";
|
||||||
import { useUpdateCheckListItem } from "../../../task/hooks/useTaskData";
|
import { useDeleteCheckListItem, useUpdateCheckListItem } from "../../../task/hooks/useTaskData";
|
||||||
import type { TaskChecklistItemType } from "../../../task/types/TaskTypes";
|
import type { TaskChecklistItemType } from "../../../task/types/TaskTypes";
|
||||||
import ChecklistItem from "./ChecklistItem";
|
import ChecklistItem from "./ChecklistItem";
|
||||||
|
|
||||||
@@ -23,6 +24,8 @@ const CheckLists = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
const updateCheckListItem = useUpdateCheckListItem();
|
const updateCheckListItem = useUpdateCheckListItem();
|
||||||
|
const deleteCheckListItem = useDeleteCheckListItem();
|
||||||
|
const [deleteItemId, setDeleteItemId] = useState<string | null>(null);
|
||||||
|
|
||||||
const items = checkListItems.map((item) => ({
|
const items = checkListItems.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
@@ -34,12 +37,39 @@ const CheckLists = ({
|
|||||||
const doneCount = completedCheckListItemCount ?? items.filter((item) => item.checked).length;
|
const doneCount = completedCheckListItemCount ?? items.filter((item) => item.checked).length;
|
||||||
const progress = totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0;
|
const progress = totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0;
|
||||||
|
|
||||||
const handleToggle = (id: string, checked: boolean) => {
|
const handleToggle = (id: string, title: string, checked: boolean) => {
|
||||||
updateCheckListItem.mutate(
|
updateCheckListItem.mutate(
|
||||||
{ id, params: { isDone: !checked }, taskId },
|
{ id, params: { title, isDone: !checked, taskId }, taskId },
|
||||||
{
|
{
|
||||||
onError: (error: ErrorType) => {
|
onError: (error: ErrorType) => {
|
||||||
toast(error.response?.data?.error.message[0], 'error');
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = (id: string, title: string, checked: boolean) => {
|
||||||
|
updateCheckListItem.mutate(
|
||||||
|
{ id, params: { title, isDone: checked, taskId }, taskId },
|
||||||
|
{
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (!deleteItemId) return;
|
||||||
|
|
||||||
|
deleteCheckListItem.mutate(
|
||||||
|
{ id: deleteItemId, taskId },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setDeleteItemId(null);
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -52,7 +82,6 @@ const CheckLists = ({
|
|||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="text-sm font-bold mt-px">{t("taskmanager.task_detail.checklist")}</div>
|
<div className="text-sm font-bold mt-px">{t("taskmanager.task_detail.checklist")}</div>
|
||||||
<Edit size={20} color="#0047FF" />
|
<Edit size={20} color="#0047FF" />
|
||||||
<TrashWithConfrim onDelete={() => {}} color="#FF0000" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex gap-2 items-center">
|
<div className="mt-4 flex gap-2 items-center">
|
||||||
@@ -66,12 +95,20 @@ const CheckLists = ({
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
checked={item.checked}
|
checked={item.checked}
|
||||||
onToggle={() => handleToggle(item.id, item.checked)}
|
onToggle={() => handleToggle(item.id, item.title, item.checked)}
|
||||||
onEdit={() => {}}
|
onSave={(title) => handleSave(item.id, title, item.checked)}
|
||||||
onDelete={() => {}}
|
onDelete={() => setDeleteItemId(item.id)}
|
||||||
|
isSaving={updateCheckListItem.isPending}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ModalConfrim
|
||||||
|
isOpen={deleteItemId !== null}
|
||||||
|
close={() => setDeleteItemId(null)}
|
||||||
|
onConfrim={handleDelete}
|
||||||
|
isLoading={deleteCheckListItem.isPending}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { CloseCircle, Document, Paperclip2 } from "iconsax-react";
|
||||||
|
import { useEffect, useState, type FC } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { isImageAttachment } from "../attachment/isImageAttachment";
|
||||||
|
|
||||||
|
const getFileName = (url: string) => {
|
||||||
|
const path = url.split("?")[0].split("#")[0];
|
||||||
|
return decodeURIComponent(path.split("/").pop() || url);
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attachments: string[];
|
||||||
|
onRemove?: (index: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommentAttachments: FC<Props> = ({ attachments, onRemove }) => {
|
||||||
|
const { t } = useTranslation("global");
|
||||||
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!previewUrl) return;
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") setPreviewUrl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [previewUrl]);
|
||||||
|
|
||||||
|
if (attachments.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{attachments.map((url, index) => {
|
||||||
|
const isImage = isImageAttachment(url);
|
||||||
|
const title = getFileName(url);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${url}-${index}`}
|
||||||
|
className="relative flex items-center gap-2 max-w-full rounded-lg bg-white/60 px-2 py-1.5"
|
||||||
|
>
|
||||||
|
{isImage ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPreviewUrl(url)}
|
||||||
|
className="shrink-0 size-8 rounded-md overflow-hidden bg-white outline-none"
|
||||||
|
>
|
||||||
|
<img src={url} alt={title} className="size-full object-cover" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="shrink-0 size-8 rounded-md bg-white flex items-center justify-center">
|
||||||
|
<Document size={16} color="#292D32" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isImage ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPreviewUrl(url)}
|
||||||
|
className="text-[11px] text-[#0047FF] underline truncate max-w-[140px] outline-none"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-[11px] text-[#0047FF] underline truncate max-w-[140px] flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Paperclip2 size={12} color="#0047FF" />
|
||||||
|
{t("attach")} {index + 1}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{onRemove ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onRemove(index)}
|
||||||
|
className="shrink-0 outline-none"
|
||||||
|
aria-label={t("cancel")}
|
||||||
|
>
|
||||||
|
<CloseCircle size={14} color="#FF0000" />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{previewUrl &&
|
||||||
|
createPortal(
|
||||||
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4" role="dialog" aria-modal="true">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t("cancel")}
|
||||||
|
className="absolute inset-0 bg-black/70 outline-none"
|
||||||
|
onClick={() => setPreviewUrl(null)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative z-10 max-w-[90vw] max-h-[90vh]">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t("cancel")}
|
||||||
|
onClick={() => setPreviewUrl(null)}
|
||||||
|
className="absolute -top-3 -left-3 size-9 rounded-full bg-white flex items-center justify-center shadow outline-none"
|
||||||
|
>
|
||||||
|
<CloseCircle size={22} color="#292D32" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src={previewUrl}
|
||||||
|
alt={getFileName(previewUrl)}
|
||||||
|
className="max-w-[90vw] max-h-[90vh] rounded-2xl object-contain bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentAttachments;
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import { Edit2 } from "iconsax-react";
|
import { CloseCircle, Edit2, Paperclip2 } from "iconsax-react";
|
||||||
import moment from "moment-jalaali";
|
import moment from "moment-jalaali";
|
||||||
import { type FC, useState } from "react";
|
import { type FC, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "../../../../../components/Toast";
|
import { toast } from "../../../../../components/Toast";
|
||||||
import TrashWithConfrim from "../../../../../components/TrashWithConfrim";
|
import TrashWithConfrim from "../../../../../components/TrashWithConfrim";
|
||||||
import { ErrorType } from "../../../../../helpers/types";
|
import { ErrorType } from "../../../../../helpers/types";
|
||||||
|
import { useMultiUpload } from "../../../../service/hooks/useServiceData";
|
||||||
import { useDeleteTaskComment, useUpdateTaskComment } from "../../../task/hooks/useTaskCommentData";
|
import { useDeleteTaskComment, useUpdateTaskComment } from "../../../task/hooks/useTaskCommentData";
|
||||||
import type { TaskCommentType } from "../../../task/types/TaskCommentTypes";
|
import type { TaskCommentType } from "../../../task/types/TaskCommentTypes";
|
||||||
import UserAvatar from "../../UserAvatar";
|
import UserAvatar from "../../UserAvatar";
|
||||||
|
import CommentAttachments from "./CommentAttachments";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
comment: TaskCommentType;
|
comment: TaskCommentType;
|
||||||
@@ -16,29 +18,86 @@ type Props = {
|
|||||||
|
|
||||||
const CommentItem: FC<Props> = ({ comment, taskId }) => {
|
const CommentItem: FC<Props> = ({ comment, taskId }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [content, setContent] = useState(comment.content);
|
const [content, setContent] = useState(comment.content);
|
||||||
|
const [attachments, setAttachments] = useState<string[]>(comment.attachments ?? []);
|
||||||
|
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
|
||||||
const updateComment = useUpdateTaskComment();
|
const updateComment = useUpdateTaskComment();
|
||||||
const deleteComment = useDeleteTaskComment();
|
const deleteComment = useDeleteTaskComment();
|
||||||
|
const multiUpload = useMultiUpload();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setContent(comment.content);
|
||||||
|
setAttachments(comment.attachments ?? []);
|
||||||
|
}, [comment.content, comment.attachments]);
|
||||||
|
|
||||||
const authorName = comment.user
|
const authorName = comment.user
|
||||||
? `${comment.user.firstName} ${comment.user.lastName}`.trim()
|
? `${comment.user.firstName} ${comment.user.lastName}`.trim()
|
||||||
: t("taskmanager.task_detail.unknown_user");
|
: t("taskmanager.task_detail.unknown_user");
|
||||||
|
|
||||||
const handleSave = () => {
|
const isSubmitting = updateComment.isPending || multiUpload.isPending;
|
||||||
|
|
||||||
|
const handleFilesSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const selected = Array.from(e.target.files ?? []);
|
||||||
|
if (selected.length === 0) return;
|
||||||
|
|
||||||
|
setPendingFiles((prev) => [...prev, ...selected]);
|
||||||
|
e.target.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
const trimmedContent = content.trim();
|
const trimmedContent = content.trim();
|
||||||
if (!trimmedContent || trimmedContent === comment.content) {
|
const originalAttachments = comment.attachments ?? [];
|
||||||
|
const contentUnchanged = trimmedContent === comment.content;
|
||||||
|
const attachmentsUnchanged =
|
||||||
|
pendingFiles.length === 0 &&
|
||||||
|
attachments.length === originalAttachments.length &&
|
||||||
|
attachments.every((url, index) => url === originalAttachments[index]);
|
||||||
|
|
||||||
|
if (contentUnchanged && attachmentsUnchanged) {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
setContent(comment.content);
|
setContent(comment.content);
|
||||||
|
setAttachments(originalAttachments);
|
||||||
|
setPendingFiles([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!trimmedContent && attachments.length === 0 && pendingFiles.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextAttachments = [...attachments];
|
||||||
|
|
||||||
|
if (pendingFiles.length > 0) {
|
||||||
|
const formData = new FormData();
|
||||||
|
pendingFiles.forEach((file) => formData.append("files", file));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadResult = await multiUpload.mutateAsync(formData);
|
||||||
|
const uploadedUrls: string[] =
|
||||||
|
uploadResult?.data?.map((item: { url: string }) => item?.url).filter(Boolean) ?? [];
|
||||||
|
nextAttachments = [...nextAttachments, ...uploadedUrls];
|
||||||
|
} catch (error) {
|
||||||
|
toast((error as ErrorType).response?.data?.error.message[0], "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateComment.mutate(
|
updateComment.mutate(
|
||||||
{ id: comment.id, params: { content: trimmedContent }, taskId },
|
{
|
||||||
|
id: comment.id,
|
||||||
|
params: {
|
||||||
|
content: trimmedContent,
|
||||||
|
attachments: nextAttachments,
|
||||||
|
},
|
||||||
|
taskId,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast(t("success"), "success");
|
toast(t("success"), "success");
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
|
setPendingFiles([]);
|
||||||
},
|
},
|
||||||
onError: (error: ErrorType) => {
|
onError: (error: ErrorType) => {
|
||||||
toast(error.response?.data?.error.message[0], "error");
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
@@ -49,6 +108,8 @@ const CommentItem: FC<Props> = ({ comment, taskId }) => {
|
|||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setContent(comment.content);
|
setContent(comment.content);
|
||||||
|
setAttachments(comment.attachments ?? []);
|
||||||
|
setPendingFiles([]);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,7 +130,7 @@ const CommentItem: FC<Props> = ({ comment, taskId }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-white/25 rounded-xl p-3">
|
<div className="bg-white/25 rounded-xl p-3">
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<UserAvatar src={comment.user?.profilePic} alt={authorName} className="size-7" />
|
<UserAvatar src={comment.user?.profilePic} alt={authorName} className="size-7" showTooltip={false} />
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
@@ -88,11 +149,42 @@ const CommentItem: FC<Props> = ({ comment, taskId }) => {
|
|||||||
className="w-full bg-white/50 rounded-lg px-3 py-2 text-xs outline-none resize-none"
|
className="w-full bg-white/50 rounded-lg px-3 py-2 text-xs outline-none resize-none"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-2 mt-2">
|
|
||||||
|
<CommentAttachments
|
||||||
|
attachments={attachments}
|
||||||
|
onRemove={(index) => setAttachments((prev) => prev.filter((_, i) => i !== index))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{pendingFiles.length > 0 ? (
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{pendingFiles.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={`${file.name}-${file.size}-${index}`}
|
||||||
|
className="flex items-center gap-1.5 max-w-full rounded-lg bg-white/60 px-2 py-1"
|
||||||
|
>
|
||||||
|
<span className="text-[11px] truncate max-w-[160px]">{file.name}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPendingFiles((prev) => prev.filter((_, i) => i !== index))}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="shrink-0 outline-none disabled:opacity-50"
|
||||||
|
aria-label={t("cancel")}
|
||||||
|
>
|
||||||
|
<CloseCircle size={14} color="#FF0000" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="flex gap-2 mt-2 items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={updateComment.isPending || !content.trim()}
|
disabled={
|
||||||
|
isSubmitting ||
|
||||||
|
(!content.trim() && attachments.length === 0 && pendingFiles.length === 0)
|
||||||
|
}
|
||||||
className="h-7 px-3 rounded-lg bg-[#292D32] text-white text-[11px] disabled:opacity-50"
|
className="h-7 px-3 rounded-lg bg-[#292D32] text-white text-[11px] disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{t("save")}
|
{t("save")}
|
||||||
@@ -100,15 +192,34 @@ const CommentItem: FC<Props> = ({ comment, taskId }) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
disabled={updateComment.isPending}
|
disabled={isSubmitting}
|
||||||
className="h-7 px-3 rounded-lg bg-white/50 text-[#292D32] text-[11px] border border-white"
|
className="h-7 px-3 rounded-lg bg-white/50 text-[#292D32] text-[11px] border border-white"
|
||||||
>
|
>
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="size-7 rounded-lg bg-white/70 flex items-center justify-center disabled:opacity-50"
|
||||||
|
aria-label={t("select_file")}
|
||||||
|
>
|
||||||
|
<Paperclip2 size={16} color="#292D32" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFilesSelected}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
<div className="mt-1 text-xs leading-6 whitespace-pre-line break-words">{comment.content}</div>
|
<div className="mt-1 text-xs leading-6 whitespace-pre-line break-words">{comment.content}</div>
|
||||||
|
<CommentAttachments attachments={comment.attachments ?? []} />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { type FC, useState } from "react";
|
import { CloseCircle, Paperclip2 } from "iconsax-react";
|
||||||
|
import { type FC, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "../../../../../components/Toast";
|
import { toast } from "../../../../../components/Toast";
|
||||||
import { ErrorType } from "../../../../../helpers/types";
|
import { ErrorType } from "../../../../../helpers/types";
|
||||||
|
import { useMultiUpload } from "../../../../service/hooks/useServiceData";
|
||||||
import { useCreateTaskComment, useGetTaskComments } from "../../../task/hooks/useTaskCommentData";
|
import { useCreateTaskComment, useGetTaskComments } from "../../../task/hooks/useTaskCommentData";
|
||||||
import CommentItem from "./CommentItem";
|
import CommentItem from "./CommentItem";
|
||||||
|
|
||||||
@@ -12,22 +14,59 @@ type Props = {
|
|||||||
|
|
||||||
const CommentList: FC<Props> = ({ taskId, enabled = true }) => {
|
const CommentList: FC<Props> = ({ taskId, enabled = true }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
const getComments = useGetTaskComments(taskId, enabled);
|
const getComments = useGetTaskComments(taskId, enabled);
|
||||||
const createComment = useCreateTaskComment();
|
const createComment = useCreateTaskComment();
|
||||||
|
const multiUpload = useMultiUpload();
|
||||||
|
|
||||||
const comments = getComments.data?.data ?? [];
|
const comments = getComments.data?.data ?? [];
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
const trimmedContent = content.trim();
|
const trimmedContent = content.trim();
|
||||||
if (!trimmedContent) return;
|
const isSubmitting = createComment.isPending || multiUpload.isPending;
|
||||||
|
const isSubmitDisabled = isSubmitting || (trimmedContent.length === 0 && files.length === 0);
|
||||||
|
|
||||||
|
const handleFilesSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const selected = Array.from(e.target.files ?? []);
|
||||||
|
if (selected.length === 0) return;
|
||||||
|
|
||||||
|
setFiles((prev) => [...prev, ...selected]);
|
||||||
|
e.target.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFile = (index: number) => {
|
||||||
|
setFiles((prev) => prev.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (isSubmitDisabled) return;
|
||||||
|
|
||||||
|
let attachments: string[] | undefined;
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
const formData = new FormData();
|
||||||
|
files.forEach((file) => formData.append("files", file));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadResult = await multiUpload.mutateAsync(formData);
|
||||||
|
attachments = uploadResult?.data?.map((item: { url: string }) => item?.url).filter(Boolean);
|
||||||
|
} catch (error) {
|
||||||
|
toast((error as ErrorType).response?.data?.error.message[0], "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createComment.mutate(
|
createComment.mutate(
|
||||||
{ content: trimmedContent, taskId },
|
{
|
||||||
|
content: trimmedContent,
|
||||||
|
taskId,
|
||||||
|
...(attachments?.length ? { attachments } : {}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast(t("success"), "success");
|
toast(t("success"), "success");
|
||||||
setContent("");
|
setContent("");
|
||||||
|
setFiles([]);
|
||||||
},
|
},
|
||||||
onError: (error: ErrorType) => {
|
onError: (error: ErrorType) => {
|
||||||
toast(error.response?.data?.error.message[0], "error");
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
@@ -41,21 +80,61 @@ const CommentList: FC<Props> = ({ taskId, enabled = true }) => {
|
|||||||
<div className="text-sm font-bold">{t("taskmanager.task_detail.comments")}</div>
|
<div className="text-sm font-bold">{t("taskmanager.task_detail.comments")}</div>
|
||||||
|
|
||||||
<div className="mt-3 relative bg-white/25 rounded-xl">
|
<div className="mt-3 relative bg-white/25 rounded-xl">
|
||||||
|
<div className="absolute top-2 left-2 z-10 flex items-center gap-1.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="size-7 rounded-lg bg-white/70 flex items-center justify-center disabled:opacity-50"
|
||||||
|
aria-label={t("select_file")}
|
||||||
|
>
|
||||||
|
<Paperclip2 size={16} color="#292D32" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={createComment.isPending || !content.trim()}
|
disabled={isSubmitDisabled}
|
||||||
className="absolute top-2 left-2 z-10 h-7 px-2.5 rounded-lg bg-[#292D32] text-white text-[11px] disabled:opacity-50"
|
className="h-7 px-2.5 rounded-lg bg-[#292D32] text-white text-[11px] disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{t("taskmanager.task_detail.add_comment")}
|
{t("taskmanager.task_detail.add_comment")}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFilesSelected}
|
||||||
|
/>
|
||||||
<textarea
|
<textarea
|
||||||
value={content}
|
value={content}
|
||||||
onChange={(e) => setContent(e.target.value)}
|
onChange={(e) => setContent(e.target.value)}
|
||||||
placeholder={t("taskmanager.task_detail.comment_placeholder")}
|
placeholder={t("taskmanager.task_detail.comment_placeholder")}
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full bg-transparent rounded-xl px-4 pt-4 pb-3 text-xs outline-none resize-none placeholder:text-description"
|
className="w-full bg-transparent rounded-xl px-4 pt-4 pb-3 pl-28 text-xs outline-none resize-none placeholder:text-description"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{files.length > 0 ? (
|
||||||
|
<div className="px-3 pb-3 flex flex-wrap gap-2">
|
||||||
|
{files.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={`${file.name}-${file.size}-${index}`}
|
||||||
|
className="flex items-center gap-1.5 max-w-full rounded-lg bg-white/60 px-2 py-1"
|
||||||
|
>
|
||||||
|
<span className="text-[11px] truncate max-w-[160px]">{file.name}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleRemoveFile(index)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="shrink-0 outline-none disabled:opacity-50"
|
||||||
|
aria-label={t("cancel")}
|
||||||
|
>
|
||||||
|
<CloseCircle size={14} color="#FF0000" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{getComments.isPending ? (
|
{getComments.isPending ? (
|
||||||
@@ -65,7 +144,11 @@ const CommentList: FC<Props> = ({ taskId, enabled = true }) => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="mt-3 flex flex-col gap-2">
|
<div className="mt-3 flex flex-col gap-2">
|
||||||
{comments.map((comment) => (
|
{comments.map((comment) => (
|
||||||
<CommentItem key={comment.id} comment={comment} taskId={taskId} />
|
<CommentItem
|
||||||
|
key={comment.id}
|
||||||
|
comment={comment}
|
||||||
|
taskId={taskId}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AddCircle, Edit } from "iconsax-react";
|
|||||||
import { type FC } from "react";
|
import { type FC } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Button from "../../../../../components/Button";
|
import Button from "../../../../../components/Button";
|
||||||
|
import TrashWithConfrim from "../../../../../components/TrashWithConfrim";
|
||||||
import TaskDetailLabelCheckbox from "./TaskDetailLabelCheckbox";
|
import TaskDetailLabelCheckbox from "./TaskDetailLabelCheckbox";
|
||||||
import type { TaskLabel } from "./types";
|
import type { TaskLabel } from "./types";
|
||||||
|
|
||||||
@@ -10,10 +11,20 @@ type Props = {
|
|||||||
selectedIds: Set<string>;
|
selectedIds: Set<string>;
|
||||||
onToggle: (id: string) => void;
|
onToggle: (id: string) => void;
|
||||||
onEdit: (label: TaskLabel) => void;
|
onEdit: (label: TaskLabel) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
onCreateClick: () => void;
|
onCreateClick: () => void;
|
||||||
|
isDeleting?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskDetailLabelsList: FC<Props> = ({ labels, selectedIds, onToggle, onEdit, onCreateClick }) => {
|
const TaskDetailLabelsList: FC<Props> = ({
|
||||||
|
labels,
|
||||||
|
selectedIds,
|
||||||
|
onToggle,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onCreateClick,
|
||||||
|
isDeleting = false,
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -32,7 +43,8 @@ const TaskDetailLabelsList: FC<Props> = ({ labels, selectedIds, onToggle, onEdit
|
|||||||
{label.title && <span className="text-xs font-medium truncate">{label.title}</span>}
|
{label.title && <span className="text-xs font-medium truncate">{label.title}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Edit onClick={() => onEdit(label)} size={16} color="#0047FF" />
|
<Edit onClick={() => onEdit(label)} size={16} color="#0047FF" className="shrink-0 cursor-pointer" />
|
||||||
|
<TrashWithConfrim onDelete={() => onDelete(label.id)} isLoading={isDeleting} size={16} color="#FF0000" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ type Props = {
|
|||||||
onViewChange: (view: LabelsView) => void;
|
onViewChange: (view: LabelsView) => void;
|
||||||
onToggle: (id: string) => void;
|
onToggle: (id: string) => void;
|
||||||
onCreate: (title: string, color: string) => void;
|
onCreate: (title: string, color: string) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
isSubmitting?: boolean;
|
isSubmitting?: boolean;
|
||||||
|
isDeleting?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskDetailLabelsPopover: FC<Props> = ({
|
const TaskDetailLabelsPopover: FC<Props> = ({
|
||||||
@@ -20,7 +22,9 @@ const TaskDetailLabelsPopover: FC<Props> = ({
|
|||||||
onViewChange,
|
onViewChange,
|
||||||
onToggle,
|
onToggle,
|
||||||
onCreate,
|
onCreate,
|
||||||
|
onDelete,
|
||||||
isSubmitting = false,
|
isSubmitting = false,
|
||||||
|
isDeleting = false,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -30,7 +34,9 @@ const TaskDetailLabelsPopover: FC<Props> = ({
|
|||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
onEdit={() => undefined}
|
onEdit={() => undefined}
|
||||||
|
onDelete={onDelete}
|
||||||
onCreateClick={() => onViewChange("create")}
|
onCreateClick={() => onViewChange("create")}
|
||||||
|
isDeleting={isDeleting}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TaskDetailNewLabelForm
|
<TaskDetailNewLabelForm
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const TaskDetailUsersList: FC<Props> = ({
|
|||||||
onToggle={() => onToggle(user.id)}
|
onToggle={() => onToggle(user.id)}
|
||||||
ariaLabel={user.name}
|
ariaLabel={user.name}
|
||||||
/>
|
/>
|
||||||
<UserAvatar src={user.avatar} alt={user.name} />
|
<UserAvatar src={user.avatar} alt={user.name} showTooltip={false} />
|
||||||
<div className="text-[10px]">{user.name}</div>
|
<div className="text-[10px]">{user.name}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ import { FC, useRef, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Select from "../../../../components/Select";
|
import Select from "../../../../components/Select";
|
||||||
import SwitchComponent from "../../../../components/Switch";
|
import SwitchComponent from "../../../../components/Switch";
|
||||||
import { useGetUsers } from "../../../users/hooks/useUserData";
|
import {
|
||||||
import { UserItemType } from "../../../users/types/UserTypes";
|
useGetWorkspaces,
|
||||||
import { useGetWorkspaces } from "../../workspace/hooks/useWorkspaceData";
|
useGetWorkspaceUsers,
|
||||||
import { WorkspaceItemType } from "../../workspace/types/WorkspaceTypes";
|
} from "../../workspace/hooks/useWorkspaceData";
|
||||||
|
import {
|
||||||
|
WorkspaceItemType,
|
||||||
|
WorkspaceUserType,
|
||||||
|
} from "../../workspace/types/WorkspaceTypes";
|
||||||
import WorkspaceColorPicker from "../../workspace/components/WorkspaceColorPicker";
|
import WorkspaceColorPicker from "../../workspace/components/WorkspaceColorPicker";
|
||||||
import UserAvatar from "../../components/UserAvatar";
|
import UserAvatar from "../../components/UserAvatar";
|
||||||
|
|
||||||
@@ -67,8 +71,8 @@ const CreateProjectSidebar: FC<Props> = ({
|
|||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
const getUsers = useGetUsers("");
|
|
||||||
const getWorkspaces = useGetWorkspaces();
|
const getWorkspaces = useGetWorkspaces();
|
||||||
|
const getWorkspaceUsers = useGetWorkspaceUsers(workspaceId);
|
||||||
const [userSelectValue, setUserSelectValue] = useState("");
|
const [userSelectValue, setUserSelectValue] = useState("");
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -82,19 +86,24 @@ const CreateProjectSidebar: FC<Props> = ({
|
|||||||
value: workspace.id,
|
value: workspace.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const userItems =
|
const workspaceUsers: WorkspaceUserType[] = Array.isArray(
|
||||||
getUsers.data?.data?.users
|
getWorkspaceUsers.data?.data,
|
||||||
?.filter((user: UserItemType) => !selectedUsers.some((selected) => selected.id === user.id))
|
)
|
||||||
.map((user: UserItemType) => ({
|
? getWorkspaceUsers.data.data
|
||||||
|
: (getWorkspaceUsers.data?.data?.users ?? []);
|
||||||
|
|
||||||
|
const userItems = workspaceUsers
|
||||||
|
.filter((user) => !selectedUsers.some((selected) => selected.id === user.id))
|
||||||
|
.map((user) => ({
|
||||||
label: `${user.firstName} ${user.lastName}`,
|
label: `${user.firstName} ${user.lastName}`,
|
||||||
value: user.id,
|
value: user.id,
|
||||||
})) ?? [];
|
}));
|
||||||
|
|
||||||
const handleUserSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleUserSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
|
|
||||||
const user = getUsers.data?.data?.users?.find((item: UserItemType) => item.id === value);
|
const user = workspaceUsers.find((item) => item.id === value);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
onSelectedUsersChange([
|
onSelectedUsersChange([
|
||||||
@@ -140,14 +149,26 @@ const CreateProjectSidebar: FC<Props> = ({
|
|||||||
placeholder={t("taskmanager.select_workspace")}
|
placeholder={t("taskmanager.select_workspace")}
|
||||||
items={workspaceItems}
|
items={workspaceItems}
|
||||||
value={workspaceId}
|
value={workspaceId}
|
||||||
onChange={(e) => onWorkspaceIdChange(e.target.value)}
|
onChange={(e) => {
|
||||||
|
onWorkspaceIdChange(e.target.value);
|
||||||
|
onSelectedUsersChange([]);
|
||||||
|
setUserSelectValue("");
|
||||||
|
}}
|
||||||
className="border"
|
className="border"
|
||||||
error_text={workspaceError ?? ""}
|
error_text={workspaceError ?? ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<Select label={t("taskmanager.users")} placeholder={t("taskmanager.select_user")} items={userItems} value={userSelectValue} onChange={handleUserSelect} className="border" />
|
<Select
|
||||||
|
label={t("taskmanager.users")}
|
||||||
|
placeholder={t("taskmanager.select_user")}
|
||||||
|
items={userItems}
|
||||||
|
value={userSelectValue}
|
||||||
|
onChange={handleUserSelect}
|
||||||
|
className="border"
|
||||||
|
disabled={!workspaceId}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="mt-3 border border-dashed border-border rounded-xl p-4 flex gap-2 flex-wrap min-h-[52px]">
|
<div className="mt-3 border border-dashed border-border rounded-xl p-4 flex gap-2 flex-wrap min-h-[52px]">
|
||||||
{selectedUsers.length === 0 ? (
|
{selectedUsers.length === 0 ? (
|
||||||
@@ -155,7 +176,7 @@ const CreateProjectSidebar: FC<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
selectedUsers.map((user) => (
|
selectedUsers.map((user) => (
|
||||||
<div key={user.id} className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg">
|
<div key={user.id} className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg">
|
||||||
<UserAvatar src={user.profilePic} alt={user.name} />
|
<UserAvatar src={user.profilePic} alt={user.name} showTooltip={false} />
|
||||||
<div>{user.name}</div>
|
<div>{user.name}</div>
|
||||||
<CloseCircle variant="Bold" className="size-4 cursor-pointer" color="red" onClick={() => handleRemoveUser(user.id)} />
|
<CloseCircle variant="Bold" className="size-4 cursor-pointer" color="red" onClick={() => handleRemoveUser(user.id)} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
|||||||
import { More } from "iconsax-react";
|
import { More } from "iconsax-react";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { usePermissions } from "../../../../hooks/usePermissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
@@ -10,6 +11,7 @@ type Props = {
|
|||||||
|
|
||||||
const ProjectCardMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
const ProjectCardMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const { canUpdate, canDelete } = usePermissions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
@@ -21,6 +23,7 @@ const ProjectCardMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
|||||||
anchor="bottom start"
|
anchor="bottom start"
|
||||||
className="z-20 mt-1 min-w-[120px] rounded-xl bg-white shadow-md border border-border py-1 text-sm"
|
className="z-20 mt-1 min-w-[120px] rounded-xl bg-white shadow-md border border-border py-1 text-sm"
|
||||||
>
|
>
|
||||||
|
{canUpdate("project") && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onEdit}
|
onClick={onEdit}
|
||||||
@@ -28,6 +31,8 @@ const ProjectCardMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
|||||||
>
|
>
|
||||||
{t("edit")}
|
{t("edit")}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
{canDelete("project") && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
@@ -35,6 +40,7 @@ const ProjectCardMenu: FC<Props> = ({ onEdit, onDelete }) => {
|
|||||||
>
|
>
|
||||||
{t("taskmanager.delete_project")}
|
{t("taskmanager.delete_project")}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</PopoverPanel>
|
</PopoverPanel>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import Button from "../../../../components/Button";
|
import Button from "../../../../components/Button";
|
||||||
import { Pages } from "../../../../config/Pages";
|
import { Pages } from "../../../../config/Pages";
|
||||||
|
import { usePermissions } from "../../../../hooks/usePermissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -13,6 +14,7 @@ type Props = {
|
|||||||
|
|
||||||
const ProjectListHeader: FC<Props> = ({ title, workspaceId, onSettingsClick }) => {
|
const ProjectListHeader: FC<Props> = ({ title, workspaceId, onSettingsClick }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const { canCreate } = usePermissions();
|
||||||
const createProjectLink = workspaceId ? `${Pages.taskmanager.createProject}?workspaceId=${workspaceId}` : Pages.taskmanager.createProject;
|
const createProjectLink = workspaceId ? `${Pages.taskmanager.createProject}?workspaceId=${workspaceId}` : Pages.taskmanager.createProject;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
@@ -31,6 +33,7 @@ const ProjectListHeader: FC<Props> = ({ title, workspaceId, onSettingsClick }) =
|
|||||||
<span>برگشت</span>
|
<span>برگشت</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{canCreate("project") && (
|
||||||
<Link to={createProjectLink}>
|
<Link to={createProjectLink}>
|
||||||
<Button
|
<Button
|
||||||
onClick={onSettingsClick}
|
onClick={onSettingsClick}
|
||||||
@@ -43,6 +46,7 @@ const ProjectListHeader: FC<Props> = ({ title, workspaceId, onSettingsClick }) =
|
|||||||
<span>{t("taskmanager.new_project")}</span>
|
<span>{t("taskmanager.new_project")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ export const useGetProject = (id: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetProjectUsers = (id: string, enabled = true) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["project-users", id],
|
||||||
|
queryFn: () => api.getProjectUsers(id),
|
||||||
|
enabled: !!id && enabled,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useCreateProject = () => {
|
export const useCreateProject = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -49,6 +57,9 @@ export const useUpdateProject = () => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["project-detail", variables.id],
|
queryKey: ["project-detail", variables.id],
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["project-users", variables.id],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios from "../../../../config/axios";
|
import axios from "../../../../config/axios";
|
||||||
import { CreateProjectType, GetProjectResponse, UpdateProjectType } from "../types/ProjectTypes";
|
import { CreateProjectType, GetProjectResponse, GetProjectUsersResponse, UpdateProjectType } from "../types/ProjectTypes";
|
||||||
|
|
||||||
export const getProjectsByWorkspace = async (workspaceId: string) => {
|
export const getProjectsByWorkspace = async (workspaceId: string) => {
|
||||||
const { data } = await axios.get(`/task-manager/projects/user/workspace/${workspaceId}`);
|
const { data } = await axios.get(`/task-manager/projects/user/workspace/${workspaceId}`);
|
||||||
@@ -16,6 +16,11 @@ export const getProject = async (id: string): Promise<GetProjectResponse> => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getProjectUsers = async (id: string): Promise<GetProjectUsersResponse> => {
|
||||||
|
const { data } = await axios.get(`/task-manager/projects/${id}/users`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const createProject = async (params: CreateProjectType) => {
|
export const createProject = async (params: CreateProjectType) => {
|
||||||
const { data } = await axios.post(`/task-manager/projects`, params);
|
const { data } = await axios.post(`/task-manager/projects`, params);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -53,3 +53,7 @@ export type ProjectWithPhasesType = {
|
|||||||
export interface GetProjectResponse extends IResponse<ProjectWithPhasesType> {
|
export interface GetProjectResponse extends IResponse<ProjectWithPhasesType> {
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetProjectUsersResponse extends IResponse<ProjectUserType[]> {
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import {
|
|||||||
CreateTaskAttachmentType,
|
CreateTaskAttachmentType,
|
||||||
CreateTaskRemarkType,
|
CreateTaskRemarkType,
|
||||||
CreateTaskType,
|
CreateTaskType,
|
||||||
|
GetTaskDetailResponse,
|
||||||
UpdateCheckListItemType,
|
UpdateCheckListItemType,
|
||||||
|
UpdateTaskAttachmentType,
|
||||||
UpdateTaskType,
|
UpdateTaskType,
|
||||||
} from "../types/TaskTypes";
|
} from "../types/TaskTypes";
|
||||||
|
|
||||||
@@ -117,6 +119,18 @@ export const useCreateTaskRemark = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useDeleteTaskRemark = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id }: { id: string; taskId: string }) => api.deleteTaskRemark(id),
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.taskId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["project"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useCreateCheckListItem = () => {
|
export const useCreateCheckListItem = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -124,6 +138,7 @@ export const useCreateCheckListItem = () => {
|
|||||||
mutationFn: (variables: CreateCheckListItemType) => api.createCheckListItem(variables),
|
mutationFn: (variables: CreateCheckListItemType) => api.createCheckListItem(variables),
|
||||||
onSuccess: (_data, variables) => {
|
onSuccess: (_data, variables) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.taskId] });
|
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.taskId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["project"] });
|
queryClient.invalidateQueries({ queryKey: ["project"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -135,8 +150,58 @@ export const useUpdateCheckListItem = () => {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, params }: { id: string; params: UpdateCheckListItemType; taskId: string }) =>
|
mutationFn: ({ id, params }: { id: string; params: UpdateCheckListItemType; taskId: string }) =>
|
||||||
api.updateCheckListItem(id, params),
|
api.updateCheckListItem(id, params),
|
||||||
|
onMutate: async ({ id, params, taskId }) => {
|
||||||
|
await queryClient.cancelQueries({ queryKey: ["task-detail", taskId] });
|
||||||
|
|
||||||
|
const previous = queryClient.getQueryData<GetTaskDetailResponse>(["task-detail", taskId]);
|
||||||
|
|
||||||
|
queryClient.setQueryData<GetTaskDetailResponse>(["task-detail", taskId], (current) => {
|
||||||
|
if (!current?.data?.checkListItems) return current;
|
||||||
|
|
||||||
|
const checkListItems = current.data.checkListItems.map((item) =>
|
||||||
|
item.id === id
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
...(params.isDone !== undefined ? { isDone: params.isDone } : {}),
|
||||||
|
...(params.title !== undefined ? { title: params.title } : {}),
|
||||||
|
}
|
||||||
|
: item,
|
||||||
|
);
|
||||||
|
const completedCheckListItemCount = checkListItems.filter((item) => item.isDone).length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
data: {
|
||||||
|
...current.data,
|
||||||
|
checkListItems,
|
||||||
|
completedCheckListItemCount,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { previous };
|
||||||
|
},
|
||||||
|
onError: (_error, variables, context) => {
|
||||||
|
if (context?.previous) {
|
||||||
|
queryClient.setQueryData(["task-detail", variables.taskId], context.previous);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSettled: (_data, _error, variables) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.taskId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["project"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteCheckListItem = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id }: { id: string; taskId: string }) => api.deleteCheckListItem(id),
|
||||||
onSuccess: (_data, variables) => {
|
onSuccess: (_data, variables) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.taskId] });
|
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.taskId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["project"] });
|
queryClient.invalidateQueries({ queryKey: ["project"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -149,6 +214,34 @@ export const useCreateTaskAttachment = () => {
|
|||||||
mutationFn: (variables: CreateTaskAttachmentType) => api.createTaskAttachment(variables),
|
mutationFn: (variables: CreateTaskAttachmentType) => api.createTaskAttachment(variables),
|
||||||
onSuccess: (_data, variables) => {
|
onSuccess: (_data, variables) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.taskId] });
|
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.taskId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["project"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateTaskAttachment = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, params }: { id: string; params: UpdateTaskAttachmentType }) =>
|
||||||
|
api.updateTaskAttachment(id, params),
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.params.taskId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["project"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteTaskAttachment = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id }: { id: string; taskId: string }) => api.deleteTaskAttachment(id),
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.taskId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["project"] });
|
queryClient.invalidateQueries({ queryKey: ["project"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
GetTasksByTaskPhaseResponse,
|
GetTasksByTaskPhaseResponse,
|
||||||
UpdateCheckListItemResponse,
|
UpdateCheckListItemResponse,
|
||||||
UpdateCheckListItemType,
|
UpdateCheckListItemType,
|
||||||
|
UpdateTaskAttachmentType,
|
||||||
UpdateTaskType,
|
UpdateTaskType,
|
||||||
} from "../types/TaskTypes";
|
} from "../types/TaskTypes";
|
||||||
|
|
||||||
@@ -63,6 +64,11 @@ export const createTaskRemark = async (params: CreateTaskRemarkType): Promise<Cr
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteTaskRemark = async (id: string) => {
|
||||||
|
const { data } = await axios.delete(`/task-manager/remarks/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const createCheckListItem = async (params: CreateCheckListItemType): Promise<CreateCheckListItemResponse> => {
|
export const createCheckListItem = async (params: CreateCheckListItemType): Promise<CreateCheckListItemResponse> => {
|
||||||
const { data } = await axios.post(`/task-manager/check-list-items`, params);
|
const { data } = await axios.post(`/task-manager/check-list-items`, params);
|
||||||
return data;
|
return data;
|
||||||
@@ -76,7 +82,22 @@ export const updateCheckListItem = async (
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteCheckListItem = async (id: string) => {
|
||||||
|
const { data } = await axios.delete(`/task-manager/check-list-items/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const createTaskAttachment = async (params: CreateTaskAttachmentType) => {
|
export const createTaskAttachment = async (params: CreateTaskAttachmentType) => {
|
||||||
const { data } = await axios.post(`/task-manager/attachments`, params);
|
const { data } = await axios.post(`/task-manager/attachments`, params);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateTaskAttachment = async (id: string, params: UpdateTaskAttachmentType) => {
|
||||||
|
const { data } = await axios.patch(`/task-manager/attachments/${id}`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteTaskAttachment = async (id: string) => {
|
||||||
|
const { data } = await axios.delete(`/task-manager/attachments/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type TaskCommentType = {
|
|||||||
taskId: string;
|
taskId: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
user?: TaskCommentAuthorType;
|
user?: TaskCommentAuthorType;
|
||||||
|
attachments?: string[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
};
|
};
|
||||||
@@ -20,10 +21,12 @@ export type TaskCommentType = {
|
|||||||
export type CreateTaskCommentType = {
|
export type CreateTaskCommentType = {
|
||||||
content: string;
|
content: string;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
|
attachments?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateTaskCommentType = {
|
export type UpdateTaskCommentType = {
|
||||||
content: string;
|
content: string;
|
||||||
|
attachments?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface GetTaskCommentsResponse extends IResponse<TaskCommentType[]> {
|
export interface GetTaskCommentsResponse extends IResponse<TaskCommentType[]> {
|
||||||
|
|||||||
@@ -23,10 +23,11 @@ export type TaskItemType = {
|
|||||||
endDate?: string | null;
|
endDate?: string | null;
|
||||||
tag?: string;
|
tag?: string;
|
||||||
dateRange?: string;
|
dateRange?: string;
|
||||||
attachments?: number;
|
attachmentCount?: number;
|
||||||
checklistDone?: number;
|
checkListItemCount?: number;
|
||||||
checklistTotal?: number;
|
completedCheckListItemCount?: number;
|
||||||
ticketId?: string | null;
|
ticketId?: string | null;
|
||||||
|
users?: TaskUserType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TaskPhaseTasksPagerType = {
|
export type TaskPhaseTasksPagerType = {
|
||||||
@@ -82,7 +83,9 @@ export interface CreateCheckListItemResponse extends IResponse<{ checkListItem:
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateCheckListItemType = {
|
export type UpdateCheckListItemType = {
|
||||||
isDone: boolean;
|
title?: string;
|
||||||
|
isDone?: boolean;
|
||||||
|
taskId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface UpdateCheckListItemResponse extends IResponse<{ checkListItem: TaskChecklistItemType }> {
|
export interface UpdateCheckListItemResponse extends IResponse<{ checkListItem: TaskChecklistItemType }> {
|
||||||
@@ -110,6 +113,13 @@ export type CreateTaskAttachmentType = {
|
|||||||
taskId: string;
|
taskId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateTaskAttachmentType = {
|
||||||
|
file: string;
|
||||||
|
title: string;
|
||||||
|
type: "file" | "link";
|
||||||
|
taskId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TaskDetailType = {
|
export type TaskDetailType = {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import type { TaskItemType } from "../types/TaskTypes";
|
|||||||
export const mapTaskItemToTask = (task: TaskItemType, taskPhaseId: string, fallbackOrder: number): Task => ({
|
export const mapTaskItemToTask = (task: TaskItemType, taskPhaseId: string, fallbackOrder: number): Task => ({
|
||||||
id: task.id,
|
id: task.id,
|
||||||
columnId: task.taskPhaseId ?? taskPhaseId,
|
columnId: task.taskPhaseId ?? taskPhaseId,
|
||||||
order: task.order ?? task.priority ?? fallbackOrder,
|
order: task.priority ?? task.order ?? fallbackOrder,
|
||||||
title: task.title,
|
title: task.title,
|
||||||
tag: task.tag ?? "",
|
tag: task.tag ?? "",
|
||||||
dateRange: task.dateRange ?? "",
|
dateRange: task.dateRange ?? "",
|
||||||
color: task.color || null,
|
color: task.color || null,
|
||||||
attachments: task.attachments ?? 0,
|
attachments: task.attachmentCount ?? 0,
|
||||||
checklistDone: task.checklistDone ?? 0,
|
checklistDone: task.completedCheckListItemCount ?? 0,
|
||||||
checklistTotal: task.checklistTotal ?? 0,
|
checklistTotal: task.checkListItemCount ?? 0,
|
||||||
ticketId: task.ticketId ?? null,
|
ticketId: task.ticketId ?? null,
|
||||||
|
users: task.users ?? [],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { TaskUserType } from "./task/types/TaskTypes";
|
||||||
|
|
||||||
export type Column = {
|
export type Column = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -17,6 +19,7 @@ export type Task = {
|
|||||||
checklistDone: number;
|
checklistDone: number;
|
||||||
checklistTotal: number;
|
checklistTotal: number;
|
||||||
ticketId?: string | null;
|
ticketId?: string | null;
|
||||||
|
users?: TaskUserType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceData = {
|
export type WorkspaceData = {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { TaskItemType } from "../task/types/TaskTypes";
|
import type { TaskItemType } from "../task/types/TaskTypes";
|
||||||
|
import { getTaskSortOrder } from "./getTaskSortOrder";
|
||||||
const getTaskSortOrder = (task: TaskItemType) => task.order ?? task.priority ?? 0;
|
|
||||||
|
|
||||||
export const getPriorityDestinationTaskId = (
|
export const getPriorityDestinationTaskId = (
|
||||||
items: TaskItemType[],
|
items: TaskItemType[],
|
||||||
@@ -9,16 +8,19 @@ export const getPriorityDestinationTaskId = (
|
|||||||
removeSource = true,
|
removeSource = true,
|
||||||
): string | null => {
|
): string | null => {
|
||||||
const sorted = [...items].sort((a, b) => getTaskSortOrder(a) - getTaskSortOrder(b));
|
const sorted = [...items].sort((a, b) => getTaskSortOrder(a) - getTaskSortOrder(b));
|
||||||
const sourceIndex = sorted.findIndex((item) => item.id === sourceId);
|
|
||||||
|
|
||||||
if (removeSource && sourceIndex === destIndex) return null;
|
if (!removeSource) {
|
||||||
|
if (sorted.length === 0) return null;
|
||||||
const list = removeSource ? sorted.filter((item) => item.id !== sourceId) : sorted;
|
if (destIndex >= sorted.length) return sorted[sorted.length - 1].id;
|
||||||
if (list.length === 0) return null;
|
return sorted[destIndex].id;
|
||||||
|
|
||||||
if (destIndex >= list.length) {
|
|
||||||
return list[list.length - 1].id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return list[destIndex].id;
|
const sourceIndex = sorted.findIndex((item) => item.id === sourceId);
|
||||||
|
if (sourceIndex === -1 || sourceIndex === destIndex) return null;
|
||||||
|
|
||||||
|
// Dest is the task currently at the drop index (مقصد), not after removing source.
|
||||||
|
const destTask = sorted[destIndex];
|
||||||
|
if (!destTask || destTask.id === sourceId) return null;
|
||||||
|
|
||||||
|
return destTask.id;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import type { TaskItemType } from "../task/types/TaskTypes";
|
||||||
|
|
||||||
|
export const getTaskSortOrder = (task: Pick<TaskItemType, "priority" | "order">) =>
|
||||||
|
task.priority ?? task.order ?? 0;
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { TaskItemType } from "../task/types/TaskTypes";
|
import type { TaskItemType } from "../task/types/TaskTypes";
|
||||||
|
import { getTaskSortOrder } from "./getTaskSortOrder";
|
||||||
const getTaskSortOrder = (task: TaskItemType) => task.order ?? task.priority ?? 0;
|
|
||||||
|
|
||||||
export const reorderTaskItems = (
|
export const reorderTaskItems = (
|
||||||
tasks: TaskItemType[],
|
tasks: TaskItemType[],
|
||||||
|
|||||||
@@ -115,10 +115,11 @@ const Workspace: FC = () => {
|
|||||||
tag: activeTask.tag,
|
tag: activeTask.tag,
|
||||||
dateRange: activeTask.dateRange,
|
dateRange: activeTask.dateRange,
|
||||||
color: activeTask.color,
|
color: activeTask.color,
|
||||||
attachments: activeTask.attachments,
|
attachmentCount: activeTask.attachments,
|
||||||
checklistDone: activeTask.checklistDone,
|
completedCheckListItemCount: activeTask.checklistDone,
|
||||||
checklistTotal: activeTask.checklistTotal,
|
checkListItemCount: activeTask.checklistTotal,
|
||||||
ticketId: activeTask.ticketId,
|
ticketId: activeTask.ticketId,
|
||||||
|
users: activeTask.users,
|
||||||
},
|
},
|
||||||
index,
|
index,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,18 +2,20 @@ import { Add, Edit, Trash } from "iconsax-react";
|
|||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { toast } from "../../../components/Toast";
|
|
||||||
import Button from "../../../components/Button";
|
import Button from "../../../components/Button";
|
||||||
import DeleteNameConfirmModal from "../../../components/DeleteNameConfirmModal";
|
import DeleteNameConfirmModal from "../../../components/DeleteNameConfirmModal";
|
||||||
import PageLoading from "../../../components/PageLoading";
|
import PageLoading from "../../../components/PageLoading";
|
||||||
import Td from "../../../components/Td";
|
import Td from "../../../components/Td";
|
||||||
|
import { toast } from "../../../components/Toast";
|
||||||
import { Pages } from "../../../config/Pages";
|
import { Pages } from "../../../config/Pages";
|
||||||
import { ErrorType } from "../../../helpers/types";
|
import { ErrorType } from "../../../helpers/types";
|
||||||
import { useDeleteWorkspace, useGetWorkspaces } from "./hooks/useWorkspaceData";
|
import { useDeleteWorkspace, useGetWorkspaces } from "./hooks/useWorkspaceData";
|
||||||
import { WorkspaceItemType } from "./types/WorkspaceTypes";
|
import { WorkspaceItemType } from "./types/WorkspaceTypes";
|
||||||
|
import { usePermissions } from "../../../hooks/usePermissions";
|
||||||
|
|
||||||
const WorkspaceList: FC = () => {
|
const WorkspaceList: FC = () => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions();
|
||||||
const getWorkspaces = useGetWorkspaces();
|
const getWorkspaces = useGetWorkspaces();
|
||||||
const deleteWorkspace = useDeleteWorkspace();
|
const deleteWorkspace = useDeleteWorkspace();
|
||||||
const [deleteTarget, setDeleteTarget] = useState<WorkspaceItemType | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<WorkspaceItemType | null>(null);
|
||||||
@@ -38,6 +40,7 @@ const WorkspaceList: FC = () => {
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<div className="flex w-full justify-between items-center">
|
<div className="flex w-full justify-between items-center">
|
||||||
<div>{t("taskmanager.workspace_list")}</div>
|
<div>{t("taskmanager.workspace_list")}</div>
|
||||||
|
{canCreate("workspace") && (
|
||||||
<Link to={Pages.taskmanager.createWorkspace}>
|
<Link to={Pages.taskmanager.createWorkspace}>
|
||||||
<Button className="px-5">
|
<Button className="px-5">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -49,6 +52,7 @@ const WorkspaceList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{getWorkspaces.isPending ? (
|
{getWorkspaces.isPending ? (
|
||||||
@@ -85,25 +89,29 @@ const WorkspaceList: FC = () => {
|
|||||||
<Td text="">
|
<Td text="">
|
||||||
<Link to={Pages.taskmanager.projectList + item.id}>
|
<Link to={Pages.taskmanager.projectList + item.id}>
|
||||||
<Button
|
<Button
|
||||||
label="مدیریت پروژه ها"
|
label="مشاهده پروژه ها"
|
||||||
className="bg-blue-400 w-fit px-3"
|
className="bg-blue-400 w-fit px-3"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</Td>
|
</Td>
|
||||||
<Td text="">
|
<Td text="">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{canUpdate("workspace") && (
|
||||||
<Link to={Pages.taskmanager.updateWorkspace + item.id}>
|
<Link to={Pages.taskmanager.updateWorkspace + item.id}>
|
||||||
<Edit
|
<Edit
|
||||||
size={20}
|
size={20}
|
||||||
color="#8C90A3"
|
color="#8C90A3"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
|
{canDelete("workspace") && (
|
||||||
<Trash
|
<Trash
|
||||||
onClick={() => setDeleteTarget(item)}
|
onClick={() => setDeleteTarget(item)}
|
||||||
color="#888"
|
color="#888"
|
||||||
size={20}
|
size={20}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { CloseCircle } from "iconsax-react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import SwitchComponent from "../../../../components/Switch";
|
import SwitchComponent from "../../../../components/Switch";
|
||||||
import Select from "../../../../components/Select";
|
import Select from "../../../../components/Select";
|
||||||
import { useGetUsers } from "../../../users/hooks/useUserData";
|
import { useGetAdminsListAll } from "../../../users/hooks/useUserData";
|
||||||
import { UserItemType } from "../../../users/types/UserTypes";
|
import { UserItemType } from "../../../users/types/UserTypes";
|
||||||
import WorkspaceColorPicker from "./WorkspaceColorPicker";
|
import WorkspaceColorPicker from "./WorkspaceColorPicker";
|
||||||
import UserAvatar from "../../components/UserAvatar";
|
import UserAvatar from "../../components/UserAvatar";
|
||||||
@@ -32,27 +32,25 @@ const CreateWorkspaceSidebar: FC<Props> = ({
|
|||||||
onSelectedUsersChange,
|
onSelectedUsersChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
const getUsers = useGetUsers("");
|
const getAdmins = useGetAdminsListAll();
|
||||||
const [userSelectValue, setUserSelectValue] = useState("");
|
const [userSelectValue, setUserSelectValue] = useState("");
|
||||||
|
|
||||||
const userItems =
|
const admins: UserItemType[] = getAdmins.data?.data ?? [];
|
||||||
getUsers.data?.data?.users
|
|
||||||
?.filter(
|
const userItems = admins
|
||||||
(user: UserItemType) =>
|
.filter(
|
||||||
!selectedUsers.some((selected) => selected.id === user.id),
|
(user) => !selectedUsers.some((selected) => selected.id === user.id),
|
||||||
)
|
)
|
||||||
.map((user: UserItemType) => ({
|
.map((user) => ({
|
||||||
label: `${user.firstName} ${user.lastName}`,
|
label: `${user.firstName} ${user.lastName}`,
|
||||||
value: user.id,
|
value: user.id,
|
||||||
})) ?? [];
|
}));
|
||||||
|
|
||||||
const handleUserSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleUserSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
|
|
||||||
const user = getUsers.data?.data?.users?.find(
|
const user = admins.find((item) => item.id === value);
|
||||||
(item: UserItemType) => item.id === value,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
onSelectedUsersChange([
|
onSelectedUsersChange([
|
||||||
@@ -113,7 +111,7 @@ const CreateWorkspaceSidebar: FC<Props> = ({
|
|||||||
key={user.id}
|
key={user.id}
|
||||||
className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg"
|
className="bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg"
|
||||||
>
|
>
|
||||||
<UserAvatar src={user.profilePic} alt={user.name} />
|
<UserAvatar src={user.profilePic} alt={user.name} showTooltip={false} />
|
||||||
<div>{user.name}</div>
|
<div>{user.name}</div>
|
||||||
<CloseCircle
|
<CloseCircle
|
||||||
variant="Bold"
|
variant="Bold"
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ export const useGetWorkspaceDetail = (id: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetWorkspaceUsers = (id: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["workspace-users", id],
|
||||||
|
queryFn: () => api.getWorkspaceUsers(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useCreateWorkspace = () => {
|
export const useCreateWorkspace = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ export const getWorkspaceDetail = async (id: string) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getWorkspaceUsers = async (id: string) => {
|
||||||
|
const { data } = await axios.get(`/task-manager/workspaces/${id}/users`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const createWorkspace = async (params: CreateWorkspaceType) => {
|
export const createWorkspace = async (params: CreateWorkspaceType) => {
|
||||||
const { data } = await axios.post(`/task-manager/workspaces`, params);
|
const { data } = await axios.post(`/task-manager/workspaces`, params);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ import { useCreateCategory, useGetCategories } from './hooks/useTicketData'
|
|||||||
import Textarea from '../../components/Textarea'
|
import Textarea from '../../components/Textarea'
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { ErrorType } from '../../helpers/types'
|
||||||
import { toast } from '../../components/Toast';
|
import { toast } from '../../components/Toast';
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const TicketCategory: FC = () => {
|
const TicketCategory: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate } = usePermissions()
|
||||||
const getGroups = useGetGroups()
|
const getGroups = useGetGroups()
|
||||||
const createCategory = useCreateCategory()
|
const createCategory = useCreateCategory()
|
||||||
const getCategories = useGetCategories()
|
const getCategories = useGetCategories()
|
||||||
@@ -100,6 +102,7 @@ const TicketCategory: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{canCreate('ticket_categories') &&
|
||||||
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
||||||
<div className='text-sm'>
|
<div className='text-sm'>
|
||||||
{t('ticket.add_category')}
|
{t('ticket.add_category')}
|
||||||
@@ -187,6 +190,7 @@ const TicketCategory: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+140
-147
@@ -1,244 +1,237 @@
|
|||||||
import { FC, useState } from 'react'
|
import { useFormik } from "formik";
|
||||||
import { useTranslation } from 'react-i18next'
|
import { TickCircle, TickSquare } from "iconsax-react";
|
||||||
import Input from '../../components/Input'
|
import { FC, useState } from "react";
|
||||||
import Select from '../../components/Select'
|
import { useTranslation } from "react-i18next";
|
||||||
import Textarea from '../../components/Textarea'
|
import { useNavigate } from "react-router-dom";
|
||||||
import UploadBox from '../../components/UploadBox'
|
import * as Yup from "yup";
|
||||||
import Button from '../../components/Button'
|
import Button from "../../components/Button";
|
||||||
import { TickCircle, TickSquare } from 'iconsax-react'
|
import Input from "../../components/Input";
|
||||||
import { useCreateTicket, useGetCategories } from './hooks/useTicketData'
|
import PageLoading from "../../components/PageLoading";
|
||||||
import { useGetProfile } from '../profile/hooks/useProfileData'
|
import Select from "../../components/Select";
|
||||||
import PageLoading from '../../components/PageLoading'
|
import Textarea from "../../components/Textarea";
|
||||||
import { useFormik } from 'formik'
|
import { toast } from "../../components/Toast";
|
||||||
import * as Yup from 'yup'
|
import UploadBox from "../../components/UploadBox";
|
||||||
import { ErrorType } from '../../helpers/types'
|
import { Pages } from "../../config/Pages";
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { ErrorType } from "../../helpers/types";
|
||||||
import { Pages } from '../../config/Pages'
|
import { useGetCustomers } from "../customer/hooks/useCustomerData";
|
||||||
import { CreateTicketAdminType } from './types/TicketTypes'
|
import { CustomerItemType } from "../customer/types/CustomerTypes";
|
||||||
import { toast } from '../../components/Toast';
|
import { useGetProfile } from "../profile/hooks/useProfileData";
|
||||||
import { useMultiUpload } from '../service/hooks/useServiceData'
|
import { useMultiUpload } from "../service/hooks/useServiceData";
|
||||||
import { useGetCustomers } from '../customer/hooks/useCustomerData'
|
import { useCreateTicket, useGetCategories } from "./hooks/useTicketData";
|
||||||
import { CustomerItemType } from '../customer/types/CustomerTypes'
|
import { CreateTicketAdminType } from "./types/TicketTypes";
|
||||||
|
|
||||||
const CreateTicket: FC = () => {
|
const CreateTicket: FC = () => {
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation("global");
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
const [files, setFiles] = useState<File[]>([])
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
const getProfile = useGetProfile()
|
const getProfile = useGetProfile();
|
||||||
const getCategories = useGetCategories()
|
const getCategories = useGetCategories();
|
||||||
const multiUpload = useMultiUpload()
|
const multiUpload = useMultiUpload();
|
||||||
const createTicket = useCreateTicket()
|
const createTicket = useCreateTicket();
|
||||||
const getCustomers = useGetCustomers(1, true)
|
const getCustomers = useGetCustomers(1, 50, true);
|
||||||
|
|
||||||
|
|
||||||
const formik = useFormik<CreateTicketAdminType>({
|
const formik = useFormik<CreateTicketAdminType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
subject: '',
|
subject: "",
|
||||||
message: '',
|
message: "",
|
||||||
categoryId: '',
|
categoryId: "",
|
||||||
priority: 'LOW',
|
priority: "LOW",
|
||||||
attachmentUrls: [],
|
attachmentUrls: [],
|
||||||
userId: ''
|
userId: "",
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
subject: Yup.string().required(t('errors.required')),
|
subject: Yup.string().required(t("errors.required")),
|
||||||
message: Yup.string().required(t('errors.required')),
|
message: Yup.string().required(t("errors.required")),
|
||||||
categoryId: Yup.string().required(t('errors.required')),
|
categoryId: Yup.string().required(t("errors.required")),
|
||||||
priority: Yup.string().required(t('errors.required')),
|
priority: Yup.string().required(t("errors.required")),
|
||||||
}),
|
}),
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
const images = new FormData()
|
const images = new FormData();
|
||||||
files.forEach(file => {
|
files.forEach((file) => {
|
||||||
images.append('files', file)
|
images.append("files", file);
|
||||||
})
|
});
|
||||||
await multiUpload.mutateAsync(images, {
|
await multiUpload.mutateAsync(images, {
|
||||||
onSuccess: async (data) => {
|
onSuccess: async (data) => {
|
||||||
values.attachmentUrls = await data.data.map((item: { url: string }) => item?.url)
|
values.attachmentUrls = await data.data.map((item: { url: string }) => item?.url);
|
||||||
},
|
},
|
||||||
onError: (error: ErrorType) => {
|
onError: (error: ErrorType) => {
|
||||||
toast(error.response?.data?.error?.message[0], 'error')
|
toast(error.response?.data?.error?.message[0], "error");
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
delete values.attachmentUrls
|
delete values.attachmentUrls;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.danakServiceId === '') {
|
if (values.danakServiceId === "") {
|
||||||
delete values.danakServiceId
|
delete values.danakServiceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
createTicket.mutate(values, {
|
createTicket.mutate(values, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
toast(t('success'), 'success')
|
toast(t("success"), "success");
|
||||||
formik.resetForm()
|
formik.resetForm();
|
||||||
navigate(Pages.ticket.detail + data.data?.ticketId)
|
navigate(Pages.ticket.detail + data.data?.ticketId);
|
||||||
},
|
},
|
||||||
onError: (error: ErrorType) => {
|
onError: (error: ErrorType) => {
|
||||||
toast(error.response?.data?.error?.message[0], 'error')
|
toast(error.response?.data?.error?.message[0], "error");
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4 pb-12'>
|
<div className="mt-4 pb-12">
|
||||||
<div>
|
<div>{t("ticket.new_ticket")}</div>
|
||||||
{t('ticket.new_ticket')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
{getProfile.isPending || getCategories.isPending ? (
|
||||||
getProfile.isPending || getCategories.isPending ?
|
|
||||||
<PageLoading />
|
<PageLoading />
|
||||||
:
|
) : (
|
||||||
<div className='flex gap-6 xl:mt-8 mt-4'>
|
<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 className="flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl">
|
||||||
<div>
|
<div>{t("ticket.submit_your_message")}</div>
|
||||||
{t('ticket.submit_your_message')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-5'>
|
<div className="mt-5">
|
||||||
<Select
|
<Select
|
||||||
label={t('receip.customer')}
|
label={t("receip.customer")}
|
||||||
placeholder={t('select')}
|
placeholder={t("select")}
|
||||||
items={getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
|
items={getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
|
||||||
return {
|
return {
|
||||||
label: item.firstName + ' ' + item.lastName,
|
label: item.firstName + " " + item.lastName,
|
||||||
value: item.id
|
value: item.id,
|
||||||
}
|
};
|
||||||
})}
|
})}
|
||||||
{...formik.getFieldProps('userId')}
|
{...formik.getFieldProps("userId")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 rowTwoInput">
|
||||||
<div className='mt-6 rowTwoInput'>
|
|
||||||
<Input
|
<Input
|
||||||
label={t('ticket.subject')}
|
label={t("ticket.subject")}
|
||||||
placeholder={t('ticket.enter_your_subject')}
|
placeholder={t("ticket.enter_your_subject")}
|
||||||
name='subject'
|
name="subject"
|
||||||
value={formik.values.subject}
|
value={formik.values.subject}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
error_text={formik.touched.subject && formik.errors.subject ? formik.errors.subject : ''}
|
error_text={formik.touched.subject && formik.errors.subject ? formik.errors.subject : ""}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
label={t('ticket.priority')}
|
label={t("ticket.priority")}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
label: t('ticket.low'),
|
label: t("ticket.low"),
|
||||||
value: 'LOW'
|
value: "LOW",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('ticket.medium'),
|
label: t("ticket.medium"),
|
||||||
value: 'MEDIUM'
|
value: "MEDIUM",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('ticket.high'),
|
label: t("ticket.high"),
|
||||||
value: 'HIGH'
|
value: "HIGH",
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
placeholder={t('select')}
|
placeholder={t("select")}
|
||||||
className='border'
|
className="border"
|
||||||
name='priority'
|
name="priority"
|
||||||
value={formik.values.priority}
|
value={formik.values.priority}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
error_text={formik.touched.priority && formik.errors.priority ? formik.errors.priority : ''}
|
error_text={formik.touched.priority && formik.errors.priority ? formik.errors.priority : ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6 rowTwoInput'>
|
<div className="mt-6 rowTwoInput">
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label={t('ticket.category')}
|
label={t("ticket.category")}
|
||||||
items={getCategories.data?.data?.ticketCategories?.map((item: { id: string, title: string }) => ({
|
items={getCategories.data?.data?.ticketCategories?.map((item: { id: string; title: string }) => ({
|
||||||
label: item.title,
|
label: item.title,
|
||||||
value: item.id
|
value: item.id,
|
||||||
})
|
}))}
|
||||||
)}
|
placeholder={t("select")}
|
||||||
placeholder={t('select')}
|
className="border"
|
||||||
className='border'
|
name="categoryId"
|
||||||
name='categoryId'
|
|
||||||
value={formik.values.categoryId}
|
value={formik.values.categoryId}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
error_text={formik.touched.categoryId && formik.errors.categoryId ? formik.errors.categoryId : ''}
|
error_text={formik.touched.categoryId && formik.errors.categoryId ? formik.errors.categoryId : ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-6'>
|
<div className="mt-6">
|
||||||
<Textarea
|
<Textarea
|
||||||
label={t('description')}
|
label={t("description")}
|
||||||
placeholder={t('ticket.your_description')}
|
placeholder={t("ticket.your_description")}
|
||||||
name='message'
|
name="message"
|
||||||
value={formik.values.message}
|
value={formik.values.message}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
error_text={formik.touched.message && formik.errors.message ? formik.errors.message : ''}
|
error_text={formik.touched.message && formik.errors.message ? formik.errors.message : ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className="mt-6">
|
||||||
<UploadBox
|
<UploadBox
|
||||||
label={t('ticket.attachment')}
|
label={t("ticket.attachment")}
|
||||||
isMultiple
|
isMultiple
|
||||||
onChange={setFiles}
|
onChange={setFiles}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-8 flex justify-end'>
|
<div className="mt-8 flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
className='xl:max-w-[100px]'
|
className="xl:max-w-[100px]"
|
||||||
onClick={() => formik.handleSubmit()}
|
onClick={() => formik.handleSubmit()}
|
||||||
isLoading={createTicket.isPending || multiUpload.isPending}
|
isLoading={createTicket.isPending || multiUpload.isPending}
|
||||||
>
|
>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className="flex gap-2 items-center">
|
||||||
<TickCircle size={20} color='white' />
|
<TickCircle
|
||||||
<div>
|
size={20}
|
||||||
{t('ticket.send')}
|
color="white"
|
||||||
</div>
|
/>
|
||||||
|
<div>{t("ticket.send")}</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
<div className="bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl">
|
||||||
<div className='text-sm'>
|
<div className="text-sm">{t("ticket.title_hint")}</div>
|
||||||
{t('ticket.title_hint')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className="mt-6">
|
||||||
<div className='flex items-start gap-2 border-b pb-5'>
|
<div className="flex items-start gap-2 border-b pb-5">
|
||||||
<div>
|
<div>
|
||||||
<TickSquare size={20} color='black' variant='Bold' />
|
<TickSquare
|
||||||
|
size={20}
|
||||||
|
color="black"
|
||||||
|
variant="Bold"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-description text-xs leading-5'>
|
<div className="text-description text-xs leading-5">سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.</div>
|
||||||
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-start gap-2 mt-6 border-b pb-5">
|
||||||
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
|
|
||||||
<div>
|
<div>
|
||||||
<TickSquare size={20} color='black' variant='Bold' />
|
<TickSquare
|
||||||
|
size={20}
|
||||||
|
color="black"
|
||||||
|
variant="Bold"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-description text-xs leading-5'>
|
<div className="text-description text-xs leading-5">لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.</div>
|
||||||
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-start gap-2 mt-6">
|
||||||
<div className='flex items-start gap-2 mt-6'>
|
|
||||||
<div>
|
<div>
|
||||||
<TickSquare size={20} color='black' variant='Bold' />
|
<TickSquare
|
||||||
|
size={20}
|
||||||
|
color="black"
|
||||||
|
variant="Bold"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-description text-xs leading-5'>
|
<div className="text-description text-xs leading-5">
|
||||||
پاسخگویی 24 ساعته تلفنی را تنها از داناک می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
|
پاسخگویی 24 ساعته تلفنی را تنها از داناک می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default CreateTicket
|
export default CreateTicket;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user