Compare commits
27 Commits
b5b8344888
...
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 |
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,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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,34 @@ 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 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;
|
||||||
|
|
||||||
@@ -180,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),
|
||||||
|
};
|
||||||
|
};
|
||||||
+19
-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": "غیرفعال",
|
||||||
@@ -785,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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import PageLoading from "../../components/PageLoading";
|
|||||||
import Pagination from "../../components/Pagination";
|
import Pagination from "../../components/Pagination";
|
||||||
import Td from "../../components/Td";
|
import Td from "../../components/Td";
|
||||||
import { Pages } from "../../config/Pages";
|
import { Pages } from "../../config/Pages";
|
||||||
|
import { usePermissions } from "../../hooks/usePermissions";
|
||||||
import { useGetCustomers } from "./hooks/useCustomerData";
|
import { useGetCustomers } from "./hooks/useCustomerData";
|
||||||
import { CustomerItemType } from "./types/CustomerTypes";
|
import { CustomerItemType } from "./types/CustomerTypes";
|
||||||
|
|
||||||
const CustomerList: FC = () => {
|
const CustomerList: FC = () => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
const { canCreate, canUpdate } = usePermissions();
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [limit, setLimit] = useState(10);
|
const [limit, setLimit] = useState(10);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -29,6 +31,7 @@ const CustomerList: 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("customer.customer_list")}</div>
|
<div>{t("customer.customer_list")}</div>
|
||||||
|
{canCreate("customers") && (
|
||||||
<Link to={Pages.customer.create}>
|
<Link to={Pages.customer.create}>
|
||||||
<Button className="px-5">
|
<Button className="px-5">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -40,6 +43,7 @@ const CustomerList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end items-center mt-12">
|
<div className="flex justify-end items-center mt-12">
|
||||||
@@ -92,12 +96,14 @@ const CustomerList: FC = () => {
|
|||||||
</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
|
<Eye
|
||||||
size={20}
|
size={20}
|
||||||
color="#8C90A3"
|
color="#8C90A3"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
<Td text={""} />
|
<Td text={""} />
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -33,17 +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}
|
||||||
{task.tag ? (
|
{task.tag ? (
|
||||||
<div className="bg-[#FF76C2] h-5 px-3 flex items-center mt-2 text-[10px] text-white rounded-md w-fit">
|
<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>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="font-bold text-xs mt-2">{task.title}</div>
|
<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}
|
||||||
@@ -72,7 +72,7 @@ const Task: FC<Props> = ({ task, isDragging, onClick }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{task.users?.length ? (
|
{task.users?.length ? (
|
||||||
<div className="mt-2 flex justify-end">
|
<div className="mt-2 flex justify-end pl-1.5">
|
||||||
<div className="flex items-center -space-x-1.5 rtl:space-x-reverse">
|
<div className="flex items-center -space-x-1.5 rtl:space-x-reverse">
|
||||||
{task.users.map((user) => (
|
{task.users.map((user) => (
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Edit } from "iconsax-react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import ModalConfrim from "../../../../../components/ModalConfrim";
|
import ModalConfrim from "../../../../../components/ModalConfrim";
|
||||||
import { toast } from '../../../../../components/Toast';
|
import { toast } from "../../../../../components/Toast";
|
||||||
import ProgressBar from "../../../../../components/ProgressBar";
|
import ProgressBar from "../../../../../components/ProgressBar";
|
||||||
import { ErrorType } from "../../../../../helpers/types";
|
import { ErrorType } from "../../../../../helpers/types";
|
||||||
import { useDeleteCheckListItem, useUpdateCheckListItem } from "../../../task/hooks/useTaskData";
|
import { useDeleteCheckListItem, useUpdateCheckListItem } from "../../../task/hooks/useTaskData";
|
||||||
@@ -37,12 +37,23 @@ 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");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -58,7 +69,7 @@ const CheckLists = ({
|
|||||||
setDeleteItemId(null);
|
setDeleteItemId(null);
|
||||||
},
|
},
|
||||||
onError: (error: ErrorType) => {
|
onError: (error: ErrorType) => {
|
||||||
toast(error.response?.data?.error.message[0], 'error');
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -84,9 +95,10 @@ 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={() => setDeleteItemId(item.id)}
|
onDelete={() => setDeleteItemId(item.id)}
|
||||||
|
isSaving={updateCheckListItem.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,45 @@ 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),
|
||||||
onSuccess: (_data, variables) => {
|
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: ["task-detail", variables.taskId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["project"] });
|
queryClient.invalidateQueries({ queryKey: ["project"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -149,6 +201,7 @@ export const useDeleteCheckListItem = () => {
|
|||||||
mutationFn: ({ id }: { id: string; taskId: string }) => api.deleteCheckListItem(id),
|
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"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -161,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;
|
||||||
@@ -85,3 +91,13 @@ 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,9 +23,9 @@ 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[];
|
users?: TaskUserType[];
|
||||||
};
|
};
|
||||||
@@ -83,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 }> {
|
||||||
@@ -111,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,14 +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 ?? [],
|
users: task.users ?? [],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,9 +115,9 @@ 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,
|
users: activeTask.users,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Input from '../../components/Input'
|
|||||||
import Textarea from '../../components/Textarea'
|
import Textarea from '../../components/Textarea'
|
||||||
import UploadBox from '../../components/UploadBox'
|
import UploadBox from '../../components/UploadBox'
|
||||||
import Button from '../../components/Button'
|
import Button from '../../components/Button'
|
||||||
import { CloseCircle, InfoCircle, Paperclip2 } from 'iconsax-react'
|
import { ArrowRight, CloseCircle, InfoCircle, Paperclip2 } from 'iconsax-react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { useAddMessageTicket, useCloseTicket, useGetMessages } from './hooks/useTicketData'
|
import { useAddMessageTicket, useCloseTicket, useGetMessages } from './hooks/useTicketData'
|
||||||
import PageLoading from '../../components/PageLoading'
|
import PageLoading from '../../components/PageLoading'
|
||||||
@@ -89,10 +89,20 @@ const TicketDetail: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
<div className='flex gap-1'>
|
<div className='flex gap-1'>
|
||||||
<div>{t('ticket.ticket_number')}</div>
|
<div>{t('ticket.ticket_number')}</div>
|
||||||
<div>{getMessages.data?.data?.ticket?.numericId}</div>
|
<div>{getMessages.data?.data?.ticket?.numericId}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
className='flex gap-1 items-center px-3 rounded-lg bg-white h-8 border border-border cursor-pointer'
|
||||||
|
>
|
||||||
|
<ArrowRight size={18} color='black' />
|
||||||
|
<span className='text-xs'>{t('back')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{
|
{
|
||||||
getMessages.isPending ?
|
getMessages.isPending ?
|
||||||
<PageLoading />
|
<PageLoading />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Eye, MessageRemove, MessageTick, MessageTime } from 'iconsax-react'
|
import { Eye, MessageRemove, MessageTick, MessageTime } from 'iconsax-react'
|
||||||
import Tabs from '../../components/Tabs'
|
import Tabs from '../../components/Tabs'
|
||||||
@@ -11,23 +11,63 @@ import { TicketItemType } from './types/TicketTypes'
|
|||||||
import OpenTicket from './components/OpenTicket'
|
import OpenTicket from './components/OpenTicket'
|
||||||
import Pagination from '../../components/Pagination'
|
import Pagination from '../../components/Pagination'
|
||||||
|
|
||||||
|
const VALID_STATUSES = ['ANSWERED', 'PENDING', 'CLOSED'] as const
|
||||||
|
type TicketStatus = (typeof VALID_STATUSES)[number] | ''
|
||||||
|
|
||||||
const TicketList: FC = () => {
|
const TicketList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [page, setPage] = useState<number>(1)
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const [searchParams] = useSearchParams()
|
|
||||||
const [customerId, setCustomerId] = useState<string>('')
|
const customerId = searchParams.get('user') ?? ''
|
||||||
const [activeTab, setActiveTab] = useState<'ANSWERED' | 'PENDING' | 'CLOSED' | ''>('PENDING')
|
const statusParam = searchParams.get('status')
|
||||||
|
|
||||||
|
const activeTab: TicketStatus = (() => {
|
||||||
|
if (statusParam && (VALID_STATUSES as readonly string[]).includes(statusParam)) {
|
||||||
|
return statusParam as (typeof VALID_STATUSES)[number]
|
||||||
|
}
|
||||||
|
// Customer deep-link without status → show all statuses for that user
|
||||||
|
if (customerId && !statusParam) return ''
|
||||||
|
return 'PENDING'
|
||||||
|
})()
|
||||||
|
|
||||||
|
const page = Math.max(1, parseInt(searchParams.get('page') || '1', 10) || 1)
|
||||||
const getTicket = useGetTickets(activeTab, customerId, page)
|
const getTicket = useGetTickets(activeTab, customerId, page)
|
||||||
|
|
||||||
|
// Keep status & page in the URL (replace so back button is not polluted)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const urlCustomerId = searchParams.get('user')
|
const next = new URLSearchParams(searchParams)
|
||||||
if (urlCustomerId) {
|
let changed = false
|
||||||
setCustomerId(urlCustomerId)
|
|
||||||
setActiveTab('')
|
|
||||||
}
|
|
||||||
}, [searchParams])
|
|
||||||
|
|
||||||
|
if (!statusParam && !customerId) {
|
||||||
|
next.set('status', 'PENDING')
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchParams.get('page')) {
|
||||||
|
next.set('page', String(page))
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
setSearchParams(next, { replace: true })
|
||||||
|
}
|
||||||
|
}, [customerId, page, searchParams, setSearchParams, statusParam])
|
||||||
|
|
||||||
|
const updateParams = (updates: { status?: string; page?: number }) => {
|
||||||
|
const next = new URLSearchParams(searchParams)
|
||||||
|
|
||||||
|
if (updates.status !== undefined) {
|
||||||
|
if (updates.status === '') next.delete('status')
|
||||||
|
else next.set('status', updates.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.page !== undefined) {
|
||||||
|
next.set('page', String(Math.max(1, updates.page)))
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchParams(next)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
@@ -75,7 +115,7 @@ const TicketList: FC = () => {
|
|||||||
value: 'CLOSED'
|
value: 'CLOSED'
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onChange={(value) => setActiveTab(value as 'ANSWERED' | 'PENDING' | 'CLOSED')}
|
onChange={(value) => updateParams({ status: value, page: 1 })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,7 +175,7 @@ const TicketList: FC = () => {
|
|||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={page}
|
currentPage={page}
|
||||||
onPageChange={setPage}
|
onPageChange={(newPage) => updateParams({ page: newPage })}
|
||||||
totalPages={getTicket.data?.data?.pager?.totalPages}
|
totalPages={getTicket.data?.data?.pager?.totalPages}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,75 +1,73 @@
|
|||||||
import { FC, useState } from 'react'
|
import { FC, useState } from "react";
|
||||||
import { useGetUsers } from '../../users/hooks/useUserData'
|
import { useTranslation } from "react-i18next";
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useParams } from "react-router-dom";
|
||||||
import { UserItemType } from '../../users/types/UserTypes'
|
import Button from "../../../components/Button";
|
||||||
import CheckBoxComponent from '../../../components/CheckBoxComponent'
|
import CheckBoxComponent from "../../../components/CheckBoxComponent";
|
||||||
import { useParams } from 'react-router-dom'
|
import { toast } from "../../../components/Toast";
|
||||||
import { useReferTicket } from '../hooks/useTicketData'
|
import { ErrorType } from "../../../helpers/types";
|
||||||
import Button from '../../../components/Button'
|
import { useGetAdminsListAll } from "../../users/hooks/useUserData";
|
||||||
import { toast } from '../../../components/Toast';
|
import { UserItemType } from "../../users/types/UserTypes";
|
||||||
import { ErrorType } from '../../../helpers/types'
|
import { useReferTicket } from "../hooks/useTicketData";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
refetch: () => void
|
refetch: () => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
const ReferTicket: FC<Props> = ({ refetch }) => {
|
const ReferTicket: FC<Props> = ({ refetch }) => {
|
||||||
|
const { t } = useTranslation("global");
|
||||||
const { t } = useTranslation('global')
|
const { id } = useParams();
|
||||||
const { id } = useParams()
|
const [userId, setUserId] = useState<string>("");
|
||||||
const [userId, setUserId] = useState<string>('')
|
const getUsers = useGetAdminsListAll();
|
||||||
const getUsers = useGetUsers('', 'tickets')
|
const referTicket = useReferTicket();
|
||||||
const referTicket = useReferTicket()
|
|
||||||
|
|
||||||
const handleReferTicket = () => {
|
const handleReferTicket = () => {
|
||||||
referTicket.mutate({ id: id as string, userId }, {
|
referTicket.mutate(
|
||||||
|
{ id: id as string, userId },
|
||||||
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
refetch()
|
refetch();
|
||||||
toast(t('ticket.refer_ticket_success'), 'success')
|
toast(t("ticket.refer_ticket_success"), "success");
|
||||||
},
|
},
|
||||||
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={'bg-white p-6 rounded-3xl mt-8'}>
|
<div className={"bg-white p-6 rounded-3xl mt-8"}>
|
||||||
<div className='flex justify-between'>
|
<div className="flex justify-between">
|
||||||
<div className='text-sm'>
|
<div className="text-sm">{t("ticket.refer_ticket")}</div>
|
||||||
{t('ticket.refer_ticket')}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
{
|
{getUsers.data?.data?.map((item: UserItemType) => (
|
||||||
getUsers.data?.data?.users?.map((item: UserItemType) => (
|
<div
|
||||||
<div key={item.id} className='flex gap-2 items-center'>
|
key={item.id}
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<CheckBoxComponent
|
<CheckBoxComponent
|
||||||
checked={userId === item.id}
|
checked={userId === item.id}
|
||||||
onChange={(e) => setUserId(e.target.checked ? item.id : '')}
|
onChange={(e) => setUserId(e.target.checked ? item.id : "")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-xs leading-5'>
|
<div className="text-xs leading-5">{item.firstName + " " + item.lastName}</div>
|
||||||
{item.firstName + ' ' + item.lastName}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className='flex justify-end mt-4'>
|
<div className="flex justify-end mt-4">
|
||||||
<Button
|
<Button
|
||||||
label={t('ticket.refer_ticket')}
|
label={t("ticket.refer_ticket")}
|
||||||
onClick={handleReferTicket}
|
onClick={handleReferTicket}
|
||||||
isLoading={referTicket.isPending}
|
isLoading={referTicket.isPending}
|
||||||
className='w-fit px-7'
|
className="w-fit px-7"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ReferTicket
|
export default ReferTicket;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
AddMessageTicketType,
|
AddMessageTicketType,
|
||||||
CreateCategoryType,
|
CreateCategoryType,
|
||||||
CreateTicketAdminType,
|
CreateTicketAdminType,
|
||||||
|
CreateTicketMasterType,
|
||||||
} from "../types/TicketTypes";
|
} from "../types/TicketTypes";
|
||||||
|
|
||||||
export const useGetTickets = (
|
export const useGetTickets = (
|
||||||
@@ -78,3 +79,53 @@ export const useCreateTicket = () => {
|
|||||||
api.createTicket(variables),
|
api.createTicket(variables),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetMasters = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["ticket-masters"],
|
||||||
|
queryFn: () => api.getMasters(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetMasterById = (id: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["ticket-master", id],
|
||||||
|
queryFn: () => api.getMasterById(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateMaster = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: CreateTicketMasterType) =>
|
||||||
|
api.createMaster(variables),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["ticket-masters"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateMaster = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: { id: string; params: CreateTicketMasterType }) =>
|
||||||
|
api.updateMaster(variables.id, variables.params),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["ticket-masters"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteMaster = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => api.deleteMaster(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["ticket-masters"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { useFormik } from "formik";
|
||||||
|
import { FC } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import Button from "../../../components/Button";
|
||||||
|
import Select from "../../../components/Select";
|
||||||
|
import { toast } from "../../../components/Toast";
|
||||||
|
import { Pages } from "../../../config/Pages";
|
||||||
|
import { ErrorType } from "../../../helpers/types";
|
||||||
|
import { useGetAdminsListAll } from "../../users/hooks/useUserData";
|
||||||
|
import { UserItemType } from "../../users/types/UserTypes";
|
||||||
|
import { useCreateMaster } from "../hooks/useTicketData";
|
||||||
|
import { CreateTicketMasterType } from "../types/TicketTypes";
|
||||||
|
|
||||||
|
const CreateTicketMaster: FC = () => {
|
||||||
|
const { t } = useTranslation("global");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const getAdmins = useGetAdminsListAll();
|
||||||
|
const createMaster = useCreateMaster();
|
||||||
|
|
||||||
|
const formik = useFormik<CreateTicketMasterType>({
|
||||||
|
initialValues: {
|
||||||
|
userId: "",
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
userId: Yup.string().required(t("errors.required")),
|
||||||
|
}),
|
||||||
|
onSubmit: (values) => {
|
||||||
|
createMaster.mutate(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast(t("success"), "success");
|
||||||
|
navigate(Pages.ticket.masters);
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div>{t("ticket.add_master")}</div>
|
||||||
|
|
||||||
|
<div className="bg-white mt-8 py-8 xl:px-10 px-4 rounded-3xl max-w-xl">
|
||||||
|
<Select
|
||||||
|
label={t("ticket.user")}
|
||||||
|
placeholder={t("select")}
|
||||||
|
className="border"
|
||||||
|
items={getAdmins.data?.data?.map((item: UserItemType) => ({
|
||||||
|
label: `${item.firstName} ${item.lastName}`,
|
||||||
|
value: item.id,
|
||||||
|
}))}
|
||||||
|
{...formik.getFieldProps("userId")}
|
||||||
|
error_text={formik.touched.userId && formik.errors.userId ? formik.errors.userId : ""}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-end">
|
||||||
|
<Button
|
||||||
|
label={t("submit")}
|
||||||
|
className="w-fit px-7"
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={createMaster.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateTicketMaster;
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { Add } from "iconsax-react";
|
||||||
|
import { FC } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import Button from "../../../components/Button";
|
||||||
|
import PageLoading from "../../../components/PageLoading";
|
||||||
|
import Td from "../../../components/Td";
|
||||||
|
import { toast } from "../../../components/Toast";
|
||||||
|
import TrashWithConfrim from "../../../components/TrashWithConfrim";
|
||||||
|
import { Pages } from "../../../config/Pages";
|
||||||
|
import { ErrorType } from "../../../helpers/types";
|
||||||
|
import { useDeleteMaster, useGetMasters } from "../hooks/useTicketData";
|
||||||
|
import { TicketMasterItemType } from "../types/TicketTypes";
|
||||||
|
import { usePermissions } from "../../../hooks/usePermissions";
|
||||||
|
|
||||||
|
const TicketMastersList: FC = () => {
|
||||||
|
const { t } = useTranslation("global");
|
||||||
|
const { canCreate, canDelete } = usePermissions();
|
||||||
|
const getMasters = useGetMasters();
|
||||||
|
const deleteMaster = useDeleteMaster();
|
||||||
|
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
deleteMaster.mutate(id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast(t("success"), "success");
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const masters: TicketMasterItemType[] = getMasters.data?.data?.masters ?? getMasters.data?.data?.ticketMasters ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="flex w-full justify-between items-center">
|
||||||
|
<div>{t("ticket.masters")}</div>
|
||||||
|
<div>
|
||||||
|
{canCreate("ticket_masters") && (
|
||||||
|
<Link to={Pages.ticket.mastersCreate}>
|
||||||
|
<Button className="px-5">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Add
|
||||||
|
className="size-5"
|
||||||
|
color="#fff"
|
||||||
|
/>
|
||||||
|
<div>{t("ticket.add_master")}</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{getMasters.isPending ? (
|
||||||
|
<PageLoading />
|
||||||
|
) : (
|
||||||
|
<div className="relative overflow-x-auto rounded-3xl mt-9 w-full">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="thead">
|
||||||
|
<tr>
|
||||||
|
<Td text={t("ticket.user_name")} />
|
||||||
|
<Td text={t("user.email")} />
|
||||||
|
<Td text={t("ticket.detail")} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{masters.map((item) => (
|
||||||
|
<tr
|
||||||
|
key={item.id}
|
||||||
|
className="tr"
|
||||||
|
>
|
||||||
|
<Td text={item.user ? `${item.user.firstName} ${item.user.lastName}` : item.userId} />
|
||||||
|
<Td text={item.user?.email ?? item.user?.phone ?? "-"} />
|
||||||
|
<Td text="">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{/* <Link to={Pages.ticket.mastersUpdate + item.id}>
|
||||||
|
<Edit size={20} color="#8C90A3" />
|
||||||
|
</Link> */}
|
||||||
|
{canDelete("ticket_masters") && (
|
||||||
|
<TrashWithConfrim
|
||||||
|
onDelete={() => handleDelete(item.id)}
|
||||||
|
isLoading={deleteMaster.isPending}
|
||||||
|
color="#8C90A3"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TicketMastersList;
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { useFormik } from "formik";
|
||||||
|
import { FC, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import Button from "../../../components/Button";
|
||||||
|
import PageLoading from "../../../components/PageLoading";
|
||||||
|
import Select from "../../../components/Select";
|
||||||
|
import { toast } from "../../../components/Toast";
|
||||||
|
import { Pages } from "../../../config/Pages";
|
||||||
|
import { ErrorType } from "../../../helpers/types";
|
||||||
|
import { useGetAdminsListAll } from "../../users/hooks/useUserData";
|
||||||
|
import { UserItemType } from "../../users/types/UserTypes";
|
||||||
|
import { useGetMasterById, useUpdateMaster } from "../hooks/useTicketData";
|
||||||
|
import { CreateTicketMasterType } from "../types/TicketTypes";
|
||||||
|
|
||||||
|
const UpdateTicketMaster: FC = () => {
|
||||||
|
const { t } = useTranslation("global");
|
||||||
|
const { id } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const getAdmins = useGetAdminsListAll();
|
||||||
|
const getMaster = useGetMasterById(id ?? "");
|
||||||
|
const updateMaster = useUpdateMaster();
|
||||||
|
|
||||||
|
const formik = useFormik<CreateTicketMasterType>({
|
||||||
|
initialValues: {
|
||||||
|
userId: "",
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
userId: Yup.string().required(t("errors.required")),
|
||||||
|
}),
|
||||||
|
onSubmit: (values) => {
|
||||||
|
updateMaster.mutate(
|
||||||
|
{ id: id as string, params: values },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast(t("success"), "success");
|
||||||
|
navigate(Pages.ticket.masters);
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error.message[0], "error");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const master = getMaster.data?.data?.master ?? getMaster.data?.data?.ticketMaster;
|
||||||
|
if (master?.userId) {
|
||||||
|
formik.setFieldValue("userId", master.userId);
|
||||||
|
}
|
||||||
|
}, [getMaster.data]);
|
||||||
|
|
||||||
|
if (getMaster.isPending) {
|
||||||
|
return <PageLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div>{t("ticket.edit_master")}</div>
|
||||||
|
|
||||||
|
<div className="bg-white mt-8 py-8 xl:px-10 px-4 rounded-3xl max-w-xl">
|
||||||
|
<Select
|
||||||
|
label={t("ticket.user")}
|
||||||
|
placeholder={t("select")}
|
||||||
|
className="border"
|
||||||
|
items={getAdmins.data?.data?.map((item: UserItemType) => ({
|
||||||
|
label: `${item.firstName} ${item.lastName}`,
|
||||||
|
value: item.id,
|
||||||
|
}))}
|
||||||
|
{...formik.getFieldProps("userId")}
|
||||||
|
error_text={formik.touched.userId && formik.errors.userId ? formik.errors.userId : ""}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-end">
|
||||||
|
<Button
|
||||||
|
label={t("submit")}
|
||||||
|
className="w-fit px-7"
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={updateMaster.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateTicketMaster;
|
||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
AddMessageTicketType,
|
AddMessageTicketType,
|
||||||
CreateCategoryType,
|
CreateCategoryType,
|
||||||
CreateTicketAdminType,
|
CreateTicketAdminType,
|
||||||
|
CreateTicketMasterType,
|
||||||
} from "../types/TicketTypes";
|
} from "../types/TicketTypes";
|
||||||
|
|
||||||
export const getTickets = async (
|
export const getTickets = async (
|
||||||
@@ -63,3 +64,28 @@ export const createTicket = async (params: CreateTicketAdminType) => {
|
|||||||
const { data } = await axios.post(`/tickets/admin`, params);
|
const { data } = await axios.post(`/tickets/admin`, params);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMasters = async () => {
|
||||||
|
const { data } = await axios.get(`/tickets/masters`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMasterById = async (id: string) => {
|
||||||
|
const { data } = await axios.get(`/tickets/masters/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createMaster = async (params: CreateTicketMasterType) => {
|
||||||
|
const { data } = await axios.post(`/tickets/masters`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateMaster = async (id: string, params: CreateTicketMasterType) => {
|
||||||
|
const { data } = await axios.patch(`/tickets/masters/${id}`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteMaster = async (id: string) => {
|
||||||
|
const { data } = await axios.delete(`/tickets/masters/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -89,3 +89,21 @@ export type CreateTicketAdminType = {
|
|||||||
attachmentUrls?: string[];
|
attachmentUrls?: string[];
|
||||||
userId: string;
|
userId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateTicketMasterType = {
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TicketMasterItemType = {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
};
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import { Pages } from '../../config/Pages'
|
|||||||
import { Add } from 'iconsax-react'
|
import { Add } from 'iconsax-react'
|
||||||
import Td from '../../components/Td'
|
import Td from '../../components/Td'
|
||||||
import { GroupItemType } from './types/UserTypes'
|
import { GroupItemType } from './types/UserTypes'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const GroupList: FC = () => {
|
const GroupList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { canCreate } = usePermissions()
|
||||||
const getGroups = useGetGroups()
|
const getGroups = useGetGroups()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -20,6 +22,7 @@ const GroupList: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('user.groups')}
|
{t('user.groups')}
|
||||||
</div>
|
</div>
|
||||||
|
{canCreate('admins') && (
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className='px-5'
|
className='px-5'
|
||||||
@@ -34,6 +37,7 @@ const GroupList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</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'>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Button from '../../components/Button'
|
import Button from '../../components/Button'
|
||||||
import { Add, Eye, Trash } from 'iconsax-react'
|
import { Add, Eye } from 'iconsax-react'
|
||||||
import Input from '../../components/Input'
|
import Input from '../../components/Input'
|
||||||
import Td from '../../components/Td'
|
import Td from '../../components/Td'
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
@@ -12,10 +12,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 Pagination from '../../components/Pagination'
|
import Pagination from '../../components/Pagination'
|
||||||
|
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
const UsersList: FC = () => {
|
const UsersList: FC = () => {
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||||
const [page, setPage] = useState<number>(1)
|
const [page, setPage] = useState<number>(1)
|
||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
const getUsers = useGetUsers(search, undefined, page)
|
const getUsers = useGetUsers(search, undefined, page)
|
||||||
@@ -39,6 +42,7 @@ const UsersList: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('user.users_list')}
|
{t('user.users_list')}
|
||||||
</div>
|
</div>
|
||||||
|
{canCreate('admins') && (
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className='px-5'
|
className='px-5'
|
||||||
@@ -53,6 +57,7 @@ const UsersList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-between items-center mt-12'>
|
<div className='flex justify-between items-center mt-12'>
|
||||||
@@ -90,6 +95,7 @@ const UsersList: FC = () => {
|
|||||||
<Td text={item.firstName + ' ' + item.lastName} />
|
<Td text={item.firstName + ' ' + item.lastName} />
|
||||||
<Td text={item.email} />
|
<Td text={item.email} />
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
|
<div className='flex flex-wrap gap-2'>
|
||||||
{
|
{
|
||||||
item.roles.map((role) => (
|
item.roles.map((role) => (
|
||||||
<div key={role.id} className='bg-primary w-fit text-white px-2 py-1 rounded-2xl text-xs'>
|
<div key={role.id} className='bg-primary w-fit text-white px-2 py-1 rounded-2xl text-xs'>
|
||||||
@@ -97,18 +103,24 @@ const UsersList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
|
{canUpdate('admins') && (
|
||||||
<Link to={Pages.users.detail + item.id}>
|
<Link to={Pages.users.detail + item.id}>
|
||||||
<Eye size={20} color='#8C90A3' />
|
<Eye size={20} color='#8C90A3' />
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
<Trash
|
{canDelete('admins') && (
|
||||||
|
<TrashWithConfrim
|
||||||
size={20}
|
size={20}
|
||||||
color='#8C90A3'
|
color='#8C90A3'
|
||||||
onClick={() => handleDeleteUser(item.id)}
|
onDelete={() => handleDeleteUser(item.id)}
|
||||||
|
isLoading={deleteUser.isPending}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td text={''} />
|
<Td text={''} />
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import { Link, useNavigate } from 'react-router-dom'
|
|||||||
import { Pages } from '../../config/Pages'
|
import { Pages } from '../../config/Pages'
|
||||||
import { useGetRoles } from './hooks/useUserData'
|
import { useGetRoles } from './hooks/useUserData'
|
||||||
import { RoleItemType } from './types/UserTypes'
|
import { RoleItemType } from './types/UserTypes'
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions'
|
||||||
|
|
||||||
const RolesList: FC = () => {
|
const RolesList: FC = () => {
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const { canCreate, canUpdate } = usePermissions()
|
||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
const getRoles = useGetRoles(search)
|
const getRoles = useGetRoles(search)
|
||||||
|
|
||||||
@@ -23,6 +25,7 @@ const RolesList: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
{t('user.roles_list')}
|
{t('user.roles_list')}
|
||||||
</div>
|
</div>
|
||||||
|
{canCreate('admins') && (
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className='px-5'
|
className='px-5'
|
||||||
@@ -37,6 +40,7 @@ const RolesList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-end items-center mt-12'>
|
<div className='flex justify-end items-center mt-12'>
|
||||||
@@ -69,9 +73,11 @@ const RolesList: FC = () => {
|
|||||||
<Td text={item.userCount + ''} />
|
<Td text={item.userCount + ''} />
|
||||||
<Td text={item.isAdmin ? t('yes') : t('no')} />
|
<Td text={item.isAdmin ? t('yes') : t('no')} />
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
|
{canUpdate('admins') && (
|
||||||
<Link to={Pages.users.roleUpdate + item.id}>
|
<Link to={Pages.users.roleUpdate + item.id}>
|
||||||
<Eye size={20} color='#8C90A3' />
|
<Eye size={20} color='#8C90A3' />
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user