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": "انتخاب"
} }
+211 -98
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,111 +84,157 @@ const CreateTicket: FC = () => {
{t('ticket.new_ticket')} {t('ticket.new_ticket')}
</div> </div>
<div className='flex gap-6 xl:mt-8 mt-4'> {
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'> getProfile.isPending || getCategories.isPending ?
<div> <PageLoading />
{t('ticket.submit_your_message')} :
</div> <div className='flex gap-6 xl:mt-8 mt-4'>
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
<div className='mt-6 rowTwoInput'>
<Input
label={t('ticket.name')}
readOnly
placeholder='مهرداد مظفری'
/>
<Input
label={t('ticket.email')}
readOnly
placeholder='Info@example.com'
/>
</div>
<div className='mt-6 rowTwoInput'>
<Input
label={t('ticket.subject')}
placeholder={t('ticket.enter_your_subject')}
/>
<Select
label={t('ticket.subject')}
items={[
{
label: '',
value: ''
}
]}
placeholder={t('ticket.priority')}
className='border'
/>
</div>
<div className='mt-6'>
<Select
label={t('ticket.select_your_service')}
items={[
{
label: '',
value: ''
}
]}
placeholder={t('ticket.not_once')}
className='border'
/>
</div>
<div className='mt-6'>
<Textarea
label={t('description')}
placeholder={t('ticket.your_description')}
/>
</div>
<div className='mt-6'>
<UploadBox
label={t('ticket.attachment')}
/>
</div>
<div className='mt-8 flex justify-end'>
<Button
label={t('ticket.send')}
className='max-w-[100px]'
/>
</div>
</div>
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
<div className='text-sm'>
{t('ticket.title_hint')}
</div>
<div className='mt-6'>
<div className='flex items-start gap-2 border-b pb-5'>
<div> <div>
<TickSquare size={20} color='black' variant='Bold' /> {t('ticket.submit_your_message')}
</div> </div>
<div className='text-description text-xs leading-5'>
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید. <div className='mt-6 rowTwoInput'>
<Input
label={t('ticket.name')}
readOnly
placeholder={getProfile?.data?.data?.user?.firstName + ' ' + getProfile?.data?.data?.user?.lastName}
/>
<Input
label={t('ticket.email')}
readOnly
placeholder={getProfile?.data?.data?.user?.email}
/>
</div>
<div className='mt-6 rowTwoInput'>
<Input
label={t('ticket.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
label={t('ticket.priority')}
items={[
{
label: t('ticket.low'),
value: 'LOW'
},
{
label: t('ticket.medium'),
value: 'MEDIUM'
},
{
label: t('ticket.high'),
value: 'HIGH'
}
]}
placeholder={t('select')}
className='border'
name='priority'
value={formik.values.priority}
onChange={formik.handleChange}
error_text={formik.touched.priority && formik.errors.priority ? formik.errors.priority : ''}
/>
</div>
<div className='mt-6 rowTwoInput'>
<Select
label={t('ticket.select_your_service')}
items={[
{
label: '',
value: ''
}
]}
placeholder={t('ticket.not_once')}
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 className='mt-6'>
<Textarea
label={t('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 className='mt-6'>
<UploadBox
label={t('ticket.attachment')}
isMultiple
onChange={setFiles}
/>
</div>
<div className='mt-8 flex justify-end'>
<Button
label={t('ticket.send')}
className='max-w-[100px]'
onClick={() => formik.handleSubmit()}
isLoading={createTicket.isPending || multiUpload.isPending}
/>
</div> </div>
</div> </div>
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
<div> <div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
<TickSquare size={20} color='black' variant='Bold' /> <div className='text-sm'>
{t('ticket.title_hint')}
</div> </div>
<div className='text-description text-xs leading-5'>
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است. <div className='mt-6'>
</div> <div className='flex items-start gap-2 border-b pb-5'>
</div> <div>
<div className='flex items-start gap-2 mt-6'> <TickSquare size={20} color='black' variant='Bold' />
<div> </div>
<TickSquare size={20} color='black' variant='Bold' /> <div className='text-description text-xs leading-5'>
</div> سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.
<div className='text-description text-xs leading-5'> </div>
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است. </div>
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
</div>
</div>
<div className='flex items-start gap-2 mt-6'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> }
</div>
</div> </div>
) )
+247 -124
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,139 +90,188 @@ const TicketDetail: FC = () => {
<div>{t('ticket.ticket_number')}</div> <div>{t('ticket.ticket_number')}</div>
<div>12312</div> <div>12312</div>
</div> </div>
<div className='flex xl:flex-row flex-col-reverse gap-6 xl:mt-8 mt-6'> {
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'> getMessages.isPending ?
<div className='gap-6 rowTwoInput'> <PageLoading />
<Input :
label={t('ticket.select_your_service')} <div className='flex xl:flex-row flex-col-reverse gap-6 xl:mt-8 mt-6'>
value={'دی منو'} <div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
readOnly <div className='gap-6 rowTwoInput'>
/> <Input
<Input label={t('ticket.select_your_service')}
label={t('ticket.user_name')} value={'دی منو'}
value={'مهرداد مظفری'} readOnly
readOnly />
/> <Input
</div> label={t('ticket.user_name')}
value={getMessages.data?.data?.ticket?.user?.firstName + ' ' + getMessages.data?.data?.ticket?.user?.lastName}
<div className='mt-8 xl:text-sm text-xs'> readOnly
موضوع درخواست پشتیبانی
</div>
<div className='mt-6 xl:text-sm text-xs bg-[#F6F7FA] p-6 rounded-3xl rounded-tr-none xl:max-w-[70%] max-w-[90%]'>
<div className='leading-7'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را می طلبد،
</div>
<div className='flex justify-end mt-6 text-xs text-description'>
10:07 | 1403/09/30
</div>
</div>
<div className='w-full flex justify-end'>
<div className='mt-6 xl:text-sm text-xs bg-[#EBEDF5] p-6 rounded-3xl rounded-tl-none xl:max-w-[70%] max-w-[90%]'>
<div className='flex gap-1'>
<div className='font-bold'>
{t('ticket.expert')}
</div>
<div>سیما فرهادی</div>
</div>
<div className='leading-7 mt-4'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را می طلبد،
</div>
<div className='flex justify-end mt-6 text-xs text-description'>
10:07 | 1403/09/30
</div>
</div>
</div>
<div className='mt-9'>
<Textarea
label={t('ticket.answer')}
placeholder={t('ticket.your_description')}
/>
</div>
<div className='mt-7'>
<UploadBox
label={t('ticket.attachment')}
/>
</div>
<div className='flex justify-end mt-6'>
<Button
label={t('ticket.send')}
className='max-w-[100px]'
/>
</div>
<div className='h-8'></div>
</div>
<div className={`h-fit xl:w-sidebar w-full`}>
<div className='bg-white xl:p-6 p-4 rounded-3xl flex gap-4'>
<Button
label={t('ticket.send_answer')}
className='text-xs xl:h-10 h-8'
/>
<Button
className='bg-[#D52903] xl:h-10 h-8'
>
<div className='flex gap-2 text-xs items-center'>
<CloseCircle
color='white'
size={20}
/> />
<div>
{t('ticket.close_ticket')}
</div>
</div> </div>
</Button>
</div> <div className='mt-8 xl:text-sm text-xs'>
<div className='bg-white p-6 rounded-3xl mt-8'> {getMessages.data?.data?.ticket?.subject}
<div className='flex justify-between'>
<div className='text-sm'>
{t('ticket.info_ticket')}
</div>
<div className='h-7 px-3 rounded-xl flex items-center text-xs bg-green-100 text-green-400'>
{t('ticket.answered')}
</div> </div>
{
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'>
{item.content}
</div>
<div className='flex dltr end mt-6 text-xs text-description'>
{moment(item.createdAt).format('jYYYY-jMM-jDD HH:mm')}
</div>
</div>
)
} else {
return (
<div className='w-full flex justify-end'>
<div className='mt-6 xl:text-sm text-xs bg-[#EBEDF5] p-6 rounded-3xl rounded-tl-none xl:max-w-[70%] max-w-[90%]'>
<div className='flex gap-1'>
<div className='font-bold'>
{t('ticket.expert')}
</div>
<div>{item.author?.firstName + ' ' + item?.author?.lastName}</div>
</div>
<div className='leading-7 mt-4'>
{item.content}
</div>
<div className='flex justify-end mt-6 text-xs text-description'>
10:07 | 1403/09/30
</div>
</div>
</div>
)
}
})
}
{
getMessages.data?.data?.ticket?.status !== 'CLOSED' &&
<Fragment>
<div className='mt-9'>
<Textarea
label={t('ticket.answer')}
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 className='mt-7'>
<UploadBox
label={t('ticket.attachment')}
onChange={setFiles}
isMultiple
isReset={isResetFiles}
/>
</div>
<div className='flex justify-end mt-6'>
<Button
label={t('ticket.send')}
className='max-w-[100px]'
onClick={() => formik.handleSubmit()}
isLoading={addMessage.isPending || multiUpload.isPending}
/>
</div>
</Fragment>
}
<div className='h-8'></div>
</div> </div>
<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'>
<Button
label={t('ticket.send_answer')}
className='text-xs xl:h-10 h-8'
/>
<Button
className='bg-[#D52903] xl:h-10 h-8'
onClick={hadleCloseTicket}
isLoading={closeTicket.isPending}
>
<div className='flex gap-2 text-xs items-center'>
<CloseCircle
color='white'
size={20}
/>
<div>
{t('ticket.close_ticket')}
</div>
</div>
</Button>
</div>
}
<div className={clx(
'bg-white p-6 rounded-3xl mt-8',
getMessages.data?.data?.ticket?.status === 'CLOSED' && 'mt-0'
)}>
<div className='flex justify-between'>
<div className='text-sm'>
{t('ticket.info_ticket')}
</div>
<div className={clx(
'h-7 px-3 rounded-xl flex items-center text-xs bg-green-100 text-green-400',
getMessages.data?.data?.ticket?.status === 'PENDING' && 'bg-orange-100 text-orange-700',
getMessages.data?.data?.ticket?.status === 'CLOSED' && 'bg-red-100 text-red-400'
)}>
{t(`ticket.${getMessages.data?.data?.ticket?.status}`)}
</div>
</div>
<div className='mt-14 text-xs w-full'> <div className='mt-14 text-xs w-full'>
<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> </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>
<div className='mt-10 border-t text-xs pt-5 border-border items-center flex gap-2'> <div className='mt-10 border-t text-xs pt-5 border-border items-center flex gap-2'>
<InfoCircle size={20} color='black' /> <InfoCircle size={20} color='black' />
<div> <div>
{t('ticket.update_sms')} {t('ticket.update_sms')}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> }
</div>
</div> </div>
) )
} }
+39 -42
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={''}> <div className='dltr text-right'>
<Link to={Pages.ticket.detail + '1'}> {moment(item.updatedAt).format('jYYYY-jMM-jDD HH:mm')}
<Eye size={20} color='#8C90A3' /> </div>
</Link> </Td>
</Td> <Td text={t(`ticket.${item.status}`)} />
<Td text={''} /> <Td text={t(`ticket.${item.priority}`)} />
</tr> <Td text={''}>
<tr className='tr'> <Link to={Pages.ticket.detail + item.id}>
<Td text={t('ticket.number')} /> <Eye size={20} color='#8C90A3' />
<Td text={t('ticket.title')} /> </Link>
<Td text={t('ticket.team')} /> </Td>
<Td text={t('ticket.date')} /> <Td text={''} />
<Td text={t('ticket.status')} /> </tr>
<Td text={t('ticket.priority')} /> ))
<Td text={''}> }
<Link to={Pages.ticket.detail + '1'}>
<Eye size={20} color='#8C90A3' />
</Link>
</Td>
<Td text={''} />
</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[];
};