Compare commits
31 Commits
a4432a30cb
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b2f2f89181 | |||
| b64f528c0d | |||
| d94a96b25e | |||
| af0149d722 | |||
| fec1da6ec8 | |||
| fe08fc46fb | |||
| 3d005859ab | |||
| f13eb9bbb4 | |||
| fb0e4aa7f4 | |||
| 94ac69d96a | |||
| 39b295037e | |||
| 31d100861b | |||
| 6510432f41 | |||
| 8fefb4568b | |||
| 32e97a7591 | |||
| fe1fb6cfa4 | |||
| bc9ce01d3f | |||
| 38051db180 | |||
| 36b747d267 | |||
| c69a1babe5 | |||
| 90cf147362 | |||
| e8b1e4add6 | |||
| 2a1f1b19d4 | |||
| 8d2aa1c214 | |||
| b592f520ab | |||
| dbc166f015 | |||
| da34813b1d | |||
| 38a396ba21 | |||
| b9ef684b51 | |||
| e700609236 | |||
| 2c617258f4 |
@@ -19,7 +19,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
BUILD_DIR="$(mktemp -d -t banu-front-XXXXXX)"
|
BUILD_DIR="$(mktemp -d -t banu-front-XXXXXX)"
|
||||||
echo "BUILD_DIR=$BUILD_DIR" >> "$GITHUB_ENV"
|
echo "BUILD_DIR=$BUILD_DIR" >> "$GITHUB_ENV"
|
||||||
git clone http://109.122.252.86:3000/front-end/banu-front.git "$BUILD_DIR"
|
git clone http://127.0.0.1:3000/front-end/banu-front.git "$BUILD_DIR"
|
||||||
git -C "$BUILD_DIR" checkout "${{ github.sha }}"
|
git -C "$BUILD_DIR" checkout "${{ github.sha }}"
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
docker run -d \
|
docker run -d \
|
||||||
--name "$CONTAINER" \
|
--name "$CONTAINER" \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
-p "$PORT:80" \
|
-p "127.0.0.1:$PORT:80" \
|
||||||
"$IMAGE:latest"
|
"$IMAGE:latest"
|
||||||
|
|
||||||
- name: Prune dangling images
|
- name: Prune dangling images
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ COPY package.json package-lock.json ./
|
|||||||
# RUN npm config set registry https://mirror-npm.runflare.com
|
# RUN npm config set registry https://mirror-npm.runflare.com
|
||||||
# RUN npm config set strict-ssl false
|
# RUN npm config set strict-ssl false
|
||||||
|
|
||||||
RUN npm config set registry https://package-mirror.liara.ir/repository/npm/ --global
|
RUN npm config set registry https://registry.npmjs.org/ --global
|
||||||
RUN npm config set strict-ssl false
|
RUN npm config set strict-ssl false
|
||||||
|
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ APP_DIR="/root/banu-front"
|
|||||||
IMAGE="banu-front"
|
IMAGE="banu-front"
|
||||||
CONTAINER="banu-front"
|
CONTAINER="banu-front"
|
||||||
PORT="8080"
|
PORT="8080"
|
||||||
VITE_API_BASE_URL="http://109.122.252.86/api"
|
VITE_API_BASE_URL="//api.madresebanoo.ir/api"
|
||||||
|
|
||||||
echo "==> Pulling latest code..."
|
echo "==> Pulling latest code..."
|
||||||
if [ -d "$APP_DIR/.git" ]; then
|
if [ -d "$APP_DIR/.git" ]; then
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Banu</title>
|
<title>مدرسه بانو</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@tanstack/vue-query-devtools": "^5.62.2",
|
"@tanstack/vue-query-devtools": "^5.62.2",
|
||||||
"@tinymce/tinymce-vue": "^4.0.7",
|
"@tinymce/tinymce-vue": "^4.0.7",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"html2pdf.js": "^0.14.0",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
"jalaali-js": "^1.2.8",
|
"jalaali-js": "^1.2.8",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
@@ -102,6 +103,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
||||||
@@ -1722,6 +1732,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pako": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/raf": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@ungap/structured-clone": {
|
"node_modules/@ungap/structured-clone": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
|
||||||
@@ -2168,6 +2198,15 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-arraybuffer": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.29",
|
"version": "2.10.29",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz",
|
||||||
@@ -2371,6 +2410,26 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/canvg": {
|
||||||
|
"version": "3.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||||
|
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/raf": "^3.4.0",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"raf": "^3.4.1",
|
||||||
|
"regenerator-runtime": "^0.13.7",
|
||||||
|
"rgbcolor": "^1.0.1",
|
||||||
|
"stackblur-canvas": "^2.0.0",
|
||||||
|
"svg-pathdata": "^6.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@@ -2656,6 +2715,18 @@
|
|||||||
"url": "https://github.com/sponsors/mesqueeb"
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/core-js": {
|
||||||
|
"version": "3.49.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
|
||||||
|
"integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/core-js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.49.0",
|
"version": "3.49.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
|
||||||
@@ -2722,6 +2793,15 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-line-break": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css-select": {
|
"node_modules/css-select": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||||
@@ -3032,6 +3112,15 @@
|
|||||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.4.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
|
||||||
|
"integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/domutils": {
|
"node_modules/domutils": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
@@ -3853,6 +3942,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-png": {
|
||||||
|
"version": "6.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
|
||||||
|
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/pako": "^2.0.3",
|
||||||
|
"iobuffer": "^5.3.2",
|
||||||
|
"pako": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
||||||
@@ -3908,6 +4008,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
|
||||||
|
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||||
@@ -4469,6 +4575,30 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html2canvas": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-line-break": "^2.1.0",
|
||||||
|
"text-segmentation": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/html2pdf.js": {
|
||||||
|
"version": "0.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/html2pdf.js/-/html2pdf.js-0.14.0.tgz",
|
||||||
|
"integrity": "sha512-yvNJgE/8yru2UeGflkPdjW8YEY+nDH5X7/2WG4uiuSCwYiCp8PZ8EKNiTAa6HxJ1NjC51fZSIEq6xld5CADKBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
|
"html2canvas": "^1.0.0",
|
||||||
|
"jspdf": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/htmlparser2": {
|
"node_modules/htmlparser2": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
@@ -4607,6 +4737,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/iobuffer": {
|
||||||
|
"version": "5.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||||
|
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -5171,6 +5307,23 @@
|
|||||||
"json5": "lib/cli.js"
|
"json5": "lib/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jspdf": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"fast-png": "^6.2.0",
|
||||||
|
"fflate": "^0.8.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"canvg": "^3.0.11",
|
||||||
|
"core-js": "^3.6.0",
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
|
"html2canvas": "^1.0.0-rc.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -6022,6 +6175,22 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nodeca"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@@ -6106,6 +6275,13 @@
|
|||||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/performance-now": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -6416,6 +6592,16 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/raf": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"performance-now": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-pkg": {
|
"node_modules/read-pkg": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
|
||||||
@@ -6563,6 +6749,13 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.13.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/regexp-tree": {
|
"node_modules/regexp-tree": {
|
||||||
"version": "0.1.27",
|
"version": "0.1.27",
|
||||||
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
|
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
|
||||||
@@ -6716,6 +6909,16 @@
|
|||||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/rgbcolor": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||||
|
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rimraf": {
|
"node_modules/rimraf": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||||
@@ -7140,6 +7343,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stackblur-canvas": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.1.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stop-iteration-iterator": {
|
"node_modules/stop-iteration-iterator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||||
@@ -7685,6 +7898,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svg-pathdata": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/svg-tags": {
|
"node_modules/svg-tags": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
|
||||||
@@ -7779,6 +8002,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/text-segmentation": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
@@ -8018,6 +8250,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/utrie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-arraybuffer": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/validate-npm-package-license": {
|
"node_modules/validate-npm-package-license": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"@tanstack/vue-query-devtools": "^5.62.2",
|
"@tanstack/vue-query-devtools": "^5.62.2",
|
||||||
"@tinymce/tinymce-vue": "^4.0.7",
|
"@tinymce/tinymce-vue": "^4.0.7",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"html2pdf.js": "^0.14.0",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
"jalaali-js": "^1.2.8",
|
"jalaali-js": "^1.2.8",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import StudentLayout from '@/layouts/StudentLayout.vue'
|
|||||||
import ConfirmModal from '@/components/ConfirmModal.vue'
|
import ConfirmModal from '@/components/ConfirmModal.vue'
|
||||||
import CounselorLayout from '@/layouts/CounselorLayout.vue'
|
import CounselorLayout from '@/layouts/CounselorLayout.vue'
|
||||||
import { tokenService } from '@/services/api/token-service'
|
import { tokenService } from '@/services/api/token-service'
|
||||||
|
import MissionaryLayout from '@/layouts/MissionaryLayout.vue'
|
||||||
import { VueQueryDevtools } from '@tanstack/vue-query-devtools'
|
import { VueQueryDevtools } from '@tanstack/vue-query-devtools'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -26,6 +27,7 @@ const layouts = {
|
|||||||
admin: AdminLayout,
|
admin: AdminLayout,
|
||||||
student: StudentLayout,
|
student: StudentLayout,
|
||||||
counselor: CounselorLayout,
|
counselor: CounselorLayout,
|
||||||
|
missionary: MissionaryLayout,
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLayout = computed(() => layouts[route.meta?.layout] || PublicLayout)
|
const currentLayout = computed(() => layouts[route.meta?.layout] || PublicLayout)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="11.625" stroke="#EEEEEE" stroke-width="0.75"/>
|
||||||
|
<g clip-path="url(#clip0_1_21196)">
|
||||||
|
<path d="M15.375 15.375L8.625 8.625" stroke="#B2B2B2" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M14.1094 8.625H8.625V14.1094" stroke="#B2B2B2" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1_21196">
|
||||||
|
<rect width="13.5" height="13.5" fill="white" transform="translate(5.25 5.25)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 604 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5.625 10.625H9.375"/>
|
||||||
|
<path d="M5.625 8.125H9.375"/>
|
||||||
|
<path d="M15.3125 13.125C17.211 13.125 18.75 11.586 18.75 9.6875C18.75 7.78902 17.211 6.25 15.3125 6.25C13.414 6.25 11.875 7.78902 11.875 9.6875C11.875 11.586 13.414 13.125 15.3125 13.125Z"/>
|
||||||
|
<path d="M13.125 15H3.125C2.95924 15 2.80027 14.9342 2.68306 14.8169C2.56585 14.6997 2.5 14.5408 2.5 14.375V4.375C2.5 4.20924 2.56585 4.05027 2.68306 3.93306C2.80027 3.81585 2.95924 3.75 3.125 3.75H16.875C17.0408 3.75 17.1997 3.81585 17.3169 3.93306C17.4342 4.05027 17.5 4.20924 17.5 4.375V7.03594"/>
|
||||||
|
<path d="M13.125 12.3389V17.4998L15.3125 16.2498L17.5 17.4998V12.3389"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 788 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.5 8.75H12.8125"/>
|
||||||
|
<path d="M7.5 11.25H12.8125"/>
|
||||||
|
<path d="M10.3125 16.875H3.75C3.58424 16.875 3.42527 16.8092 3.30806 16.6919C3.19085 16.5747 3.125 16.4158 3.125 16.25V9.6875C3.125 7.78126 3.88225 5.95309 5.23017 4.60517C6.57809 3.25725 8.40626 2.5 10.3125 2.5C11.2564 2.5 12.191 2.68591 13.063 3.04712C13.9351 3.40832 14.7274 3.93775 15.3948 4.60517C16.0623 5.27259 16.5917 6.06493 16.9529 6.93696C17.3141 7.80899 17.5 8.74362 17.5 9.6875C17.5 10.6314 17.3141 11.566 16.9529 12.438C16.5917 13.3101 16.0623 14.1024 15.3948 14.7698C14.7274 15.4373 13.9351 15.9667 13.063 16.3279C12.191 16.6891 11.2564 16.875 10.3125 16.875Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 794 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1_24766)">
|
||||||
|
<path d="M4.64062 7.17188L5.90625 8.4375L8.85938 5.48438" stroke="#B8B8B8" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M6.75 11.8125C9.54594 11.8125 11.8125 9.54594 11.8125 6.75C11.8125 3.95406 9.54594 1.6875 6.75 1.6875C3.95406 1.6875 1.6875 3.95406 1.6875 6.75C1.6875 9.54594 3.95406 11.8125 6.75 11.8125Z" stroke="#B8B8B8" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1_24766">
|
||||||
|
<rect width="13.5" height="13.5" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 655 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1_21191)">
|
||||||
|
<path d="M6.5 11.375C9.19239 11.375 11.375 9.19239 11.375 6.5C11.375 3.80761 9.19239 1.625 6.5 1.625C3.80761 1.625 1.625 3.80761 1.625 6.5C1.625 9.19239 3.80761 11.375 6.5 11.375Z" stroke="currentColor" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M6.5 3.65625V6.5H9.34375" stroke="currentColor" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1_21191">
|
||||||
|
<rect width="12.75" height="12.75" fill="white" transform="scale(1.01961)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 676 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17.5 7.5V15.625C17.5 15.7908 17.4342 15.9497 17.3169 16.0669C17.1997 16.1842 17.0408 16.25 16.875 16.25H3.125C2.95924 16.25 2.80027 16.1842 2.68306 16.0669C2.56585 15.9497 2.5 15.7908 2.5 15.625V7.5L10 2.5L17.5 7.5Z"/>
|
||||||
|
<path d="M8.63711 11.875L2.69336 16.0734"/>
|
||||||
|
<path d="M17.307 16.0734L11.3633 11.875"/>
|
||||||
|
<path d="M17.5 7.5L11.3641 11.875H8.63672L2.5 7.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 526 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9.375 16.25H3.125C2.95924 16.25 2.80027 16.1842 2.68306 16.0669C2.56585 15.9497 2.5 15.7908 2.5 15.625V5C2.5 4.83424 2.56585 4.67527 2.68306 4.55806C2.80027 4.44085 2.95924 4.375 3.125 4.375H7.29141C7.42664 4.375 7.55822 4.41886 7.66641 4.5L10 6.25H16.875C17.0408 6.25 17.1997 6.31585 17.3169 6.43306C17.4342 6.55027 17.5 6.70924 17.5 6.875V9.375"/>
|
||||||
|
<path d="M14.375 15.5109L16.693 16.875L16.0625 14.3289L18.125 12.6258L15.418 12.4172L14.375 10L13.332 12.4172L10.625 12.6258L12.6875 14.3289L12.057 16.875L14.375 15.5109Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 687 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.625 7.5L10 2.5L19.375 7.5L10 12.5L0.625 7.5Z"/>
|
||||||
|
<path d="M10 7.5L14.375 9.83359V18.75"/>
|
||||||
|
<path d="M16.875 8.83398V12.9918C16.8754 13.1445 16.8198 13.2921 16.7188 13.4066C15.8359 14.3902 13.6797 16.2504 10 16.2504C6.32031 16.2504 4.16563 14.3902 3.28125 13.4066C3.18023 13.2921 3.12465 13.1445 3.125 12.9918V8.83398"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 485 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_2_135332)">
|
||||||
|
<path d="M4.21875 16.875L18.2813 16.875C19.0579 16.875 19.6875 16.2454 19.6875 15.4688L19.6875 5.625C19.6875 4.84835 19.0579 4.21875 18.2813 4.21875L4.21875 4.21875C3.4421 4.21875 2.8125 4.84835 2.8125 5.625L2.8125 15.4687C2.8125 16.2454 3.4421 16.875 4.21875 16.875Z" stroke="currentColor" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M14.0625 19.6875L8.4375 19.6875" stroke="currentColor" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2_135332">
|
||||||
|
<rect width="22.5" height="22.5" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 746 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.4375 9.84375L15.4688 9.84375" stroke="currentColor" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M8.4375 12.6562L15.4688 12.6563" stroke="currentColor" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M2.8125 17.5781C3.18546 17.5781 3.54315 17.43 3.80687 17.1662C4.07059 16.9025 4.21875 16.5448 4.21875 16.1719L4.21875 5.625C4.21875 5.43852 4.29283 5.25968 4.42469 5.12782C4.55655 4.99595 4.7354 4.92188 4.92188 4.92188L18.9844 4.92188C19.1709 4.92188 19.3497 4.99595 19.4816 5.12782C19.6134 5.25968 19.6875 5.43852 19.6875 5.625L19.6875 16.1719C19.6875 16.5448 19.5393 16.9025 19.2756 17.1662C19.0119 17.43 18.6542 17.5781 18.2813 17.5781L2.8125 17.5781Z" stroke="currentColor" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M2.8125 17.5781C2.43954 17.5781 2.08185 17.43 1.81813 17.1662C1.55441 16.9025 1.40625 16.5448 1.40625 16.1719L1.40625 7.73438" stroke="currentColor" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7 3.75v12.5"/>
|
||||||
|
<path d="M13 3.75v12.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 199 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1_21269)">
|
||||||
|
<path d="M5.625 3.1154V16.8841C5.62704 16.994 5.65802 17.1014 5.7148 17.1955C5.77158 17.2896 5.85217 17.367 5.94843 17.42C6.0447 17.473 6.15323 17.4997 6.2631 17.4974C6.37297 17.4951 6.48028 17.4638 6.57422 17.4068L17.8305 10.5224C17.9204 10.468 17.9947 10.3913 18.0463 10.2998C18.0979 10.2082 18.1251 10.1049 18.1251 9.99977C18.1251 9.89467 18.0979 9.79135 18.0463 9.69979C17.9947 9.60823 17.9204 9.53154 17.8305 9.47711L6.57422 2.59274C6.48028 2.53571 6.37297 2.50447 6.2631 2.50214C6.15323 2.49982 6.0447 2.52651 5.94843 2.57951C5.85217 2.63252 5.77158 2.70996 5.7148 2.80405C5.65802 2.89813 5.62704 3.00552 5.625 3.1154Z" stroke="currentColor" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1_21269">
|
||||||
|
<rect width="19.5" height="19.5" fill="white" transform="scale(1.02564)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 993 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 3v9"/>
|
||||||
|
<path d="M15.5 4.5a8 8 0 1 1-7 0"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 228 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1_21319)">
|
||||||
|
<path d="M11.8125 2.625H2.1875C1.94588 2.625 1.75 2.82088 1.75 3.0625V10.9375C1.75 11.1791 1.94588 11.375 2.1875 11.375H11.8125C12.0541 11.375 12.25 11.1791 12.25 10.9375V3.0625C12.25 2.82088 12.0541 2.625 11.8125 2.625Z" stroke="#B8B8B8" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M9.1875 4.8125C9.1875 5.39266 8.95703 5.94906 8.5468 6.3593C8.13656 6.76953 7.58016 7 7 7C6.41984 7 5.86344 6.76953 5.4532 6.3593C5.04297 5.94906 4.8125 5.39266 4.8125 4.8125" stroke="#B8B8B8" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1_21319">
|
||||||
|
<rect width="13.5" height="13.5" fill="white" transform="scale(1.03704)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 855 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg viewBox="0 0 171 171" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M85.125 148.969C120.385 148.969 148.969 120.385 148.969 85.125C148.969 49.8651 120.385 21.2812 85.125 21.2812C49.8651 21.2812 21.2812 49.8651 21.2812 85.125C21.2812 120.385 49.8651 148.969 85.125 148.969Z" stroke="#CACACA" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M61.1816 65.2207C64.8287 65.2207 67.7852 68.1772 67.7852 71.8242C67.7852 75.4712 64.8287 78.4277 61.1816 78.4277C57.5346 78.4277 54.5781 75.4712 54.5781 71.8242C54.5781 68.1772 57.5346 65.2207 61.1816 65.2207Z" fill="#F55D6D" stroke="#F55D6D" stroke-width="0.09375"/>
|
||||||
|
<path d="M109.064 65.2207C112.711 65.2207 115.668 68.1772 115.668 71.8242C115.668 75.4712 112.711 78.4277 109.064 78.4277C105.417 78.4277 102.461 75.4712 102.461 71.8242C102.461 68.1772 105.417 65.2207 109.064 65.2207Z" fill="#F55D6D" stroke="#F55D6D" stroke-width="0.09375"/>
|
||||||
|
<path d="M111.727 117.047C106.207 107.504 96.9427 101.086 85.125 101.086C73.3073 101.086 64.0433 107.504 58.5234 117.047" stroke="#F55D6D" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 12.5C11.7259 12.5 13.125 11.1009 13.125 9.375C13.125 7.64911 11.7259 6.25 10 6.25C8.27411 6.25 6.875 7.64911 6.875 9.375C6.875 11.1009 8.27411 12.5 10 12.5Z"/>
|
||||||
|
<path d="M4.98438 15.5757C5.45462 14.6494 6.17216 13.8715 7.05745 13.3281C7.94275 12.7847 8.96123 12.4971 10 12.4971C11.0388 12.4971 12.0572 12.7847 12.9425 13.3281C13.8278 13.8715 14.5454 14.6494 15.0156 15.5757"/>
|
||||||
|
<path d="M17.3963 8.75003C17.6599 10.3167 17.4189 11.9266 16.7079 13.3474C15.9969 14.7681 14.8528 15.9261 13.4408 16.6542C12.0287 17.3823 10.4218 17.6428 8.85205 17.3981C7.28231 17.1533 5.83096 16.416 4.70757 15.2926C3.58419 14.1693 2.84689 12.7179 2.60215 11.1482C2.35742 9.57842 2.6179 7.97151 3.34598 6.55946C4.07407 5.14741 5.23209 4.00329 6.65283 3.2923C8.07357 2.58132 9.6835 2.34026 11.2502 2.60394"/>
|
||||||
|
<path d="M14.375 4.375L15.625 5.625L18.125 3.125"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1007 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5.48438 8.53125C7.16712 8.53125 8.53125 7.16712 8.53125 5.48438C8.53125 3.80163 7.16712 2.4375 5.48438 2.4375C3.80163 2.4375 2.4375 3.80163 2.4375 5.48438C2.4375 7.16712 3.80163 8.53125 5.48438 8.53125Z" stroke-width="0.75"/>
|
||||||
|
<path d="M1.21875 10.5625C2.2623 9.3209 3.73547 8.53125 5.48438 8.53125C7.23328 8.53125 8.70645 9.3209 9.75 10.5625" stroke-width="0.75"/>
|
||||||
|
<path d="M10.1562 3.49096C10.4243 4.12144 10.5625 4.79952 10.5625 5.48463C10.5625 6.16975 10.4243 6.84782 10.1562 7.47831" stroke-width="0.75"/>
|
||||||
|
<path d="M11.6468 2.84375C12.0033 3.67846 12.1871 4.57672 12.1871 5.48438C12.1871 6.39203 12.0033 7.29029 11.6468 8.125" stroke-width="0.75"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 820 B |
@@ -0,0 +1,12 @@
|
|||||||
|
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1_21228)">
|
||||||
|
<path d="M8.125 5.6875L5.6875 4.0625V7.3125L8.125 5.6875Z" stroke="#B8B8B8" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M10.9688 2.4375H2.03125C1.80688 2.4375 1.625 2.61938 1.625 2.84375V8.53125C1.625 8.75562 1.80688 8.9375 2.03125 8.9375H10.9688C11.1931 8.9375 11.375 8.75562 11.375 8.53125V2.84375C11.375 2.61938 11.1931 2.4375 10.9688 2.4375Z" stroke="#B8B8B8" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M1.625 10.5625H11.375" stroke="#B8B8B8" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1_21228">
|
||||||
|
<rect width="12.75" height="12.75" fill="white" transform="scale(1.01961)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 863 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 400 B |
@@ -4,6 +4,10 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 106.25%;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: montserrat;
|
font-family: montserrat;
|
||||||
src: url('../fonts/montserrat/Montserrat-ExtraLight.ttf') format('truetype');
|
src: url('../fonts/montserrat/Montserrat-Regular.ttf') format('truetype');
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: aria;
|
font-family: aria;
|
||||||
src: url('../fonts/aria/AriaWebFaNum-Light.woff2') format('woff2');
|
src: url('../fonts/aria/AriaWeb-Normal.woff2') format('woff2');
|
||||||
font-weight: 300;
|
font-weight: 200;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: aria;
|
font-family: aria;
|
||||||
src: url('../fonts/aria/AriaWebFaNum-Regular.woff2') format('woff2');
|
src: url('../fonts/aria/AriaWebFaNum-Regular.woff2') format('woff2');
|
||||||
font-weight: 400;
|
font-weight: 300;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,14 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: aria;
|
font-family: aria;
|
||||||
src: url('../fonts/aria/AriaWebFaNum-Medium.woff2') format('woff2');
|
src: url('../fonts/aria/AriaWebFaNum-Medium.woff2') format('woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: aria;
|
||||||
|
src: url('../fonts/aria/AriaWebFaNum-SemiBold.woff2') format('woff2');
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
@@ -46,25 +54,17 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: aria;
|
|
||||||
src: url('../fonts/aria/AriaWeb-Normal.woff2') format('woff2');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: iran-yekan;
|
|
||||||
src: url('../fonts/iran-yekan/iranyekanwebregularfanum.woff') format('woff');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: iran-yekan;
|
font-family: iran-yekan;
|
||||||
src: url('../fonts/iran-yekan/iranyekanwebmediumfanum.woff') format('woff');
|
src: url('../fonts/iran-yekan/iranyekanwebmediumfanum.woff') format('woff');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: iran-yekan;
|
||||||
|
src: url('../fonts/iran-yekan/iranyekanwebboldfanum.woff') format('woff');
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: iran-yekan;
|
font-family: iran-yekan;
|
||||||
src: url('../fonts/iran-yekan/iranyekanweblightfanum.woff') format('woff');
|
src: url('../fonts/iran-yekan/iranyekanwebregularfanum.woff') format('woff');
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { computed } from 'vue'
|
|||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
/** @type {import('vue').PropType<'neutral' | 'success' | 'danger' | 'warning' | 'primary' | 'info' | 'cyan'>} */
|
/** @type {import('vue').PropType<'neutral' | 'success' | 'danger' | 'warning' | 'primary' | 'info' | 'cyan' | 'blue'>} */
|
||||||
variant: { type: String, default: 'neutral' },
|
variant: { type: String, default: 'neutral' },
|
||||||
/** @type {import('vue').PropType<'sm' | 'md' | 'lg'>} */
|
/** @type {import('vue').PropType<'sm' | 'md' | 'lg'>} */
|
||||||
size: { type: String, default: 'md' },
|
size: { type: String, default: 'md' },
|
||||||
@@ -138,5 +138,10 @@ const onClick = (event) => {
|
|||||||
background: rgba(104, 104, 104, 10%);
|
background: rgba(104, 104, 104, 10%);
|
||||||
color: #686868;
|
color: #686868;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--blue {
|
||||||
|
background: rgba(0, 72, 255, 8%);
|
||||||
|
color: #0048ff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -39,8 +39,9 @@ const social = [
|
|||||||
.copyright {
|
.copyright {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
justify-content: center;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-top: 3rem;
|
||||||
|
|
||||||
&__text {
|
&__text {
|
||||||
padding: 0 0.25rem;
|
padding: 0 0.25rem;
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="dropdown">
|
<Transition name="dropdown">
|
||||||
<div v-if="modelValue" ref="floatingEl" class="dropdown-menu" :style="floatingStyle">
|
<div
|
||||||
|
v-if="modelValue"
|
||||||
|
ref="floatingEl"
|
||||||
|
class="dropdown-menu"
|
||||||
|
:class="{ 'dropdown-menu--center': align === 'center' }"
|
||||||
|
:style="floatingStyle"
|
||||||
|
>
|
||||||
<ul class="dropdown-menu__list">
|
<ul class="dropdown-menu__list">
|
||||||
<li
|
<li
|
||||||
v-for="(item, i) in items"
|
v-for="(item, i) in items"
|
||||||
@@ -11,11 +17,21 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="dropdown-menu__item"
|
class="dropdown-menu__item"
|
||||||
:class="{ 'dropdown-menu__item--danger': item.danger }"
|
:class="{
|
||||||
|
'dropdown-menu__item--danger': item.danger,
|
||||||
|
'dropdown-menu__item--colored': item.color,
|
||||||
|
}"
|
||||||
|
:style="{ color: item.color }"
|
||||||
:disabled="item.disabled"
|
:disabled="item.disabled"
|
||||||
@click="handleClick(item)"
|
@click="handleClick(item)"
|
||||||
>
|
>
|
||||||
<SvgIcon v-if="item.icon" :name="item.icon" :size="16" class="dropdown-menu__icon" />
|
<SvgIcon
|
||||||
|
v-if="item.icon"
|
||||||
|
:name="item.icon"
|
||||||
|
:size="16"
|
||||||
|
:color="item.iconColor ?? item.color ?? 'currentColor'"
|
||||||
|
class="dropdown-menu__icon"
|
||||||
|
/>
|
||||||
<span class="dropdown-menu__label">{{ item.label }}</span>
|
<span class="dropdown-menu__label">{{ item.label }}</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@@ -43,6 +59,7 @@ const props = defineProps({
|
|||||||
placement: { type: String, default: 'bottom-end' },
|
placement: { type: String, default: 'bottom-end' },
|
||||||
offset: { type: Number, default: 8 },
|
offset: { type: Number, default: 8 },
|
||||||
minWidth: { type: String, default: '12rem' },
|
minWidth: { type: String, default: '12rem' },
|
||||||
|
align: { type: String, default: 'start' },
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'select'])
|
const emit = defineEmits(['update:modelValue', 'select'])
|
||||||
@@ -180,6 +197,21 @@ onBeforeUnmount(unmountFloating)
|
|||||||
&__icon {
|
&__icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__item--colored &__icon {
|
||||||
|
:deep([stroke]) {
|
||||||
|
stroke: currentcolor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--center &__item {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--center &__label {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-enter-active,
|
.dropdown-enter-active,
|
||||||
|
|||||||
@@ -11,14 +11,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="header-block__title">
|
<div class="header-block__title">
|
||||||
<slot>
|
<slot>
|
||||||
<div v-if="!hasSlotContent && pageTitle">{{ pageTitle }}</div>
|
<div v-if="pageTitle || pageSubtitle" class="header-block__heading">
|
||||||
|
<span v-if="pageSubtitle" class="header-block__heading-eyebrow">{{ pageTitle }}</span>
|
||||||
|
<span>{{ pageSubtitle || pageTitle }}</span>
|
||||||
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-block__icons">
|
<div class="header-block__icons">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="item in iconList" :key="item.icon">
|
<li v-for="item in iconList" :key="item.icon">
|
||||||
<button type="button" class="header-block__icon-btn" @click="item.action">
|
<button type="button" class="header-block__icon-btn" @click="item.action">
|
||||||
<SvgIcon :name="item.icon" :size="24" color="#9a9a9a" />
|
<SvgIcon :name="item.icon" :size="24" color="var(--header-icon-color)" />
|
||||||
<span v-if="item.count" class="header-block__icon-badge">{{ item.count }}</span>
|
<span v-if="item.count" class="header-block__icon-badge">{{ item.count }}</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@@ -27,33 +30,91 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="header-block__title-mobile">
|
<div class="header-block__title-mobile">
|
||||||
<slot>
|
<slot>
|
||||||
<div v-if="!hasSlotContent && pageTitle">{{ pageTitle }}</div>
|
<div v-if="pageTitle || pageSubtitle" class="header-block__heading">
|
||||||
|
<span v-if="pageSubtitle" class="header-block__heading-eyebrow">{{ pageTitle }}</span>
|
||||||
|
<span>{{ pageSubtitle || pageTitle }}</span>
|
||||||
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DropdownMenu
|
||||||
|
v-model="userMenuOpen"
|
||||||
|
:reference="userMenuTrigger"
|
||||||
|
:items="userMenuItems"
|
||||||
|
placement="bottom-end"
|
||||||
|
align="center"
|
||||||
|
min-width="9rem"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { useUIStore } from '@/store/ui'
|
import { useUIStore } from '@/store/ui'
|
||||||
import { computed, useSlots } from 'vue'
|
|
||||||
import { gallery } from '@/utils/gallery'
|
import { gallery } from '@/utils/gallery'
|
||||||
|
import useAuth from '@/composables/useAuth'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||||
|
import { useLogoutMutation } from '@/services/query/auth'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
pageTitle: { type: String, default: '' },
|
pageTitle: { type: String, default: '' },
|
||||||
|
pageSubtitle: { type: String, default: '' },
|
||||||
})
|
})
|
||||||
|
|
||||||
const slots = useSlots()
|
|
||||||
const hasSlotContent = computed(() => !!slots.default)
|
|
||||||
|
|
||||||
const ui = useUIStore()
|
const ui = useUIStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const { role, clearSession } = useAuth()
|
||||||
|
const logoutMutation = useLogoutMutation()
|
||||||
|
|
||||||
const logo = gallery.authLogo
|
const logo = gallery.authLogo
|
||||||
|
|
||||||
|
const userMenuOpen = ref(false)
|
||||||
|
const userMenuTrigger = ref(null)
|
||||||
|
|
||||||
|
const openUserMenu = (event) => {
|
||||||
|
const trigger = event.currentTarget
|
||||||
|
if (userMenuOpen.value && userMenuTrigger.value === trigger) {
|
||||||
|
userMenuOpen.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userMenuTrigger.value = trigger
|
||||||
|
userMenuOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const goEditProfile = () => {
|
||||||
|
router.push({ name: 'student-profile' }).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLogout = async () => {
|
||||||
|
try {
|
||||||
|
await logoutMutation.mutateAsync()
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
} finally {
|
||||||
|
clearSession()
|
||||||
|
router.push({ name: 'login' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMenuItems = computed(() => [
|
||||||
|
...(role.value === 'student'
|
||||||
|
? [{ key: 'edit-profile', label: 'ویرایش پروفایل', icon: 'pencil', onClick: goEditProfile }]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
key: 'logout',
|
||||||
|
label: 'خروج',
|
||||||
|
icon: 'heart',
|
||||||
|
color: 'var(--color-primary)',
|
||||||
|
onClick: onLogout,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
const iconList = [
|
const iconList = [
|
||||||
{ icon: 'chat', action: () => {} },
|
// { icon: 'chat', action: () => {} },
|
||||||
{ icon: 'bell', action: () => {}, count: 5 },
|
// { icon: 'bell', action: () => {}, count: 5 },
|
||||||
{ icon: 'user', action: () => {} },
|
{ icon: 'user', action: openUserMenu },
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -121,6 +182,18 @@ const iconList = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__heading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__heading-eyebrow {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #a7a7a7;
|
||||||
|
}
|
||||||
|
|
||||||
&__icons {
|
&__icons {
|
||||||
flex-basis: 33.33%;
|
flex-basis: 33.33%;
|
||||||
|
|
||||||
@@ -136,12 +209,18 @@ const iconList = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__icon-btn {
|
&__icon-btn {
|
||||||
|
--header-icon-color: #9a9a9a;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--header-icon-color: var(--color-primary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__icon-badge {
|
&__icon-badge {
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ defineProps({
|
|||||||
|
|
||||||
&__bar {
|
&__bar {
|
||||||
background-color: var(--color-primary);
|
background-color: var(--color-primary);
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 5%);
|
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 3px 8px rgba(243, 102, 117, 45%);
|
||||||
height: 1.9rem;
|
height: 1.9rem;
|
||||||
width: 0.375rem;
|
width: 0.375rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="message-attachments">
|
||||||
|
<template v-for="item in media" :key="item.id">
|
||||||
|
<a
|
||||||
|
v-if="isImage(item)"
|
||||||
|
:href="item.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="message-attachments__image-link"
|
||||||
|
>
|
||||||
|
<img :src="item.url" :alt="item.fileName" class="message-attachments__image" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
:href="item.downloadUrl || item.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="message-attachments__file"
|
||||||
|
>
|
||||||
|
<SvgIcon name="file" :size="18" color="currentColor" />
|
||||||
|
<span class="message-attachments__name">{{ item.fileName }}</span>
|
||||||
|
<span class="message-attachments__size">{{ formatFileSize(item.fileSize) }}</span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
media: { type: Array, default: () => [] },
|
||||||
|
})
|
||||||
|
|
||||||
|
const isImage = (item) =>
|
||||||
|
(item.mimeType || '').startsWith('image/') || /\.(jpe?g|png|gif|webp)$/i.test(item.fileName || '')
|
||||||
|
|
||||||
|
const formatFileSize = (bytes) => {
|
||||||
|
if (!bytes) return ''
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.message-attachments {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.375rem;
|
||||||
|
margin-bottom: 0.375rem;
|
||||||
|
|
||||||
|
&__image-link {
|
||||||
|
display: block;
|
||||||
|
max-width: 16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__image {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 12rem;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__file {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.375rem 0.625rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
background: rgba(0, 0, 0, 8%);
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 14%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__size {
|
||||||
|
font-family: var(--font-family-en);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
opacity: 0.75;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="no-items">
|
<div class="no-items">
|
||||||
<div class="no-items__inner">
|
<div class="no-items__inner">
|
||||||
<SvgIcon name="warning" :size="120" color="#cbd5e1" />
|
<SvgIcon name="smiley-sad" :size="171" />
|
||||||
<p class="no-items__title">{{ title }}</p>
|
<p class="no-items__title">{{ title }}</p>
|
||||||
<p class="no-items__desc">{{ desc }}</p>
|
<p class="no-items__desc">{{ desc }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,14 +11,15 @@
|
|||||||
@click="onChange(tab.name)"
|
@click="onChange(tab.name)"
|
||||||
>
|
>
|
||||||
<span class="tabs-block__tab-card">
|
<span class="tabs-block__tab-card">
|
||||||
<SvgIcon v-if="tab.icon" :name="tab.icon" :size="16" color="#bcbcbc" />
|
<SvgIcon v-if="tab.icon" :name="tab.icon" :size="18" color="#bcbcbc" />
|
||||||
<span class="tabs-block__tab-label">{{ tab.label }}</span>
|
<span class="tabs-block__tab-label">{{ tab.label }}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template v-for="tab in tabs">
|
||||||
<div
|
<div
|
||||||
v-for="tab in tabs"
|
v-if="tab.hasButton"
|
||||||
:key="`btn-${tab.name}`"
|
:key="`btn-${tab.name}`"
|
||||||
class="tabs-block__action tabs-block__action--desktop"
|
class="tabs-block__action tabs-block__action--desktop"
|
||||||
>
|
>
|
||||||
@@ -33,10 +34,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template v-for="tab in tabs">
|
||||||
<div
|
<div
|
||||||
v-for="tab in tabs"
|
v-if="tab.hasButton"
|
||||||
:key="`mbtn-${tab.name}`"
|
:key="`mbtn-${tab.name}`"
|
||||||
class="tabs-block__action tabs-block__action--mobile"
|
class="tabs-block__action tabs-block__action--mobile"
|
||||||
>
|
>
|
||||||
@@ -51,6 +54,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="tabs-block__content">
|
<div class="tabs-block__content">
|
||||||
<slot :name="activeTab" />
|
<slot :name="activeTab" />
|
||||||
@@ -126,7 +130,7 @@ const onActionClick = (tab) => {
|
|||||||
gap: 0.375rem;
|
gap: 0.375rem;
|
||||||
background: rgba(255, 255, 255, 80%);
|
background: rgba(255, 255, 255, 80%);
|
||||||
box-shadow: 0 10px 15px -3px rgba(241, 241, 241, 100%);
|
box-shadow: 0 10px 15px -3px rgba(241, 241, 241, 100%);
|
||||||
padding: 0.875rem;
|
padding: 0.775rem;
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +148,8 @@ const onActionClick = (tab) => {
|
|||||||
&--desktop {
|
&--desktop {
|
||||||
display: none;
|
display: none;
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
margin-block-end: 0.625rem;
|
|
||||||
|
// margin-block-end: 0.625rem;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -162,7 +167,7 @@ const onActionClick = (tab) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__action-btn {
|
&__action-btn {
|
||||||
padding: 0 1.25rem;
|
height: 100%;
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div class="activity-card">
|
||||||
|
<SvgIcon name="card-corner-arrow" :size="'1.5rem'" class="activity-card__corner" />
|
||||||
|
|
||||||
|
<div class="activity-card__thumb" :class="{ 'activity-card__thumb--circle': circle }">
|
||||||
|
<img v-if="avatarUrl" :src="avatarUrl" :alt="title" class="activity-card__thumb-img" />
|
||||||
|
<SvgIcon v-else :name="thumbIcon" :size="'1.5rem'" color="#848484" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="activity-card__body">
|
||||||
|
<p class="activity-card__title">{{ title }}</p>
|
||||||
|
|
||||||
|
<div class="activity-card__meta">
|
||||||
|
<SvgIcon :name="metaIcon" :size="'0.875rem'" class="activity-card__meta-icon" />
|
||||||
|
<div class="activity-card__rows">
|
||||||
|
<p v-for="(row, i) in meta" :key="i" class="activity-card__row">
|
||||||
|
<span class="activity-card__row-label">{{ row.label }}</span>
|
||||||
|
<span
|
||||||
|
class="activity-card__row-value"
|
||||||
|
:class="{ 'activity-card__row-value--num': row.numeric }"
|
||||||
|
>
|
||||||
|
{{ row.value }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
title: { type: String, default: '' },
|
||||||
|
/** rows of { label, value, numeric? } */
|
||||||
|
meta: { type: Array, default: () => [] },
|
||||||
|
/** @type {import('vue').PropType<import('@/components/icons/icon-names').IconName>} */
|
||||||
|
metaIcon: { type: String, default: 'calendar' },
|
||||||
|
/** thumbnail glyph shown when there is no avatarUrl */
|
||||||
|
thumbIcon: { type: String, default: 'play' },
|
||||||
|
/** render the thumbnail as a circular avatar instead of a rounded square */
|
||||||
|
circle: { type: Boolean, default: false },
|
||||||
|
avatarUrl: { type: String, default: '' },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.activity-card {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
min-height: 5.9375rem;
|
||||||
|
background: rgba(255, 255, 255, 58%);
|
||||||
|
box-shadow: 0 6.75px 19.425px rgba(0, 0, 0, 4%);
|
||||||
|
border-radius: 0.9375rem;
|
||||||
|
|
||||||
|
&__corner {
|
||||||
|
position: absolute;
|
||||||
|
inset-block-start: 0.75rem;
|
||||||
|
inset-inline-end: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__thumb {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 3.6875rem;
|
||||||
|
height: 3.6875rem;
|
||||||
|
background: #f4f4f4;
|
||||||
|
border-radius: 0.9375rem;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&--circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__thumb-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.625rem;
|
||||||
|
text-align: start;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #4b4b4b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta-icon {
|
||||||
|
order: 2;
|
||||||
|
margin-top: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__rows {
|
||||||
|
order: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: #4b4b4b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__row-value {
|
||||||
|
&--num {
|
||||||
|
font-family: var(--font-family-number-fa);
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="activity-group">
|
||||||
|
<div class="activity-group__pill">
|
||||||
|
<SvgIcon :name="pillIcon" :size="'1.125rem'" class="activity-group__pill-icon" />
|
||||||
|
<span class="activity-group__pill-label">{{ pillLabel }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DashboardActivityCard
|
||||||
|
:title="title"
|
||||||
|
:meta="meta"
|
||||||
|
:meta-icon="metaIcon"
|
||||||
|
:thumb-icon="thumbIcon"
|
||||||
|
:circle="circle"
|
||||||
|
:avatar-url="avatarUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import DashboardActivityCard from '@/components/dashboard/DashboardActivityCard.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
/** @type {import('vue').PropType<import('@/components/icons/icon-names').IconName>} */
|
||||||
|
pillIcon: { type: String, default: 'shopping-bag' },
|
||||||
|
pillLabel: { type: String, default: '' },
|
||||||
|
title: { type: String, default: '' },
|
||||||
|
meta: { type: Array, default: () => [] },
|
||||||
|
metaIcon: { type: String, default: 'calendar' },
|
||||||
|
thumbIcon: { type: String, default: 'play' },
|
||||||
|
circle: { type: Boolean, default: false },
|
||||||
|
avatarUrl: { type: String, default: '' },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.activity-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
&__pill {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
background: rgba(255, 255, 255, 58%);
|
||||||
|
box-shadow: 0 6.75px 19.425px rgba(0, 0, 0, 4%);
|
||||||
|
border-radius: 0.9375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pill-icon {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pill-label {
|
||||||
|
order: 1;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: #4b4b4b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<div class="calendar">
|
||||||
|
<p class="calendar__title">{{ monthLabel }}</p>
|
||||||
|
|
||||||
|
<div class="calendar__week">
|
||||||
|
<div
|
||||||
|
v-for="(d, i) in days"
|
||||||
|
:key="i"
|
||||||
|
class="calendar__day"
|
||||||
|
:class="{ 'calendar__day--active': d.selected }"
|
||||||
|
>
|
||||||
|
<span v-if="d.marker" class="calendar__marker" />
|
||||||
|
<span class="calendar__weekday">{{ d.weekday }}</span>
|
||||||
|
<span class="calendar__date">{{ d.day }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
monthLabel: { type: String, default: '' },
|
||||||
|
/** ordered right-to-left: { weekday, day, selected?, marker? } */
|
||||||
|
days: { type: Array, default: () => [] },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.calendar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 1.125rem 1rem;
|
||||||
|
background: rgba(255, 255, 255, 58%);
|
||||||
|
box-shadow: 0 6.75px 19.425px rgba(0, 0, 0, 4%);
|
||||||
|
border-radius: 0.9375rem;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin: 0;
|
||||||
|
text-align: start;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__week {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__day {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.375rem 0.125rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background: var(--color-secondary);
|
||||||
|
box-shadow: 0 9px 9.75px rgba(0, 112, 116, 19%);
|
||||||
|
|
||||||
|
.calendar__weekday,
|
||||||
|
.calendar__date {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__marker {
|
||||||
|
position: absolute;
|
||||||
|
inset-block-start: 0.125rem;
|
||||||
|
inset-inline-start: 0.25rem;
|
||||||
|
width: 0.25rem;
|
||||||
|
height: 0.25rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__weekday {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.625rem;
|
||||||
|
color: #828282;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__date {
|
||||||
|
font-family: var(--font-family-number-fa);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #717171;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<div class="profile-card">
|
||||||
|
<div class="profile-card__avatar">
|
||||||
|
<img v-if="avatarUrl" :src="avatarUrl" :alt="name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="profile-card__name">{{ name }}</p>
|
||||||
|
|
||||||
|
<div class="profile-card__location">
|
||||||
|
<SvgIcon name="map-pin-simple-area" :size="'0.9375rem'" />
|
||||||
|
<span>{{ location }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="profile-card__stats">
|
||||||
|
<div v-for="(stat, i) in stats" :key="i" class="profile-card__stat">
|
||||||
|
<span class="profile-card__stat-label">{{ stat.label }}</span>
|
||||||
|
<span
|
||||||
|
class="profile-card__stat-value"
|
||||||
|
:class="{ 'profile-card__stat-value--text': !isNumeric(stat.value) }"
|
||||||
|
>
|
||||||
|
{{ stat.value }}
|
||||||
|
<span v-if="stat.unit" class="profile-card__stat-unit">{{ stat.unit }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
name: { type: String, default: '' },
|
||||||
|
location: { type: String, default: '' },
|
||||||
|
avatarUrl: { type: String, default: '' },
|
||||||
|
/** mini-stats of { label, value, unit? } */
|
||||||
|
stats: { type: Array, default: () => [] },
|
||||||
|
})
|
||||||
|
|
||||||
|
const isNumeric = (value) => /^[\d۰-۹]+$/.test(String(value ?? '').trim())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.profile-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 2rem 1.25rem 1.5rem;
|
||||||
|
background: rgba(255, 255, 255, 58%);
|
||||||
|
box-shadow: 0 6.75px 19.425px rgba(0, 0, 0, 4%);
|
||||||
|
border-radius: 0.9375rem;
|
||||||
|
|
||||||
|
&__avatar {
|
||||||
|
width: 7.75rem;
|
||||||
|
height: 7.75rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f4f4f4;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__location {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #989898;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat-label {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: #a7a7a7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat-value {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-family: var(--font-family-number-fa);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.5625rem;
|
||||||
|
color: #818181;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&--text {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stat-unit {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #929292;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="to ? RouterLink : 'div'"
|
||||||
|
:to="to || undefined"
|
||||||
|
class="stat-card"
|
||||||
|
:class="{ 'stat-card--linked': to }"
|
||||||
|
>
|
||||||
|
<SvgIcon name="card-corner-arrow" :size="'1.5rem'" class="stat-card__corner" />
|
||||||
|
|
||||||
|
<div class="stat-card__head">
|
||||||
|
<SvgIcon :name="icon" :size="'1.0625rem'" class="stat-card__head-icon" />
|
||||||
|
<span class="stat-card__label">{{ label }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card__body">
|
||||||
|
<span class="stat-card__value">{{ value }}</span>
|
||||||
|
<div class="stat-card__meta">
|
||||||
|
<span class="stat-card__unit">{{ unit }}</span>
|
||||||
|
<span class="stat-card__subtitle">{{ subtitle }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { RouterLink } from 'vue-router'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
/** @type {import('vue').PropType<import('@/components/icons/icon-names').IconName>} */
|
||||||
|
icon: { type: String, required: true },
|
||||||
|
label: { type: String, default: '' },
|
||||||
|
value: { type: [String, Number], default: '' },
|
||||||
|
unit: { type: String, default: '' },
|
||||||
|
subtitle: { type: String, default: '' },
|
||||||
|
to: { type: [String, Object], default: null },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.stat-card {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1.125rem 1.25rem;
|
||||||
|
min-height: 8.75rem;
|
||||||
|
background: rgba(255, 255, 255, 58%);
|
||||||
|
box-shadow: 0 6.75px 19.425px rgba(0, 0, 0, 4%);
|
||||||
|
border-radius: 0.9375rem;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
|
||||||
|
&--linked {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 9px 24px rgba(0, 0, 0, 7%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__corner {
|
||||||
|
position: absolute;
|
||||||
|
inset-block-start: 0.875rem;
|
||||||
|
inset-inline-end: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__head-icon {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
order: 1;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: #4b4b4b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.625rem;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
font-family: var(--font-family-number-fa);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 4rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-shadow: 0 5.25px 8.625px rgba(243, 102, 117, 18%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__unit {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: #6e6e6e;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtitle {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #6e6e6e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<div class="badge-select">
|
||||||
|
<label v-if="label" class="badge-select__label">{{ label }}</label>
|
||||||
|
<div class="badge-select__chips" :class="`badge-select__chips--${size}`">
|
||||||
|
<button
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option[optionValue]"
|
||||||
|
type="button"
|
||||||
|
class="badge-select__chip"
|
||||||
|
:class="[
|
||||||
|
`badge-select__chip--${variant}`,
|
||||||
|
{ 'badge-select__chip--selected': modelValue === option[optionValue] },
|
||||||
|
]"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="select(option)"
|
||||||
|
>
|
||||||
|
{{ option[optionLabel] }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="error" class="badge-select__error">{{ error }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: [String, Number], default: null },
|
||||||
|
label: { type: String, default: '' },
|
||||||
|
options: { type: Array, default: () => [] },
|
||||||
|
optionLabel: { type: String, default: 'label' },
|
||||||
|
optionValue: { type: String, default: 'value' },
|
||||||
|
variant: { type: String, default: 'neutral' },
|
||||||
|
size: { type: String, default: 'md' },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
error: { type: String, default: '' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
|
||||||
|
const select = (option) => {
|
||||||
|
if (props.disabled) return
|
||||||
|
const value = option[props.optionValue]
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
emit('change', value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.badge-select {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 0.375rem;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-prim-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chip {
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: transparent;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
color: #b5b5b5;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover:enabled {
|
||||||
|
color: #7a7a7a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #f0f0f0;
|
||||||
|
box-shadow: 0 6.75px 19.425px rgba(0, 0, 0, 6%);
|
||||||
|
color: #4b4b4b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--primary#{&}--selected {
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chips--sm .badge-select__chip {
|
||||||
|
padding: 0.25rem 0.875rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chips--md .badge-select__chip {
|
||||||
|
padding: 0.375rem 1.125rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chips--lg .badge-select__chip {
|
||||||
|
padding: 0.5rem 1.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
@click="toggle"
|
@click="toggle"
|
||||||
>
|
>
|
||||||
<span class="datepicker-field__value">{{ displayValue }}</span>
|
<span class="datepicker-field__value">{{ displayValue }}</span>
|
||||||
<SvgIcon name="calendar" :size="20" class="datepicker-field__icon" />
|
<SvgIcon name="calendar" :size="20" color="#DDDDDD" class="datepicker-field__icon" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="datepicker-field__error" :class="{ 'animate-shake': vibration }">
|
<div v-if="error" class="datepicker-field__error" :class="{ 'animate-shake': vibration }">
|
||||||
@@ -377,6 +377,7 @@ export default {
|
|||||||
this.$emit('change', gregorian)
|
this.$emit('change', gregorian)
|
||||||
this.close()
|
this.close()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error('Error in handleConfirm:', error)
|
console.error('Error in handleConfirm:', error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,8 +9,11 @@
|
|||||||
@dragleave="isDragging = false"
|
@dragleave="isDragging = false"
|
||||||
@drop="handleDrop"
|
@drop="handleDrop"
|
||||||
>
|
>
|
||||||
<SvgIcon name="upload" :size="32" class="file-uploader__upload-icon" />
|
<img :src="uploadSimple" class="file-uploader__upload-icon" />
|
||||||
<span class="file-uploader__upload-text">{{ uploadText }}</span>
|
<span class="file-uploader__upload-text">
|
||||||
|
<span class="file-uploader__upload-text--light">بارگذاری</span>
|
||||||
|
<span class="file-uploader__upload-text--bold">فایل ضمیمــــه</span>
|
||||||
|
</span>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
class="file-uploader__input"
|
class="file-uploader__input"
|
||||||
@@ -56,14 +59,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="file-uploader__empty">هیچ فایلی آپلود نشده است</div>
|
<!-- <div v-else class="file-uploader__empty">هیچ فایلی آپلود نشده است</div> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import CircleButton from '@/components/CircleButton.vue'
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
|
import uploadSimple from '@/assets/images/upload-simple.png'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: Array, default: () => [] },
|
modelValue: { type: Array, default: () => [] },
|
||||||
@@ -77,10 +81,6 @@ const emit = defineEmits(['update:modelValue', 'select', 'remove', 'error'])
|
|||||||
|
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
|
|
||||||
const uploadText = computed(() =>
|
|
||||||
props.modelValue.length === 0 ? 'فایل جدید اضافه کنید' : 'فایل دیگری اضافه کنید'
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleFileSelect = (event) => {
|
const handleFileSelect = (event) => {
|
||||||
const files = [...event.target.files]
|
const files = [...event.target.files]
|
||||||
emitFiles(files)
|
emitFiles(files)
|
||||||
@@ -140,14 +140,15 @@ const formatFileSize = (bytes) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__dropzone {
|
&__dropzone {
|
||||||
border: 2px dashed #d1d5db;
|
background-color: #f4f4f4;
|
||||||
border-radius: 0.5rem;
|
border-radius: 1rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
@@ -161,10 +162,20 @@ const formatFileSize = (bytes) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__upload-text {
|
&__upload-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
font-family: var(--font-family-fa);
|
font-family: var(--font-family-fa);
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 500;
|
|
||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
|
|
||||||
|
&--light {
|
||||||
|
text-align: right;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--bold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__input {
|
&__input {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="image-cropper__hint">
|
<div class="image-cropper__hint">
|
||||||
<SvgIcon name="warning" class="image-cropper__hint-icon" />
|
<SvgIcon :name="helperIcon" color="#B8B8B8" :size="20" />
|
||||||
<p>{{ helperText }}</p>
|
<p>{{ helperText }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,11 +94,12 @@ const props = defineProps({
|
|||||||
name: { type: String, default: '' },
|
name: { type: String, default: '' },
|
||||||
vibration: { type: Boolean, default: false },
|
vibration: { type: Boolean, default: false },
|
||||||
bgColor: { type: String, default: '#f3f4f6' },
|
bgColor: { type: String, default: '#f3f4f6' },
|
||||||
maxSize: { type: Number, default: 250 * 1024 },
|
maxSize: { type: Number, default: 5 * 1024 * 1024 },
|
||||||
helperText: {
|
helperText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'سایز تصویر رسمی 3x4 و حجم تصویر بیشتر از 250 کیلو بایت نباشد.',
|
default: 'سایز تصویر رسمی 3x4 و حجم تصویر بیشتر از ۵ مگابایت نباشد.',
|
||||||
},
|
},
|
||||||
|
helperIcon: { type: String, default: 'warning' },
|
||||||
error: { type: String, default: '' },
|
error: { type: String, default: '' },
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -143,7 +144,7 @@ const onFileChange = (event) => {
|
|||||||
if (!file) return
|
if (!file) return
|
||||||
|
|
||||||
if (props.maxSize > 0 && file.size > props.maxSize) {
|
if (props.maxSize > 0 && file.size > props.maxSize) {
|
||||||
emit('error', 'حجم تصویر نباید بیشتر از ۲۵۰ کیلوبایت باشد.')
|
emit('error', 'حجم تصویر نباید بیشتر از ۵ مگابایت باشد.')
|
||||||
event.target.value = null
|
event.target.value = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -314,7 +315,7 @@ onBeforeUnmount(cleanup)
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.75rem;
|
gap: 0.25rem;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
font-family: var(--font-family-fa);
|
font-family: var(--font-family-fa);
|
||||||
@@ -324,8 +325,7 @@ onBeforeUnmount(cleanup)
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
line-height: 1.75rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,19 @@
|
|||||||
@click="toggle"
|
@click="toggle"
|
||||||
>
|
>
|
||||||
<span class="select-field__value">{{ selectedLabel }}</span>
|
<span class="select-field__value">{{ selectedLabel }}</span>
|
||||||
|
<button
|
||||||
|
v-if="showClear"
|
||||||
|
type="button"
|
||||||
|
class="select-field__clear"
|
||||||
|
aria-label="پاک کردن"
|
||||||
|
@click.stop="clearSelection"
|
||||||
|
>
|
||||||
|
<SvgIcon name="close" :size="14" color="currentColor" />
|
||||||
|
</button>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
name="caret-down"
|
name="caret-down"
|
||||||
:size="20"
|
:size="20"
|
||||||
|
color="#DDDDDD"
|
||||||
class="select-field__caret"
|
class="select-field__caret"
|
||||||
:class="{ 'select-field__caret--open': isOpen }"
|
:class="{ 'select-field__caret--open': isOpen }"
|
||||||
/>
|
/>
|
||||||
@@ -89,6 +99,7 @@ const props = defineProps({
|
|||||||
optionLabel: { type: String, default: 'name' },
|
optionLabel: { type: String, default: 'name' },
|
||||||
optionValue: { type: String, default: 'id' },
|
optionValue: { type: String, default: 'id' },
|
||||||
multiple: { type: Boolean, default: false },
|
multiple: { type: Boolean, default: false },
|
||||||
|
clearable: { type: Boolean, default: true },
|
||||||
disabled: { type: Boolean, default: false },
|
disabled: { type: Boolean, default: false },
|
||||||
error: { type: String, default: '' },
|
error: { type: String, default: '' },
|
||||||
vibration: { type: Boolean, default: false },
|
vibration: { type: Boolean, default: false },
|
||||||
@@ -129,6 +140,20 @@ const selectedLabel = computed(() => {
|
|||||||
return found ? found[props.optionLabel] : props.placeholder
|
return found ? found[props.optionLabel] : props.placeholder
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const hasValue = computed(() =>
|
||||||
|
props.multiple
|
||||||
|
? Array.isArray(props.modelValue) && props.modelValue.length > 0
|
||||||
|
: props.modelValue !== undefined && props.modelValue !== null && props.modelValue !== ''
|
||||||
|
)
|
||||||
|
|
||||||
|
const showClear = computed(() => props.clearable && !props.disabled && hasValue.value)
|
||||||
|
|
||||||
|
const clearSelection = () => {
|
||||||
|
const cleared = props.multiple ? [] : null
|
||||||
|
emit('update:modelValue', cleared)
|
||||||
|
emit('change', cleared)
|
||||||
|
}
|
||||||
|
|
||||||
const select = (opt) => {
|
const select = (opt) => {
|
||||||
if (props.disabled) return
|
if (props.disabled) return
|
||||||
const value = opt[props.optionValue]
|
const value = opt[props.optionValue]
|
||||||
@@ -242,10 +267,6 @@ onBeforeUnmount(() => unmountFloating())
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
&:focus {
|
|
||||||
box-shadow: 0 0 0 1px var(--color-validate);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--error {
|
&--error {
|
||||||
border-color: var(--color-error);
|
border-color: var(--color-error);
|
||||||
|
|
||||||
@@ -278,6 +299,24 @@ onBeforeUnmount(() => unmountFloating())
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__clear {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
margin-inline-end: 0.375rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-thd-gray);
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: color 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__error {
|
&__error {
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
font-family: var(--font-family-fa);
|
font-family: var(--font-family-fa);
|
||||||
@@ -308,10 +347,6 @@ onBeforeUnmount(() => unmountFloating())
|
|||||||
border: 1px solid var(--color-thd-gray);
|
border: 1px solid var(--color-thd-gray);
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
&:focus {
|
|
||||||
box-shadow: 0 0 0 1px var(--color-validate);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__empty {
|
&__empty {
|
||||||
@@ -336,12 +371,15 @@ onBeforeUnmount(() => unmountFloating())
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.25rem 0.75rem;
|
min-height: 2.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 1.5rem;
|
border-radius: 1.5rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-family: var(--font-family-fa);
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.15s;
|
transition: background-color 0.15s;
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,6 @@ defineExpose({ focus: () => inputEl.value?.focus() })
|
|||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: var(--color-thd-gray);
|
border-color: var(--color-thd-gray);
|
||||||
box-shadow: 0 0 0 1px var(--color-validate);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +134,7 @@ defineExpose({ focus: () => inputEl.value?.focus() })
|
|||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset-inline-start: 0.75rem;
|
inset-inline-end: 0.75rem;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: var(--color-thd-gray);
|
border-color: var(--color-thd-gray);
|
||||||
box-shadow: 0 0 0 1px var(--color-validate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&--no-resize {
|
&--no-resize {
|
||||||
|
|||||||
@@ -20,20 +20,29 @@
|
|||||||
<SvgIcon name="close" :size="40" color="var(--color-error)" />
|
<SvgIcon name="close" :size="40" color="var(--color-error)" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-else class="voice-recorder__preview">
|
<div v-else class="voice-recorder__player">
|
||||||
<button type="button" class="voice-recorder__main-btn" @click="togglePlay">
|
<button type="button" class="voice-recorder__play-btn" @click="togglePlay">
|
||||||
<SvgIcon
|
<SvgIcon :name="isPlaying ? 'pause' : 'play'" :size="24" color="#848484" />
|
||||||
:name="isPlaying ? 'close' : 'paper-plane-right'"
|
|
||||||
:size="40"
|
|
||||||
color="var(--color-thd-gray)"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
|
<div
|
||||||
|
ref="trackEl"
|
||||||
|
class="voice-recorder__track"
|
||||||
|
@pointerdown="onSeekStart"
|
||||||
|
@pointermove="onSeekMove"
|
||||||
|
@pointerup="onSeekEnd"
|
||||||
|
@pointercancel="onSeekEnd"
|
||||||
|
>
|
||||||
|
<div class="voice-recorder__track-fill" :style="{ width: `${progress}%` }" />
|
||||||
|
<span class="voice-recorder__knob" :style="{ left: `${progress}%` }" />
|
||||||
|
</div>
|
||||||
|
<span class="voice-recorder__time">{{ timeLabel }}</span>
|
||||||
<audio
|
<audio
|
||||||
ref="audioEl"
|
ref="audioEl"
|
||||||
:src="audioUrl"
|
:src="audioUrl"
|
||||||
class="voice-recorder__audio"
|
class="voice-recorder__audio"
|
||||||
@ended="onEnded"
|
@ended="onEnded"
|
||||||
@timeupdate="onTimeUpdate"
|
@timeupdate="onTimeUpdate"
|
||||||
|
@loadedmetadata="onLoadedMetadata"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -75,10 +84,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="audioUrl" class="voice-recorder__progress">
|
|
||||||
<div class="voice-recorder__progress-fill" :style="{ width: `${progress}%` }" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p v-if="error" class="voice-recorder__error">{{ error }}</p>
|
<p v-if="error" class="voice-recorder__error">{{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -113,14 +118,35 @@ const isRecording = ref(false)
|
|||||||
const isPlaying = ref(false)
|
const isPlaying = ref(false)
|
||||||
const elapsedMs = ref(0)
|
const elapsedMs = ref(0)
|
||||||
const progress = ref(0)
|
const progress = ref(0)
|
||||||
|
const duration = ref(0)
|
||||||
|
const currentSeconds = ref(0)
|
||||||
const audioEl = ref(null)
|
const audioEl = ref(null)
|
||||||
|
const trackEl = ref(null)
|
||||||
const fileInput = ref(null)
|
const fileInput = ref(null)
|
||||||
|
let seeking = false
|
||||||
|
|
||||||
let mediaRecorder = null
|
let mediaRecorder = null
|
||||||
let mediaStream = null
|
let mediaStream = null
|
||||||
let chunks = []
|
let chunks = []
|
||||||
let timerHandle = null
|
let timerHandle = null
|
||||||
let timerStart = 0
|
let timerStart = 0
|
||||||
|
let recorderType = null
|
||||||
|
|
||||||
|
// The backend only accepts these audio extensions. Browsers can't record mp3/wav,
|
||||||
|
// but most can record into an mp4 container (→ .m4a), so prefer a supported type
|
||||||
|
// whose extension the backend allows; fall back to webm only as a last resort.
|
||||||
|
const ACCEPTED_AUDIO_TYPES = [
|
||||||
|
{ mime: 'audio/mp4', ext: 'm4a' },
|
||||||
|
{ mime: 'audio/mpeg', ext: 'mp3' },
|
||||||
|
{ mime: 'audio/wav', ext: 'wav' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const pickRecorderType = () => {
|
||||||
|
const supported = ACCEPTED_AUDIO_TYPES.find((c) =>
|
||||||
|
window.MediaRecorder?.isTypeSupported?.(c.mime)
|
||||||
|
)
|
||||||
|
return supported || { mime: 'audio/webm', ext: 'webm' }
|
||||||
|
}
|
||||||
|
|
||||||
const formattedTime = computed(() => {
|
const formattedTime = computed(() => {
|
||||||
const s = Math.floor(elapsedMs.value / 1000)
|
const s = Math.floor(elapsedMs.value / 1000)
|
||||||
@@ -154,6 +180,8 @@ const setRecording = (file) => {
|
|||||||
audioFile.value = file
|
audioFile.value = file
|
||||||
audioUrl.value = URL.createObjectURL(file)
|
audioUrl.value = URL.createObjectURL(file)
|
||||||
progress.value = 0
|
progress.value = 0
|
||||||
|
duration.value = 0
|
||||||
|
currentSeconds.value = 0
|
||||||
isPlaying.value = false
|
isPlaying.value = false
|
||||||
emit('update:modelValue', file)
|
emit('update:modelValue', file)
|
||||||
emit('recorded', file)
|
emit('recorded', file)
|
||||||
@@ -163,16 +191,19 @@ const startRecording = async () => {
|
|||||||
try {
|
try {
|
||||||
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
||||||
chunks = []
|
chunks = []
|
||||||
mediaRecorder = new window.MediaRecorder(mediaStream)
|
recorderType = pickRecorderType()
|
||||||
|
mediaRecorder = new window.MediaRecorder(mediaStream, { mimeType: recorderType.mime })
|
||||||
mediaRecorder.ondataavailable = (e) => {
|
mediaRecorder.ondataavailable = (e) => {
|
||||||
if (e.data?.size > 0) chunks.push(e.data)
|
if (e.data?.size > 0) chunks.push(e.data)
|
||||||
}
|
}
|
||||||
mediaRecorder.onstop = () => {
|
mediaRecorder.onstop = () => {
|
||||||
isRecording.value = false
|
isRecording.value = false
|
||||||
stopTracks()
|
stopTracks()
|
||||||
const blob = new Blob(chunks, { type: 'audio/webm' })
|
const mime = recorderType?.mime || mediaRecorder?.mimeType || 'audio/webm'
|
||||||
const file = new File([blob], `recording-${Date.now()}.webm`, {
|
const ext = recorderType?.ext || 'webm'
|
||||||
type: 'audio/webm',
|
const blob = new Blob(chunks, { type: mime })
|
||||||
|
const file = new File([blob], `recording-${Date.now()}.${ext}`, {
|
||||||
|
type: mime,
|
||||||
lastModified: Date.now(),
|
lastModified: Date.now(),
|
||||||
})
|
})
|
||||||
setRecording(file)
|
setRecording(file)
|
||||||
@@ -238,12 +269,71 @@ const togglePlay = () => {
|
|||||||
const onEnded = () => {
|
const onEnded = () => {
|
||||||
isPlaying.value = false
|
isPlaying.value = false
|
||||||
progress.value = 0
|
progress.value = 0
|
||||||
|
currentSeconds.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTimeUpdate = () => {
|
const onTimeUpdate = () => {
|
||||||
const a = audioEl.value
|
const a = audioEl.value
|
||||||
if (!a?.duration) return
|
if (!a || !Number.isFinite(duration.value) || duration.value <= 0) return
|
||||||
progress.value = (a.currentTime / a.duration) * 100
|
currentSeconds.value = a.currentTime
|
||||||
|
progress.value = (a.currentTime / duration.value) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaRecorder blobs report `Infinity` duration in Chromium until the element
|
||||||
|
// is seeked past the end once — force that, then rewind.
|
||||||
|
const onLoadedMetadata = () => {
|
||||||
|
const a = audioEl.value
|
||||||
|
if (!a) return
|
||||||
|
if (Number.isFinite(a.duration)) {
|
||||||
|
duration.value = a.duration
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const restore = () => {
|
||||||
|
if (!Number.isFinite(a.duration)) return
|
||||||
|
duration.value = a.duration
|
||||||
|
a.currentTime = 0
|
||||||
|
a.removeEventListener('timeupdate', restore)
|
||||||
|
}
|
||||||
|
a.addEventListener('timeupdate', restore)
|
||||||
|
a.currentTime = 1e10
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatClock = (secs) => {
|
||||||
|
if (!Number.isFinite(secs) || secs < 0) return '00:00'
|
||||||
|
const total = Math.floor(secs)
|
||||||
|
const h = Math.floor(total / 3600)
|
||||||
|
const mm = String(Math.floor((total % 3600) / 60)).padStart(2, '0')
|
||||||
|
const ss = String(total % 60).padStart(2, '0')
|
||||||
|
return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeLabel = computed(() =>
|
||||||
|
formatClock(isPlaying.value || currentSeconds.value > 0 ? currentSeconds.value : duration.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const seekTo = (clientX) => {
|
||||||
|
const a = audioEl.value
|
||||||
|
const track = trackEl.value
|
||||||
|
if (!a || !track || !Number.isFinite(duration.value) || duration.value <= 0) return
|
||||||
|
const rect = track.getBoundingClientRect()
|
||||||
|
const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width))
|
||||||
|
a.currentTime = ratio * duration.value
|
||||||
|
currentSeconds.value = a.currentTime
|
||||||
|
progress.value = ratio * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSeekStart = (event) => {
|
||||||
|
seeking = true
|
||||||
|
trackEl.value?.setPointerCapture?.(event.pointerId)
|
||||||
|
seekTo(event.clientX)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSeekMove = (event) => {
|
||||||
|
if (seeking) seekTo(event.clientX)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSeekEnd = () => {
|
||||||
|
seeking = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -318,11 +408,76 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__preview {
|
&__player {
|
||||||
|
direction: ltr;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 1.25rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__play-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__track {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: #e8e8e8;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -0.625rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__track-fill {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: #5d5d5d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__knob {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 0.625rem;
|
||||||
|
height: 0.625rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: #4b4b4b;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time {
|
||||||
|
font-family: var(--font-family-en);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #b5b5b5;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 3rem;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__audio {
|
&__audio {
|
||||||
@@ -386,21 +541,6 @@ onBeforeUnmount(() => {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__progress {
|
|
||||||
width: 100%;
|
|
||||||
height: 0.125rem;
|
|
||||||
border-radius: 9999px;
|
|
||||||
background: var(--color-thd-gray);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 40%);
|
|
||||||
border-radius: 9999px;
|
|
||||||
transition: width 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__error {
|
&__error {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: var(--font-family-fa);
|
font-family: var(--font-family-fa);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const props = defineProps({
|
|||||||
const component = computed(() => {
|
const component = computed(() => {
|
||||||
const c = iconRegistry[props.name]
|
const c = iconRegistry[props.name]
|
||||||
if (!c && import.meta.env.DEV) {
|
if (!c && import.meta.env.DEV) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.warn(`[SvgIcon] unknown icon "${props.name}"`)
|
console.warn(`[SvgIcon] unknown icon "${props.name}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +47,5 @@ const iconStyle = computed(() => ({
|
|||||||
.svg-icon {
|
.svg-icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
fill: currentcolor;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,19 +8,27 @@ export type IconName =
|
|||||||
| 'bell'
|
| 'bell'
|
||||||
| 'book'
|
| 'book'
|
||||||
| 'calendar'
|
| 'calendar'
|
||||||
|
| 'card-corner-arrow'
|
||||||
| 'caret-down'
|
| 'caret-down'
|
||||||
| 'caret-left'
|
| 'caret-left'
|
||||||
| 'caret-right'
|
| 'caret-right'
|
||||||
|
| 'certificate'
|
||||||
| 'chat'
|
| 'chat'
|
||||||
| 'chat-centered-dots'
|
| 'chat-centered-dots'
|
||||||
|
| 'chat-teardrop-text'
|
||||||
| 'check'
|
| 'check'
|
||||||
|
| 'check-circle'
|
||||||
| 'check-square'
|
| 'check-square'
|
||||||
|
| 'clock'
|
||||||
| 'close'
|
| 'close'
|
||||||
| 'copy'
|
| 'copy'
|
||||||
|
| 'envelope-open'
|
||||||
| 'eye'
|
| 'eye'
|
||||||
| 'eye-slash'
|
| 'eye-slash'
|
||||||
| 'file'
|
| 'file'
|
||||||
|
| 'folder-simple-star'
|
||||||
| 'funnel'
|
| 'funnel'
|
||||||
|
| 'graduation-cap'
|
||||||
| 'heart'
|
| 'heart'
|
||||||
| 'instagram'
|
| 'instagram'
|
||||||
| 'link'
|
| 'link'
|
||||||
@@ -28,21 +36,31 @@ export type IconName =
|
|||||||
| 'map-pin-simple-area'
|
| 'map-pin-simple-area'
|
||||||
| 'menu'
|
| 'menu'
|
||||||
| 'microphone'
|
| 'microphone'
|
||||||
|
| 'monitor'
|
||||||
| 'mood'
|
| 'mood'
|
||||||
|
| 'newspaper'
|
||||||
| 'paper-plane'
|
| 'paper-plane'
|
||||||
| 'paper-plane-right'
|
| 'paper-plane-right'
|
||||||
|
| 'pause'
|
||||||
| 'pencil'
|
| 'pencil'
|
||||||
| 'phone'
|
| 'phone'
|
||||||
|
| 'play'
|
||||||
| 'plus'
|
| 'plus'
|
||||||
|
| 'power'
|
||||||
| 'scroll'
|
| 'scroll'
|
||||||
|
| 'shopping-bag'
|
||||||
|
| 'smiley-sad'
|
||||||
| 'spinner'
|
| 'spinner'
|
||||||
| 'square'
|
| 'square'
|
||||||
| 'telegram'
|
| 'telegram'
|
||||||
| 'trash'
|
| 'trash'
|
||||||
| 'upload'
|
| 'upload'
|
||||||
| 'user'
|
| 'user'
|
||||||
|
| 'user-circle-check'
|
||||||
|
| 'user-sound'
|
||||||
| 'users'
|
| 'users'
|
||||||
| 'users-three'
|
| 'users-three'
|
||||||
|
| 'video'
|
||||||
| 'warning'
|
| 'warning'
|
||||||
|
|
||||||
export const iconNames: readonly IconName[]
|
export const iconNames: readonly IconName[]
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { toFaDigits } from '@/utils/persian'
|
||||||
|
import { toJalaali } from '@/utils/date-utils'
|
||||||
|
|
||||||
|
const WEEKDAY_NAMES = ['شنبه', 'یکشنبه', 'دوشنبه', 'سه شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه']
|
||||||
|
const MONTH_NAMES = [
|
||||||
|
'فروردین',
|
||||||
|
'اردیبهشت',
|
||||||
|
'خرداد',
|
||||||
|
'تیر',
|
||||||
|
'مرداد',
|
||||||
|
'شهریور',
|
||||||
|
'مهر',
|
||||||
|
'آبان',
|
||||||
|
'آذر',
|
||||||
|
'دی',
|
||||||
|
'بهمن',
|
||||||
|
'اسفند',
|
||||||
|
]
|
||||||
|
const GREGORIAN_WEEKDAYS = ['Sat', 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri']
|
||||||
|
const DAY_MS = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the current Jalaali week (Saturday → Friday) anchored to the Asia/Tehran
|
||||||
|
* timezone, independent of the viewer's local clock. The day matching Tehran's
|
||||||
|
* "today" is flagged `selected`. Returns `{ month, days }` for DashboardCalendar.
|
||||||
|
*/
|
||||||
|
export const useTehranCalendar = () => {
|
||||||
|
const parts = new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone: 'Asia/Tehran',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
weekday: 'short',
|
||||||
|
}).formatToParts(new Date())
|
||||||
|
const part = (type) => parts.find((p) => p.type === type)?.value
|
||||||
|
const todayIndex = GREGORIAN_WEEKDAYS.indexOf(part('weekday'))
|
||||||
|
const todayUtc = Date.UTC(Number(part('year')), Number(part('month')) - 1, Number(part('day')))
|
||||||
|
const weekStartUtc = todayUtc - todayIndex * DAY_MS
|
||||||
|
|
||||||
|
const today = new Date(todayUtc)
|
||||||
|
const { jm } = toJalaali(today.getUTCFullYear(), today.getUTCMonth() + 1, today.getUTCDate())
|
||||||
|
|
||||||
|
const days = WEEKDAY_NAMES.map((weekday, i) => {
|
||||||
|
const date = new Date(weekStartUtc + i * DAY_MS)
|
||||||
|
const { jd } = toJalaali(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate())
|
||||||
|
return { weekday, day: toFaDigits(jd), selected: i === todayIndex }
|
||||||
|
})
|
||||||
|
|
||||||
|
return { month: `${MONTH_NAMES[jm - 1]} ماه`, days }
|
||||||
|
}
|
||||||
@@ -15,8 +15,14 @@ export const fields = {
|
|||||||
lastName: 'نام خانوادگی',
|
lastName: 'نام خانوادگی',
|
||||||
nationalCode: 'کد ملی',
|
nationalCode: 'کد ملی',
|
||||||
postalCode: 'کد پستی',
|
postalCode: 'کد پستی',
|
||||||
|
virtualPhoneNumber: 'شماره تلفن مجازی',
|
||||||
|
seminaryCode: 'کد حوزوی',
|
||||||
|
seminaryServiceCenterCode: 'کد مرکز خدمات حوزه',
|
||||||
|
childrenCount: 'تعداد فرزند',
|
||||||
|
marriageStatus: 'وضعیت تأهل',
|
||||||
|
|
||||||
birthDate: 'تاریخ تولد',
|
birthDate: 'تاریخ تولد',
|
||||||
|
birthday: 'تاریخ تولد',
|
||||||
gender: 'جنسیت',
|
gender: 'جنسیت',
|
||||||
maritalStatus: 'وضعیت تأهل',
|
maritalStatus: 'وضعیت تأهل',
|
||||||
|
|
||||||
@@ -27,16 +33,22 @@ export const fields = {
|
|||||||
address: 'آدرس',
|
address: 'آدرس',
|
||||||
|
|
||||||
title: 'عنوان',
|
title: 'عنوان',
|
||||||
|
subject: 'موضوع',
|
||||||
|
category: 'نوع خدمت',
|
||||||
|
note: 'یادداشت',
|
||||||
description: 'توضیحات',
|
description: 'توضیحات',
|
||||||
bio: 'بیوگرافی',
|
bio: 'بیوگرافی',
|
||||||
message: 'پیام',
|
message: 'پیام',
|
||||||
content: 'محتوا',
|
content: 'محتوا',
|
||||||
rejectionReason: 'دلیل رد',
|
rejectionReason: 'دلیل رد',
|
||||||
|
reason: 'دلیل رد',
|
||||||
adminNote: 'یادداشت مدیر',
|
adminNote: 'یادداشت مدیر',
|
||||||
|
reviewerNote: 'یادداشت ارزیاب',
|
||||||
activitySummary: 'خلاصهٔ فعالیت',
|
activitySummary: 'خلاصهٔ فعالیت',
|
||||||
|
|
||||||
avatar: 'تصویر پروفایل',
|
avatar: 'تصویر پروفایل',
|
||||||
avatarId: 'تصویر پروفایل',
|
avatarId: 'تصویر پروفایل',
|
||||||
|
avatarMediaId: 'تصویر پروفایل',
|
||||||
image: 'تصویر',
|
image: 'تصویر',
|
||||||
|
|
||||||
status: 'وضعیت',
|
status: 'وضعیت',
|
||||||
@@ -47,12 +59,15 @@ export const fields = {
|
|||||||
|
|
||||||
startDate: 'تاریخ شروع',
|
startDate: 'تاریخ شروع',
|
||||||
endDate: 'تاریخ پایان',
|
endDate: 'تاریخ پایان',
|
||||||
|
deadline: 'مهلت تحویل',
|
||||||
fromDate: 'از تاریخ',
|
fromDate: 'از تاریخ',
|
||||||
toDate: 'تا تاریخ',
|
toDate: 'تا تاریخ',
|
||||||
|
|
||||||
capacity: 'ظرفیت',
|
capacity: 'ظرفیت',
|
||||||
minCapacity: 'حداقل ظرفیت',
|
minCapacity: 'حداقل ظرفیت',
|
||||||
maxCapacity: 'حداکثر ظرفیت',
|
maxCapacity: 'حداکثر ظرفیت',
|
||||||
|
score: 'نمره',
|
||||||
|
minimumScore: 'حد نصاب قبولی',
|
||||||
|
|
||||||
order: 'ترتیب',
|
order: 'ترتیب',
|
||||||
priority: 'اولویت',
|
priority: 'اولویت',
|
||||||
@@ -65,23 +80,32 @@ export const fields = {
|
|||||||
termId: 'ترم',
|
termId: 'ترم',
|
||||||
courseId: 'دوره',
|
courseId: 'دوره',
|
||||||
sessionId: 'جلسه',
|
sessionId: 'جلسه',
|
||||||
|
missionaryRequestId: 'درخواست مبلغ',
|
||||||
sessionType: 'نوع جلسه',
|
sessionType: 'نوع جلسه',
|
||||||
|
type: 'نوع جلسه',
|
||||||
|
link: 'لینک جلسه',
|
||||||
teacherId: 'مدرس',
|
teacherId: 'مدرس',
|
||||||
studentIds: 'دانشآموزان',
|
studentIds: 'دانشآموزان',
|
||||||
|
prerequisiteCourseIds: 'دورههای پیشنیاز',
|
||||||
|
contentMediaId: 'فایل محتوای دوره',
|
||||||
|
coverMediaId: 'تصویر جلد',
|
||||||
|
imageId: 'تصویر',
|
||||||
|
|
||||||
educationStatus: 'وضعیت تحصیلی',
|
educationStatus: 'وضعیت تحصیلی',
|
||||||
fieldOfStudy: 'رشتهٔ تحصیلی',
|
fieldOfStudy: 'رشتهٔ تحصیلی',
|
||||||
seminaryLevel: 'مقطع حوزه',
|
seminaryLevel: 'مقطع حوزه',
|
||||||
universityLevel: 'مقطع دانشگاهی',
|
universityLevel: 'مقطع دانشگاهی',
|
||||||
universityName: 'نام حوزه علمیه / دانشگاه',
|
universityName: 'نام حوزه علمیه / دانشگاه',
|
||||||
workExperienceSummary: 'خلاصهٔ سوابق شغلی',
|
degree: 'مقطع تحصیلی',
|
||||||
|
jobTitle: 'عنوان شغل',
|
||||||
|
organizationName: 'نام سازمان',
|
||||||
hijabApproach: 'رویکرد حجاب',
|
hijabApproach: 'رویکرد حجاب',
|
||||||
|
|
||||||
prerequisites: 'پیشنیازها',
|
prerequisites: 'پیشنیازها',
|
||||||
specializedTopics: 'موضوعات تخصصی',
|
specializedTopics: 'موضوعات تخصصی',
|
||||||
propagationPlatforms: 'بسترهای تبلیغ',
|
propagationPlatforms: 'بسترهای تبلیغ',
|
||||||
propagationMethodDescription: 'شرح روش تبلیغ',
|
propagationMethodDescription: 'شرح روش تبلیغ',
|
||||||
propagationExperienceYears: 'سنوات تجربهٔ تبلیغ',
|
propagationExperienceYears: 'سابقه فعالیت تبلیغی',
|
||||||
relevantCertificates: 'گواهینامههای مرتبط',
|
relevantCertificates: 'گواهینامههای مرتبط',
|
||||||
|
|
||||||
onlinePlatformDetails: 'جزئیات بستر آنلاین',
|
onlinePlatformDetails: 'جزئیات بستر آنلاین',
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { string } from 'yup'
|
import { string } from 'yup'
|
||||||
|
|
||||||
import { fields } from '@/constants/fields'
|
|
||||||
|
|
||||||
export const phoneNumberRule = string()
|
export const phoneNumberRule = string()
|
||||||
.required()
|
.required()
|
||||||
.matches(/^09\d{9}$/, ({ path }) => `${fields[path] || path} نامعتبر است`)
|
.matches(/^09\d{9}$/, 'شماره موبایل باید با ۰۹ شروع شود و ۱۱ رقم باشد')
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const getEnumObject = (enumArray, key = 'value', value = 'label') =>
|
|||||||
|
|
||||||
export const ROLES = Object.freeze([
|
export const ROLES = Object.freeze([
|
||||||
{ value: 'admin', label: 'مدیر', changeable: false },
|
{ value: 'admin', label: 'مدیر', changeable: false },
|
||||||
{ value: 'student', label: 'دانشجو', changeable: true },
|
{ value: 'student', label: 'طلبه', changeable: true },
|
||||||
{ value: 'teacher', label: 'استاد', changeable: true },
|
{ value: 'teacher', label: 'استاد', changeable: true },
|
||||||
{ value: 'counselor', label: 'مشاور', changeable: true },
|
{ value: 'counselor', label: 'مشاور', changeable: true },
|
||||||
{ value: 'missionary', label: 'مبلغ', changeable: true },
|
{ value: 'missionary', label: 'مبلغ', changeable: true },
|
||||||
@@ -53,8 +53,6 @@ export const USER_STATUS = Object.freeze({
|
|||||||
export const MARITAL_STATUS = Object.freeze({
|
export const MARITAL_STATUS = Object.freeze({
|
||||||
single: 'مجرد',
|
single: 'مجرد',
|
||||||
married: 'متاهل',
|
married: 'متاهل',
|
||||||
divorced: 'مطلقه',
|
|
||||||
widowed: 'بیوه',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const GENDER = Object.freeze({
|
export const GENDER = Object.freeze({
|
||||||
@@ -68,7 +66,6 @@ export const CHANGEABLE_ROLES = Object.freeze(ROLES.filter((r) => r.changeable).
|
|||||||
|
|
||||||
export const EDUCATION_STATUS = Object.freeze({
|
export const EDUCATION_STATUS = Object.freeze({
|
||||||
seminary_student: 'طلبه',
|
seminary_student: 'طلبه',
|
||||||
seminary_and_university_student: 'طلبه و دانشجو',
|
|
||||||
graduate: 'فارغ التحصیلی',
|
graduate: 'فارغ التحصیلی',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -76,14 +73,13 @@ export const SEMINARY_LEVEL = Object.freeze({
|
|||||||
level_2: 'سطح دو',
|
level_2: 'سطح دو',
|
||||||
level_3: 'سطح سه',
|
level_3: 'سطح سه',
|
||||||
level_4: 'سطح چهار',
|
level_4: 'سطح چهار',
|
||||||
not_applicable: 'طلبه نیستم',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const UNIVERSITY_LEVEL = Object.freeze({
|
export const UNIVERSITY_LEVEL = Object.freeze({
|
||||||
bachelor: ' مقطع کارشناسی (لیسانس)',
|
bachelor: ' مقطع کارشناسی (لیسانس)',
|
||||||
master: ' مقطع کارشناسی ارشد (فوق لیسانس)',
|
master: ' مقطع کارشناسی ارشد (فوق لیسانس)',
|
||||||
phd: ' مقطع دکتری',
|
phd: ' مقطع دکتری',
|
||||||
not_applicable: 'دانشجو نیستم',
|
not_applicable: 'طلبه نیستم',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const PROPAGATION_PLATFORM = Object.freeze({
|
export const PROPAGATION_PLATFORM = Object.freeze({
|
||||||
@@ -206,3 +202,21 @@ export const SERVICE_TYPE = Object.freeze({
|
|||||||
educational: 'آموزشی',
|
educational: 'آموزشی',
|
||||||
other: 'سایر',
|
other: 'سایر',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Lifecycle of a dispatch request assigned to a missionary. `pending` is the
|
||||||
|
// initial backend state; the missionary may set seen, accepted, or rejected.
|
||||||
|
export const MISSIONARY_REQUEST_STATUS = Object.freeze({
|
||||||
|
pending: 'در انتظار بررسی',
|
||||||
|
seen: 'دیده شده',
|
||||||
|
accepted: 'پذیرفته شده',
|
||||||
|
rejected: 'رد شده',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Landing route per role after login / when a logged-in user hits a guest
|
||||||
|
// page. Roles not listed here fall back to the student dashboard.
|
||||||
|
export const ROLE_HOME_ROUTES = Object.freeze({
|
||||||
|
admin: 'admin-dashboard',
|
||||||
|
counselor: 'counselor-dashboard',
|
||||||
|
missionary: 'missionary-dashboard',
|
||||||
|
student: 'student-dashboard',
|
||||||
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="assignments-filters__grid">
|
<div class="assignments-filters__grid">
|
||||||
<TextField v-model="form.title" name="title" label="عنوان تکلیف">
|
<TextField v-model="form.title" name="title" label="عنوان تکلیف">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
|
<SvgIcon name="monitor" :size="20" color="var(--color-thd-gray)" />
|
||||||
</template>
|
</template>
|
||||||
</TextField>
|
</TextField>
|
||||||
<SelectField
|
<SelectField
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
@click="onReset"
|
@click="onReset"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" />
|
<SvgIcon name="close" color="black" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
<SelectField
|
<SelectField
|
||||||
v-model="form.priority"
|
v-model="form.priority"
|
||||||
name="priority"
|
name="priority"
|
||||||
label="اولویت"
|
label="اولویت تکلیف"
|
||||||
:options="priorityOptions"
|
:options="priorityOptions"
|
||||||
option-label="label"
|
option-label="label"
|
||||||
option-value="value"
|
option-value="value"
|
||||||
|
|||||||
@@ -21,10 +21,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="assignment-details__grid">
|
<div class="assignment-details__grid">
|
||||||
<LineInfoBlock
|
|
||||||
title="تاریخ شروع"
|
|
||||||
:numeric-desc="formatJalaaliDate(assignment.startDate) || '—'"
|
|
||||||
/>
|
|
||||||
<LineInfoBlock title="تاریخ پایان" :numeric-desc="deadlineLabel" />
|
<LineInfoBlock title="تاریخ پایان" :numeric-desc="deadlineLabel" />
|
||||||
<LineInfoBlock title="مدت زمان" :numeric-desc="durationLabel" />
|
<LineInfoBlock title="مدت زمان" :numeric-desc="durationLabel" />
|
||||||
<LineInfoBlock title="اولویت" :desc="priorityLabel" />
|
<LineInfoBlock title="اولویت" :desc="priorityLabel" />
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="submission-details__row">
|
<div class="submission-details__row">
|
||||||
<LineInfoBlock title="پاسخ دانشجو" :desc="submission.userDescription || '—'" />
|
<LineInfoBlock title="پاسخ طلبه" :desc="submission.userDescription || '—'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="submission.attachments?.length" class="submission-details__attachments">
|
<div v-if="submission.attachments?.length" class="submission-details__attachments">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
desc="در این قسمت میتوانید تکالیف خود را مدیریت کنید"
|
desc="در این قسمت میتوانید تکالیف خود را مدیریت کنید"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="check-square" :size="24" color="var(--color-primary)" />
|
<img :src="assignmentIcon" alt="" />
|
||||||
</template>
|
</template>
|
||||||
</BoxedIconTitleBlock>
|
</BoxedIconTitleBlock>
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
</SimpleTitleIconBlock>
|
</SimpleTitleIconBlock>
|
||||||
<BaseButton text="افزودن تکلیف جدید" custom-class="assignments-page__add-btn" @click="onAdd">
|
<BaseButton text="افزودن تکلیف جدید" custom-class="assignments-page__add-btn" @click="onAdd">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="plus" :size="18" color="#fff" />
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
</template>
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import { gallery } from '@/utils/gallery'
|
||||||
import useModal from '@/composables/useModal'
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
@@ -75,6 +76,8 @@ import {
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { openModal, isModal } = useModal()
|
const { openModal, isModal } = useModal()
|
||||||
|
|
||||||
|
const assignmentIcon = gallery.assignmentIcon
|
||||||
|
|
||||||
const filters = ref({
|
const filters = ref({
|
||||||
title: '',
|
title: '',
|
||||||
termId: '',
|
termId: '',
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
@click="onReset"
|
@click="onReset"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" />
|
<SvgIcon name="close" color="black" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="courses-filters__grid">
|
<div class="courses-filters__grid">
|
||||||
<TextField v-model="form.title" name="title" label="عنوان دوره">
|
<TextField v-model="form.title" name="title" label="عنوان دوره">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
|
<SvgIcon name="graduation-cap" :size="20" color="var(--color-thd-gray)" />
|
||||||
</template>
|
</template>
|
||||||
</TextField>
|
</TextField>
|
||||||
<SelectField
|
<SelectField
|
||||||
|
|||||||
@@ -14,16 +14,6 @@
|
|||||||
|
|
||||||
<form class="course-form__form" @submit.prevent="onSubmit">
|
<form class="course-form__form" @submit.prevent="onSubmit">
|
||||||
<div class="course-form__grid">
|
<div class="course-form__grid">
|
||||||
<div class="course-form__image-col">
|
|
||||||
<ImageCropper
|
|
||||||
v-model="image"
|
|
||||||
name="image"
|
|
||||||
bg-color="#eeeeee"
|
|
||||||
@crop="onImageCropped"
|
|
||||||
@error="onImageError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="course-form__main-col">
|
<div class="course-form__main-col">
|
||||||
<div class="course-form__row">
|
<div class="course-form__row">
|
||||||
<div class="course-form__cell course-form__cell--third">
|
<div class="course-form__cell course-form__cell--third">
|
||||||
@@ -100,6 +90,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="course-form__image-col">
|
||||||
|
<ImageCropper
|
||||||
|
v-model="image"
|
||||||
|
name="image"
|
||||||
|
bg-color="#eeeeee"
|
||||||
|
@crop="onImageCropped"
|
||||||
|
@error="onImageError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="course-form__divider" />
|
<div class="course-form__divider" />
|
||||||
@@ -309,29 +309,23 @@ const onCancel = () => router.push({ name: 'admin-courses' })
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
grid-template-columns: 4fr 8fr;
|
grid-template-columns: 8fr 4fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
grid-template-columns: 3fr 9fr;
|
grid-template-columns: 9fr 3fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__image-col {
|
&__image-col {
|
||||||
order: 2;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
order: 1;
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__main-col {
|
&__main-col {
|
||||||
order: 1;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
order: 2;
|
padding-inline-end: 1.25rem;
|
||||||
padding-inline-start: 1.25rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
desc="در این قسمت میتوانید دورههای خود را مدیریت کنید"
|
desc="در این قسمت میتوانید دورههای خود را مدیریت کنید"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="book" :size="24" color="var(--color-primary)" />
|
<img :src="courseIcon" alt="" />
|
||||||
</template>
|
</template>
|
||||||
</BoxedIconTitleBlock>
|
</BoxedIconTitleBlock>
|
||||||
|
|
||||||
@@ -37,10 +37,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { gallery } from '@/utils/gallery'
|
||||||
import useModal from '@/composables/useModal'
|
import useModal from '@/composables/useModal'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import NoItems from '@/components/blocks/NoItems.vue'
|
import NoItems from '@/components/blocks/NoItems.vue'
|
||||||
import TabsBlock from '@/components/blocks/TabsBlock.vue'
|
import TabsBlock from '@/components/blocks/TabsBlock.vue'
|
||||||
@@ -64,6 +64,8 @@ const router = useRouter()
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { openModal, isModal } = useModal()
|
const { openModal, isModal } = useModal()
|
||||||
|
|
||||||
|
const courseIcon = gallery.courseIcon
|
||||||
|
|
||||||
const routeTermId = computed(() => (route.params.termId ? Number(route.params.termId) : null))
|
const routeTermId = computed(() => (route.params.termId ? Number(route.params.termId) : null))
|
||||||
|
|
||||||
const onAdd = () => {
|
const onAdd = () => {
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import { array, boolean, mixed, number, object, string } from 'yup'
|
|||||||
export const courseSchema = object().shape({
|
export const courseSchema = object().shape({
|
||||||
title: string().required().min(3).max(255),
|
title: string().required().min(3).max(255),
|
||||||
teacherId: mixed().required(),
|
teacherId: mixed().required(),
|
||||||
capacity: number().typeError('ظرفیت باید عدد باشد').required().min(1),
|
capacity: number()
|
||||||
|
.typeError('ظرفیت باید بهصورت عدد وارد شود')
|
||||||
|
.required()
|
||||||
|
.integer('ظرفیت باید عدد صحیح باشد')
|
||||||
|
.min(1, 'ظرفیت باید حداقل ۱ نفر باشد'),
|
||||||
prerequisiteCourseIds: array().nullable().default([]),
|
prerequisiteCourseIds: array().nullable().default([]),
|
||||||
contentMediaId: number().nullable().notRequired(),
|
coverMediaId: number().typeError('شناسه تصویر دوره باید عدد باشد').nullable().notRequired(),
|
||||||
description: string().nullable().notRequired(),
|
description: string().nullable().notRequired(),
|
||||||
termId: mixed().required(),
|
termId: mixed().required(),
|
||||||
isActive: boolean().nullable().notRequired(),
|
isActive: boolean().nullable().notRequired(),
|
||||||
@@ -14,7 +18,11 @@ export const courseSchema = object().shape({
|
|||||||
export const offeredCourseSchema = object().shape({
|
export const offeredCourseSchema = object().shape({
|
||||||
termId: string().required(),
|
termId: string().required(),
|
||||||
title: string().required(),
|
title: string().required(),
|
||||||
capacity: string().required(),
|
capacity: number()
|
||||||
|
.typeError('ظرفیت باید بهصورت عدد وارد شود')
|
||||||
|
.required()
|
||||||
|
.integer('ظرفیت باید عدد صحیح باشد')
|
||||||
|
.min(1, 'ظرفیت باید حداقل ۱ نفر باشد'),
|
||||||
imageId: string().nullable().notRequired(),
|
imageId: string().nullable().notRequired(),
|
||||||
isActive: boolean().nullable().notRequired(),
|
isActive: boolean().nullable().notRequired(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="exams-filters__grid">
|
<div class="exams-filters__grid">
|
||||||
<TextField v-model="form.title" name="title" label="عنوان آزمون">
|
<TextField v-model="form.title" name="title" label="عنوان آزمون">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
|
<SvgIcon name="monitor" :size="20" color="var(--color-thd-gray)" />
|
||||||
</template>
|
</template>
|
||||||
</TextField>
|
</TextField>
|
||||||
<SelectField
|
<SelectField
|
||||||
|
|||||||
@@ -30,7 +30,11 @@
|
|||||||
label="عنوان آزمون"
|
label="عنوان آزمون"
|
||||||
:error="errors.title"
|
:error="errors.title"
|
||||||
@blur="validateAt('title', form.title)"
|
@blur="validateAt('title', form.title)"
|
||||||
/>
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
|
||||||
|
</template>
|
||||||
|
</TextField>
|
||||||
<TextField
|
<TextField
|
||||||
v-model="form.minimumScore"
|
v-model="form.minimumScore"
|
||||||
name="minimumScore"
|
name="minimumScore"
|
||||||
@@ -39,7 +43,11 @@
|
|||||||
:convert-digits="true"
|
:convert-digits="true"
|
||||||
:error="errors.minimumScore"
|
:error="errors.minimumScore"
|
||||||
@blur="validateAt('minimumScore', form.minimumScore)"
|
@blur="validateAt('minimumScore', form.minimumScore)"
|
||||||
/>
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="newspaper" :size="20" color="var(--color-thd-gray)" />
|
||||||
|
</template>
|
||||||
|
</TextField>
|
||||||
<TextField
|
<TextField
|
||||||
v-model="form.score"
|
v-model="form.score"
|
||||||
name="score"
|
name="score"
|
||||||
@@ -48,7 +56,11 @@
|
|||||||
:convert-digits="true"
|
:convert-digits="true"
|
||||||
:error="errors.score"
|
:error="errors.score"
|
||||||
@blur="validateAt('score', form.score)"
|
@blur="validateAt('score', form.score)"
|
||||||
/>
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="newspaper" :size="20" color="var(--color-thd-gray)" />
|
||||||
|
</template>
|
||||||
|
</TextField>
|
||||||
<TextField
|
<TextField
|
||||||
v-model="form.durationMinutes"
|
v-model="form.durationMinutes"
|
||||||
name="durationMinutes"
|
name="durationMinutes"
|
||||||
@@ -57,14 +69,15 @@
|
|||||||
:convert-digits="true"
|
:convert-digits="true"
|
||||||
:error="errors.durationMinutes"
|
:error="errors.durationMinutes"
|
||||||
@blur="validateAt('durationMinutes', form.durationMinutes)"
|
@blur="validateAt('durationMinutes', form.durationMinutes)"
|
||||||
/>
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="clock" :size="20" color="var(--color-thd-gray)" />
|
||||||
|
</template>
|
||||||
|
</TextField>
|
||||||
<div class="exam-form__toggle-cell">
|
<div class="exam-form__toggle-cell">
|
||||||
<ToggleSwitch v-model="form.isRandom" label="به صــــورت رندوم باشد." />
|
<ToggleSwitch v-model="form.isRandom" label="به صــــورت رندوم باشد." />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="exam-form__card">
|
|
||||||
<TextareaField
|
<TextareaField
|
||||||
v-model="form.description"
|
v-model="form.description"
|
||||||
name="description"
|
name="description"
|
||||||
@@ -198,7 +211,6 @@ const normalizeExistingQuestions = (raw = []) => {
|
|||||||
optionText: o.optionText || '',
|
optionText: o.optionText || '',
|
||||||
isCorrect: !!o.isCorrect,
|
isCorrect: !!o.isCorrect,
|
||||||
})),
|
})),
|
||||||
// No `__local` flag — these came from the server, so the builder will lock them.
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -280,7 +292,6 @@ const submitting = computed(
|
|||||||
|
|
||||||
const postQuestionsSequentially = async (id, list) => {
|
const postQuestionsSequentially = async (id, list) => {
|
||||||
for (const payload of list) {
|
for (const payload of list) {
|
||||||
// Sequential so question position ordering is preserved on the backend.
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await addQuestionMutation.mutateAsync({ examId: id, payload })
|
await addQuestionMutation.mutateAsync({ examId: id, payload })
|
||||||
}
|
}
|
||||||
@@ -302,7 +313,7 @@ const onSubmit = async () => {
|
|||||||
if (targetExamId && newQuestions.length > 0) {
|
if (targetExamId && newQuestions.length > 0) {
|
||||||
await postQuestionsSequentially(targetExamId, newQuestions)
|
await postQuestionsSequentially(targetExamId, newQuestions)
|
||||||
}
|
}
|
||||||
await queryClient.invalidateQueries({ queryKey: adminExamsKeys.all })
|
await queryClient.invalidateQueries({ queryKey: adminExamsKeys.all, refetchType: 'all' })
|
||||||
router.push({ name: 'admin-exams' })
|
router.push({ name: 'admin-exams' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
desc="در این قسمت میتوانید آزمونهای خود را مدیریت کنید"
|
desc="در این قسمت میتوانید آزمونهای خود را مدیریت کنید"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="pencil" :size="24" color="var(--color-primary)" />
|
<img :src="sessionIcon" alt="" />
|
||||||
</template>
|
</template>
|
||||||
</BoxedIconTitleBlock>
|
</BoxedIconTitleBlock>
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
</SimpleTitleIconBlock>
|
</SimpleTitleIconBlock>
|
||||||
<BaseButton text="افزودن آزمون جدید" custom-class="exams-page__add-btn" @click="onAdd">
|
<BaseButton text="افزودن آزمون جدید" custom-class="exams-page__add-btn" @click="onAdd">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="plus" :size="18" color="#fff" />
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
</template>
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { gallery } from '@/utils/gallery'
|
||||||
import useModal from '@/composables/useModal'
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
@@ -75,6 +76,8 @@ const router = useRouter()
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { openModal, isModal } = useModal()
|
const { openModal, isModal } = useModal()
|
||||||
|
|
||||||
|
const sessionIcon = gallery.sessionIcon
|
||||||
|
|
||||||
const filters = ref({
|
const filters = ref({
|
||||||
title: '',
|
title: '',
|
||||||
termId: '',
|
termId: '',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<form class="tickets-filters" @submit.prevent="onSubmit">
|
<form class="tickets-filters" @submit.prevent="onSubmit">
|
||||||
<div class="tickets-filters__grid">
|
<div class="tickets-filters__grid">
|
||||||
<TextField v-model="form.userName" name="userName" label="نام کاربر">
|
<TextField v-model="form.userName" name="userName" label="جستجو اسم کاربر">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="user" :size="20" color="var(--color-thd-gray)" />
|
<SvgIcon name="user" :size="20" color="var(--color-thd-gray)" />
|
||||||
</template>
|
</template>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
@click="onReset"
|
@click="onReset"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" />
|
<SvgIcon name="close" color="black" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
|
|||||||
@@ -14,7 +14,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SkeletonLoaderBlock v-if="isLoading && !ticket" :rows="3" :cols-per-row="1" />
|
<SkeletonLoaderBlock v-if="isLoading && !ticket" :rows="3" :cols-per-row="1" />
|
||||||
<div v-else-if="messages.length > 0" class="ticket-details__thread" ref="thread">
|
<div
|
||||||
|
v-else-if="messages.length > 0 || ticketMedia.length > 0"
|
||||||
|
class="ticket-details__thread"
|
||||||
|
ref="thread"
|
||||||
|
>
|
||||||
|
<div v-if="ticketMedia.length > 0" class="ticket-details__row ticket-details__row--user">
|
||||||
|
<div class="ticket-details__bubble">
|
||||||
|
<MessageAttachments :media="ticketMedia" />
|
||||||
|
<span class="ticket-details__time">{{ messageTime(ticket) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="message in messages"
|
v-for="message in messages"
|
||||||
:key="message.id"
|
:key="message.id"
|
||||||
@@ -22,6 +32,7 @@
|
|||||||
:class="`ticket-details__row--${senderClass(message)}`"
|
:class="`ticket-details__row--${senderClass(message)}`"
|
||||||
>
|
>
|
||||||
<div class="ticket-details__bubble">
|
<div class="ticket-details__bubble">
|
||||||
|
<MessageAttachments v-if="message.media?.length" :media="message.media" />
|
||||||
<p class="ticket-details__text">{{ message.message }}</p>
|
<p class="ticket-details__text">{{ message.message }}</p>
|
||||||
<span class="ticket-details__time">{{ messageTime(message) }}</span>
|
<span class="ticket-details__time">{{ messageTime(message) }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,6 +42,25 @@
|
|||||||
|
|
||||||
<div class="ticket-details__divider" />
|
<div class="ticket-details__divider" />
|
||||||
|
|
||||||
|
<div v-if="attachments.length > 0" class="ticket-details__attachments">
|
||||||
|
<span
|
||||||
|
v-for="(file, index) in attachments"
|
||||||
|
:key="file.id ?? index"
|
||||||
|
class="ticket-details__attachment"
|
||||||
|
>
|
||||||
|
<SvgIcon name="file" :size="14" color="currentColor" />
|
||||||
|
<span class="ticket-details__attachment-name">{{ file.name }}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ticket-details__attachment-remove"
|
||||||
|
aria-label="حذف فایل"
|
||||||
|
@click="removeAttachment(index)"
|
||||||
|
>
|
||||||
|
<SvgIcon name="close" :size="12" color="var(--color-error)" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form class="ticket-details__compose" @submit.prevent="onSend">
|
<form class="ticket-details__compose" @submit.prevent="onSend">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -57,6 +87,23 @@
|
|||||||
>
|
>
|
||||||
<SvgIcon name="mood" :size="30" color="currentColor" />
|
<SvgIcon name="mood" :size="30" color="currentColor" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ticket-details__compose-btn"
|
||||||
|
:disabled="uploadMutation.isPending.value"
|
||||||
|
aria-label="پیوست فایل"
|
||||||
|
@click="fileInput?.click()"
|
||||||
|
>
|
||||||
|
<SvgIcon name="attach-file" :size="30" color="currentColor" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
class="ticket-details__file-input"
|
||||||
|
accept="image/*,application/pdf"
|
||||||
|
multiple
|
||||||
|
@change="onFilesPicked"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,13 +128,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { toast } from 'vue3-toastify'
|
||||||
import useModal from '@/composables/useModal'
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import NoItems from '@/components/blocks/NoItems.vue'
|
import NoItems from '@/components/blocks/NoItems.vue'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||||
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
|
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
import MessageAttachments from '@/components/blocks/MessageAttachments.vue'
|
||||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||||
import {
|
import {
|
||||||
autoUpdate,
|
autoUpdate,
|
||||||
@@ -143,6 +194,8 @@ const { data: ticket, isLoading } = useAdminTicketQuery(ticketId, {
|
|||||||
|
|
||||||
const messages = computed(() => ticket.value?.messages ?? [])
|
const messages = computed(() => ticket.value?.messages ?? [])
|
||||||
|
|
||||||
|
const ticketMedia = computed(() => ticket.value?.media ?? [])
|
||||||
|
|
||||||
const ticketDate = computed(() => formatJalaaliDate(ticket.value?.createdAt) || '—')
|
const ticketDate = computed(() => formatJalaaliDate(ticket.value?.createdAt) || '—')
|
||||||
|
|
||||||
// Anyone whose id matches the ticket's student is "the student"; everyone else
|
// Anyone whose id matches the ticket's student is "the student"; everyone else
|
||||||
@@ -157,7 +210,49 @@ const messageTime = (message) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const text = ref('')
|
const text = ref('')
|
||||||
const canSend = computed(() => text.value.trim().length > 0)
|
|
||||||
|
const MAX_ATTACHMENTS = 5
|
||||||
|
|
||||||
|
const fileInput = ref(null)
|
||||||
|
const attachments = ref([])
|
||||||
|
|
||||||
|
const uploadMutation = useUploadMediaMutation()
|
||||||
|
|
||||||
|
const onFilesPicked = async (event) => {
|
||||||
|
const picked = [...event.target.files]
|
||||||
|
event.target.value = ''
|
||||||
|
if (attachments.value.length + picked.length > MAX_ATTACHMENTS) {
|
||||||
|
toast.warning(`حداکثر تعداد فایل مجاز ${MAX_ATTACHMENTS} عدد است.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const file of picked) {
|
||||||
|
try {
|
||||||
|
const fd = objectToFormData({ file, purpose: 'attachment', context: 'ticket' })
|
||||||
|
const response = await uploadMutation.mutateAsync(fd)
|
||||||
|
const payload = response?.data ?? response
|
||||||
|
const id = payload?.id ?? payload?.uploadId
|
||||||
|
if (id == null) continue
|
||||||
|
attachments.value = [
|
||||||
|
...attachments.value,
|
||||||
|
{ id, name: file.name, size: file.size, url: payload?.url ?? '' },
|
||||||
|
]
|
||||||
|
} catch {
|
||||||
|
toast.error(`بارگذاری فایل "${file.name}" با خطا مواجه شد.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeAttachment = (index) => {
|
||||||
|
const next = [...attachments.value]
|
||||||
|
next.splice(index, 1)
|
||||||
|
attachments.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
const canSend = computed(
|
||||||
|
() =>
|
||||||
|
(text.value.trim().length > 0 || attachments.value.length > 0) &&
|
||||||
|
!uploadMutation.isPending.value
|
||||||
|
)
|
||||||
|
|
||||||
const emojiOpen = ref(false)
|
const emojiOpen = ref(false)
|
||||||
const emojiButton = ref(null)
|
const emojiButton = ref(null)
|
||||||
@@ -213,10 +308,15 @@ const sendMutation = useSendAdminTicketMessageMutation()
|
|||||||
const onSend = async () => {
|
const onSend = async () => {
|
||||||
if (!canSend.value || !ticketId.value) return
|
if (!canSend.value || !ticketId.value) return
|
||||||
const value = text.value.trim()
|
const value = text.value.trim()
|
||||||
|
const mediaIds = attachments.value.map((f) => f.id).filter((id) => id != null)
|
||||||
text.value = ''
|
text.value = ''
|
||||||
|
attachments.value = []
|
||||||
emojiOpen.value = false
|
emojiOpen.value = false
|
||||||
// Backend POST /admin/tickets/:id/messages — body is { message: string }.
|
// Backend POST /admin/tickets/:id/messages — body is { message: string,
|
||||||
await sendMutation.mutateAsync({ id: ticketId.value, payload: { message: value } })
|
// mediaIds?: number[] } where mediaIds reference previously uploaded media.
|
||||||
|
const payload = { message: value }
|
||||||
|
if (mediaIds.length > 0) payload.mediaIds = mediaIds
|
||||||
|
await sendMutation.mutateAsync({ id: ticketId.value, payload })
|
||||||
await queryClient.invalidateQueries({ queryKey: adminTicketsKeys.all })
|
await queryClient.invalidateQueries({ queryKey: adminTicketsKeys.all })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,6 +505,12 @@ watch(messages, async () => {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__attachments {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
&__attachment {
|
&__attachment {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ const { data, isLoading } = useAdminTicketsListQuery(filters, pagination, {
|
|||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const tickets = computed(() => data.value?.data ?? [])
|
const tickets = computed(() => data.value?.items ?? [])
|
||||||
|
|
||||||
const paginationMeta = computed(() => ({
|
const paginationMeta = computed(() => ({
|
||||||
page: pagination.value.page,
|
page: pagination.value.page,
|
||||||
perPage: pagination.value.perPage,
|
perPage: pagination.value.perPage,
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-missionary-page">
|
||||||
|
<BoxedIconTitleBlock
|
||||||
|
class="admin-missionary-page__heading"
|
||||||
|
title="مدیریت مبلغین"
|
||||||
|
desc="در این قسمت میتوانید درخواستهای اعزام مبلغین را مشاهده و مدیریت کنید"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon name="user-sound" :size="24" color="var(--color-primary)" />
|
||||||
|
</template>
|
||||||
|
</BoxedIconTitleBlock>
|
||||||
|
|
||||||
|
<SimpleTitleIconBlock title="لیست همه درخواستها" class="admin-missionary-page__list-title">
|
||||||
|
<template #header-icon>
|
||||||
|
<SvgIcon name="list-bullets" :size="16" color="#bcbcbc" />
|
||||||
|
</template>
|
||||||
|
</SimpleTitleIconBlock>
|
||||||
|
|
||||||
|
<SkeletonLoaderBlock v-if="isLoading" :rows="6" :cols-per-row="1" />
|
||||||
|
<div v-else-if="requests.length > 0">
|
||||||
|
<MissionaryRequestItem
|
||||||
|
v-for="request in requests"
|
||||||
|
:key="request.id"
|
||||||
|
:request="request"
|
||||||
|
:always-actionable="true"
|
||||||
|
@show-details="onShowDetails"
|
||||||
|
@change-status="onAskChangeStatus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<NoItems v-else title="متاسفیم" desc="درخواستی برای نمایش وجود ندارد." />
|
||||||
|
|
||||||
|
<PaginationBlock :pagination="paginationMeta" @update:page="setPage" />
|
||||||
|
|
||||||
|
<RequestDetailsModal v-if="isModal('RequestDetailsModal')" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
|
import { MISSIONARY_REQUEST_STATUS } from '@/enums'
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import NoItems from '@/components/blocks/NoItems.vue'
|
||||||
|
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||||
|
import { usePagination } from '@/composables/usePagination'
|
||||||
|
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
|
||||||
|
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||||
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||||
|
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
|
||||||
|
import MissionaryRequestItem from '@/features/missionary/components/MissionaryRequestItem.vue'
|
||||||
|
import RequestDetailsModal from '@/features/missionary/components/modals/RequestDetailsModal.vue'
|
||||||
|
import {
|
||||||
|
adminMissionaryRequestsKeys,
|
||||||
|
useAdminMissionaryRequestsListQuery,
|
||||||
|
useChangeAdminMissionaryRequestStatusMutation,
|
||||||
|
} from '@/services/query/admin-missionary-requests'
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const { openModal, isModal } = useModal()
|
||||||
|
|
||||||
|
const filters = ref({})
|
||||||
|
const { pagination, setPage } = usePagination({ page: 1, perPage: 10 })
|
||||||
|
|
||||||
|
const { data, isLoading } = useAdminMissionaryRequestsListQuery(filters, pagination, {
|
||||||
|
keepPreviousData: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const toRequestView = (request) => ({
|
||||||
|
...request,
|
||||||
|
code: request.id,
|
||||||
|
requestNumber: request.id,
|
||||||
|
requestDate: formatJalaaliDate(request.requestedDate),
|
||||||
|
statusLabel: MISSIONARY_REQUEST_STATUS[request.status] || request.status || '—',
|
||||||
|
phone: request.requesterPhone,
|
||||||
|
address: request.location,
|
||||||
|
})
|
||||||
|
|
||||||
|
const requests = computed(() => (data.value?.data ?? []).map((r) => toRequestView(r)))
|
||||||
|
|
||||||
|
const paginationMeta = computed(() => ({
|
||||||
|
page: pagination.value.page,
|
||||||
|
perPage: pagination.value.perPage,
|
||||||
|
...data.value?.meta,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const statusMutation = useChangeAdminMissionaryRequestStatusMutation()
|
||||||
|
|
||||||
|
const invalidate = () =>
|
||||||
|
queryClient.invalidateQueries({ queryKey: adminMissionaryRequestsKeys.all, refetchType: 'all' })
|
||||||
|
|
||||||
|
const STATUS_CONFIRMS = {
|
||||||
|
accepted: { title: 'پذیرش درخواست', verb: 'پذیرش' },
|
||||||
|
rejected: { title: 'رد درخواست', verb: 'رد' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAskChangeStatus = ({ id, status }) => {
|
||||||
|
const config = STATUS_CONFIRMS[status]
|
||||||
|
if (!config) return
|
||||||
|
const request = requests.value.find((r) => r.id === id)
|
||||||
|
openModal('ConfirmModal', {
|
||||||
|
title: config.title,
|
||||||
|
message: `آیا از ${config.verb} درخواست <strong>${request?.title || ''}</strong> مطمئن هستید؟`,
|
||||||
|
onConfirm: () => statusMutation.mutate({ id, payload: { status } }, { onSuccess: invalidate }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onShowDetails = (request) => {
|
||||||
|
openModal('RequestDetailsModal', request)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.admin-missionary-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
&__heading {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list-title {
|
||||||
|
margin-bottom: 0.375rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,53 +1,276 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="admin-dashboard">
|
<div class="dashboard">
|
||||||
<header class="admin-dashboard__heading">
|
<div class="dashboard__main">
|
||||||
<p class="admin-dashboard__title">پنل مدیریت</p>
|
<!-- اطلاعات مدرسه -->
|
||||||
<p class="admin-dashboard__subtitle">آخرین اطلاعات موسسه شما</p>
|
<section class="dashboard__section">
|
||||||
</header>
|
<BoxedIconTitleBlock title="اطلاعات مدرسه" desc="آخرین اطلاعات مدرسه شما.">
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon name="heart" :size="24" color="var(--color-primary)" />
|
||||||
|
</template>
|
||||||
|
</BoxedIconTitleBlock>
|
||||||
|
|
||||||
<p class="admin-dashboard__placeholder">این بخش در مراحل بعدی توسعه مییابد.</p>
|
<SkeletonLoaderBlock v-if="isLoading" :rows="2" :cols-per-row="3" />
|
||||||
|
<div v-else class="dashboard__stats">
|
||||||
|
<DashboardStatCard
|
||||||
|
v-for="(stat, i) in institutionStats"
|
||||||
|
:key="i"
|
||||||
|
:icon="stat.icon"
|
||||||
|
:label="stat.label"
|
||||||
|
:value="stat.value"
|
||||||
|
:unit="stat.unit"
|
||||||
|
:subtitle="stat.subtitle"
|
||||||
|
:to="stat.to"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- آخرین فعالیت ها -->
|
||||||
|
<section class="dashboard__section">
|
||||||
|
<BoxedIconTitleBlock
|
||||||
|
title="آخرین فعالیت ها"
|
||||||
|
desc="گزارشی از جدیدترین افراد ثبت نام شده و درخواستها."
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon name="heart" :size="24" color="var(--color-primary)" />
|
||||||
|
</template>
|
||||||
|
</BoxedIconTitleBlock>
|
||||||
|
|
||||||
|
<SkeletonLoaderBlock v-if="isLoading" :rows="2" :cols-per-row="3" />
|
||||||
|
<div v-else class="dashboard__activities">
|
||||||
|
<DashboardActivityGroup
|
||||||
|
v-for="(act, i) in activities"
|
||||||
|
:key="i"
|
||||||
|
circle
|
||||||
|
thumb-icon="user"
|
||||||
|
:pill-icon="act.pillIcon"
|
||||||
|
:pill-label="act.pillLabel"
|
||||||
|
:title="act.title"
|
||||||
|
:meta="act.meta"
|
||||||
|
:meta-icon="act.metaIcon"
|
||||||
|
:avatar-url="act.avatarUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="dashboard__aside">
|
||||||
|
<SkeletonLoaderBlock v-if="isLoading" :rows="2" :cols-per-row="1" />
|
||||||
|
<DashboardProfileCard
|
||||||
|
v-else
|
||||||
|
:name="profile.name"
|
||||||
|
:location="profile.location"
|
||||||
|
:avatar-url="profile.avatarUrl"
|
||||||
|
:stats="profile.stats"
|
||||||
|
/>
|
||||||
|
<DashboardCalendar :month-label="calendar.month" :days="calendar.days" />
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup></script>
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import { toFaDigits, formatJalaaliMeta } from '@/utils/persian'
|
||||||
|
import { useTehranCalendar } from '@/composables/useTehranCalendar'
|
||||||
|
import { useAdminDashboardQuery } from '@/services/query/admin-dashboard'
|
||||||
|
import DashboardStatCard from '@/components/dashboard/DashboardStatCard.vue'
|
||||||
|
import DashboardCalendar from '@/components/dashboard/DashboardCalendar.vue'
|
||||||
|
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||||
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||||
|
import DashboardProfileCard from '@/components/dashboard/DashboardProfileCard.vue'
|
||||||
|
import DashboardActivityGroup from '@/components/dashboard/DashboardActivityGroup.vue'
|
||||||
|
|
||||||
|
const { data, isLoading } = useAdminDashboardQuery()
|
||||||
|
|
||||||
|
const dashboard = computed(() => data.value?.data ?? {})
|
||||||
|
|
||||||
|
const ROLE_LABELS = {
|
||||||
|
admin: 'مدیر سیستم',
|
||||||
|
teacher: 'استاد',
|
||||||
|
counselor: 'مشاور',
|
||||||
|
missionary: 'مبلغ',
|
||||||
|
student: 'دانشپژوه',
|
||||||
|
}
|
||||||
|
const ACCESS_LABELS = {
|
||||||
|
full: 'کامل',
|
||||||
|
limited: 'محدود',
|
||||||
|
}
|
||||||
|
|
||||||
|
const institutionStats = computed(() => {
|
||||||
|
const stats = dashboard.value.stats ?? {}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
icon: 'users-three',
|
||||||
|
label: 'مجموع کل متقاضیان',
|
||||||
|
value: toFaDigits(stats.totalApplicants ?? 0),
|
||||||
|
unit: 'نفر',
|
||||||
|
subtitle: 'احراز شده و نشده',
|
||||||
|
to: { name: 'admin-users' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'clock',
|
||||||
|
label: 'تعداد کاربران در انتظار ارزیابی',
|
||||||
|
value: toFaDigits(stats.awaitingEvaluation ?? 0),
|
||||||
|
unit: 'نفر',
|
||||||
|
subtitle: 'در انتظار ارزیابی',
|
||||||
|
to: { name: 'admin-user-verification' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'book',
|
||||||
|
label: 'آمار طلبههای ترم ۱',
|
||||||
|
value: toFaDigits(stats.term1Students ?? 0),
|
||||||
|
unit: 'نفر',
|
||||||
|
subtitle: 'ترم اول',
|
||||||
|
to: { name: 'admin-terms' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'book',
|
||||||
|
label: 'آمار طلبههای ترم ۲',
|
||||||
|
value: toFaDigits(stats.term2Students ?? 0),
|
||||||
|
unit: 'نفر',
|
||||||
|
subtitle: 'ترم دوم',
|
||||||
|
to: { name: 'admin-terms' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'book',
|
||||||
|
label: 'آمار طلبههای ترم ۳',
|
||||||
|
value: toFaDigits(stats.term3Students ?? 0),
|
||||||
|
unit: 'نفر',
|
||||||
|
subtitle: 'ترم سوم',
|
||||||
|
to: { name: 'admin-terms' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'graduation-cap',
|
||||||
|
label: 'آمار فارغالتحصیلان',
|
||||||
|
value: toFaDigits(stats.graduates ?? 0),
|
||||||
|
unit: 'نفر',
|
||||||
|
subtitle: 'فارغالتحصیل',
|
||||||
|
to: { name: 'admin-users' },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const activities = computed(() => {
|
||||||
|
const list = []
|
||||||
|
const registration = dashboard.value.recentRegistrations?.[0]
|
||||||
|
if (registration) {
|
||||||
|
list.push({
|
||||||
|
pillIcon: 'user',
|
||||||
|
pillLabel: 'آخرین افراد ثبت نام شده',
|
||||||
|
title: registration.name,
|
||||||
|
avatarUrl: registration.avatarUrl ?? '',
|
||||||
|
metaIcon: 'calendar',
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
label: 'تاریخ ثبت نام:',
|
||||||
|
value: formatJalaaliMeta(registration.registeredAt),
|
||||||
|
numeric: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const advise = dashboard.value.recentAdviseRequests?.[0]
|
||||||
|
if (advise) {
|
||||||
|
list.push({
|
||||||
|
pillIcon: 'chat',
|
||||||
|
pillLabel: 'اخرین درخواست مشاوره',
|
||||||
|
title: advise.studentName,
|
||||||
|
avatarUrl: advise.avatarUrl ?? '',
|
||||||
|
metaIcon: 'calendar',
|
||||||
|
meta: [
|
||||||
|
{ label: 'تاریخ درخواست:', value: formatJalaaliMeta(advise.createdAt), numeric: true },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const service = dashboard.value.recentServiceRequests?.[0]
|
||||||
|
if (service) {
|
||||||
|
list.push({
|
||||||
|
pillIcon: 'users-three',
|
||||||
|
pillLabel: 'اخرین درخواست خدمات',
|
||||||
|
title: service.studentName,
|
||||||
|
avatarUrl: service.avatarUrl ?? '',
|
||||||
|
metaIcon: 'calendar',
|
||||||
|
meta: [
|
||||||
|
{ label: 'تاریخ درخواست:', value: formatJalaaliMeta(service.createdAt), numeric: true },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
|
||||||
|
const profile = computed(() => {
|
||||||
|
const info = dashboard.value.profile ?? {}
|
||||||
|
return {
|
||||||
|
name: info.name ?? '',
|
||||||
|
avatarUrl: info.avatarUrl ?? '',
|
||||||
|
location: [info.city || info.province, 'ایران'].filter(Boolean).join('، '),
|
||||||
|
stats: [
|
||||||
|
{ label: 'نقش', value: ROLE_LABELS[info.role] ?? info.role ?? '' },
|
||||||
|
{ label: 'سطح دسترسی', value: ACCESS_LABELS[info.accessLevel] ?? info.accessLevel ?? '' },
|
||||||
|
{ label: 'تعداد تیکتهای باز', value: toFaDigits(info.openTickets ?? 0) },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const calendar = useTehranCalendar()
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.admin-dashboard {
|
.dashboard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
gap: 2rem;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.875rem;
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__activities {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1.25rem;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__aside {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
|
|
||||||
&__heading {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
font-family: var(--font-family-fa);
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
font-size: 1.5rem;
|
width: clamp(21rem, 26vw, 24rem);
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-inline-start: 1px solid #e6e6e6;
|
||||||
|
padding-inline-start: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__subtitle {
|
|
||||||
font-family: var(--font-family-fa);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-prim-gray);
|
|
||||||
margin: 0.25rem 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__placeholder {
|
|
||||||
font-family: var(--font-family-fa);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-prim-gray);
|
|
||||||
text-align: center;
|
|
||||||
padding: 3rem 0;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 1rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,138 +3,264 @@ export default [
|
|||||||
path: '/main-dashboard',
|
path: '/main-dashboard',
|
||||||
name: 'admin-dashboard',
|
name: 'admin-dashboard',
|
||||||
component: () => import('@/features/admin/pages/AdminDashboardPage.vue'),
|
component: () => import('@/features/admin/pages/AdminDashboardPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'پیشخوان' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'سلام بر شما',
|
||||||
|
subtitle: 'به سامانه مدرسه بانو خوش آمدید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/users',
|
path: '/users',
|
||||||
name: 'admin-users',
|
name: 'admin-users',
|
||||||
component: () => import('@/features/admin/users/pages/UsersListPage.vue'),
|
component: () => import('@/features/admin/users/pages/UsersListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت کاربران' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'لیست کاربران',
|
||||||
|
subtitle: 'کنترل کامل روی اطلاعات، نقشها و وضعیت کاربران',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/add-user',
|
path: '/add-user',
|
||||||
name: 'admin-add-user',
|
name: 'admin-add-user',
|
||||||
component: () => import('@/features/admin/users/pages/UserFormPage.vue'),
|
component: () => import('@/features/admin/users/pages/UserFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'افزودن کاربر جدید' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'لیست کاربران',
|
||||||
|
subtitle: 'کنترل کامل روی اطلاعات، نقشها و وضعیت کاربران',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/edit-user/:id',
|
path: '/edit-user/:id',
|
||||||
name: 'admin-edit-user',
|
name: 'admin-edit-user',
|
||||||
component: () => import('@/features/admin/users/pages/UserFormPage.vue'),
|
component: () => import('@/features/admin/users/pages/UserFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'ویرایش کاربر' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'لیست کاربران',
|
||||||
|
subtitle: 'کنترل کامل روی اطلاعات، نقشها و وضعیت کاربران',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/user-verification',
|
path: '/user-verification',
|
||||||
name: 'admin-user-verification',
|
name: 'admin-user-verification',
|
||||||
component: () => import('@/features/admin/users/pages/UserVerificationPage.vue'),
|
component: () => import('@/features/admin/users/pages/UserVerificationPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'تایید هویت کاربران' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'کاربران سیستم من',
|
||||||
|
subtitle: 'دسترسی کامل به اطلاعات، نقشها و وضعیت کاربران',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/terms',
|
path: '/terms',
|
||||||
name: 'admin-terms',
|
name: 'admin-terms',
|
||||||
component: () => import('@/features/admin/terms/pages/TermsListPage.vue'),
|
component: () => import('@/features/admin/terms/pages/TermsListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت ترم' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/add-term',
|
path: '/add-term',
|
||||||
name: 'admin-add-term',
|
name: 'admin-add-term',
|
||||||
component: () => import('@/features/admin/terms/pages/TermFormPage.vue'),
|
component: () => import('@/features/admin/terms/pages/TermFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'افزودن ترم جدید' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/edit-term/:id',
|
path: '/edit-term/:id',
|
||||||
name: 'admin-edit-term',
|
name: 'admin-edit-term',
|
||||||
component: () => import('@/features/admin/terms/pages/TermFormPage.vue'),
|
component: () => import('@/features/admin/terms/pages/TermFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'ویرایش ترم' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/courses',
|
path: '/courses',
|
||||||
name: 'admin-courses',
|
name: 'admin-courses',
|
||||||
component: () => import('@/features/admin/courses/pages/CoursesListPage.vue'),
|
component: () => import('@/features/admin/courses/pages/CoursesListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت دوره' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/add-course',
|
path: '/add-course',
|
||||||
name: 'admin-add-course',
|
name: 'admin-add-course',
|
||||||
component: () => import('@/features/admin/courses/pages/CourseFormPage.vue'),
|
component: () => import('@/features/admin/courses/pages/CourseFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'افزودن دوره' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/edit-course/:id',
|
path: '/edit-course/:id',
|
||||||
name: 'admin-edit-course',
|
name: 'admin-edit-course',
|
||||||
component: () => import('@/features/admin/courses/pages/CourseFormPage.vue'),
|
component: () => import('@/features/admin/courses/pages/CourseFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'ویرایش دوره' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/terms/:termId/courses',
|
path: '/terms/:termId/courses',
|
||||||
name: 'admin-term-courses',
|
name: 'admin-term-courses',
|
||||||
component: () => import('@/features/admin/courses/pages/CoursesListPage.vue'),
|
component: () => import('@/features/admin/courses/pages/CoursesListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'دورههای ترم' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/courses/:courseId/sessions',
|
path: '/courses/:courseId/sessions',
|
||||||
name: 'admin-course-sessions',
|
name: 'admin-course-sessions',
|
||||||
component: () => import('@/features/admin/sessions/pages/SessionsListPage.vue'),
|
component: () => import('@/features/admin/sessions/pages/SessionsListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'جلسات دوره' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/sessions',
|
path: '/sessions',
|
||||||
name: 'admin-sessions',
|
name: 'admin-sessions',
|
||||||
component: () => import('@/features/admin/sessions/pages/SessionsListPage.vue'),
|
component: () => import('@/features/admin/sessions/pages/SessionsListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت جلسات' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/add-session',
|
path: '/add-session',
|
||||||
name: 'admin-add-session',
|
name: 'admin-add-session',
|
||||||
component: () => import('@/features/admin/sessions/pages/SessionFormPage.vue'),
|
component: () => import('@/features/admin/sessions/pages/SessionFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'افزودن جلسه جدید' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/edit-session/:id',
|
path: '/edit-session/:id',
|
||||||
name: 'admin-edit-session',
|
name: 'admin-edit-session',
|
||||||
component: () => import('@/features/admin/sessions/pages/SessionFormPage.vue'),
|
component: () => import('@/features/admin/sessions/pages/SessionFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'ویرایش جلسه' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/exams',
|
path: '/exams',
|
||||||
name: 'admin-exams',
|
name: 'admin-exams',
|
||||||
component: () => import('@/features/admin/exams/pages/ExamsListPage.vue'),
|
component: () => import('@/features/admin/exams/pages/ExamsListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت آزمونها' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/add-exam',
|
path: '/add-exam',
|
||||||
name: 'admin-add-exam',
|
name: 'admin-add-exam',
|
||||||
component: () => import('@/features/admin/exams/pages/ExamFormPage.vue'),
|
component: () => import('@/features/admin/exams/pages/ExamFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'افزودن آزمون جدید' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/edit-exam/:id',
|
path: '/edit-exam/:id',
|
||||||
name: 'admin-edit-exam',
|
name: 'admin-edit-exam',
|
||||||
component: () => import('@/features/admin/exams/pages/ExamFormPage.vue'),
|
component: () => import('@/features/admin/exams/pages/ExamFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'ویرایش آزمون' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/assignments',
|
path: '/assignments',
|
||||||
name: 'admin-assignments',
|
name: 'admin-assignments',
|
||||||
component: () => import('@/features/admin/assignments/pages/AssignmentsListPage.vue'),
|
component: () => import('@/features/admin/assignments/pages/AssignmentsListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت تکالیف' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/messages',
|
path: '/messages',
|
||||||
name: 'admin-messages',
|
name: 'admin-messages',
|
||||||
component: () => import('@/features/admin/messages/pages/TicketsListPage.vue'),
|
component: () => import('@/features/admin/messages/pages/TicketsListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت پیامها' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز پیامها',
|
||||||
|
subtitle: 'تمام ارتباطات سیستمی و کاربری را در این بخش مدیریت نمایید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/services',
|
path: '/services',
|
||||||
name: 'admin-services',
|
name: 'admin-services',
|
||||||
component: () => import('@/features/admin/services/pages/ServicesListPage.vue'),
|
component: () => import('@/features/admin/services/pages/ServicesListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت خدمات' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز خدمات',
|
||||||
|
subtitle: 'لیست خدمات را مشاهده و مدیریت نمایید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/consultations',
|
path: '/consultations',
|
||||||
name: 'admin-consultations',
|
name: 'admin-consultations',
|
||||||
component: () => import('@/features/admin/consultations/pages/ConsultationsListPage.vue'),
|
component: () => import('@/features/admin/consultations/pages/ConsultationsListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت مشاوره' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز مشاوره',
|
||||||
|
subtitle: 'جلسات مشاوره را پیگیری کرده و پاسخگوی نیازهای کاربران باشید.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/missionary-requests',
|
||||||
|
name: 'admin-missionary-requests',
|
||||||
|
component: () => import('@/features/admin/missionary/pages/AdminMissionaryRequestsPage.vue'),
|
||||||
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مدیریت مبلغین',
|
||||||
|
subtitle: 'درخواستهای اعزام مبلغین را مشاهده و مدیریت نمایید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
@click="onReset"
|
@click="onReset"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" />
|
<SvgIcon name="close" color="black" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
v-model="form.description"
|
v-model="form.description"
|
||||||
name="description"
|
name="description"
|
||||||
label="توضیحات خود را وارد کنید"
|
label="توضیحات خود را وارد کنید"
|
||||||
placeholder="لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است."
|
|
||||||
:row="4"
|
:row="4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="sessions-filters__grid">
|
<div class="sessions-filters__grid">
|
||||||
<TextField v-model="form.title" name="title" label="عنوان جلسه">
|
<TextField v-model="form.title" name="title" label="عنوان جلسه">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
|
<SvgIcon name="monitor" :size="20" color="var(--color-thd-gray)" />
|
||||||
</template>
|
</template>
|
||||||
</TextField>
|
</TextField>
|
||||||
<SelectField
|
<SelectField
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
@click="onReset"
|
@click="onReset"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" />
|
<SvgIcon name="close" color="black" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
|
|||||||
@@ -39,11 +39,9 @@
|
|||||||
:src="mediaUrl"
|
:src="mediaUrl"
|
||||||
:video-id="session.id"
|
:video-id="session.id"
|
||||||
/>
|
/>
|
||||||
<VoiceRecorder
|
<div v-else-if="contentType === 'voice'" class="session-details__voice">
|
||||||
v-else-if="contentType === 'voice'"
|
<VoiceRecorder :model-value="mediaUrl" :disabled="true" />
|
||||||
:model-value="mediaUrl"
|
</div>
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
<a
|
<a
|
||||||
v-else-if="mediaUrl"
|
v-else-if="mediaUrl"
|
||||||
:href="mediaUrl"
|
:href="mediaUrl"
|
||||||
@@ -111,9 +109,9 @@ const courseTitle = computed(() => session.value?.course?.title || '—')
|
|||||||
const isOnline = computed(() => session.value?.type === 'online')
|
const isOnline = computed(() => session.value?.type === 'online')
|
||||||
|
|
||||||
const collectionToContentType = (collectionName) => {
|
const collectionToContentType = (collectionName) => {
|
||||||
if (collectionName === 'videos') return 'video'
|
if (collectionName === 'video') return 'video'
|
||||||
if (collectionName === 'voice') return 'voice'
|
if (collectionName === 'voice') return 'voice'
|
||||||
if (collectionName === 'pdfs') return 'text'
|
if (collectionName === 'pdf') return 'text'
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +171,22 @@ const mediaFileName = computed(() => contentMedia.value?.fileName || 'فایل
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__voice {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
/* stylelint-disable-next-line selector-class-pattern */
|
||||||
|
:deep(.voice-recorder) {
|
||||||
|
min-height: auto;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* stylelint-disable-next-line selector-class-pattern */
|
||||||
|
:deep(.voice-recorder__main-btn) {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__file {
|
&__file {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -12,16 +12,6 @@
|
|||||||
|
|
||||||
<form class="session-form__form" @submit.prevent="onSubmit">
|
<form class="session-form__form" @submit.prevent="onSubmit">
|
||||||
<div class="session-form__grid">
|
<div class="session-form__grid">
|
||||||
<div class="session-form__image-col">
|
|
||||||
<ImageCropper
|
|
||||||
v-model="image"
|
|
||||||
name="image"
|
|
||||||
bg-color="#eeeeee"
|
|
||||||
@crop="onImageCropped"
|
|
||||||
@error="onImageError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="session-form__main-col">
|
<div class="session-form__main-col">
|
||||||
<LineTitleBlock title="اطلاعات جلسه" title-en="Session Details" />
|
<LineTitleBlock title="اطلاعات جلسه" title-en="Session Details" />
|
||||||
|
|
||||||
@@ -112,6 +102,16 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="session-form__image-col">
|
||||||
|
<ImageCropper
|
||||||
|
v-model="image"
|
||||||
|
name="image"
|
||||||
|
bg-color="#eeeeee"
|
||||||
|
@crop="onImageCropped"
|
||||||
|
@error="onImageError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="session-form__divider" />
|
<div class="session-form__divider" />
|
||||||
@@ -357,7 +357,10 @@ const onSubmit = async () => {
|
|||||||
} else {
|
} else {
|
||||||
await addMutation.mutateAsync(payload)
|
await addMutation.mutateAsync(payload)
|
||||||
}
|
}
|
||||||
await queryClient.invalidateQueries({ queryKey: adminSessionsKeys.all })
|
// The list query is inactive while we're on the form page, and the global
|
||||||
|
// `refetchOnMount: false` means navigating back won't refetch a merely-stale
|
||||||
|
// query — so force a refetch of all matching queries, active or not.
|
||||||
|
await queryClient.invalidateQueries({ queryKey: adminSessionsKeys.all, refetchType: 'all' })
|
||||||
router.push({ name: 'admin-sessions' })
|
router.push({ name: 'admin-sessions' })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,32 +392,27 @@ const onCancel = () => router.push({ name: 'admin-sessions' })
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
grid-template-columns: 4fr 8fr;
|
grid-template-columns: 8fr 4fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
grid-template-columns: 3fr 9fr;
|
grid-template-columns: 9fr 3fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__image-col {
|
&__image-col {
|
||||||
order: 2;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
order: 1;
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__main-col {
|
&__main-col {
|
||||||
order: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
order: 2;
|
padding-inline-end: 1.25rem;
|
||||||
padding-inline-start: 1.25rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
desc="در این قسمت میتوانید جلسات خود را مدیریت کنید"
|
desc="در این قسمت میتوانید جلسات خود را مدیریت کنید"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="book" :size="24" color="var(--color-primary)" />
|
<img :src="sessionIcon" alt="" />
|
||||||
</template>
|
</template>
|
||||||
</BoxedIconTitleBlock>
|
</BoxedIconTitleBlock>
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
</SimpleTitleIconBlock>
|
</SimpleTitleIconBlock>
|
||||||
<BaseButton text="افزودن جلسه جدید" custom-class="sessions-page__add-btn" @click="onAdd">
|
<BaseButton text="افزودن جلسه جدید" custom-class="sessions-page__add-btn" @click="onAdd">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="plus" :size="18" color="#fff" />
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
</template>
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,12 +12,8 @@
|
|||||||
|
|
||||||
<div class="term-item__dates">
|
<div class="term-item__dates">
|
||||||
<div class="term-item__date">
|
<div class="term-item__date">
|
||||||
<span class="term-item__date-label">تاریخ شروع:</span>
|
<span class="term-item__date-label">تاریخ ایجاد:</span>
|
||||||
<span class="term-item__date-value">{{ startDate || '—' }}</span>
|
<span class="term-item__date-value">{{ createdDate || '—' }}</span>
|
||||||
</div>
|
|
||||||
<div class="term-item__date">
|
|
||||||
<span class="term-item__date-label">تاریخ پایان:</span>
|
|
||||||
<span class="term-item__date-value">{{ endDate || '—' }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -80,8 +76,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['edit', 'delete', 'clone', 'change-status', 'show-details'])
|
const emit = defineEmits(['edit', 'delete', 'clone', 'change-status', 'show-details'])
|
||||||
|
|
||||||
const startDate = computed(() => formatJalaaliDate(props.term.startsAt) || '')
|
const createdDate = computed(() => formatJalaaliDate(props.term.createdAt) || '')
|
||||||
const endDate = computed(() => formatJalaaliDate(props.term.endsAt) || '')
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="terms-filters__grid">
|
<div class="terms-filters__grid">
|
||||||
<TextField v-model="form.title" name="title" label="عنوان">
|
<TextField v-model="form.title" name="title" label="عنوان">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
|
<SvgIcon name="graduation-cap" :size="20" color="var(--color-thd-gray)" />
|
||||||
</template>
|
</template>
|
||||||
</TextField>
|
</TextField>
|
||||||
<SelectField
|
<SelectField
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<BasicModal
|
<BasicModal
|
||||||
title="افزودن دانشجو"
|
title="افزودن طلبه"
|
||||||
title-en="Add Student"
|
title-en="Add Seminarian"
|
||||||
width="95%"
|
width="95%"
|
||||||
max-width="42rem"
|
max-width="42rem"
|
||||||
min-width="auto"
|
min-width="auto"
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
v-model="searchInput"
|
v-model="searchInput"
|
||||||
type="text"
|
type="text"
|
||||||
class="add-term-student__search-input"
|
class="add-term-student__search-input"
|
||||||
placeholder="اسم دانشجو را وارد نمایید"
|
placeholder="اسم طلبه را وارد نمایید"
|
||||||
@input="onSearchInput"
|
@input="onSearchInput"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NoItems v-else title="بدون نتیجه" desc="دانشجویی یافت نشد." />
|
<NoItems v-else title="بدون نتیجه" desc="طلبهای یافت نشد." />
|
||||||
|
|
||||||
<div class="add-term-student__divider" />
|
<div class="add-term-student__divider" />
|
||||||
|
|
||||||
|
|||||||
@@ -163,8 +163,6 @@ const invalidate = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onAttach = async (template) => {
|
const onAttach = async (template) => {
|
||||||
console.log(template)
|
|
||||||
|
|
||||||
if (!termId.value) return
|
if (!termId.value) return
|
||||||
pendingId.value = template.id
|
pendingId.value = template.id
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -21,14 +21,10 @@
|
|||||||
<div class="term-details__overview-grid">
|
<div class="term-details__overview-grid">
|
||||||
<LineInfoBlock title="عنوان ترم" :desc="term.title || '-'" />
|
<LineInfoBlock title="عنوان ترم" :desc="term.title || '-'" />
|
||||||
<LineInfoBlock
|
<LineInfoBlock
|
||||||
title="تاریخ شروع"
|
title="تاریخ ایجاد"
|
||||||
:numeric-desc="formatJalaaliDate(term.startsAt) || '-'"
|
:numeric-desc="formatJalaaliDate(term.createdAt) || '-'"
|
||||||
/>
|
/>
|
||||||
<LineInfoBlock
|
<LineInfoBlock title="تعداد طلبهها" :numeric-desc="term.studentsCount ?? 0" />
|
||||||
title="تاریخ پایان"
|
|
||||||
:numeric-desc="formatJalaaliDate(term.endsAt) || '-'"
|
|
||||||
/>
|
|
||||||
<LineInfoBlock title="تعداد دانشجویان" :numeric-desc="term.studentsCount ?? 0" />
|
|
||||||
<LineInfoBlock title="تعداد دورهها" :numeric-desc="term.coursesCount ?? 0" />
|
<LineInfoBlock title="تعداد دورهها" :numeric-desc="term.coursesCount ?? 0" />
|
||||||
<LineInfoBlock title="وضعیت" :desc="term.isActive ? 'فعال' : 'غیرفعال'" />
|
<LineInfoBlock title="وضعیت" :desc="term.isActive ? 'فعال' : 'غیرفعال'" />
|
||||||
<div v-if="term.description" class="term-details__cell--full">
|
<div v-if="term.description" class="term-details__cell--full">
|
||||||
@@ -41,9 +37,9 @@
|
|||||||
<TabsBlock :tabs="tabs" v-model="activeTab">
|
<TabsBlock :tabs="tabs" v-model="activeTab">
|
||||||
<template #students>
|
<template #students>
|
||||||
<div class="term-details__panel-header">
|
<div class="term-details__panel-header">
|
||||||
<LineTitleBlock title="لیست دانشجویان این ترم" title-en="Students List" />
|
<LineTitleBlock title="لیست طلبههای این ترم" title-en="Seminarians List" />
|
||||||
<BaseButton
|
<BaseButton
|
||||||
text="افزودن دانشجو"
|
text="افزودن طلبه"
|
||||||
custom-class="term-details__panel-btn"
|
custom-class="term-details__panel-btn"
|
||||||
@click="onOpenAddStudent"
|
@click="onOpenAddStudent"
|
||||||
>
|
>
|
||||||
@@ -76,7 +72,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NoItems v-else title="متاسفیم" desc="دانشجویی برای نمایش وجود ندارد." />
|
<NoItems v-else title="متاسفیم" desc="طلبهای برای نمایش وجود ندارد." />
|
||||||
|
|
||||||
<PaginationBlock :pagination="studentPaginationMeta" @update:page="setStudentPage" />
|
<PaginationBlock :pagination="studentPaginationMeta" @update:page="setStudentPage" />
|
||||||
</template>
|
</template>
|
||||||
@@ -161,7 +157,7 @@ const { data: term } = useAdminTermQuery(termId, {
|
|||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ name: 'courses', label: 'دورهها', icon: 'list-bullets' },
|
{ name: 'courses', label: 'دورهها', icon: 'list-bullets' },
|
||||||
{ name: 'students', label: 'دانشجویان', icon: 'users' },
|
{ name: 'students', label: 'طلبهها', icon: 'users' },
|
||||||
]
|
]
|
||||||
const activeTab = ref('courses')
|
const activeTab = ref('courses')
|
||||||
|
|
||||||
|
|||||||
@@ -14,16 +14,6 @@
|
|||||||
|
|
||||||
<form class="term-form__form" @submit.prevent="onSubmit">
|
<form class="term-form__form" @submit.prevent="onSubmit">
|
||||||
<div class="term-form__grid">
|
<div class="term-form__grid">
|
||||||
<div class="term-form__image-col">
|
|
||||||
<ImageCropper
|
|
||||||
v-model="image"
|
|
||||||
name="image"
|
|
||||||
bg-color="#eeeeee"
|
|
||||||
@crop="onImageCropped"
|
|
||||||
@error="onImageError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="term-form__main-col">
|
<div class="term-form__main-col">
|
||||||
<LineTitleBlock title="اطلاعات ترم" title-en="Term Details" />
|
<LineTitleBlock title="اطلاعات ترم" title-en="Term Details" />
|
||||||
<div class="term-form__row">
|
<div class="term-form__row">
|
||||||
@@ -40,24 +30,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</TextField>
|
</TextField>
|
||||||
</div>
|
</div>
|
||||||
<div class="term-form__cell term-form__cell--third">
|
|
||||||
<DatePickerField
|
|
||||||
v-model="form.startsAt"
|
|
||||||
name="startsAt"
|
|
||||||
label="تاریخ شروع"
|
|
||||||
:min="todayIso"
|
|
||||||
:error="errors.startsAt"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="term-form__cell term-form__cell--third">
|
|
||||||
<DatePickerField
|
|
||||||
v-model="form.endsAt"
|
|
||||||
name="endsAt"
|
|
||||||
label="تاریخ پایان"
|
|
||||||
:min="todayIso"
|
|
||||||
:error="errors.endsAt"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="term-form__cell term-form__cell--third">
|
<div class="term-form__cell term-form__cell--third">
|
||||||
<TextField
|
<TextField
|
||||||
v-model="form.minimumScore"
|
v-model="form.minimumScore"
|
||||||
@@ -91,6 +63,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="term-form__image-col">
|
||||||
|
<ImageCropper
|
||||||
|
v-model="image"
|
||||||
|
name="image"
|
||||||
|
bg-color="#eeeeee"
|
||||||
|
@crop="onImageCropped"
|
||||||
|
@error="onImageError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="term-form__divider" />
|
<div class="term-form__divider" />
|
||||||
@@ -136,7 +118,6 @@ import ImageCropper from '@/components/form/ImageCropper.vue'
|
|||||||
import { objectToFormData } from '@/utils/object-to-formdata'
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
import { useUploadMediaMutation } from '@/services/query/auth'
|
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||||
import TextareaField from '@/components/form/TextareaField.vue'
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
import DatePickerField from '@/components/form/DatePickerField.vue'
|
|
||||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||||
import {
|
import {
|
||||||
adminTermsKeys,
|
adminTermsKeys,
|
||||||
@@ -152,14 +133,10 @@ const queryClient = useQueryClient()
|
|||||||
const termId = computed(() => (route.params.id ? Number(route.params.id) : null))
|
const termId = computed(() => (route.params.id ? Number(route.params.id) : null))
|
||||||
const isEditMode = computed(() => !!termId.value)
|
const isEditMode = computed(() => !!termId.value)
|
||||||
|
|
||||||
const todayIso = new Date().toISOString()
|
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
startsAt: '',
|
|
||||||
endsAt: '',
|
|
||||||
minimumScore: '',
|
minimumScore: '',
|
||||||
score: '',
|
score: '',
|
||||||
coverMediaId: null,
|
coverMediaId: null,
|
||||||
@@ -181,8 +158,6 @@ watch(
|
|||||||
title: term.title || '',
|
title: term.title || '',
|
||||||
description: term.description || '',
|
description: term.description || '',
|
||||||
isActive: term.isActive ?? true,
|
isActive: term.isActive ?? true,
|
||||||
startsAt: term.startsAt || '',
|
|
||||||
endsAt: term.endsAt || '',
|
|
||||||
minimumScore: term.minimumScore ?? '',
|
minimumScore: term.minimumScore ?? '',
|
||||||
score: term.score ?? '',
|
score: term.score ?? '',
|
||||||
}
|
}
|
||||||
@@ -249,29 +224,23 @@ const onCancel = () => router.push({ name: 'admin-terms' })
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
grid-template-columns: 4fr 8fr;
|
grid-template-columns: 8fr 4fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
grid-template-columns: 3fr 9fr;
|
grid-template-columns: 9fr 3fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__image-col {
|
&__image-col {
|
||||||
order: 2;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
order: 1;
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__main-col {
|
&__main-col {
|
||||||
order: 1;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
order: 2;
|
padding-inline-end: 1.25rem;
|
||||||
padding-inline-start: 1.25rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +281,7 @@ const onCancel = () => router.push({ name: 'admin-terms' })
|
|||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
gap: 0.625rem;
|
gap: 0.625rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ export const termSchema = object().shape({
|
|||||||
title: string().required().min(3),
|
title: string().required().min(3),
|
||||||
description: string().nullable().notRequired(),
|
description: string().nullable().notRequired(),
|
||||||
isActive: boolean().default(true),
|
isActive: boolean().default(true),
|
||||||
startsAt: string().required(),
|
|
||||||
endsAt: string().required(),
|
|
||||||
minimumScore: number().typeError('حد نصاب قبولی باید عدد باشد').required(),
|
minimumScore: number().typeError('حد نصاب قبولی باید عدد باشد').required(),
|
||||||
score: number().typeError('کل نمره آزمون باید عدد باشد').required(),
|
score: number().typeError('کل نمره آزمون باید عدد باشد').required(),
|
||||||
coverMediaId: number().nullable().notRequired(),
|
coverMediaId: number().typeError('شناسه تصویر جلد باید عدد باشد').nullable().notRequired(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const addTermStudentSchema = object().shape({
|
export const addTermStudentSchema = object().shape({
|
||||||
studentIds: array().of(mixed()).min(1, 'حداقل یک دانشجو را انتخاب کنید').required(),
|
studentIds: array().of(mixed()).min(1, 'حداقل یک طلبه را انتخاب کنید').required(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
@click="onReset"
|
@click="onReset"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" />
|
<SvgIcon name="close" color="black" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
|
|||||||
@@ -14,9 +14,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Badge v-if="user.status === 'blocked'" variant="danger" size="sm" dot>
|
<div class="users-table__middle">
|
||||||
{{ USER_STATUS.blocked }}
|
<div class="users-table__date">
|
||||||
</Badge>
|
<span class="users-table__date-label">تاریخ ثبت نام:</span>
|
||||||
|
<span class="users-table__date-value">
|
||||||
|
{{ formatJalaaliDateTime(user.createdAt) || '—' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -29,6 +33,10 @@
|
|||||||
<span>{{ roleLabel(user) || 'بدون نقش' }}</span>
|
<span>{{ roleLabel(user) || 'بدون نقش' }}</span>
|
||||||
<SvgIcon name="caret-down" :size="12" color="currentColor" />
|
<SvgIcon name="caret-down" :size="12" color="currentColor" />
|
||||||
</button>
|
</button>
|
||||||
|
<Badge v-if="user.status === 'blocked'" variant="danger" size="sm" dot>
|
||||||
|
{{ USER_STATUS.blocked }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="users-table__actions">
|
<div class="users-table__actions">
|
||||||
<button
|
<button
|
||||||
@@ -77,6 +85,7 @@ import { useQueryClient } from '@tanstack/vue-query'
|
|||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import DropdownMenu from '@/components/DropdownMenu.vue'
|
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||||
|
import { formatJalaaliDateTime } from '@/utils/date-utils'
|
||||||
import { adminUsersKeys, useUpdateAdminUserRoleMutation } from '@/services/query/admin-users'
|
import { adminUsersKeys, useUpdateAdminUserRoleMutation } from '@/services/query/admin-users'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
@@ -219,10 +228,38 @@ const menuItems = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
flex: 1;
|
flex: 1 1 33%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__middle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
flex: 1 1 33%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__date {
|
||||||
|
background: rgba(107, 107, 107, 5%);
|
||||||
|
padding: 0.25rem 1rem;
|
||||||
|
border-radius: 0.875rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__date-label {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #848484;
|
||||||
|
margin-inline-end: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__date-value {
|
||||||
|
font-family: var(--font-family-en);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
&__name {
|
&__name {
|
||||||
font-family: var(--font-family-fa);
|
font-family: var(--font-family-fa);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -13,11 +13,7 @@
|
|||||||
|
|
||||||
<template v-else-if="user">
|
<template v-else-if="user">
|
||||||
<div class="user-details__section">
|
<div class="user-details__section">
|
||||||
<div v-if="isStudent && !user.profileCompleted" class="user-details__notice">
|
<div class="user-details__row user-details__row--main">
|
||||||
<SvgIcon name="warning" :size="20" color="var(--color-primary)" />
|
|
||||||
<span>اطلاعات ثبت نام این دانشجو کامل نیست.</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="user-details__head">
|
<div class="user-details__head">
|
||||||
<div class="user-details__avatar">
|
<div class="user-details__avatar">
|
||||||
<img v-if="user.avatarUrl" :src="user.avatarUrl" :alt="fullName" />
|
<img v-if="user.avatarUrl" :src="user.avatarUrl" :alt="fullName" />
|
||||||
@@ -30,14 +26,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="user-details__label">نام و نام خانوادگی</p>
|
<p class="user-details__label">نام و نام خانوادگی</p>
|
||||||
<p class="user-details__name">{{ fullName || '—' }}</p>
|
<p class="user-details__name">{{ fullName || '—' }}</p>
|
||||||
<p class="user-details__role">
|
<Badge
|
||||||
<span class="user-details__role-key">نقش:</span>
|
class="user-details__role"
|
||||||
<span class="user-details__role-val">{{ roleText }}</span>
|
:variant="roleVariant"
|
||||||
</p>
|
label="نقش:"
|
||||||
|
:value="roleText"
|
||||||
|
dot
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-details__grid">
|
|
||||||
<LineInfoBlock
|
<LineInfoBlock
|
||||||
title="تاریخ تولد"
|
title="تاریخ تولد"
|
||||||
:numeric-desc="formatJalaaliDate(user.birthday) || '—'"
|
:numeric-desc="formatJalaaliDate(user.birthday) || '—'"
|
||||||
@@ -49,11 +46,14 @@
|
|||||||
:desc="MARITAL_STATUS[user.marriageStatus] || '—'"
|
:desc="MARITAL_STATUS[user.marriageStatus] || '—'"
|
||||||
/>
|
/>
|
||||||
<LineInfoBlock title="جنسیت" :desc="GENDER[user.gender] || '—'" />
|
<LineInfoBlock title="جنسیت" :desc="GENDER[user.gender] || '—'" />
|
||||||
<LineInfoBlock title="استان" :desc="user?.province?.name || '—'" />
|
|
||||||
<LineInfoBlock title="شهر" :desc="user?.city?.name || '—'" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="user-details__row user-details__row--location">
|
||||||
|
<LineInfoBlock title="استان" :desc="user?.province?.name || '—'" />
|
||||||
|
<LineInfoBlock title="شهر" :desc="user?.city?.name || '—'" />
|
||||||
<LineInfoBlock title="آدرس" :desc="user?.address || '—'" />
|
<LineInfoBlock title="آدرس" :desc="user?.address || '—'" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<LineInfoBlock title="بیوگرافی" :desc="user.bio || '—'" />
|
<LineInfoBlock title="بیوگرافی" :desc="user.bio || '—'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -95,6 +95,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import Badge from '@/components/Badge.vue'
|
||||||
import useModal from '@/composables/useModal'
|
import useModal from '@/composables/useModal'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
@@ -137,6 +138,16 @@ const roleText = computed(() => {
|
|||||||
return name ? ROLE_LABELS[name] || name : 'بدون نقش'
|
return name ? ROLE_LABELS[name] || name : 'بدون نقش'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const ROLE_BADGE_VARIANTS = {
|
||||||
|
student: 'danger',
|
||||||
|
teacher: 'warning',
|
||||||
|
counselor: 'blue',
|
||||||
|
missionary: 'info',
|
||||||
|
admin: 'neutral',
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleVariant = computed(() => ROLE_BADGE_VARIANTS[roleName.value] || 'neutral')
|
||||||
|
|
||||||
const isStudent = computed(() => roleName.value === 'student')
|
const isStudent = computed(() => roleName.value === 'student')
|
||||||
|
|
||||||
const statusModifier = computed(() =>
|
const statusModifier = computed(() =>
|
||||||
@@ -172,7 +183,43 @@ const statusModifier = computed(() =>
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.875rem;
|
gap: 0.875rem;
|
||||||
margin-bottom: 0.75rem;
|
}
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__row--main {
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
grid-template-columns: auto repeat(5, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__row--main &__head {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__row--location {
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__avatar {
|
&__avatar {
|
||||||
@@ -245,23 +292,7 @@ const statusModifier = computed(() =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__role {
|
&__role {
|
||||||
display: inline-flex;
|
margin-top: 0.25rem;
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
margin: 0.25rem 0 0;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
background-color: rgba(0, 0, 0, 4%);
|
|
||||||
font-family: var(--font-family-fa);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__role-key {
|
|
||||||
color: #808080;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__role-val {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__grid {
|
&__grid {
|
||||||
|
|||||||
@@ -21,25 +21,25 @@
|
|||||||
<div class="verification-item__date">
|
<div class="verification-item__date">
|
||||||
<span class="verification-item__date-label">تاریخ ثبت نام:</span>
|
<span class="verification-item__date-label">تاریخ ثبت نام:</span>
|
||||||
<span class="verification-item__date-value">
|
<span class="verification-item__date-value">
|
||||||
{{ formatJalaaliDate(user.createdAt) || '—' }}
|
{{ formatJalaaliDateTime(user.createdAt) || '—' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<Badge
|
||||||
v-if="user.status === 'verified'"
|
v-if="user.status === 'rejected'"
|
||||||
class="verification-item__badge verification-item__badge--pending"
|
variant="danger"
|
||||||
>
|
:dot="true"
|
||||||
<span class="verification-item__dot" />
|
value="رد شده"
|
||||||
<span>در انتظار تایید</span>
|
:clickable="true"
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-else-if="user.status === 'rejected'"
|
|
||||||
type="button"
|
|
||||||
class="verification-item__badge verification-item__badge--rejected"
|
|
||||||
@click="emit('show-reject-reason', user)"
|
@click="emit('show-reject-reason', user)"
|
||||||
>
|
/>
|
||||||
<span class="verification-item__dot" />
|
<Badge
|
||||||
<span>رد شده</span>
|
v-else-if="statusLabel"
|
||||||
</button>
|
:variant="statusVariant"
|
||||||
|
:dot="true"
|
||||||
|
:value="statusLabel"
|
||||||
|
:clickable="user.status === 'blocked'"
|
||||||
|
@click="emit('show-reject-reason', user)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="verification-item__actions">
|
<div class="verification-item__actions">
|
||||||
@@ -54,11 +54,11 @@
|
|||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
v-if="user.status !== 'rejected'"
|
v-if="user.status !== 'blocked'"
|
||||||
tooltip="عدم تایید"
|
tooltip="عدم تایید"
|
||||||
bg-color="rgba(204, 40, 49, 0.08)"
|
bg-color="rgba(204, 40, 49, 0.08)"
|
||||||
size="2.5rem"
|
size="2.5rem"
|
||||||
@click="emit('verify-user', { id: user.id, status: 'rejected' })"
|
@click="emit('verify-user', { id: user.id, status: 'blocked' })"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" color="var(--color-error)" />
|
<SvgIcon name="close" :size="20" color="var(--color-error)" />
|
||||||
@@ -80,10 +80,12 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { USER_STATUS } from '@/enums'
|
||||||
|
import Badge from '@/components/Badge.vue'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
|
||||||
import CircleButton from '@/components/CircleButton.vue'
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
|
import { formatJalaaliDateTime } from '@/utils/date-utils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
user: { type: Object, required: true },
|
user: { type: Object, required: true },
|
||||||
@@ -92,7 +94,16 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['show-details', 'show-reject-reason', 'verify-user'])
|
const emit = defineEmits(['show-details', 'show-reject-reason', 'verify-user'])
|
||||||
|
|
||||||
|
const STATUS_VARIANTS = {
|
||||||
|
pending: 'neutral',
|
||||||
|
verified: 'warning',
|
||||||
|
approved: 'success',
|
||||||
|
blocked: 'danger',
|
||||||
|
}
|
||||||
|
|
||||||
const fullName = computed(() => `${props.user.name || ''}`.trim() || '—')
|
const fullName = computed(() => `${props.user.name || ''}`.trim() || '—')
|
||||||
|
const statusLabel = computed(() => USER_STATUS[props.user.status] || '')
|
||||||
|
const statusVariant = computed(() => STATUS_VARIANTS[props.user.status] || 'neutral')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -202,37 +213,6 @@ const fullName = computed(() => `${props.user.name || ''}`.trim() || '—')
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.375rem;
|
|
||||||
padding: 0.25rem 1rem;
|
|
||||||
border-radius: 0.875rem;
|
|
||||||
font-family: var(--font-family-fa);
|
|
||||||
font-size: 0.7rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
border: none;
|
|
||||||
cursor: default;
|
|
||||||
|
|
||||||
&--pending {
|
|
||||||
background: rgba(0, 255, 68, 6%);
|
|
||||||
color: #009a12;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--rejected {
|
|
||||||
background: rgba(204, 40, 49, 6%);
|
|
||||||
color: var(--color-error);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__dot {
|
|
||||||
width: 0.375rem;
|
|
||||||
height: 0.375rem;
|
|
||||||
border-radius: 9999px;
|
|
||||||
background: currentcolor;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
@click="onReset"
|
@click="onReset"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" />
|
<SvgIcon name="close" color="black" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="reject-reason-details">
|
<div class="reject-reason-details">
|
||||||
<div class="reject-reason-details__grid">
|
<div class="reject-reason-details__grid">
|
||||||
<div class="reject-reason-details__cell--full">
|
<div class="reject-reason-details__cell--full">
|
||||||
<LineInfoBlock title="توضیحات" :desc="verification(data).adminNote || ''" />
|
<LineInfoBlock title="توضیحات" :desc="data?.user?.blockReason || '—'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -34,8 +34,6 @@ import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|||||||
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'RejectReasonDetailsModal' })
|
defineOptions({ name: 'RejectReasonDetailsModal' })
|
||||||
|
|
||||||
const verification = (data) => data?.verification || {}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -9,31 +9,13 @@
|
|||||||
>
|
>
|
||||||
<template #default="{ data, close }">
|
<template #default="{ data, close }">
|
||||||
<form class="reject-reason-modal" @submit.prevent="onSubmit(data, close)">
|
<form class="reject-reason-modal" @submit.prevent="onSubmit(data, close)">
|
||||||
<div class="reject-reason-modal__row">
|
|
||||||
<TextField
|
|
||||||
v-model="form.rejectionReason"
|
|
||||||
name="rejectionReason"
|
|
||||||
label="علت رد"
|
|
||||||
:error="errors.rejectionReason"
|
|
||||||
@blur="validateAt('rejectionReason', form.rejectionReason)"
|
|
||||||
/>
|
|
||||||
<SelectField
|
|
||||||
:model-value="form.priority?.id"
|
|
||||||
name="priority"
|
|
||||||
label="وضعیت تیکت"
|
|
||||||
:options="statusOptions"
|
|
||||||
option-label="label"
|
|
||||||
option-value="value"
|
|
||||||
:error="errors.priority"
|
|
||||||
@update:model-value="(e) => handleChange('priority', { id: e })"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<TextareaField
|
<TextareaField
|
||||||
v-model="form.adminNote"
|
v-model="form.reason"
|
||||||
name="adminNote"
|
name="reason"
|
||||||
label="لطفا علت رد را توضیح دهید"
|
label="لطفا علت رد را توضیح دهید"
|
||||||
:row="4"
|
:row="4"
|
||||||
:error="errors.adminNote"
|
:error="errors.reason"
|
||||||
|
@blur="validateAt('reason', form.reason)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="reject-reason-modal__divider" />
|
<div class="reject-reason-modal__divider" />
|
||||||
@@ -68,14 +50,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import useYup from '@/composables/useYup'
|
import useYup from '@/composables/useYup'
|
||||||
import { PRIORITY_STATUS } from '@/enums'
|
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import TextField from '@/components/form/TextField.vue'
|
|
||||||
import { rejectionReasonSchema } from '../../../schema'
|
import { rejectionReasonSchema } from '../../../schema'
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
|
||||||
import TextareaField from '@/components/form/TextareaField.vue'
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
import { adminUsersKeys, useChangeAdminUserStatusMutation } from '@/services/query/admin-users'
|
import { adminUsersKeys, useChangeAdminUserStatusMutation } from '@/services/query/admin-users'
|
||||||
|
|
||||||
@@ -84,22 +63,12 @@ defineOptions({ name: 'RejectReasonModal' })
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const changeStatusMutation = useChangeAdminUserStatusMutation()
|
const changeStatusMutation = useChangeAdminUserStatusMutation()
|
||||||
|
|
||||||
const DEFAULT_FORM = { rejectionReason: '', adminNote: '' }
|
const DEFAULT_FORM = { reason: '' }
|
||||||
|
|
||||||
const { validate, validateAt, errors, resetErrors } = useYup(rejectionReasonSchema)
|
const { validate, validateAt, errors, resetErrors } = useYup(rejectionReasonSchema)
|
||||||
|
|
||||||
const form = ref(structuredClone({ ...DEFAULT_FORM }))
|
const form = ref(structuredClone({ ...DEFAULT_FORM }))
|
||||||
|
|
||||||
const statusOptions = Object.entries(PRIORITY_STATUS).map(([value, label]) => ({
|
|
||||||
value,
|
|
||||||
label,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const handleChange = (key, value) => {
|
|
||||||
form.value = { ...form.value, [key]: value }
|
|
||||||
validateAt(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSubmit = async (data, close) => {
|
const onSubmit = async (data, close) => {
|
||||||
const { isValid, payload } = await validate(form.value)
|
const { isValid, payload } = await validate(form.value)
|
||||||
if (!isValid) return
|
if (!isValid) return
|
||||||
@@ -107,7 +76,7 @@ const onSubmit = async (data, close) => {
|
|||||||
if (!id) return
|
if (!id) return
|
||||||
await changeStatusMutation.mutateAsync({
|
await changeStatusMutation.mutateAsync({
|
||||||
id,
|
id,
|
||||||
payload: { status: 'blocked', ...payload },
|
payload: { status: 'blocked', reason: payload.reason },
|
||||||
})
|
})
|
||||||
await queryClient.invalidateQueries({ queryKey: adminUsersKeys.all })
|
await queryClient.invalidateQueries({ queryKey: adminUsersKeys.all })
|
||||||
form.value = structuredClone({ ...DEFAULT_FORM })
|
form.value = structuredClone({ ...DEFAULT_FORM })
|
||||||
@@ -122,11 +91,6 @@ const onSubmit = async (data, close) => {
|
|||||||
text-align: start;
|
text-align: start;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
|
||||||
&__row {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__divider {
|
&__divider {
|
||||||
border-block-end: 1px solid var(--color-thd-gray);
|
border-block-end: 1px solid var(--color-thd-gray);
|
||||||
margin-block: 1rem;
|
margin-block: 1rem;
|
||||||
|
|||||||
@@ -8,28 +8,30 @@
|
|||||||
:show-close-button="true"
|
:show-close-button="true"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<div v-if="!user" class="user-verification-details">
|
<div v-if="detailsLoading || !user" class="user-verification-details">
|
||||||
<p class="user-verification-details__loading">در حال بارگذاری…</p>
|
<p class="user-verification-details__loading">در حال بارگذاری…</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="user-verification-details">
|
<div v-else ref="pdfContent" class="user-verification-details">
|
||||||
|
<header class="user-verification-details__pdf-header">
|
||||||
|
<h1>اطلاعات احراز هویت کاربر</h1>
|
||||||
|
<p>{{ fullName }}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
<section class="user-verification-details__section">
|
<section class="user-verification-details__section">
|
||||||
<div class="user-verification-details__grid user-verification-details__grid--6">
|
<div class="user-verification-details__grid user-verification-details__grid--6">
|
||||||
<LineInfoBlock title="نام و نام خانوادگی" :numeric-desc="fullName" />
|
<LineInfoBlock title="نام و نام خانوادگی" :numeric-desc="fullName" />
|
||||||
<LineInfoBlock
|
<LineInfoBlock
|
||||||
title="تاریخ تولد"
|
title="تاریخ تولد"
|
||||||
:numeric-desc="profile.faBirthDate || formatJalaaliDate(profile.birthDate) || ''"
|
:numeric-desc="formatJalaaliDate(personal.birthDate) || ''"
|
||||||
/>
|
/>
|
||||||
<LineInfoBlock title="شماره تماس" :numeric-desc="user.phoneNumber" />
|
<LineInfoBlock title="شماره تماس" :numeric-desc="personal.phone" />
|
||||||
<LineInfoBlock title="کد ملی" :numeric-desc="user.nationalCode" />
|
<LineInfoBlock title="کد ملی" :numeric-desc="personal.nationalCode" />
|
||||||
<LineInfoBlock
|
<LineInfoBlock title="وضعیت" :desc="MARITAL_STATUS[personal.maritalStatus] || ''" />
|
||||||
title="وضعیت تاهل"
|
<LineInfoBlock title="جنسیت" :desc="GENDER[personal.gender] || ''" />
|
||||||
:desc="profile.faMaritalStatus || MARITAL_STATUS[profile.maritalStatus] || ''"
|
<LineInfoBlock title="استان" :desc="personal.province || '-'" />
|
||||||
/>
|
<LineInfoBlock title="شهر" :desc="personal.city || '-'" />
|
||||||
<LineInfoBlock title="جنسیت" :desc="profile.faGender || GENDER[profile.gender] || ''" />
|
|
||||||
<LineInfoBlock title="استان" :desc="user?.province?.name || '-'" />
|
|
||||||
<LineInfoBlock title="شهر" :desc="user?.city?.name || '-'" />
|
|
||||||
<div class="user-verification-details__cell user-verification-details__cell--span-3">
|
<div class="user-verification-details__cell user-verification-details__cell--span-3">
|
||||||
<LineInfoBlock title="آدرس" :desc="user?.address || '-'" />
|
<LineInfoBlock title="آدرس" :desc="personal.address || '-'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -39,15 +41,15 @@
|
|||||||
<div class="user-verification-details__grid user-verification-details__grid--4">
|
<div class="user-verification-details__grid user-verification-details__grid--4">
|
||||||
<LineInfoBlock
|
<LineInfoBlock
|
||||||
title="وضعیت تحصیلی"
|
title="وضعیت تحصیلی"
|
||||||
:desc="profile.faEducationStatus || EDUCATION_STATUS[profile.educationStatus] || ''"
|
:desc="EDUCATION_STATUS[profile.educationStatus] || ''"
|
||||||
/>
|
/>
|
||||||
<LineInfoBlock
|
<LineInfoBlock
|
||||||
title="آخرین مدرک تحصیلی (طلبه)"
|
title="آخرین مدرک تحصیلی (طلبه)"
|
||||||
:desc="profile.faSeminaryLevel || SEMINARY_LEVEL[profile.seminaryLevel] || ''"
|
:desc="SEMINARY_LEVEL[profile.seminaryLevel] || ''"
|
||||||
/>
|
/>
|
||||||
<LineInfoBlock
|
<LineInfoBlock
|
||||||
title="آخرین مدرک تحصیلی"
|
title="آخرین مدرک تحصیلی"
|
||||||
:desc="profile.faUniversityLevel || UNIVERSITY_LEVEL[profile.universityLevel] || ''"
|
:desc="UNIVERSITY_LEVEL[profile.universityLevel] || ''"
|
||||||
/>
|
/>
|
||||||
<LineInfoBlock title="نام حوزه علمیه/دانشگاه" :desc="profile.universityName || ''" />
|
<LineInfoBlock title="نام حوزه علمیه/دانشگاه" :desc="profile.universityName || ''" />
|
||||||
<div class="user-verification-details__cell user-verification-details__cell--full">
|
<div class="user-verification-details__cell user-verification-details__cell--full">
|
||||||
@@ -56,36 +58,12 @@
|
|||||||
:desc="profile.fieldOfStudy || ''"
|
:desc="profile.fieldOfStudy || ''"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-verification-details__cell user-verification-details__cell--full">
|
|
||||||
<LineInfoBlock
|
|
||||||
title="خلاصهای از سوابق شغلی"
|
|
||||||
:desc="profile.workExperienceSummary || ''"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="user-verification-details__cell user-verification-details__cell--full">
|
|
||||||
<LineInfoBlock
|
|
||||||
title="خلاصهای از فعالیتها در موقعیتهای شغلی"
|
|
||||||
:desc="profile.activitySummary || ''"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="user-verification-details__section">
|
<section class="user-verification-details__section">
|
||||||
<LineTitleBlock title="اطلاعات شغلی" title-en="Job information" />
|
<LineTitleBlock title="اطلاعات شغلی" title-en="Job information" />
|
||||||
<div class="user-verification-details__grid user-verification-details__grid--4">
|
<div class="user-verification-details__grid user-verification-details__grid--4">
|
||||||
<div class="user-verification-details__cell user-verification-details__cell--full">
|
|
||||||
<LineInfoBlock
|
|
||||||
title="گرایش تخصصی حوزوی / رشته تحصیلی دانشگاهی"
|
|
||||||
:desc="profile.fieldOfStudy || ''"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="user-verification-details__cell user-verification-details__cell--full">
|
|
||||||
<LineInfoBlock
|
|
||||||
title="خلاصهای از سوابق شغلی"
|
|
||||||
:desc="profile.workExperienceSummary || ''"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="user-verification-details__cell user-verification-details__cell--full">
|
<div class="user-verification-details__cell user-verification-details__cell--full">
|
||||||
<LineInfoBlock
|
<LineInfoBlock
|
||||||
title="خلاصهای از فعالیتها در موقعیتهای شغلی"
|
title="خلاصهای از فعالیتها در موقعیتهای شغلی"
|
||||||
@@ -190,7 +168,9 @@
|
|||||||
<section v-if="faithProduction?.url" class="user-verification-details__section">
|
<section v-if="faithProduction?.url" class="user-verification-details__section">
|
||||||
<LineTitleBlock title="بخش صوت" title-en="Audio section" />
|
<LineTitleBlock title="بخش صوت" title-en="Audio section" />
|
||||||
<LineInfoBlock title="نظر و تحلیل از ویدئو ارائه شده" />
|
<LineInfoBlock title="نظر و تحلیل از ویدئو ارائه شده" />
|
||||||
<audio :src="faithProduction.url" controls class="user-verification-details__audio" />
|
<div class="user-verification-details__voice">
|
||||||
|
<VoiceRecorder :model-value="faithProduction" disabled />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-if="leaderMessage?.url" class="user-verification-details__section">
|
<section v-if="leaderMessage?.url" class="user-verification-details__section">
|
||||||
@@ -203,17 +183,36 @@
|
|||||||
class="user-verification-details__video"
|
class="user-verification-details__video"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div class="user-verification-details__divider" />
|
||||||
|
|
||||||
|
<div class="user-verification-details__actions" data-pdf-ignore>
|
||||||
|
<BaseButton
|
||||||
|
text="ذخیره به صورت PDF"
|
||||||
|
:loading="pdfLoading"
|
||||||
|
custom-class="user-verification-details__pdf-btn"
|
||||||
|
@click="onSaveAsPdf"
|
||||||
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
|
</template>
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</BasicModal>
|
</BasicModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import { toast } from 'vue3-toastify'
|
||||||
import useModal from '@/composables/useModal'
|
import useModal from '@/composables/useModal'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||||
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||||
|
import VoiceRecorder from '@/components/form/VoiceRecorder.vue'
|
||||||
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
||||||
import { useAdminUserQuery, useAdminUserRegisterDataQuery } from '@/services/query/admin-users'
|
import { useAdminUserQuery, useAdminUserRegisterDataQuery } from '@/services/query/admin-users'
|
||||||
import {
|
import {
|
||||||
@@ -227,7 +226,6 @@ import {
|
|||||||
SEMINARY_LEVEL,
|
SEMINARY_LEVEL,
|
||||||
SESSION_CANCELLATION_RESPONSE,
|
SESSION_CANCELLATION_RESPONSE,
|
||||||
UNIVERSITY_LEVEL,
|
UNIVERSITY_LEVEL,
|
||||||
VERIFICATION_MEDIA_TYPE,
|
|
||||||
} from '@/enums'
|
} from '@/enums'
|
||||||
|
|
||||||
defineOptions({ name: 'UserVerificationDetailsModal' })
|
defineOptions({ name: 'UserVerificationDetailsModal' })
|
||||||
@@ -238,25 +236,37 @@ const modalData = computed(() => getModal('UserVerificationDetailsModal')?.data
|
|||||||
const userId = computed(() => modalData.value.id ?? null)
|
const userId = computed(() => modalData.value.id ?? null)
|
||||||
const userIdRef = computed(() => userId.value)
|
const userIdRef = computed(() => userId.value)
|
||||||
|
|
||||||
const { data: fetched } = useAdminUserQuery(userIdRef, {
|
const { data: fetched, isLoading: userLoading } = useAdminUserQuery(userIdRef, {
|
||||||
enabled: () => !!userIdRef.value,
|
enabled: () => !!userIdRef.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: registerData } = useAdminUserRegisterDataQuery(userIdRef, {
|
const { data: registerData, isLoading: registerDataLoading } = useAdminUserRegisterDataQuery(
|
||||||
|
userIdRef,
|
||||||
|
{
|
||||||
enabled: () => !!userIdRef.value,
|
enabled: () => !!userIdRef.value,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const user = computed(() => modalData.value.user || fetched.value || null)
|
const user = computed(() => modalData.value.user || fetched.value || null)
|
||||||
|
const detailsLoading = computed(() => userLoading.value || registerDataLoading.value)
|
||||||
|
|
||||||
// The register-data endpoint returns the answers the user gave during the
|
|
||||||
// registration steps — the same fields the sections below render. Merge it over
|
|
||||||
// the user's stored profile so reviewing admins see what was actually submitted.
|
|
||||||
const profile = computed(() => ({ ...user.value?.profile, ...registerData.value }))
|
const profile = computed(() => ({ ...user.value?.profile, ...registerData.value }))
|
||||||
|
|
||||||
const fullName = computed(
|
const fullName = computed(() => user.value?.name || '—')
|
||||||
() =>
|
const personal = computed(() => {
|
||||||
user.value?.name || `${user.value?.firstName || ''} ${user.value?.lastName || ''}`.trim() || '—'
|
const u = user.value || {}
|
||||||
)
|
const p = u.profile || {}
|
||||||
|
return {
|
||||||
|
birthDate: u.birthday || p.birthDate || '',
|
||||||
|
phone: u.phone || u.phoneNumber || '',
|
||||||
|
nationalCode: u.nationalCode || '',
|
||||||
|
maritalStatus: u.marriageStatus || p.maritalStatus || '',
|
||||||
|
gender: u.gender || p.gender || '',
|
||||||
|
address: typeof u.address === 'string' ? u.address : u.address?.address || '',
|
||||||
|
province: u.province?.name || u.address?.province?.name || '',
|
||||||
|
city: u.city?.name || u.address?.city?.name || '',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const platforms = computed(() =>
|
const platforms = computed(() =>
|
||||||
Array.isArray(profile.value.propagationPlatforms) ? profile.value.propagationPlatforms : []
|
Array.isArray(profile.value.propagationPlatforms) ? profile.value.propagationPlatforms : []
|
||||||
@@ -276,13 +286,53 @@ const onlinePlatformDetails = computed(
|
|||||||
() => platforms.value.find((p) => p.platform === 'online')?.platformDetails || ''
|
() => platforms.value.find((p) => p.platform === 'online')?.platformDetails || ''
|
||||||
)
|
)
|
||||||
|
|
||||||
const verificationMedia = computed(() => user.value?.verification?.media || [])
|
// Skill parts 6 & 7 store the uploaded media objects in register-data under
|
||||||
const faithProduction = computed(() =>
|
// `faithProductionAudio` / `leaderMessageVideo` (each carries a `url`).
|
||||||
verificationMedia.value.find((m) => m.type === VERIFICATION_MEDIA_TYPE.FAITH_PRODUCTION)
|
const faithProduction = computed(() => profile.value.faithProductionAudio || null)
|
||||||
)
|
const leaderMessage = computed(() => profile.value.leaderMessageVideo || null)
|
||||||
const leaderMessage = computed(() =>
|
|
||||||
verificationMedia.value.find((m) => m.type === VERIFICATION_MEDIA_TYPE.LEADER_MESSAGE)
|
const pdfContent = ref(null)
|
||||||
)
|
const pdfLoading = ref(false)
|
||||||
|
|
||||||
|
const onSaveAsPdf = async () => {
|
||||||
|
if (!pdfContent.value || !userId.value || detailsLoading.value || pdfLoading.value) return
|
||||||
|
|
||||||
|
pdfLoading.value = true
|
||||||
|
try {
|
||||||
|
await document.fonts?.ready
|
||||||
|
|
||||||
|
const pdfSource = pdfContent.value.cloneNode(true)
|
||||||
|
pdfSource.classList.add('user-verification-details--pdf')
|
||||||
|
pdfSource.querySelectorAll('[data-pdf-ignore]').forEach((element) => element.remove())
|
||||||
|
|
||||||
|
const html2pdfModule = await import('html2pdf.js')
|
||||||
|
const html2pdf = html2pdfModule.default || html2pdfModule
|
||||||
|
|
||||||
|
await html2pdf()
|
||||||
|
.set({
|
||||||
|
margin: [8, 8, 8, 8],
|
||||||
|
filename: `user-verification-${userId.value}.pdf`,
|
||||||
|
image: { type: 'jpeg', quality: 0.98 },
|
||||||
|
html2canvas: {
|
||||||
|
scale: 2,
|
||||||
|
useCORS: true,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
logging: false,
|
||||||
|
},
|
||||||
|
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
|
||||||
|
pagebreak: {
|
||||||
|
mode: ['css', 'legacy'],
|
||||||
|
avoid: ['.user-verification-details__section'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.from(pdfSource)
|
||||||
|
.save()
|
||||||
|
} catch {
|
||||||
|
toast.error('ساخت فایل PDF با خطا مواجه شد.')
|
||||||
|
} finally {
|
||||||
|
pdfLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -290,6 +340,10 @@ const leaderMessage = computed(() =>
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
|
|
||||||
|
&__pdf-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
&__loading {
|
&__loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
@@ -428,6 +482,22 @@ const leaderMessage = computed(() =>
|
|||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__voice {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
|
||||||
|
/* stylelint-disable-next-line selector-class-pattern */
|
||||||
|
:deep(.voice-recorder) {
|
||||||
|
min-height: auto;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* stylelint-disable-next-line selector-class-pattern */
|
||||||
|
:deep(.voice-recorder__main-btn) {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__divider {
|
&__divider {
|
||||||
border-block-end: 1px solid var(--color-thd-gray);
|
border-block-end: 1px solid var(--color-thd-gray);
|
||||||
margin-block: 1.5rem;
|
margin-block: 1.5rem;
|
||||||
@@ -443,5 +513,61 @@ const leaderMessage = computed(() =>
|
|||||||
&__pdf-btn {
|
&__pdf-btn {
|
||||||
min-width: 12rem;
|
min-width: 12rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--pdf {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.25rem;
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pdf &__pdf-header {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
text-align: right;
|
||||||
|
break-after: avoid;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #4b4b4b;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0.25rem 0 0;
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pdf &__section {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pdf &__grid--4 {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pdf &__grid--6 {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pdf &__cell--span-3 {
|
||||||
|
grid-column: span 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pdf :deep(.line-info__title) {
|
||||||
|
overflow: visible;
|
||||||
|
text-overflow: initial;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||