This commit is contained in:
hamid zarghami
2025-02-01 12:33:33 +03:30
parent 49e09fa9ab
commit 893f5e2c8d
16 changed files with 808 additions and 278 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
VITE_TOKEN_NAME = 'dsc_token' VITE_TOKEN_NAME = 'dsc_token'
VITE_REFRESH_TOKEN_NAME = 'dsc_refresh_token' VITE_REFRESH_TOKEN_NAME = 'dsc_refresh_token'
VITE_BASE_URL = 'http://192.168.0.235:3500' VITE_BASE_URL = 'http://192.168.0.221:3500'
+50
View File
@@ -15,6 +15,7 @@
"formik": "^2.4.6", "formik": "^2.4.6",
"i18next": "^24.2.0", "i18next": "^24.2.0",
"iconsax-react": "^0.0.8", "iconsax-react": "^0.0.8",
"moment-jalaali": "^0.10.4",
"rc-rate": "^2.13.0", "rc-rate": "^2.13.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
@@ -33,6 +34,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",
"@types/moment-jalaali": "^0.7.9",
"@types/react": "^18.3.17", "@types/react": "^18.3.17",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",
"@types/swiper": "^5.4.3", "@types/swiper": "^5.4.3",
@@ -1841,6 +1843,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/moment-jalaali": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/@types/moment-jalaali/-/moment-jalaali-0.7.9.tgz",
"integrity": "sha512-gsDOoAzRnCfQTbfdlUrCvX6R0wIto6CvwfvV2C3j4qJLK+DEiTK8Rl/xlOCBO9C6qeUfX8oyZ2UfjnXJTOvHSA==",
"dev": true,
"license": "MIT",
"dependencies": {
"moment": ">=2.14.0"
}
},
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
"version": "15.7.14", "version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
@@ -3492,6 +3504,12 @@
"@pkgjs/parseargs": "^0.11.0" "@pkgjs/parseargs": "^0.11.0"
} }
}, },
"node_modules/jalaali-js": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.7.tgz",
"integrity": "sha512-gE+YHWSbygYAoJa+Xg8LWxGILqFOxZSBQQw39ghel01fVFUxV7bjL0x1JFsHcLQ3uPjvn81HQMa+kxwyPWnxGQ==",
"license": "MIT"
},
"node_modules/jiti": { "node_modules/jiti": {
"version": "1.21.7", "version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
@@ -3743,6 +3761,38 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/moment-jalaali": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/moment-jalaali/-/moment-jalaali-0.10.4.tgz",
"integrity": "sha512-/eD0HeyvATznb5iE0G1BHjKRZAFEpJ9ZNUkcHwXhNgt1WJJVVzHD7+uDmqzZWVFLdbGme2gvIXKb3ezDYOXcZA==",
"license": "MIT",
"dependencies": {
"jalaali-js": "^1.2.7",
"moment": "^2.29.4",
"moment-timezone": "^0.5.46"
}
},
"node_modules/moment-timezone": {
"version": "0.5.47",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.47.tgz",
"integrity": "sha512-UbNt/JAWS0m/NJOebR0QMRHBk0hu03r5dx9GK8Cs0AS3I81yDcOc9k+DytPItgVvBP7J6Mf6U2n3BPAacAV9oA==",
"license": "MIT",
"dependencies": {
"moment": "^2.29.4"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+2
View File
@@ -17,6 +17,7 @@
"formik": "^2.4.6", "formik": "^2.4.6",
"i18next": "^24.2.0", "i18next": "^24.2.0",
"iconsax-react": "^0.0.8", "iconsax-react": "^0.0.8",
"moment-jalaali": "^0.10.4",
"rc-rate": "^2.13.0", "rc-rate": "^2.13.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
@@ -35,6 +36,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",
"@types/moment-jalaali": "^0.7.9",
"@types/react": "^18.3.17", "@types/react": "^18.3.17",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",
"@types/swiper": "^5.4.3", "@types/swiper": "^5.4.3",
+45
View File
@@ -0,0 +1,45 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 201.8 46.7" xml:space="preserve" class="logo-shape dark" style="enable-background:new 0 0 201.8 46.7;"><g><g><path d="M51,38.6h-1.9v5.1h-0.8v-5.1h-1.9V38H51L51,38.6L51,38.6z" class="st0"></path> <path d="M57.6,43.8c-0.6,0-1.1-0.2-1.4-0.6c-0.3-0.4-0.5-0.9-0.5-1.5v-0.2c0-0.6,0.2-1.1,0.5-1.5s0.8-0.6,1.3-0.6
c0.6,0,1,0.2,1.3,0.5s0.4,0.8,0.4,1.4v0.4h-2.7l0,0c0,0.4,0.1,0.8,0.3,1c0.2,0.3,0.5,0.4,0.9,0.4c0.3,0,0.5,0,0.7-0.1
c0.2-0.1,0.4-0.2,0.5-0.3l0.3,0.5c-0.1,0.1-0.4,0.3-0.6,0.4C58.3,43.7,58,43.8,57.6,43.8z M57.5,40c-0.3,0-0.5,0.1-0.7,0.3
c-0.2,0.2-0.3,0.5-0.3,0.8l0,0h1.9V41c0-0.3-0.1-0.6-0.2-0.8S57.8,40,57.5,40z" class="st0"></path> <path d="M66.3,43.2c0.2,0,0.5-0.1,0.6-0.2s0.3-0.4,0.3-0.6h0.7l0,0c0,0.4-0.1,0.7-0.5,1c-0.3,0.3-0.7,0.4-1.1,0.4
c-0.6,0-1.1-0.2-1.4-0.6c-0.3-0.4-0.5-0.9-0.5-1.5v-0.2c0-0.6,0.2-1.1,0.5-1.5s0.8-0.6,1.4-0.6c0.5,0,0.9,0.1,1.2,0.4
c0.3,0.3,0.5,0.7,0.4,1.1l0,0h-0.7c0-0.3-0.1-0.5-0.3-0.7S66.5,40,66.3,40c-0.4,0-0.7,0.1-0.8,0.4c-0.2,0.3-0.3,0.6-0.3,1.1v0.2
c0,0.4,0.1,0.8,0.3,1.1C65.6,43,65.9,43.2,66.3,43.2z" class="st0"></path> <path d="M74,40c0.1-0.2,0.3-0.4,0.5-0.5s0.5-0.2,0.7-0.2c0.5,0,0.8,0.1,1.1,0.4s0.4,0.7,0.4,1.3v2.7H76V41
c0-0.4-0.1-0.6-0.2-0.8C75.6,40.1,75.3,40,75,40c-0.2,0-0.4,0-0.6,0.1c-0.2,0.1-0.3,0.2-0.4,0.4v3.2h-0.8v-6.1H74L74,40L74,40z" class="st0"></path> <path d="M83,39.4l0.1,0.6c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.7-0.2c0.5,0,0.8,0.1,1.1,0.4s0.4,0.7,0.4,1.3
v2.7H85V41c0-0.4-0.1-0.6-0.2-0.8c-0.1-0.2-0.4-0.2-0.7-0.2c-0.2,0-0.4,0-0.6,0.1c-0.2,0.1-0.3,0.2-0.4,0.4v3.2h-0.8v-4.3L83,39.4
L83,39.4z" class="st0"></path> <path d="M91.2,41.5c0-0.6,0.2-1.1,0.5-1.6s0.8-0.6,1.4-0.6c0.6,0,1.1,0.2,1.4,0.6s0.5,0.9,0.5,1.6v0.1
c0,0.6-0.2,1.2-0.5,1.6s-0.8,0.6-1.4,0.6c-0.6,0-1-0.2-1.4-0.6C91.4,42.8,91.2,42.3,91.2,41.5L91.2,41.5z M92,41.6
c0,0.5,0.1,0.8,0.3,1.1s0.5,0.4,0.9,0.4c0.4,0,0.6-0.1,0.8-0.4s0.3-0.7,0.3-1.1v-0.1c0-0.5-0.1-0.8-0.3-1.1
c-0.2-0.3-0.5-0.5-0.9-0.5s-0.6,0.1-0.8,0.5S92,41.1,92,41.6L92,41.6z" class="st0"></path> <path d="M101.3,43.7h-0.8v-6.1h0.8V43.7z" class="st0"></path> <path d="M106.8,41.5c0-0.6,0.2-1.1,0.5-1.6s0.8-0.6,1.4-0.6c0.6,0,1.1,0.2,1.4,0.6s0.5,0.9,0.5,1.6v0.1
c0,0.6-0.2,1.2-0.5,1.6s-0.8,0.6-1.4,0.6s-1-0.2-1.4-0.6C107,42.8,106.8,42.3,106.8,41.5L106.8,41.5z M107.6,41.6
c0,0.5,0.1,0.8,0.3,1.1s0.5,0.4,0.9,0.4c0.4,0,0.6-0.1,0.8-0.4s0.3-0.7,0.3-1.1v-0.1c0-0.5-0.1-0.8-0.3-1.1s-0.5-0.5-0.9-0.5
s-0.6,0.1-0.8,0.5S107.6,41.1,107.6,41.6L107.6,41.6z" class="st0"></path> <path d="M115.9,41.7c0-0.7,0.1-1.2,0.4-1.7s0.7-0.6,1.2-0.6c0.3,0,0.5,0,0.7,0.2c0.2,0.1,0.4,0.3,0.5,0.5l0.1-0.6h0.6
v4.3c0,0.5-0.2,1-0.5,1.3s-0.8,0.4-1.4,0.4c-0.2,0-0.4,0-0.7-0.1c-0.2-0.1-0.5-0.1-0.6-0.2l0.1-0.6c0.1,0.1,0.3,0.1,0.5,0.2
c0.2,0,0.4,0.1,0.6,0.1c0.4,0,0.6-0.1,0.8-0.3c0.2-0.2,0.3-0.4,0.3-0.8v-0.5c-0.1,0.2-0.3,0.3-0.5,0.4c-0.2,0.1-0.4,0.1-0.7,0.1
c-0.5,0-0.9-0.2-1.2-0.6S115.9,42.4,115.9,41.7L115.9,41.7L115.9,41.7z M116.7,41.7c0,0.4,0.1,0.8,0.3,1s0.5,0.4,0.8,0.4
c0.2,0,0.4,0,0.6-0.2c0.2-0.1,0.3-0.3,0.4-0.5v-2c-0.1-0.2-0.2-0.3-0.4-0.4c-0.2-0.1-0.4-0.2-0.6-0.2c-0.4,0-0.6,0.1-0.8,0.5
S116.7,41.2,116.7,41.7L116.7,41.7L116.7,41.7z" class="st0"></path> <path d="M126.4,42.1l0.1,0.5h0l1-3.2h0.9l-1.8,4.9c-0.1,0.3-0.3,0.5-0.5,0.8c-0.2,0.2-0.5,0.3-0.8,0.3
c-0.1,0-0.1,0-0.2,0c-0.1,0-0.2,0-0.2,0l0.1-0.6c0,0,0,0,0.1,0c0.1,0,0.2,0,0.2,0c0.2,0,0.3-0.1,0.4-0.2c0.1-0.1,0.2-0.3,0.3-0.5
l0.2-0.5l-1.6-4.2h0.9L126.4,42.1z" class="st0"></path> <path d="M144.4,41.9L144.4,41.9c0,0.6-0.2,1-0.6,1.4s-0.9,0.5-1.5,0.5c-0.6,0-1.2-0.2-1.6-0.7s-0.6-1-0.6-1.8v-1
c0-0.7,0.2-1.3,0.6-1.8s0.9-0.7,1.6-0.7s1.2,0.2,1.5,0.5c0.4,0.3,0.6,0.8,0.6,1.4l0,0h-0.8c0-0.4-0.1-0.7-0.4-1
c-0.2-0.2-0.6-0.4-1-0.4s-0.8,0.2-1,0.5c-0.3,0.4-0.4,0.8-0.4,1.3v1c0,0.5,0.1,1,0.4,1.3s0.6,0.5,1,0.5c0.4,0,0.8-0.1,1-0.4
s0.4-0.5,0.4-1L144.4,41.9L144.4,41.9z" class="st0"></path> <path d="M149.6,41.5c0-0.6,0.2-1.1,0.5-1.6s0.8-0.6,1.4-0.6c0.6,0,1.1,0.2,1.4,0.6s0.5,0.9,0.5,1.6v0.1
c0,0.6-0.2,1.2-0.5,1.6s-0.8,0.6-1.4,0.6s-1-0.2-1.4-0.6C149.8,42.8,149.6,42.3,149.6,41.5L149.6,41.5z M150.4,41.6
c0,0.5,0.1,0.8,0.3,1.1s0.5,0.4,0.9,0.4c0.4,0,0.6-0.1,0.8-0.4s0.3-0.7,0.3-1.1v-0.1c0-0.5-0.1-0.8-0.3-1.1s-0.5-0.5-0.9-0.5
s-0.6,0.1-0.8,0.5S150.4,41.1,150.4,41.6L150.4,41.6z" class="st0"></path> <path d="M159.5,39.4l0,0.6c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.8-0.2s0.5,0.1,0.7,0.2s0.4,0.3,0.5,0.6
c0.1-0.3,0.3-0.4,0.5-0.6c0.2-0.1,0.5-0.2,0.8-0.2c0.4,0,0.8,0.1,1,0.5c0.3,0.3,0.4,0.8,0.4,1.3v2.5H164v-2.6
c0-0.4-0.1-0.7-0.2-0.9c-0.1-0.2-0.4-0.3-0.6-0.3c-0.3,0-0.5,0.1-0.6,0.3c-0.2,0.2-0.3,0.4-0.3,0.7v0v2.7h-0.8v-2.6
c0-0.4-0.1-0.7-0.2-0.9s-0.4-0.3-0.6-0.3c-0.2,0-0.4,0-0.6,0.1s-0.3,0.2-0.4,0.4v3.2h-0.8v-4.3L159.5,39.4L159.5,39.4z" class="st0"></path> <path d="M174,41.7c0,0.6-0.1,1.1-0.4,1.5s-0.7,0.6-1.2,0.6c-0.3,0-0.5,0-0.7-0.1s-0.4-0.2-0.5-0.4v2.1h-0.8v-5.9h0.6
l0.1,0.5c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.7-0.2c0.5,0,0.9,0.2,1.2,0.6S174,41,174,41.7L174,41.7L174,41.7z M173.2,41.7
c0-0.5-0.1-0.9-0.3-1.2s-0.5-0.5-0.8-0.5c-0.2,0-0.4,0-0.6,0.1s-0.3,0.2-0.4,0.4v2.1c0.1,0.2,0.2,0.3,0.4,0.4s0.4,0.1,0.6,0.1
c0.4,0,0.6-0.1,0.8-0.4C173.1,42.5,173.2,42.2,173.2,41.7L173.2,41.7L173.2,41.7z" class="st0"></path> <path d="M182.1,43.7c0-0.1,0-0.2-0.1-0.3s0-0.2,0-0.3c-0.1,0.2-0.3,0.4-0.6,0.5s-0.5,0.2-0.8,0.2
c-0.4,0-0.8-0.1-1-0.3s-0.4-0.5-0.4-0.9c0-0.4,0.2-0.7,0.5-1c0.3-0.2,0.8-0.3,1.3-0.3h0.8v-0.4c0-0.3-0.1-0.5-0.2-0.6
s-0.4-0.2-0.6-0.2c-0.3,0-0.5,0.1-0.6,0.2c-0.2,0.1-0.2,0.3-0.2,0.5l-0.7,0l0,0c0-0.3,0.1-0.6,0.4-0.9s0.7-0.4,1.2-0.4
c0.5,0,0.9,0.1,1.2,0.4c0.3,0.3,0.4,0.6,0.4,1.1v2c0,0.1,0,0.3,0,0.4c0,0.1,0,0.3,0.1,0.4L182.1,43.7L182.1,43.7z M180.8,43.1
c0.3,0,0.5-0.1,0.8-0.2c0.2-0.1,0.4-0.3,0.4-0.5v-0.7h-0.9c-0.3,0-0.6,0.1-0.8,0.2s-0.3,0.3-0.3,0.6c0,0.2,0.1,0.3,0.2,0.5
S180.5,43.1,180.8,43.1z" class="st0"></path> <path d="M189.1,39.4l0.1,0.6c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.7-0.2c0.5,0,0.8,0.1,1.1,0.4s0.4,0.7,0.4,1.3
v2.7H191V41c0-0.4-0.1-0.6-0.2-0.8c-0.1-0.2-0.4-0.2-0.7-0.2c-0.2,0-0.4,0-0.6,0.1c-0.2,0.1-0.3,0.2-0.4,0.4v3.2h-0.8v-4.3
L189.1,39.4L189.1,39.4z" class="st0"></path> <path d="M198.7,42.1l0.1,0.5h0l1-3.2h0.9l-1.8,4.9c-0.1,0.3-0.3,0.5-0.5,0.8s-0.5,0.3-0.8,0.3c-0.1,0-0.1,0-0.2,0
c-0.1,0-0.2,0-0.2,0l0.1-0.6c0,0,0,0,0.1,0s0.2,0,0.2,0c0.2,0,0.3-0.1,0.4-0.2s0.2-0.3,0.3-0.5l0.2-0.5l-1.6-4.2h0.9L198.7,42.1z" class="st0"></path></g></g> <g><path d="M46.4,31.9V9.9h6.2c1.9,0,3.6,0.4,5.1,1.3c1.5,0.9,2.6,2.1,3.4,3.6c0.8,1.6,1.2,3.4,1.2,5.4v1.4
c0,2.1-0.4,3.9-1.2,5.5c-0.8,1.6-2,2.8-3.4,3.6s-3.2,1.3-5.2,1.3H46.4z M49.3,12.2v17.3h3.1c2.2,0,4-0.7,5.2-2.1
c1.3-1.4,1.9-3.4,1.9-6v-1.3c0-2.5-0.6-4.5-1.8-5.9s-2.9-2.1-5-2.1L49.3,12.2L49.3,12.2z" class="st0"></path> <path d="M131.9,31.9H129l-11.1-17v17h-2.9V9.9h2.9L129,26.9V9.9h2.9L131.9,31.9L131.9,31.9z" class="st0"></path> <path d="M189.6,21.7l-2.7,2.8v7.5H184V9.8h2.9v10.9l9.8-10.9h3.5l-8.7,9.7l9.4,12.3h-3.5L189.6,21.7z" class="st0"></path> <polygon points="88.9,13.4 95.6,31.9 98.6,31.9 90.1,9.8 87.6,9.8 79.2,31.9 82.2,31.9 " class="st0"></polygon> <polygon points="157.9,13.4 164.7,31.9 167.6,31.9 159.2,9.8 156.7,9.8 148.2,31.9 151.2,31.9 " class="st0"></polygon></g> <path d="M27.5,30.2c0,0-5.5,5.3-10.1,9.8v6.1c-0.5,0.4,16.4-15.9,16.4-15.9L17.4,13v6.5L27.5,30.2z" class="st0"></path> <g><rect x="9.1" y="13.1" transform="matrix(0.7298 -0.6837 0.6837 0.7298 -14.5845 13.0725)" width="0.3" height="23.8" class="st0"></rect> <rect x="9.9" y="13.4" transform="matrix(0.7287 -0.6849 0.6849 0.7287 -13.8472 13.4064)" width="0.3" height="21.6" class="st0"></rect> <polygon points="17.3,30.5 4,16.4 4.2,16.2 17.5,30.3 " class="st0"></polygon> <polygon points="17.3,28.9 5.5,16.5 5.7,16.3 17.5,28.7 " class="st0"></polygon> <polygon points="17.2,27.2 7,16.5 7.2,16.3 17.5,27.1 " class="st0"></polygon></g> <g><polygon points="1.3,16.5 1.1,16.3 17.3,0.5 17.5,0.7 " class="st0"></polygon> <polygon points="2.8,16.5 2.6,16.3 17.3,2 17.4,2.2 " class="st0"></polygon> <polygon points="4.3,16.5 4.1,16.3 17.2,3.5 17.4,3.7 " class="st0"></polygon> <polygon points="5.8,16.5 5.6,16.3 17.2,5 17.4,5.2 " class="st0"></polygon> <polygon points="7.4,16.5 7.2,16.3 17.2,6.5 17.4,6.7 " class="st0"></polygon></g></svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

+1 -1
View File
@@ -7,7 +7,7 @@ type Props = {
className?: string; className?: string;
isLoading?: boolean, isLoading?: boolean,
} & ButtonHTMLAttributes<HTMLButtonElement> & } & ButtonHTMLAttributes<HTMLButtonElement> &
XOR<{ children: ReactNode }, { label: string, isLoading?: boolean }>; XOR<{ children: ReactNode }, { label: string }>;
const Button: FC<Props> = memo((props: Props) => { const Button: FC<Props> = memo((props: Props) => {
+19
View File
@@ -0,0 +1,19 @@
import { FC } from 'react'
import Logo from '../assets/images/logo_orig.svg'
import { useTranslation } from 'react-i18next'
const PageLoading: FC = () => {
const { t } = useTranslation('global')
return (
<div className='flex bg-transparent w-fit mx-auto flex-col gap-4 items-center'>
<img src={Logo} alt='logo' className='w-28' />
<div className='text-xs text-gray-700'>
{t('loading')}
</div>
</div>
)
}
export default PageLoading
+1 -1
View File
@@ -45,7 +45,7 @@ const Select: FC<Props> = (props: Props) => {
</select> </select>
<ArrowDown2 size={16} color='black' className={clx( <ArrowDown2 size={16} color='black' className={clx(
'absolute z-0 top-3 left-2', 'absolute z-0 top-3 left-2',
props.label && 'top-9' props.label && 'top-10'
)} /> )} />
{ {
props.error_text && props.error_text !== '' ? props.error_text && props.error_text !== '' ?
+6
View File
@@ -1,7 +1,9 @@
import { FC, InputHTMLAttributes } from 'react' import { FC, InputHTMLAttributes } from 'react'
import Error from './Error'
type Props = { type Props = {
label: string, label: string,
error_text?: string
} & InputHTMLAttributes<HTMLTextAreaElement> } & InputHTMLAttributes<HTMLTextAreaElement>
const Textarea: FC<Props> = (props: Props) => { const Textarea: FC<Props> = (props: Props) => {
@@ -17,6 +19,10 @@ const Textarea: FC<Props> = (props: Props) => {
> >
</textarea> </textarea>
{
props.error_text &&
<Error errorText={props.error_text} />
}
</div> </div>
) )
+50 -9
View File
@@ -1,22 +1,51 @@
import { FC, useCallback, useState } from 'react' import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDropzone } from 'react-dropzone' import { useDropzone } from 'react-dropzone'
import { CloseCircle } from 'iconsax-react'
type Props = { type Props = {
label: string, label: string,
isMultiple?: boolean,
onChange: (file: File[]) => void;
isReset?: boolean;
} }
const UploadBox: FC<Props> = (props: Props) => { const UploadBox: FC<Props> = (props: Props) => {
const { t } = useTranslation('global') const { t } = useTranslation('global')
const [file, setFile] = useState<File>() const [files, setFiles] = useState<File[]>([])
const onDrop = useCallback((acceptedFiles: File[]) => { const onDrop = useCallback((acceptedFiles: File[]) => {
setFile(acceptedFiles[0]) if (props.isMultiple) {
}, []) const array = [...files]
array.push(acceptedFiles[0])
setFiles(array)
props.onChange(array)
} else {
setFiles([acceptedFiles[0]])
props.onChange([acceptedFiles[0]])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [files])
const { getRootProps, getInputProps } = useDropzone({ onDrop }) const { getRootProps, getInputProps } = useDropzone({ onDrop })
const handleRemove = (index: number) => {
const array = [...files]
array.splice(index, 1)
setFiles(array)
props.onChange(array)
}
useEffect(() => {
if (props.isReset) {
setFiles([])
}
}, [props.isReset])
return ( return (
<div> <div>
@@ -28,15 +57,27 @@ const UploadBox: FC<Props> = (props: Props) => {
{t('select_file')} {t('select_file')}
</button> </button>
<div className='text-description text-xs'> <div className='lg:flex gap-2 hidden'>
{ {
file ? files?.map((item, index) => (
file.name <div key={index} className='flex bg-gray-200 py-1.5 px-2 rounded-lg items-center gap-1'>
: <CloseCircle onClick={() => handleRemove(index)} size={16} color='red' />
t('no_select_file') <div className='text-xs'>{item.name}</div>
</div>
))
} }
</div> </div>
</div> </div>
<div className='lg:hidden gap-2 flex flex-wrap mt-4'>
{
files?.map((item, index) => (
<div key={index} className='flex bg-gray-200 py-1.5 px-2 rounded-lg items-center gap-1'>
<CloseCircle onClick={() => handleRemove(index)} size={16} color='red' />
<div className='text-xs'>{item.name}</div>
</div>
))
}
</div>
</div> </div>
) )
} }
+15 -2
View File
@@ -148,8 +148,20 @@
"send_answer": "ارسال پاسخ", "send_answer": "ارسال پاسخ",
"close_ticket": "بستن تیکت", "close_ticket": "بستن تیکت",
"info_ticket": "اطلاعات تیکت", "info_ticket": "اطلاعات تیکت",
"update_sms": "به روزرسانی این تیکت از طریق پیامک اطلاع داده میشود" "update_sms": "به روزرسانی این تیکت از طریق پیامک اطلاع داده میشود",
"low": "پایین",
"medium": "متوسط",
"high": "بالا",
"category": "دسته بندی",
"asignto": "کارشناس پاسخگویی",
"MEDIUM": "متوسط",
"LOW": "پایین",
"HIGH": "بالا",
"PENDING": "در انتظار",
"ANSWERED": "پاسخ داده شده",
"CLOSED": "بسته شده"
}, },
"date": "تاریخ",
"receip": { "receip": {
"receip_list": "لیست صورتحساب ها", "receip_list": "لیست صورتحساب ها",
"dont_pay": "پرداخت نشده", "dont_pay": "پرداخت نشده",
@@ -288,5 +300,6 @@
"username_save_success": "نام کاربری با موفقیت ذخیره شد" "username_save_success": "نام کاربری با موفقیت ذخیره شد"
}, },
"email": "ایمیل", "email": "ایمیل",
"save": "ذخیره" "save": "ذخیره",
"select": "انتخاب"
} }
+121 -8
View File
@@ -1,4 +1,4 @@
import { FC } from 'react' import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Input from '../../components/Input' import Input from '../../components/Input'
import Select from '../../components/Select' import Select from '../../components/Select'
@@ -6,10 +6,77 @@ 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 { TickSquare } from 'iconsax-react' import { TickSquare } from 'iconsax-react'
import { useCreateTicket, useGetCategoriesTicket, useMultiUpload } from './hooks/useTicketData'
import { useGetProfile } from '../profile/hooks/useProfileData'
import PageLoading from '../../components/PageLoading'
import { useFormik } from 'formik'
import { CreateTicketType } from './types/TicketTypes'
import * as Yup from 'yup'
import { toast } from 'react-toastify'
import { ErrorType } from '../../helpers/types'
import { useNavigate } from 'react-router-dom'
import { Pages } from '../../config/Pages'
const CreateTicket: FC = () => { const CreateTicket: FC = () => {
const { t } = useTranslation('global') const { t } = useTranslation('global')
const navigate = useNavigate()
const [files, setFiles] = useState<File[]>([])
const getProfile = useGetProfile()
const getCategories = useGetCategoriesTicket()
const multiUpload = useMultiUpload()
const createTicket = useCreateTicket()
const formik = useFormik<CreateTicketType>({
initialValues: {
subject: '',
message: '',
categoryId: '',
priority: 'LOW',
danakServiceId: '',
title: '',
attachmentUrls: []
},
validationSchema: Yup.object({
subject: Yup.string().required(t('errors.required')),
message: Yup.string().required(t('errors.required')),
categoryId: Yup.string().required(t('errors.required')),
priority: Yup.string().required(t('errors.required')),
}),
onSubmit: async (values) => {
if (files.length > 0) {
const images = new FormData()
files.forEach(file => {
images.append('files', file)
})
await multiUpload.mutateAsync(images, {
onSuccess: async (data) => {
values.attachmentUrls = await data.data.map((item: { url: string }) => item?.url)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
} else {
delete values.attachmentUrls
}
createTicket.mutate(values, {
onSuccess: (data) => {
toast.success(t('success'))
formik.resetForm()
navigate(Pages.ticket.detail + data.data?.ticketId)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
}
})
console.log(formik.errors);
return ( return (
<div className='mt-4'> <div className='mt-4'>
@@ -17,6 +84,10 @@ const CreateTicket: FC = () => {
{t('ticket.new_ticket')} {t('ticket.new_ticket')}
</div> </div>
{
getProfile.isPending || getCategories.isPending ?
<PageLoading />
:
<div className='flex gap-6 xl:mt-8 mt-4'> <div className='flex gap-6 xl:mt-8 mt-4'>
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'> <div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
<div> <div>
@@ -27,12 +98,12 @@ const CreateTicket: FC = () => {
<Input <Input
label={t('ticket.name')} label={t('ticket.name')}
readOnly readOnly
placeholder='مهرداد مظفری' placeholder={getProfile?.data?.data?.user?.firstName + ' ' + getProfile?.data?.data?.user?.lastName}
/> />
<Input <Input
label={t('ticket.email')} label={t('ticket.email')}
readOnly readOnly
placeholder='Info@example.com' placeholder={getProfile?.data?.data?.user?.email}
/> />
</div> </div>
@@ -41,21 +112,37 @@ const CreateTicket: FC = () => {
<Input <Input
label={t('ticket.subject')} label={t('ticket.subject')}
placeholder={t('ticket.enter_your_subject')} placeholder={t('ticket.enter_your_subject')}
name='subject'
value={formik.values.subject}
onChange={formik.handleChange}
error_text={formik.touched.subject && formik.errors.subject ? formik.errors.subject : ''}
/> />
<Select <Select
label={t('ticket.subject')} label={t('ticket.priority')}
items={[ items={[
{ {
label: '', label: t('ticket.low'),
value: '' value: 'LOW'
},
{
label: t('ticket.medium'),
value: 'MEDIUM'
},
{
label: t('ticket.high'),
value: 'HIGH'
} }
]} ]}
placeholder={t('ticket.priority')} placeholder={t('select')}
className='border' className='border'
name='priority'
value={formik.values.priority}
onChange={formik.handleChange}
error_text={formik.touched.priority && formik.errors.priority ? formik.errors.priority : ''}
/> />
</div> </div>
<div className='mt-6'> <div className='mt-6 rowTwoInput'>
<Select <Select
label={t('ticket.select_your_service')} label={t('ticket.select_your_service')}
items={[ items={[
@@ -66,18 +153,39 @@ const CreateTicket: FC = () => {
]} ]}
placeholder={t('ticket.not_once')} placeholder={t('ticket.not_once')}
className='border' className='border'
name='danakServiceId'
/>
<Select
label={t('ticket.category')}
items={getCategories.data?.data?.ticketCategories?.map((item: { id: string, title: string }) => ({
label: item.title,
value: item.id
})
)}
placeholder={t('select')}
className='border'
name='categoryId'
value={formik.values.categoryId}
onChange={formik.handleChange}
error_text={formik.touched.categoryId && formik.errors.categoryId ? formik.errors.categoryId : ''}
/> />
</div> </div>
<div className='mt-6'> <div className='mt-6'>
<Textarea <Textarea
label={t('description')} label={t('description')}
placeholder={t('ticket.your_description')} placeholder={t('ticket.your_description')}
name='message'
value={formik.values.message}
onChange={formik.handleChange}
error_text={formik.touched.message && formik.errors.message ? formik.errors.message : ''}
/> />
</div> </div>
<div className='mt-6'> <div className='mt-6'>
<UploadBox <UploadBox
label={t('ticket.attachment')} label={t('ticket.attachment')}
isMultiple
onChange={setFiles}
/> />
</div> </div>
@@ -85,6 +193,8 @@ const CreateTicket: FC = () => {
<Button <Button
label={t('ticket.send')} label={t('ticket.send')}
className='max-w-[100px]' className='max-w-[100px]'
onClick={() => formik.handleSubmit()}
isLoading={createTicket.isPending || multiUpload.isPending}
/> />
</div> </div>
</div> </div>
@@ -122,6 +232,9 @@ const CreateTicket: FC = () => {
</div> </div>
</div> </div>
</div> </div>
}
</div> </div>
) )
+145 -22
View File
@@ -1,14 +1,88 @@
import { FC } from 'react' import { FC, Fragment, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Input from '../../components/Input' 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 } from 'iconsax-react' import { CloseCircle, InfoCircle } from 'iconsax-react'
import { useNavigate, useParams } from 'react-router-dom'
import { useAddMessageTicket, useCloseTicket, useGetMessages, useMultiUpload } from './hooks/useTicketData'
import PageLoading from '../../components/PageLoading'
import moment from 'moment-jalaali'
import { useFormik } from 'formik'
import { AddMessageTicketType } from './types/TicketTypes'
import * as Yup from 'yup'
import { toast } from 'react-toastify'
import { ErrorType } from '../../helpers/types'
import { clx } from '../../helpers/utils'
import { Pages } from '../../config/Pages'
const TicketDetail: FC = () => { const TicketDetail: FC = () => {
const { t } = useTranslation('global') const { t } = useTranslation('global')
const navigate = useNavigate()
const { id } = useParams()
const getMessages = useGetMessages(id ? id : '')
const [files, setFiles] = useState<File[]>([])
const addMessage = useAddMessageTicket(id ? id : '')
const multiUpload = useMultiUpload()
const [isResetFiles, setIsResetFiles] = useState<boolean>(false)
const closeTicket = useCloseTicket()
const formik = useFormik<AddMessageTicketType>({
initialValues: {
content: '',
attachmentUrls: [],
},
validationSchema: Yup.object().shape({
content: Yup.string().required(t('errors.required')),
}),
onSubmit: async (values) => {
if (files.length > 0) {
const images = new FormData()
files.forEach(file => {
images.append('files', file)
})
await multiUpload.mutateAsync(images, {
onSuccess: async (data) => {
values.attachmentUrls = await data.data.map((item: { url: string }) => item?.url)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
} else {
delete values.attachmentUrls
}
addMessage.mutate(values, {
onSuccess: () => {
toast.success(t('success'))
formik.resetForm()
setFiles([])
setIsResetFiles(true)
setTimeout(() => {
setIsResetFiles(false)
}, 1000);
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
},
})
const hadleCloseTicket = () => {
closeTicket.mutate(id ? id : '', {
onSuccess: () => {
toast.success(t('success'))
navigate(Pages.ticket.list)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
}
return ( return (
<div className='mt-4'> <div className='mt-4'>
@@ -16,6 +90,10 @@ const TicketDetail: FC = () => {
<div>{t('ticket.ticket_number')}</div> <div>{t('ticket.ticket_number')}</div>
<div>12312</div> <div>12312</div>
</div> </div>
{
getMessages.isPending ?
<PageLoading />
:
<div className='flex xl:flex-row flex-col-reverse gap-6 xl:mt-8 mt-6'> <div className='flex xl:flex-row flex-col-reverse gap-6 xl:mt-8 mt-6'>
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'> <div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
<div className='gap-6 rowTwoInput'> <div className='gap-6 rowTwoInput'>
@@ -26,51 +104,73 @@ const TicketDetail: FC = () => {
/> />
<Input <Input
label={t('ticket.user_name')} label={t('ticket.user_name')}
value={'مهرداد مظفری'} value={getMessages.data?.data?.ticket?.user?.firstName + ' ' + getMessages.data?.data?.ticket?.user?.lastName}
readOnly readOnly
/> />
</div> </div>
<div className='mt-8 xl:text-sm text-xs'> <div className='mt-8 xl:text-sm text-xs'>
موضوع درخواست پشتیبانی {getMessages.data?.data?.ticket?.subject}
</div> </div>
<div className='mt-6 xl:text-sm text-xs bg-[#F6F7FA] p-6 rounded-3xl rounded-tr-none xl:max-w-[70%] max-w-[90%]'> {
getMessages.data?.data?.messages.map((item: any) => {
if (item?.author?.role?.name === 'user') {
return (
<div key={item.id} className='mt-6 xl:text-sm text-xs bg-[#F6F7FA] p-6 rounded-3xl rounded-tr-none xl:max-w-[70%] max-w-[90%]'>
<div className='leading-7'> <div className='leading-7'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را می طلبد، {item.content}
</div> </div>
<div className='flex justify-end mt-6 text-xs text-description'> <div className='flex dltr end mt-6 text-xs text-description'>
10:07 | 1403/09/30 {moment(item.createdAt).format('jYYYY-jMM-jDD HH:mm')}
</div> </div>
</div> </div>
)
} else {
return (
<div className='w-full flex justify-end'> <div className='w-full flex justify-end'>
<div className='mt-6 xl:text-sm text-xs bg-[#EBEDF5] p-6 rounded-3xl rounded-tl-none xl:max-w-[70%] max-w-[90%]'> <div className='mt-6 xl:text-sm text-xs bg-[#EBEDF5] p-6 rounded-3xl rounded-tl-none xl:max-w-[70%] max-w-[90%]'>
<div className='flex gap-1'> <div className='flex gap-1'>
<div className='font-bold'> <div className='font-bold'>
{t('ticket.expert')} {t('ticket.expert')}
</div> </div>
<div>سیما فرهادی</div> <div>{item.author?.firstName + ' ' + item?.author?.lastName}</div>
</div> </div>
<div className='leading-7 mt-4'> <div className='leading-7 mt-4'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را می طلبد، {item.content}
</div> </div>
<div className='flex justify-end mt-6 text-xs text-description'> <div className='flex justify-end mt-6 text-xs text-description'>
10:07 | 1403/09/30 10:07 | 1403/09/30
</div> </div>
</div> </div>
</div> </div>
)
}
})
}
{
getMessages.data?.data?.ticket?.status !== 'CLOSED' &&
<Fragment>
<div className='mt-9'> <div className='mt-9'>
<Textarea <Textarea
label={t('ticket.answer')} label={t('ticket.answer')}
placeholder={t('ticket.your_description')} placeholder={t('ticket.your_description')}
value={formik.values.content}
onChange={formik.handleChange}
name='content'
error_text={formik.touched.content && formik.errors.content ? formik.errors.content : ''}
/> />
</div> </div>
<div className='mt-7'> <div className='mt-7'>
<UploadBox <UploadBox
label={t('ticket.attachment')} label={t('ticket.attachment')}
onChange={setFiles}
isMultiple
isReset={isResetFiles}
/> />
</div> </div>
@@ -78,11 +178,17 @@ const TicketDetail: FC = () => {
<Button <Button
label={t('ticket.send')} label={t('ticket.send')}
className='max-w-[100px]' className='max-w-[100px]'
onClick={() => formik.handleSubmit()}
isLoading={addMessage.isPending || multiUpload.isPending}
/> />
</div> </div>
</Fragment>
}
<div className='h-8'></div> <div className='h-8'></div>
</div> </div>
<div className={`h-fit xl:w-sidebar w-full`}> <div className={`h-fit lg:sticky left-0 lg:top-0 xl:w-sidebar w-full`}>
{
getMessages.data?.data?.ticket?.status !== 'CLOSED' &&
<div className='bg-white xl:p-6 p-4 rounded-3xl flex gap-4'> <div className='bg-white xl:p-6 p-4 rounded-3xl flex gap-4'>
<Button <Button
label={t('ticket.send_answer')} label={t('ticket.send_answer')}
@@ -90,6 +196,8 @@ const TicketDetail: FC = () => {
/> />
<Button <Button
className='bg-[#D52903] xl:h-10 h-8' className='bg-[#D52903] xl:h-10 h-8'
onClick={hadleCloseTicket}
isLoading={closeTicket.isPending}
> >
<div className='flex gap-2 text-xs items-center'> <div className='flex gap-2 text-xs items-center'>
<CloseCircle <CloseCircle
@@ -102,13 +210,21 @@ const TicketDetail: FC = () => {
</div> </div>
</Button> </Button>
</div> </div>
<div className='bg-white p-6 rounded-3xl mt-8'> }
<div className={clx(
'bg-white p-6 rounded-3xl mt-8',
getMessages.data?.data?.ticket?.status === 'CLOSED' && 'mt-0'
)}>
<div className='flex justify-between'> <div className='flex justify-between'>
<div className='text-sm'> <div className='text-sm'>
{t('ticket.info_ticket')} {t('ticket.info_ticket')}
</div> </div>
<div className='h-7 px-3 rounded-xl flex items-center text-xs bg-green-100 text-green-400'> <div className={clx(
{t('ticket.answered')} 'h-7 px-3 rounded-xl flex items-center text-xs bg-green-100 text-green-400',
getMessages.data?.data?.ticket?.status === 'PENDING' && 'bg-orange-100 text-orange-700',
getMessages.data?.data?.ticket?.status === 'CLOSED' && 'bg-red-100 text-red-400'
)}>
{t(`ticket.${getMessages.data?.data?.ticket?.status}`)}
</div> </div>
</div> </div>
@@ -116,25 +232,31 @@ const TicketDetail: FC = () => {
<div className='flex justify-between text-description'> <div className='flex justify-between text-description'>
<div>{t('ticket.ticket_number')}</div> <div>{t('ticket.ticket_number')}</div>
<div> <div>
۱۲۳۴۵۵ {getMessages.data?.data?.ticket?.numericId}
</div> </div>
</div> </div>
<div className='flex mt-6 justify-between text-description'> <div className='flex mt-6 justify-between text-description'>
<div>{t('ticket.ticket_number')}</div> <div>{t('ticket.asignto')}</div>
<div> <div>
۱۲۳۴۵۵ {getMessages.data?.data?.ticket?.assignedTo ? getMessages.data?.data?.ticket?.assignedTo?.firstName + ' ' + getMessages.data?.data?.ticket?.assignedTo?.lastName : ''}
</div> </div>
</div> </div>
<div className='flex mt-6 justify-between text-description'> <div className='flex mt-6 justify-between text-description'>
<div>{t('ticket.ticket_number')}</div> <div>{t('date')}</div>
<div> <div className='dltr'>
۱۲۳۴۵۵ {moment(getMessages.data?.data?.ticket?.createdAt).format('jYYYY/jMM/jDD HH:mm')}
</div> </div>
</div> </div>
<div className='flex mt-6 justify-between text-description'> <div className='flex mt-6 justify-between text-description'>
<div>{t('ticket.ticket_number')}</div> <div>{t('ticket.category')}</div>
<div> <div>
۱۲۳۴۵۵ {getMessages.data?.data?.ticket?.category?.title}
</div>
</div>
<div className='flex mt-6 justify-between text-description'>
<div>{t('ticket.priority')}</div>
<div>
{getMessages.data?.data?.ticket?.priority === 'low' ? t('ticket.low') : getMessages.data?.data?.ticket?.priority === 'medium' ? t('ticket.medium') : t('ticket.high')}
</div> </div>
</div> </div>
@@ -149,6 +271,7 @@ const TicketDetail: FC = () => {
</div> </div>
</div> </div>
</div> </div>
}
</div> </div>
) )
} }
+33 -36
View File
@@ -1,16 +1,19 @@
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, Message2, MessageRemove, MessageTick, MessageTime } from 'iconsax-react' import { Add, Eye, MessageRemove, MessageTick, MessageTime } from 'iconsax-react'
import Tabs from '../../components/Tabs' import Tabs from '../../components/Tabs'
import Td from '../../components/Td' import Td from '../../components/Td'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { Pages } from '../../config/Pages' import { Pages } from '../../config/Pages'
import { useGetTickets } from './hooks/useTicketData'
import moment from 'moment-jalaali'
const TicketList: FC = () => { const TicketList: FC = () => {
const { t } = useTranslation('global') const { t } = useTranslation('global')
const [activeTab, setActiveTab] = useState<string>('answered') const [activeTab, setActiveTab] = useState<'ANSWERED' | 'PENDING' | 'CLOSED'>('ANSWERED')
const getTickets = useGetTickets(activeTab)
return ( return (
<div className='mt-4'> <div className='mt-4'>
@@ -38,27 +41,27 @@ const TicketList: FC = () => {
active={activeTab} active={activeTab}
items={[ items={[
{ {
icon: <MessageTick color={activeTab === 'answered' ? 'black' : '#8C90A3'} size={22} />, icon: <MessageTick color={activeTab === 'ANSWERED' ? 'black' : '#8C90A3'} size={22} />,
label: t('ticket.answered'), label: t('ticket.answered'),
value: 'answered' value: 'ANSWERED'
}, },
{ {
icon: <MessageTime color={activeTab === 'checking' ? 'black' : '#8C90A3'} size={22} />, icon: <MessageTime color={activeTab === 'PENDING' ? 'black' : '#8C90A3'} size={22} />,
label: t('ticket.checking'), label: t('ticket.checking'),
value: 'checking' value: 'PENDING'
}, },
// {
// icon: <Message2 color={activeTab === 'wating' ? 'black' : '#8C90A3'} size={22} />,
// label: t('ticket.wait_answered'),
// value: 'wating'
// },
{ {
icon: <Message2 color={activeTab === 'wating' ? 'black' : '#8C90A3'} size={22} />, icon: <MessageRemove color={activeTab === 'CLOSED' ? 'black' : '#8C90A3'} size={22} />,
label: t('ticket.wait_answered'),
value: 'wating'
},
{
icon: <MessageRemove color={activeTab === 'closed' ? 'black' : '#8C90A3'} size={22} />,
label: t('ticket.closed'), label: t('ticket.closed'),
value: 'closed' value: 'CLOSED'
}, },
]} ]}
onChange={setActiveTab} onChange={(value) => setActiveTab(value as 'ANSWERED' | 'PENDING' | 'CLOSED')}
/> />
</div> </div>
@@ -77,34 +80,28 @@ const TicketList: FC = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr className='tr'> {
<Td text={t('ticket.number')} /> getTickets.data?.data?.tickets?.map((item: any) => (
<Td text={t('ticket.title')} /> <tr key={item.id} className='tr'>
<Td text={t('ticket.team')} /> <Td text={item.numericId} />
<Td text={t('ticket.date')} /> <Td text={item.subject} />
<Td text={t('ticket.status')} /> <Td text={item?.category?.title} />
<Td text={t('ticket.priority')} />
<Td text={''}> <Td text={''}>
<Link to={Pages.ticket.detail + '1'}> <div className='dltr text-right'>
<Eye size={20} color='#8C90A3' /> {moment(item.updatedAt).format('jYYYY-jMM-jDD HH:mm')}
</Link> </div>
</Td> </Td>
<Td text={''} /> <Td text={t(`ticket.${item.status}`)} />
</tr> <Td text={t(`ticket.${item.priority}`)} />
<tr className='tr'> <Td text={''}>
<Td text={t('ticket.number')} /> <Link to={Pages.ticket.detail + item.id}>
<Td text={t('ticket.title')} />
<Td text={t('ticket.team')} />
<Td text={t('ticket.date')} />
<Td text={t('ticket.status')} />
<Td text={t('ticket.priority')} />
<Td text={''}>
<Link to={Pages.ticket.detail + '1'}>
<Eye size={20} color='#8C90A3' /> <Eye size={20} color='#8C90A3' />
</Link> </Link>
</Td> </Td>
<Td text={''} /> <Td text={''} />
</tr> </tr>
))
}
</tbody> </tbody>
</table> </table>
</div> </div>
+62
View File
@@ -0,0 +1,62 @@
import * as api from "../service/TicketService";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AddMessageTicketType, CreateTicketType } from "../types/TicketTypes";
export const useGetTickets = (status: string) => {
return useQuery({
queryKey: ["tickets", status],
queryFn: () => api.getTickets(status),
});
};
export const useCreateTicket = () => {
return useMutation({
mutationFn: (variables: CreateTicketType) => api.createTicket(variables),
});
};
export const useGetCategoriesTicket = () => {
return useQuery({
queryKey: ["category-tickets"],
queryFn: () => api.getCategoriesTicket(),
});
};
export const useSingleUpload = () => {
return useMutation({
mutationFn: (variables: FormData) => api.uploadSingle(variables),
});
};
export const useMultiUpload = () => {
return useMutation({
mutationFn: (variables: FormData) => api.uploadMultiple(variables),
});
};
export const useGetMessages = (id: string) => {
return useQuery({
queryKey: ["ticket-messages", id],
queryFn: () => api.getMessages(id),
enabled: !!id,
});
};
export const useAddMessageTicket = (id: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables: AddMessageTicketType) =>
api.addMessageTicket(id, variables),
onSuccess: () => {
queryClient.refetchQueries({
queryKey: ["ticket-messages", id],
});
},
});
};
export const useCloseTicket = () => {
return useMutation({
mutationFn: (id: string) => api.closeTicket(id),
});
};
+45
View File
@@ -0,0 +1,45 @@
import axios from "../../../config/axios";
import { AddMessageTicketType, CreateTicketType } from "../types/TicketTypes";
export const getTickets = async (status: string) => {
const { data } = await axios.get(`/tickets?status=${status}`);
return data;
};
export const createTicket = async (params: CreateTicketType) => {
const { data } = await axios.post(`/tickets`, params);
return data;
};
export const getCategoriesTicket = async () => {
const { data } = await axios.get(`/tickets/categories`);
return data;
};
export const uploadSingle = async (params: FormData) => {
const { data } = await axios.post(`/uploader/single-file`, params);
return data;
};
export const uploadMultiple = async (params: FormData) => {
const { data } = await axios.post(`/uploader/multi-file`, params);
return data;
};
export const getMessages = async (id: string) => {
const { data } = await axios.get(`/tickets/${id}/messages`);
return data;
};
export const addMessageTicket = async (
id: string,
params: AddMessageTicketType
) => {
const { data } = await axios.post(`/tickets/${id}/messages`, params);
return data;
};
export const closeTicket = async (id: string) => {
const { data } = await axios.post(`/tickets/${id}/close`);
return data;
};
+14
View File
@@ -0,0 +1,14 @@
export type CreateTicketType = {
title: string;
subject: string;
priority: "LOW" | "MEDIUM" | "HIGH";
danakServiceId?: string;
categoryId: string;
message: string;
attachmentUrls?: string[];
};
export type AddMessageTicketType = {
content: string;
attachmentUrls?: string[];
};