before store
This commit is contained in:
BIN
i18n-2.xlsm
BIN
i18n-2.xlsm
Binary file not shown.
@@ -14,6 +14,7 @@
|
|||||||
<meta name="robots" content="noindex, nofollow"/>
|
<meta name="robots" content="noindex, nofollow"/>
|
||||||
<meta name="msapplication-tap-highlight" content="no">
|
<meta name="msapplication-tap-highlight" content="no">
|
||||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||||
|
<script src="http://localhost:8098"></script>
|
||||||
<!--
|
<!--
|
||||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
|
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
|
||||||
-->
|
-->
|
||||||
|
|||||||
128
package-lock.json
generated
128
package-lock.json
generated
@@ -26,7 +26,7 @@
|
|||||||
"@twa-dev/types": "^8.0.2",
|
"@twa-dev/types": "^8.0.2",
|
||||||
"@types/node": "^20.17.30",
|
"@types/node": "^20.17.30",
|
||||||
"@types/telegram-web-app": "^7.10.1",
|
"@types/telegram-web-app": "^7.10.1",
|
||||||
"@vue/devtools": "^7.7.2",
|
"@vue/devtools": "^7.7.6",
|
||||||
"@vue/eslint-config-typescript": "^14.1.3",
|
"@vue/eslint-config-typescript": "^14.1.3",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
@@ -2142,9 +2142,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/cors": {
|
"node_modules/@types/cors": {
|
||||||
"version": "2.8.17",
|
"version": "2.8.19",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||||
"integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
|
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2642,14 +2642,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools": {
|
"node_modules/@vue/devtools": {
|
||||||
"version": "7.7.2",
|
"version": "7.7.6",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/devtools/-/devtools-7.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/devtools/-/devtools-7.7.6.tgz",
|
||||||
"integrity": "sha512-YLGea5P5cX3av6ExnQ08cbk/BYSUyfp0frRPQQgEYVfC53QV8UVisYFVdB2eFCjsQ9b+z0LmEIGtXAmTMnKQNw==",
|
"integrity": "sha512-i/mADVyhxpvy6F2nFzN/3eY0OkGZDiDmIxGcAx/0BziDpHpi+1lWJRiJrt8yuKhpKe7Zfv9FM+UqXuZf6xHkHw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/devtools-electron": "^7.7.2",
|
"@vue/devtools-electron": "^7.7.6",
|
||||||
"@vue/devtools-kit": "^7.7.2"
|
"@vue/devtools-kit": "^7.7.6"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vue-devtools": "cli.mjs"
|
"vue-devtools": "cli.mjs"
|
||||||
@@ -2662,18 +2662,18 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools-core": {
|
"node_modules/@vue/devtools-core": {
|
||||||
"version": "7.7.2",
|
"version": "7.7.6",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.7.6.tgz",
|
||||||
"integrity": "sha512-lexREWj1lKi91Tblr38ntSsy6CvI8ba7u+jmwh2yruib/ltLUcsIzEjCnrkh1yYGGIKXbAuYV2tOG10fGDB9OQ==",
|
"integrity": "sha512-ghVX3zjKPtSHu94Xs03giRIeIWlb9M+gvDRVpIZ/cRIxKHdW6HE/sm1PT3rUYS3aV92CazirT93ne+7IOvGUWg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/devtools-kit": "^7.7.2",
|
"@vue/devtools-kit": "^7.7.6",
|
||||||
"@vue/devtools-shared": "^7.7.2",
|
"@vue/devtools-shared": "^7.7.6",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"nanoid": "^5.0.9",
|
"nanoid": "^5.1.0",
|
||||||
"pathe": "^2.0.2",
|
"pathe": "^2.0.3",
|
||||||
"vite-hot-client": "^0.2.4"
|
"vite-hot-client": "^2.0.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "^3.0.0"
|
"vue": "^3.0.0"
|
||||||
@@ -2706,43 +2706,43 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools-electron": {
|
"node_modules/@vue/devtools-electron": {
|
||||||
"version": "7.7.2",
|
"version": "7.7.6",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/devtools-electron/-/devtools-electron-7.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/devtools-electron/-/devtools-electron-7.7.6.tgz",
|
||||||
"integrity": "sha512-WxCwLdqBdKDGHwEAU9BozOgPIOMwncM8lcze4fDs5HYRGaICclW9du1ARH5eewJl8wTvSGs2I0r/p5q60p6wAA==",
|
"integrity": "sha512-RAQr0hRiZbXE86OZi/gOWguTPEW2kUWk7ox1CO24e8H7P+85YdYByv/rYBccGukdrfIygbuJmhkiiYLmdsS3EQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/devtools-core": "^7.7.2",
|
"@vue/devtools-core": "^7.7.6",
|
||||||
"@vue/devtools-kit": "^7.7.2",
|
"@vue/devtools-kit": "^7.7.6",
|
||||||
"@vue/devtools-shared": "^7.7.2",
|
"@vue/devtools-shared": "^7.7.6",
|
||||||
"electron": "^32.2.6",
|
"electron": "^33.4.8",
|
||||||
"execa": "^9.5.1",
|
"execa": "^9.5.2",
|
||||||
"h3": "^1.13.0",
|
"h3": "^1.15.1",
|
||||||
"ip": "^2.0.1",
|
"ip": "^2.0.1",
|
||||||
"pathe": "^2.0.2",
|
"pathe": "^2.0.3",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-client": "^4.8.1"
|
"socket.io-client": "^4.8.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools-electron/node_modules/execa": {
|
"node_modules/@vue/devtools-electron/node_modules/execa": {
|
||||||
"version": "9.5.2",
|
"version": "9.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz",
|
||||||
"integrity": "sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==",
|
"integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sindresorhus/merge-streams": "^4.0.0",
|
"@sindresorhus/merge-streams": "^4.0.0",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.6",
|
||||||
"figures": "^6.1.0",
|
"figures": "^6.1.0",
|
||||||
"get-stream": "^9.0.0",
|
"get-stream": "^9.0.0",
|
||||||
"human-signals": "^8.0.0",
|
"human-signals": "^8.0.1",
|
||||||
"is-plain-obj": "^4.1.0",
|
"is-plain-obj": "^4.1.0",
|
||||||
"is-stream": "^4.0.1",
|
"is-stream": "^4.0.1",
|
||||||
"npm-run-path": "^6.0.0",
|
"npm-run-path": "^6.0.0",
|
||||||
"pretty-ms": "^9.0.0",
|
"pretty-ms": "^9.2.0",
|
||||||
"signal-exit": "^4.1.0",
|
"signal-exit": "^4.1.0",
|
||||||
"strip-final-newline": "^4.0.0",
|
"strip-final-newline": "^4.0.0",
|
||||||
"yoctocolors": "^2.0.0"
|
"yoctocolors": "^2.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.19.0 || >=20.5.0"
|
"node": "^18.19.0 || >=20.5.0"
|
||||||
@@ -2855,25 +2855,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools-kit": {
|
"node_modules/@vue/devtools-kit": {
|
||||||
"version": "7.7.2",
|
"version": "7.7.6",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.6.tgz",
|
||||||
"integrity": "sha512-CY0I1JH3Z8PECbn6k3TqM1Bk9ASWxeMtTCvZr7vb+CHi+X/QwQm5F1/fPagraamKMAHVfuuCbdcnNg1A4CYVWQ==",
|
"integrity": "sha512-geu7ds7tem2Y7Wz+WgbnbZ6T5eadOvozHZ23Atk/8tksHMFOFylKi1xgGlQlVn0wlkEf4hu+vd5ctj1G4kFtwA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/devtools-shared": "^7.7.2",
|
"@vue/devtools-shared": "^7.7.6",
|
||||||
"birpc": "^0.2.19",
|
"birpc": "^2.3.0",
|
||||||
"hookable": "^5.5.3",
|
"hookable": "^5.5.3",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"perfect-debounce": "^1.0.0",
|
"perfect-debounce": "^1.0.0",
|
||||||
"speakingurl": "^14.0.1",
|
"speakingurl": "^14.0.1",
|
||||||
"superjson": "^2.2.1"
|
"superjson": "^2.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools-shared": {
|
"node_modules/@vue/devtools-shared": {
|
||||||
"version": "7.7.2",
|
"version": "7.7.6",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.6.tgz",
|
||||||
"integrity": "sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA==",
|
"integrity": "sha512-yFEgJZ/WblEsojQQceuyK6FzpFDx4kqrz2ohInxNj5/DnhoX023upTv4OD6lNPLAA5LLkbwPVb10o/7b+Y4FVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3488,9 +3488,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/birpc": {
|
"node_modules/birpc": {
|
||||||
"version": "0.2.19",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz",
|
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz",
|
||||||
"integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==",
|
"integrity": "sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -4485,9 +4485,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/crossws": {
|
"node_modules/crossws": {
|
||||||
"version": "0.3.4",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz",
|
||||||
"integrity": "sha512-uj0O1ETYX1Bh6uSgktfPvwDiPYGQ3aI4qVsaC/LWpkIzGj1nUYm5FK3K+t11oOlpN01lGbprFCH4wBlKdJjVgw==",
|
"integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4914,9 +4914,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/electron": {
|
"node_modules/electron": {
|
||||||
"version": "32.3.3",
|
"version": "33.4.11",
|
||||||
"resolved": "https://registry.npmjs.org/electron/-/electron-32.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz",
|
||||||
"integrity": "sha512-7FT8tDg+MueAw8dBn5LJqDvlM4cZkKJhXfgB3w7P5gvSoUQVAY6LIQcXJxgL+vw2rIRY/b9ak7ZBFbCMF2Bk4w==",
|
"integrity": "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -6636,20 +6636,20 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/h3": {
|
"node_modules/h3": {
|
||||||
"version": "1.15.1",
|
"version": "1.15.3",
|
||||||
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.3.tgz",
|
||||||
"integrity": "sha512-+ORaOBttdUm1E2Uu/obAyCguiI7MbBvsLTndc3gyK3zU+SYLoZXlyCP9Xgy0gikkGufFLTZXCXD6+4BsufnmHA==",
|
"integrity": "sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie-es": "^1.2.2",
|
"cookie-es": "^1.2.2",
|
||||||
"crossws": "^0.3.3",
|
"crossws": "^0.3.4",
|
||||||
"defu": "^6.1.4",
|
"defu": "^6.1.4",
|
||||||
"destr": "^2.0.3",
|
"destr": "^2.0.5",
|
||||||
"iron-webcrypto": "^1.2.1",
|
"iron-webcrypto": "^1.2.1",
|
||||||
"node-mock-http": "^1.0.0",
|
"node-mock-http": "^1.0.0",
|
||||||
"radix3": "^1.1.2",
|
"radix3": "^1.1.2",
|
||||||
"ufo": "^1.5.4",
|
"ufo": "^1.6.1",
|
||||||
"uncrypto": "^0.1.3"
|
"uncrypto": "^0.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -11326,9 +11326,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ufo": {
|
"node_modules/ufo": {
|
||||||
"version": "1.5.4",
|
"version": "1.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
|
||||||
"integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==",
|
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -11619,9 +11619,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-hot-client": {
|
"node_modules/vite-hot-client": {
|
||||||
"version": "0.2.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.0.4.tgz",
|
||||||
"integrity": "sha512-a1nzURqO7DDmnXqabFOliz908FRmIppkBKsJthS8rbe8hBEXwEwe4C3Pp33Z1JoFCYfVL4kTOMLKk0ZZxREIeA==",
|
"integrity": "sha512-W9LOGAyGMrbGArYJN4LBCdOC5+Zwh7dHvOHC0KmGKkJhsOzaKbpo/jEjpPKVHIW0/jBWj8RZG0NUxfgA8BxgAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
"@twa-dev/types": "^8.0.2",
|
"@twa-dev/types": "^8.0.2",
|
||||||
"@types/node": "^20.17.30",
|
"@types/node": "^20.17.30",
|
||||||
"@types/telegram-web-app": "^7.10.1",
|
"@types/telegram-web-app": "^7.10.1",
|
||||||
"@vue/devtools": "^7.7.2",
|
"@vue/devtools": "^7.7.6",
|
||||||
"@vue/eslint-config-typescript": "^14.1.3",
|
"@vue/eslint-config-typescript": "^14.1.3",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ export default defineConfig((ctx) => {
|
|||||||
|
|
||||||
alias: {
|
alias: {
|
||||||
'composables': path.resolve(__dirname, './src/composables'),
|
'composables': path.resolve(__dirname, './src/composables'),
|
||||||
'types': path.resolve(__dirname, './src/types')
|
'types': path.resolve(__dirname, './src/types'),
|
||||||
|
'helpers': path.resolve(__dirname, './src/helpers')
|
||||||
},
|
},
|
||||||
|
|
||||||
typescript: {
|
typescript: {
|
||||||
|
|||||||
116
src/App.vue
116
src/App.vue
@@ -3,13 +3,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject, onMounted } from 'vue'
|
import { inject, onMounted, ref } from 'vue'
|
||||||
import { api } from 'boot/axios'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useAuthStore } from 'stores/auth'
|
||||||
import { useSettingsStore } from 'stores/settings'
|
import { useSettingsStore } from 'stores/settings'
|
||||||
|
import { useProjectsStore } from 'stores/projects'
|
||||||
import { useQuasar } from 'quasar'
|
import { useQuasar } from 'quasar'
|
||||||
import type { WebApp } from '@twa-dev/types'
|
import type { WebApp } from '@twa-dev/types'
|
||||||
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const tg = inject('tg') as WebApp
|
const tg = inject('tg') as WebApp
|
||||||
|
|
||||||
@@ -29,94 +31,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseIdString (input: string): { id: number; taskId?: number; meetingId?: number } | null {
|
||||||
|
const pattern = /^p(?<id>\d+)(?:t(?<taskId>\d+))?(?:m(?<meetingId>\d+))?$/
|
||||||
|
const match = input.match(pattern)
|
||||||
|
|
||||||
|
if (!match?.groups?.id) return null
|
||||||
|
|
||||||
|
const id = parseInt(match.groups.id, 10)
|
||||||
|
const taskId = match.groups.taskId ? parseInt(match.groups.taskId, 10) : undefined
|
||||||
|
const meetingId = match.groups.meetingId ? parseInt(match.groups.meetingId, 10) : undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
...(taskId !== undefined && { taskId }),
|
||||||
|
...(meetingId !== undefined && { meetingId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
|
const projectsStore = useProjectsStore()
|
||||||
|
|
||||||
|
const startRouteInfo = ref<{ id: number; taskId?: number; meetingId?: number } | null>(null)
|
||||||
|
if (tg.initDataUnsafe.start_param) {
|
||||||
|
startRouteInfo.value = parseIdString(tg.initDataUnsafe.start_param)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
console.log('app mount')
|
||||||
try {
|
try {
|
||||||
await api.post('/auth?' + tg.initData)
|
if (startRouteInfo.value) projectsStore.setStartRouteInfo(startRouteInfo.value)
|
||||||
await settingsStore.init()
|
if (!authStore.isInit) await authStore.init(tg)
|
||||||
|
if (!settingsStore.isInit) await settingsStore.init()
|
||||||
} catch {
|
} catch {
|
||||||
// await router.push({ name: 'server_error'})
|
// await router.push({ name: 'server_error'})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- <script setup lang="ts">
|
|
||||||
import { inject, onMounted } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import type { WebApp } from '@twa-dev/types'
|
|
||||||
import { useTextSizeStore } from 'stores/textSize'
|
|
||||||
import { useQuasar } from 'quasar'
|
|
||||||
const $q = useQuasar()
|
|
||||||
|
|
||||||
$q.iconMapFn = (iconName) => {
|
|
||||||
if (iconName.startsWith('pn-') === true) {
|
|
||||||
const name = iconName.substring(3)
|
|
||||||
|
|
||||||
return {
|
|
||||||
cls: 'pn-icon ' + name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const tg = inject('tg') as WebApp
|
|
||||||
tg.onEvent('settingsButtonClicked', async () => {
|
|
||||||
await router.push({ name: 'settings' })
|
|
||||||
})
|
|
||||||
|
|
||||||
const textSizeStore = useTextSizeStore()
|
|
||||||
onMounted(async () => {
|
|
||||||
console.log(tg.initDataUnsafe)
|
|
||||||
try {
|
|
||||||
await textSizeStore.initialize()
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error load font size:', err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<router-view/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { inject, onMounted } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import type { WebApp } from '@twa-dev/types'
|
|
||||||
import { useTextSizeStore } from 'stores/textSize'
|
|
||||||
import { useQuasar } from 'quasar'
|
|
||||||
const $q = useQuasar()
|
|
||||||
|
|
||||||
$q.iconMapFn = (iconName) => {
|
|
||||||
if (iconName.startsWith('pn-') === true) {
|
|
||||||
const name = iconName.substring(3)
|
|
||||||
|
|
||||||
return {
|
|
||||||
cls: 'pn-icon ' + name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const tg = inject('tg') as WebApp
|
|
||||||
tg.onEvent('settingsButtonClicked', async () => {
|
|
||||||
await router.push({ name: 'settings' })
|
|
||||||
})
|
|
||||||
|
|
||||||
const textSizeStore = useTextSizeStore()
|
|
||||||
onMounted(async () => {
|
|
||||||
console.log(tg.initDataUnsafe)
|
|
||||||
try {
|
|
||||||
await textSizeStore.initialize()
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error load font size:', err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
-->
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { defineBoot } from '#q-app/wrappers'
|
import { defineBoot } from '#q-app/wrappers'
|
||||||
import axios, { type AxiosInstance } from 'axios'
|
import axios, { type AxiosInstance } from 'axios'
|
||||||
import { useAuthStore } from 'src/stores/auth'
|
|
||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
interface ComponentCustomProperties {
|
interface ComponentCustomProperties {
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import pnPageCard from 'components/pnPageCard.vue'
|
|||||||
import pnScrollList from 'components/pnScrollList.vue'
|
import pnScrollList from 'components/pnScrollList.vue'
|
||||||
import pnAutoAvatar from 'components/pnAutoAvatar.vue'
|
import pnAutoAvatar from 'components/pnAutoAvatar.vue'
|
||||||
import pnOverlay from 'components/pnOverlay.vue'
|
import pnOverlay from 'components/pnOverlay.vue'
|
||||||
import pnDialogBody from 'components/pnDialogBody.vue'
|
import pnSmallDialog from 'components/pnSmallDialog.vue'
|
||||||
import pnImageSelector from 'components/pnImageSelector.vue'
|
import pnImageSelector from 'components/pnImageSelector.vue'
|
||||||
import pnTaskPriorityIcon from 'components/pnTaskPriorityIcon.vue'
|
import pnTaskPriorityIcon from 'components/pnTaskPriorityIcon.vue'
|
||||||
|
import pnChainAvatar from 'components/pnChainAvatar.vue'
|
||||||
|
import pnShadowScroll from 'components/pnShadowScroll.vue'
|
||||||
|
import pnFileUploader from 'components/pnFileUploader.vue'
|
||||||
|
import pnBottomSheetDialog from 'components/pnBottomSheetDialog.vue'
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
pnPageCard,
|
pnPageCard,
|
||||||
@@ -13,8 +17,12 @@ const components = {
|
|||||||
pnAutoAvatar,
|
pnAutoAvatar,
|
||||||
pnOverlay,
|
pnOverlay,
|
||||||
pnImageSelector,
|
pnImageSelector,
|
||||||
pnDialogBody,
|
pnSmallDialog,
|
||||||
pnTaskPriorityIcon
|
pnBottomSheetDialog,
|
||||||
|
pnTaskPriorityIcon,
|
||||||
|
pnChainAvatar,
|
||||||
|
pnShadowScroll,
|
||||||
|
pnFileUploader
|
||||||
}
|
}
|
||||||
|
|
||||||
export default boot(({ app }) => {
|
export default boot(({ app }) => {
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
function isDirty (
|
|
||||||
obj1: Record<string, unknown> | null | undefined,
|
|
||||||
obj2: Record<string, unknown> | null | undefined
|
|
||||||
): boolean {
|
|
||||||
const actualObj1 = obj1 ?? {}
|
|
||||||
const actualObj2 = obj2 ?? {}
|
|
||||||
|
|
||||||
const filteredObj1 = filterIgnored(actualObj1)
|
|
||||||
const filteredObj2 = filterIgnored(actualObj2)
|
|
||||||
|
|
||||||
const allKeys = new Set([...Object.keys(filteredObj1), ...Object.keys(filteredObj2)])
|
|
||||||
|
|
||||||
for (const key of allKeys) {
|
|
||||||
const hasKey1 = Object.hasOwn(filteredObj1, key)
|
|
||||||
const hasKey2 = Object.hasOwn(filteredObj2, key)
|
|
||||||
|
|
||||||
if (hasKey1 !== hasKey2) return false
|
|
||||||
|
|
||||||
if (hasKey1 && hasKey2) {
|
|
||||||
const val1 = filteredObj1[key]
|
|
||||||
const val2 = filteredObj2[key]
|
|
||||||
|
|
||||||
if (typeof val1 === 'string' && typeof val2 === 'string') {
|
|
||||||
if (val1.trim() !== val2.trim()) return false
|
|
||||||
} else if (val1 !== val2) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterIgnored(obj: Record<string, unknown>): Record<string, string | number | boolean> {
|
|
||||||
const filtered: Record<string, string | number | boolean> = {}
|
|
||||||
|
|
||||||
for (const key in obj) {
|
|
||||||
const originalValue = obj[key]
|
|
||||||
|
|
||||||
// Пропускаем значения, которые не string, number или boolean
|
|
||||||
if (
|
|
||||||
typeof originalValue !== 'string' &&
|
|
||||||
typeof originalValue !== 'number' &&
|
|
||||||
typeof originalValue !== 'boolean'
|
|
||||||
) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = originalValue
|
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
value = value.trim()
|
|
||||||
if (value === '') continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === 0 || value === false) continue
|
|
||||||
|
|
||||||
filtered[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseIntString (s: string | string[] | undefined) :number | null {
|
|
||||||
if (typeof s !== 'string') return null
|
|
||||||
const regex = /^[+-]?\d+$/
|
|
||||||
return regex.test(s) ? Number(s) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
isDirty,
|
|
||||||
parseIntString
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { defineBoot } from '#q-app/wrappers'
|
import { defineBoot } from '#q-app/wrappers'
|
||||||
import type { WebApp } from "@twa-dev/types"
|
import type { WebApp } from "@twa-dev/types"
|
||||||
import { useProjectsStore } from 'stores/projects'
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -16,10 +15,6 @@ export default defineBoot(({ app }) => {
|
|||||||
webApp.ready()
|
webApp.ready()
|
||||||
webApp.SettingsButton.isVisible = true
|
webApp.SettingsButton.isVisible = true
|
||||||
app.config.globalProperties.$tg = webApp
|
app.config.globalProperties.$tg = webApp
|
||||||
const projectStore = useProjectsStore()
|
|
||||||
if (Number(webApp.initDataUnsafe.start_param)) {
|
|
||||||
projectStore.setStartProjectId(Number(webApp.initDataUnsafe.start_param))
|
|
||||||
}
|
|
||||||
app.provide('tg', webApp)
|
app.provide('tg', webApp)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,10 +13,11 @@
|
|||||||
@click="goPersonInfo()"
|
@click="goPersonInfo()"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar>
|
<pn-auto-avatar
|
||||||
<img v-if="item.logo" :src="item.logo"/>
|
:img="item.logo"
|
||||||
<pn-auto-avatar v-else :name="item.name"/>
|
:name="item.name"
|
||||||
</q-avatar>
|
size="sm"
|
||||||
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label lines="1" class="text-bold">
|
<q-item-label lines="1" class="text-bold">
|
||||||
|
|||||||
@@ -1,253 +1,307 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex column items-center q-pa-lg">
|
<pn-page-card>
|
||||||
<div class="q-gutter-y-lg w100">
|
<template #title>
|
||||||
<q-input
|
{{ $t(title) }}
|
||||||
v-model.trim="modelValue.name"
|
</template>
|
||||||
dense
|
<template #footer>
|
||||||
filled
|
<q-btn
|
||||||
class = "w100 fix-bottom-padding"
|
rounded color="primary"
|
||||||
:rules="[rules.name]"
|
class="w100 q-mt-md q-mb-xs"
|
||||||
no-error-icon
|
:disable="!(isFormValid && (isDirty(initialMeeting, modelValue) || newFiles.length !== 0))"
|
||||||
label-slot
|
@click = "emit('update', newFiles)"
|
||||||
>
|
>
|
||||||
<template #label>
|
{{ $t(btnText) }}
|
||||||
{{$t('meeting_info__name') }}
|
</q-btn>
|
||||||
<span class="text-red">*</span>
|
</template>
|
||||||
</template>
|
|
||||||
</q-input>
|
<pn-scroll-list>
|
||||||
|
<div class="flex column items-center q-pa-md q-pb-sm">
|
||||||
|
<div class="q-gutter-y-lg w100">
|
||||||
|
<q-select
|
||||||
|
v-model="modelValue.chat_id"
|
||||||
|
:options="displayChats"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
class="w100"
|
||||||
|
:label = "$t('meeting_block__attach_chat')"
|
||||||
|
option-value="id"
|
||||||
|
option-label="name"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="mdi-chat-outline"/>
|
||||||
|
</template>
|
||||||
|
<template #option="scope">
|
||||||
|
<q-item v-bind="scope.itemProps">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar rounded>
|
||||||
|
<template v-if="scope.opt.id">
|
||||||
|
<pn-auto-avatar
|
||||||
|
:img="scope.opt.logo"
|
||||||
|
:name="scope.opt.name"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<q-icon size="32px" color="grey" name="mdi-cancel"/>
|
||||||
|
</template>
|
||||||
|
</q-avatar>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ scope.opt.name }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="modelValue.description"
|
v-model.trim="modelValue.name"
|
||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
autogrow
|
class = "w100 fix-bottom-padding q-pt-sm"
|
||||||
class="w100 q-pt-sm"
|
:rules="[rules.name]"
|
||||||
:label="$t('meeting_info__description')"
|
no-error-icon
|
||||||
/>
|
label-slot
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
{{$t('meeting_block__name') }}
|
||||||
|
<span class="text-red">*</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
<div class="flex no-wrap justify-between q-gutter-x-md q-pt-sm">
|
<q-input
|
||||||
<q-input
|
v-model="modelValue.description"
|
||||||
v-model="meetingDate"
|
dense
|
||||||
dense filled
|
filled
|
||||||
mask="##/##/####"
|
autogrow
|
||||||
:label="$t('meeting_info__date')"
|
class="w100 q-pt-sm"
|
||||||
hide-bottom-space
|
:label="$t('meeting_block__description')"
|
||||||
>
|
/>
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="event" class="cursor-pointer">
|
|
||||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
|
||||||
<q-date
|
|
||||||
v-model="meetingDate"
|
|
||||||
mask="DD/MM/YYYY"
|
|
||||||
class="relative-position"
|
|
||||||
:options="d => d >= date.formatDate(Date.now(), 'YYYY/MM/DD')"
|
|
||||||
:navigation-min-year-month="date.formatDate(Date.now(), 'YYYY/MM')"
|
|
||||||
>
|
|
||||||
<div class="absolute" style="top: 0; right: 0;">
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
round flat
|
|
||||||
color="white"
|
|
||||||
icon="mdi-close"
|
|
||||||
class="q-ma-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-date>
|
|
||||||
</q-popup-proxy>
|
|
||||||
</q-icon>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<q-input
|
<div class="flex no-wrap justify-between q-gutter-x-md q-pt-sm">
|
||||||
v-model="meetingTime"
|
<q-input
|
||||||
dense filled
|
v-model="meetingDate"
|
||||||
mask="time"
|
dense filled
|
||||||
:rules="['time']"
|
mask="##/##/####"
|
||||||
:label="$t('meeting_info__time')"
|
:rules=[rules.date]
|
||||||
hide-bottom-space
|
no-error-icon
|
||||||
>
|
:label="$t('meeting_block__date')"
|
||||||
<template #prepend>
|
hide-bottom-space
|
||||||
<q-icon name="access_time" class="cursor-pointer">
|
>
|
||||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
<template #prepend>
|
||||||
<q-time
|
<q-icon name="event" class="cursor-pointer">
|
||||||
v-model="meetingTime"
|
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||||
mask="HH:mm"
|
<q-date
|
||||||
format24h
|
v-model="meetingDate"
|
||||||
class="relative-position"
|
mask="DD/MM/YYYY"
|
||||||
>
|
class="relative-position"
|
||||||
<div class="absolute" style="top: 0; right: 0;">
|
:options="d => d >= date.formatDate(Date.now(), 'YYYY/MM/DD')"
|
||||||
<q-btn
|
:navigation-min-year-month="date.formatDate(Date.now(), 'YYYY/MM')"
|
||||||
v-close-popup
|
>
|
||||||
round flat
|
<div class="absolute" style="top: 0; right: 0;">
|
||||||
color="white"
|
<q-btn
|
||||||
icon="mdi-close"
|
v-close-popup
|
||||||
class="q-ma-sm"
|
round flat
|
||||||
/>
|
color="white"
|
||||||
</div>
|
icon="mdi-close"
|
||||||
</q-time>
|
class="q-ma-sm"
|
||||||
</q-popup-proxy>
|
/>
|
||||||
</q-icon>
|
</div>
|
||||||
</template>
|
</q-date>
|
||||||
</q-input>
|
</q-popup-proxy>
|
||||||
|
</q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model="meetingTime"
|
||||||
|
dense filled
|
||||||
|
mask="time"
|
||||||
|
:rules="[rules.time]"
|
||||||
|
no-error-icon
|
||||||
|
:label="$t('meeting_block__time')"
|
||||||
|
hide-bottom-space
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="access_time" class="cursor-pointer">
|
||||||
|
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||||
|
<q-time
|
||||||
|
v-model="meetingTime"
|
||||||
|
mask="HH:mm"
|
||||||
|
format24h
|
||||||
|
class="relative-position"
|
||||||
|
>
|
||||||
|
<div class="absolute" style="top: 0; right: 0;">
|
||||||
|
<q-btn
|
||||||
|
v-close-popup
|
||||||
|
round flat
|
||||||
|
color="white"
|
||||||
|
icon="mdi-close"
|
||||||
|
class="q-ma-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-time>
|
||||||
|
</q-popup-proxy>
|
||||||
|
</q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model.trim="modelValue.place"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
class = "w100 q-pt-sm"
|
||||||
|
no-error-icon
|
||||||
|
:label="$t('meeting_block__place')"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="mdi-map-marker-outline"/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
v-model="modelValue.participants"
|
||||||
|
:options="displayUsers"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
class="w100 file-input-fix q-pt-sm"
|
||||||
|
:label = "$t('meeting_block__participants')"
|
||||||
|
option-value="id"
|
||||||
|
option-label="displayName"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
use-chips
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="mdi-account-outline"/>
|
||||||
|
</template>
|
||||||
|
<template #option="scope">
|
||||||
|
<q-item v-bind="scope.itemProps">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<pn-auto-avatar
|
||||||
|
:img="scope.opt.photo"
|
||||||
|
:name="scope.opt.displayName"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ scope.opt.displayName }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
<pn-file-uploader
|
||||||
|
v-model:exist-files ="modelValue.files"
|
||||||
|
v-model:new-files ="newFiles"
|
||||||
|
:existFileData="files"
|
||||||
|
:label="$t('meeting_block__attach_files')"
|
||||||
|
class="q-pt-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</pn-scroll-list>
|
||||||
|
</pn-page-card>
|
||||||
|
|
||||||
<q-select
|
|
||||||
v-model="modelValue.chat_attach"
|
|
||||||
:options="chats"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class="w100 q-pt-sm"
|
|
||||||
:label = "$t('meeting_info__attach_chat')"
|
|
||||||
option-value="id"
|
|
||||||
option-label="name"
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
label-slot
|
|
||||||
:disable="chats.length<=1"
|
|
||||||
:placeholder="chats.length<=1 ? undefined : t('meeting_info__choose_chat_placeholder')"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-chat-outline"/>
|
|
||||||
</template>
|
|
||||||
<template #label>
|
|
||||||
{{$t('meeting_info__attach_chat') }}
|
|
||||||
<span class="text-red" v-if="chats.length>1">*</span>
|
|
||||||
</template>
|
|
||||||
<template #option="scope">
|
|
||||||
<q-item v-bind="scope.itemProps">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar rounded size="md">
|
|
||||||
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
|
|
||||||
<pn-auto-avatar v-else :name="scope.opt.name"/>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>{{ scope.opt.name }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
|
|
||||||
<q-select
|
|
||||||
v-model="modelValue.participants"
|
|
||||||
:options="displayUsers"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class="w100 file-input-fix q-pt-sm"
|
|
||||||
:label = "$t('meeting_info__participants')"
|
|
||||||
option-value="id"
|
|
||||||
option-label="displayName"
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
use-chips
|
|
||||||
multiple
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-account-outline"/>
|
|
||||||
</template>
|
|
||||||
<template #option="scope">
|
|
||||||
<q-item v-bind="scope.itemProps">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar round size="md">
|
|
||||||
<img v-if="scope.opt.photo" :src="scope.opt.photo"/>
|
|
||||||
<pn-auto-avatar v-else :name="scope.opt.name"/>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>{{ scope.opt.displayName }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
|
|
||||||
<q-file
|
|
||||||
v-model="modelValue.files"
|
|
||||||
:label="$t('meeting_info__attach_files')"
|
|
||||||
outlined
|
|
||||||
use-chips
|
|
||||||
multiple
|
|
||||||
dense
|
|
||||||
class="file-input-fix q-pt-sm"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="attach_file"/>
|
|
||||||
</template>
|
|
||||||
</q-file>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, watch, computed } from 'vue'
|
import { onMounted, computed, ref } from 'vue'
|
||||||
import type { MeetingParams } from 'types/Meeting'
|
import type { MeetingParams } from 'types/Meeting'
|
||||||
import { useChatsStore } from 'stores/chats'
|
import { useChatsStore } from 'stores/chats'
|
||||||
import { useUsersStore } from 'stores/users'
|
import { useUsersStore } from 'stores/users'
|
||||||
|
import { useFilesStore } from 'stores/files'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { isDirty } from 'helpers/helpers'
|
||||||
import { date } from 'quasar'
|
import { date } from 'quasar'
|
||||||
const { t }= useI18n()
|
const { t } = useI18n()
|
||||||
|
const filesStore = useFilesStore()
|
||||||
|
const files = filesStore.getFiles
|
||||||
|
const newFiles=ref<File[]>([])
|
||||||
|
|
||||||
const modelValue = defineModel<MeetingParams>({
|
const modelValue = defineModel<MeetingParams>({
|
||||||
required: true
|
required: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['valid'])
|
defineProps<{
|
||||||
const rulesErrorMessage = {
|
title: string,
|
||||||
name: t('meeting_info__error_name'),
|
btnText: string
|
||||||
dateMeeting: t('meeting_info__error_date'),
|
}>()
|
||||||
timeMeeting: t('meeting_info__error_time')
|
|
||||||
}
|
const emit = defineEmits(['update'])
|
||||||
|
|
||||||
const chatsStore = useChatsStore()
|
const chatsStore = useChatsStore()
|
||||||
const chats = computed(() => chatsStore.chats)
|
const chats = chatsStore.getChats
|
||||||
|
const displayChats = computed(() => [
|
||||||
|
...chats.map(el => ({
|
||||||
|
id: el.id,
|
||||||
|
name: el.name,
|
||||||
|
logo: el.logo
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
id: null,
|
||||||
|
name: t('meeting_block__no_chat'),
|
||||||
|
logo: ''
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
const usersStore = useUsersStore()
|
const usersStore = useUsersStore()
|
||||||
const users = computed(() => usersStore.users)
|
const users = usersStore.getUsers
|
||||||
|
|
||||||
const displayUsers = computed(() => {
|
const displayUsers = computed(() => {
|
||||||
return users.value
|
return users
|
||||||
.map(el => ({ ...el, displayName: usersStore.userNameById(el.id) }))
|
.map(el => ({ ...el, displayName: usersStore.userNameById(el.id) }))
|
||||||
})
|
})
|
||||||
|
|
||||||
const meetingDate = computed({
|
const meetingDate = computed({
|
||||||
get: () => date.formatDate(modelValue.value.meet_date, 'DD/MM/YYYY'),
|
get: () => date.formatDate(modelValue.value.meet_date * 1000, 'DD/MM/YYYY'),
|
||||||
set: (d) => updateDateTime(d, meetingTime.value)
|
set: (d) => updateDateTime(d, meetingTime.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const meetingTime = computed({
|
const meetingTime = computed({
|
||||||
get: () => date.formatDate(modelValue.value.meet_date, 'HH:mm'),
|
get: () => date.formatDate(modelValue.value.meet_date * 1000, 'HH:mm'),
|
||||||
set: (t) => updateDateTime(meetingDate.value, t)
|
set: (t) => updateDateTime(meetingDate.value, t)
|
||||||
})
|
})
|
||||||
|
|
||||||
function updateDateTime(dateStr: string, timeStr: string) {
|
function updateDateTime (dateStr: string, timeStr: string) {
|
||||||
if (dateStr.length === 10 && timeStr.length === 5) {
|
if (dateStr.length === 10 && timeStr.length === 5) {
|
||||||
const newDate = date.extractDate(`${dateStr} ${timeStr}`, 'DD/MM/YYYY HH:mm')
|
const newDate = date.extractDate(`${dateStr} ${timeStr}`, 'DD/MM/YYYY HH:mm')
|
||||||
if (!isNaN(newDate.getTime())) {
|
if (!isNaN(newDate.getTime())) {
|
||||||
modelValue.value.meet_date = newDate.getTime()
|
modelValue.value.meet_date = newDate.getTime() / 1000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rules = {
|
const rulesErrorMessage = {
|
||||||
name: (val: MeetingParams['name']) => !!val?.trim() || rulesErrorMessage['name']
|
name: t('meeting_block__error_name'),
|
||||||
|
date: t('meeting_block__error_date'),
|
||||||
|
time: t('meeting_block__error_time')
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValid = computed(() => {
|
const rules = {
|
||||||
const checkName = rules.name(modelValue.value.name)
|
name: (val: MeetingParams['name']) => !!val?.trim() || rulesErrorMessage['name'],
|
||||||
return { name: checkName && (checkName !== rulesErrorMessage['name']) }
|
date: () => (!!modelValue.value.meet_date && modelValue.value.meet_date > Date.now() / 1000) || rulesErrorMessage['date'],
|
||||||
|
time: () => (!!modelValue.value.meet_date && modelValue.value.meet_date > Date.now() / 1000) || rulesErrorMessage['time']
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFormValid = computed(() => {
|
||||||
|
const validations = {
|
||||||
|
name: rules.name(modelValue.value.name) === true,
|
||||||
|
date: rules.date() === true,
|
||||||
|
time: rules.time() === true
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(validations).every(Boolean)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(isValid, (newVal) => {
|
|
||||||
const allValid = Object.values(newVal).every(v => v)
|
const initialMeeting = ref({} as MeetingParams)
|
||||||
emit('valid', allValid)
|
|
||||||
}, { immediate: true})
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (chats.value.length === 1 && !modelValue.value.chat_attach) {
|
initialMeeting.value = { ...modelValue.value }
|
||||||
modelValue.value.chat_attach = chats.value[0]?.id ?? null
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,4 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<q-avatar
|
||||||
|
:square="type==='square'"
|
||||||
|
:rounded="type==='rounded'"
|
||||||
|
:size="size"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="img"
|
||||||
|
:src="img"
|
||||||
|
style=" object-fit: cover;"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:style="{ backgroundColor: stringToColour(name) } "
|
||||||
|
class="fit flex items-center justify-center text-white"
|
||||||
|
>
|
||||||
|
{{ name ? name.substring(0, 1) : '-' }}
|
||||||
|
</div>
|
||||||
|
</q-avatar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
img?: string | null
|
||||||
|
name: string,
|
||||||
|
size?: string,
|
||||||
|
type?: 'rounded' | 'square' | ''
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
img: null,
|
||||||
|
name: '-',
|
||||||
|
size: 'md',
|
||||||
|
type: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const stringToColour = (str: string) => {
|
||||||
|
if (!str) return '#eee'
|
||||||
|
let hash = 0
|
||||||
|
str.split('').forEach(char => {
|
||||||
|
hash = char.charCodeAt(0) + ((hash << 5) - hash)
|
||||||
|
})
|
||||||
|
let colour = '#'
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const value = (hash >> (i * 8)) & 0xff
|
||||||
|
colour += value.toString(16).padStart(2, '0')
|
||||||
|
}
|
||||||
|
return colour
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <template>
|
||||||
<div
|
<div
|
||||||
:style="{ backgroundColor: stringToColour(props.name) } "
|
:style="{ backgroundColor: stringToColour(props.name) } "
|
||||||
class="fit flex items-center justify-center text-white"
|
class="fit flex items-center justify-center text-white"
|
||||||
@@ -30,4 +89,4 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
</style>
|
</style> -->
|
||||||
|
|||||||
97
src/components/pnBottomSheetDialog.vue
Normal file
97
src/components/pnBottomSheetDialog.vue
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<q-dialog
|
||||||
|
v-model="modelValue"
|
||||||
|
maximized
|
||||||
|
transition-show="slide-up"
|
||||||
|
transition-hide="slide-down"
|
||||||
|
position="bottom"
|
||||||
|
>
|
||||||
|
<q-card
|
||||||
|
class="fix-card-width flex column no-scroll no-wrap q-px-none"
|
||||||
|
style="
|
||||||
|
border-top-left-radius: var(--top-raduis) !important;
|
||||||
|
border-top-right-radius: var(--top-raduis) !important;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="cardHeaderRef"
|
||||||
|
class="flex items-center no-wrap justify-between w100 q-my-none q-pa-md"
|
||||||
|
>
|
||||||
|
<div class="text-h6 q-mx-xs ellipsis">{{ $t(title) }}</div>
|
||||||
|
<div class="flex items-center justify-between no-wrap">
|
||||||
|
<slot name="btnSlot">
|
||||||
|
<q-btn
|
||||||
|
icon="mdi-close"
|
||||||
|
@click="modelValue=false"
|
||||||
|
flat round
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref="cardBodyRef"
|
||||||
|
class="q-px-none q-ma-none"
|
||||||
|
>
|
||||||
|
<pn-shadow-scroll
|
||||||
|
:hideShadows="false"
|
||||||
|
:height="bodyHeight"
|
||||||
|
>
|
||||||
|
<div ref="cardBodyInnerRef" class="q-px-md q-ma-none">
|
||||||
|
<q-resize-observer @resize="updateDimensions" />
|
||||||
|
<slot/>
|
||||||
|
</div>
|
||||||
|
</pn-shadow-scroll>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref="cardFooterRef"
|
||||||
|
class="q-pa-md"
|
||||||
|
>
|
||||||
|
<slot name="footer"/>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useSlots } from 'vue'
|
||||||
|
|
||||||
|
const modelValue = defineModel<boolean>({
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
title: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const slots = useSlots()
|
||||||
|
const cardHeaderRef = ref<HTMLElement | null>(null)
|
||||||
|
const cardFooterRef = ref<HTMLElement | null>(null)
|
||||||
|
const cardBodyRef = ref<HTMLElement | null>(null)
|
||||||
|
const cardBodyInnerRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const headerHeight = ref(0)
|
||||||
|
const footerHeight = ref(0)
|
||||||
|
const bodyInnerHeight = ref(0)
|
||||||
|
const bodyHeight = ref(0)
|
||||||
|
|
||||||
|
const updateDimensions = () => {
|
||||||
|
headerHeight.value = cardHeaderRef.value?.offsetHeight || 0
|
||||||
|
footerHeight.value = cardFooterRef.value?.offsetHeight || 0
|
||||||
|
bodyInnerHeight.value = cardBodyInnerRef.value?.offsetHeight || 0
|
||||||
|
const needScroll = headerHeight.value + footerHeight.value + bodyInnerHeight.value > window.innerHeight - 48
|
||||||
|
bodyHeight.value = needScroll
|
||||||
|
? window.innerHeight - 48 - headerHeight.value - footerHeight.value
|
||||||
|
: bodyInnerHeight.value + 16
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => slots.body?.(), updateDimensions, { flush: 'post' })
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fix-card-width {
|
||||||
|
width: var(--body-width) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
56
src/components/pnChainAvatar.vue
Normal file
56
src/components/pnChainAvatar.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center no-wrap">
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in displayUsers"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<pn-auto-avatar
|
||||||
|
:img="usersStore.userById(item)?.photo"
|
||||||
|
:name="usersStore.userNameById(item)"
|
||||||
|
size="sm"
|
||||||
|
:style="{ marginLeft: idx !== 0 ? -overlap+'px' : 0 +'px' }"
|
||||||
|
class="avatar-overlap"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-avatar
|
||||||
|
v-if="!checkUsersLength"
|
||||||
|
round
|
||||||
|
style="background-color: #eee; color: var(--q-primary);"
|
||||||
|
:style="{ marginLeft: -overlap + 'px'}"
|
||||||
|
class="avatar-overlap"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
+{{ users.length - displayUsers.length < 100 ? users.length - displayUsers.length : '' }}
|
||||||
|
</q-avatar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useUsersStore } from 'stores/users'
|
||||||
|
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
users: number[]
|
||||||
|
overlap: number
|
||||||
|
maxDisplayUsers: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const checkUsersLength = computed(() => props.users.length <= props.maxDisplayUsers)
|
||||||
|
|
||||||
|
const displayUsers = computed(()=>{
|
||||||
|
return checkUsersLength.value
|
||||||
|
? props.users
|
||||||
|
: [...props.users].slice(0, props.maxDisplayUsers)
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.avatar-overlap {
|
||||||
|
border: 2px solid white;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<template>
|
|
||||||
<q-card class="q-pa-none q-ma-none w100" align="center">
|
|
||||||
<q-card-section>
|
|
||||||
<q-avatar :color :icon size="60px" font-size="45px" text-color="white"/>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section
|
|
||||||
class="text-h6 text-bold q-pt-none wrap no-scroll"
|
|
||||||
style="overflow-wrap: break-word"
|
|
||||||
>
|
|
||||||
{{ $t(title)}}
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section v-if="message1">
|
|
||||||
{{ $t(message1)}}
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section v-if="message2">
|
|
||||||
{{ $t(message2)}}
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-actions align="center" vertical>
|
|
||||||
<slot name="actions"/>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{
|
|
||||||
icon?: string
|
|
||||||
color?: string
|
|
||||||
title: string
|
|
||||||
message1?: string
|
|
||||||
message2?: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
158
src/components/pnFileUploader.vue
Normal file
158
src/components/pnFileUploader.vue
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<template>
|
||||||
|
<q-file
|
||||||
|
ref="imgFileSelector"
|
||||||
|
v-model="selectFiles"
|
||||||
|
:style="{ display: 'none' }"
|
||||||
|
@update:model-value="addFiles"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-field
|
||||||
|
filled
|
||||||
|
:stack-label="newFiles?.length!==0 || existFiles.length!==0"
|
||||||
|
:label="label"
|
||||||
|
readonly
|
||||||
|
dense
|
||||||
|
:bottom-slots="isAnroid"
|
||||||
|
|
||||||
|
class="fix-border-bottom fix-icon-position fix-bottom-padding q-pt-sm"
|
||||||
|
>
|
||||||
|
<template #hint>
|
||||||
|
<span class="text-red">{{ $t('file_upload__comment') }} </span>
|
||||||
|
</template>
|
||||||
|
<template #before>
|
||||||
|
<q-btn
|
||||||
|
@click = "imgFileSelectorClick"
|
||||||
|
dense flat
|
||||||
|
icon="mdi-paperclip-plus"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #control>
|
||||||
|
<q-chip
|
||||||
|
v-for="file in existingFiles"
|
||||||
|
:key="file.id"
|
||||||
|
square
|
||||||
|
removable
|
||||||
|
size="sm"
|
||||||
|
@remove="removeExistingFile(file.id)"
|
||||||
|
class="full-width q-mt-xs"
|
||||||
|
>
|
||||||
|
<q-avatar>
|
||||||
|
<q-icon
|
||||||
|
:name="getFileIcon(file.id).icon"
|
||||||
|
:style="{ color: getFileIcon(file.id).color }"
|
||||||
|
/>
|
||||||
|
</q-avatar>
|
||||||
|
<div class="ellipsis relative-position">
|
||||||
|
{{ file.fullname }}
|
||||||
|
</div>
|
||||||
|
</q-chip>
|
||||||
|
|
||||||
|
<q-chip
|
||||||
|
v-for="(file, idx) in newFiles"
|
||||||
|
:key="file.name"
|
||||||
|
square
|
||||||
|
color="white"
|
||||||
|
removable
|
||||||
|
size="sm"
|
||||||
|
@remove="removeNewFile(idx)"
|
||||||
|
class="full-width q-mt-xs"
|
||||||
|
>
|
||||||
|
<q-avatar>
|
||||||
|
<q-icon
|
||||||
|
:name="fileIcon(file.name).icon"
|
||||||
|
:style="{ color: fileIcon(file.name).color }"
|
||||||
|
/>
|
||||||
|
</q-avatar>
|
||||||
|
<div class="ellipsis relative-position">
|
||||||
|
{{ file.name }}
|
||||||
|
</div>
|
||||||
|
</q-chip>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</q-field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, inject, ref, Ref } from 'vue'
|
||||||
|
import { useFilesStore } from 'stores/files'
|
||||||
|
import { fileIcon } from 'helpers/files-functions'
|
||||||
|
import type { FileLink } from 'types/FileLink'
|
||||||
|
import type { QFile } from 'quasar'
|
||||||
|
import type { WebApp } from '@twa-dev/types'
|
||||||
|
|
||||||
|
const tg = inject('tg') as WebApp
|
||||||
|
const filesStore = useFilesStore()
|
||||||
|
|
||||||
|
const isAnroid = computed(() => tg.platform === 'android_x' || tg.platform === 'android')
|
||||||
|
|
||||||
|
|
||||||
|
const existFiles = defineModel<number[]>('existFiles', {
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const newFiles = defineModel<File[]>('newFiles', {
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
existFileData: FileLink[]
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
existFileData: () => [],
|
||||||
|
label: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const existingFiles = computed<FileLink[]>(() => {
|
||||||
|
return existFiles.value
|
||||||
|
.map(id => props.existFileData.find(file => file.id === id))
|
||||||
|
.filter((file): file is FileLink => file !== undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
const removeExistingFile = (id: number) => {
|
||||||
|
const idx = existFiles.value.findIndex(el => el === id)
|
||||||
|
existFiles.value.splice(idx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeNewFile(index: number) {
|
||||||
|
newFiles.value && newFiles.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileIcon (id: number) {
|
||||||
|
const file = filesStore.fileById(id)
|
||||||
|
return file
|
||||||
|
? fileIcon(file.filename)
|
||||||
|
: { color: '', icon: ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imgFileSelector= ref() as Ref<QFile>
|
||||||
|
const selectFiles = ref<File[] | null>(null)
|
||||||
|
|
||||||
|
function imgFileSelectorClick () {
|
||||||
|
imgFileSelector.value.pickFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFiles (file: File[] | null) {
|
||||||
|
if (file && file.length !== 0) {
|
||||||
|
newFiles.value?.push(...file)
|
||||||
|
selectFiles.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fix-border-bottom :deep(.q-field__control:before) {
|
||||||
|
border-bottom-style: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fix-icon-position :deep(.q-field__marginal) {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fix-bottom-padding.q-field--with-bottom {
|
||||||
|
padding-bottom: 8px !important
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,15 +6,13 @@
|
|||||||
<slot name="title"/>
|
<slot name="title"/>
|
||||||
</div>
|
</div>
|
||||||
<slot/>
|
<slot/>
|
||||||
<slot name="footer"/>
|
<div class="bg-white w100 q-ma-none q-px-md">
|
||||||
|
<slot name="footer"/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.glass-card {
|
|
||||||
opacity: 1 !important;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,62 +5,32 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id="card-body-header"
|
id="card-body-header"
|
||||||
style="flex-shrink: 0"
|
style="flex-shrink: 0; min-height: var(--top-raduis);"
|
||||||
>
|
>
|
||||||
<q-resize-observer @resize="onHeaderResize"/>
|
<q-resize-observer @resize="onHeaderResize"/>
|
||||||
<slot name="card-body-header"/>
|
<slot name="card-body-header"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="card-body" >
|
||||||
<div id="card-body">
|
|
||||||
<q-resize-observer @resize="onBodyResize"/>
|
<q-resize-observer @resize="onBodyResize"/>
|
||||||
<q-scroll-area
|
<pn-shadow-scroll :hideShadows="isResizing" :height="scrollAreaHeight">
|
||||||
v-if="!isResizing"
|
|
||||||
:style="{ height: scrollAreaHeight + 'px' }"
|
|
||||||
class="w100 q-pa-none q-ma-none"
|
|
||||||
@scroll="onScroll"
|
|
||||||
:class=" {
|
|
||||||
'shadow-top': hasScrolled,
|
|
||||||
'shadow-bottom': hasScrolledBottom
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<slot/>
|
<slot/>
|
||||||
<div class="q-pa-sm"/>
|
</pn-shadow-scroll>
|
||||||
</q-scroll-area>
|
|
||||||
<q-scroll-area
|
|
||||||
v-if="isResizing"
|
|
||||||
:style="{ height: scrollAreaHeight + 'px' }"
|
|
||||||
class="w100 q-pa-none q-ma-none"
|
|
||||||
@scroll="onScroll"
|
|
||||||
>
|
|
||||||
<slot/>
|
|
||||||
<div class="q-pa-sm"/>
|
|
||||||
</q-scroll-area>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, nextTick } from 'vue'
|
import { ref, watch, nextTick } from 'vue'
|
||||||
import type { QScrollArea } from 'quasar'
|
|
||||||
|
|
||||||
const heightCard = ref(100)
|
const heightCard = ref(100)
|
||||||
const scrollAreaHeight = ref(100)
|
const scrollAreaHeight = ref(100)
|
||||||
const headerHeight = ref(0)
|
const headerHeight = ref(0)
|
||||||
const hasScrolled = ref(false)
|
|
||||||
const hasScrolledBottom = ref(false)
|
|
||||||
|
|
||||||
interface sizeParams {
|
interface sizeParams {
|
||||||
height: number,
|
height: number,
|
||||||
width: number
|
width: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ScrollInfo {
|
|
||||||
verticalPosition: number;
|
|
||||||
verticalPercentage: number;
|
|
||||||
verticalSize: number;
|
|
||||||
verticalContainerSize: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onHeaderResize(size: sizeParams) {
|
async function onHeaderResize(size: sizeParams) {
|
||||||
headerHeight.value = size.height
|
headerHeight.value = size.height
|
||||||
await updateScrollAreaHeight()
|
await updateScrollAreaHeight()
|
||||||
@@ -77,12 +47,6 @@ async function updateScrollAreaHeight() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function onScroll(info: ScrollInfo) {
|
|
||||||
hasScrolled.value = info.verticalPosition > 0
|
|
||||||
const scrollEnd = info.verticalPosition + info.verticalContainerSize >= info.verticalSize - 1
|
|
||||||
hasScrolledBottom.value = !scrollEnd
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(heightCard, updateScrollAreaHeight)
|
watch(heightCard, updateScrollAreaHeight)
|
||||||
watch(headerHeight, updateScrollAreaHeight)
|
watch(headerHeight, updateScrollAreaHeight)
|
||||||
|
|
||||||
@@ -100,12 +64,17 @@ watch(heightCard, () => {
|
|||||||
resizeTimer = setTimeout(() => {
|
resizeTimer = setTimeout(() => {
|
||||||
isResizing.value = false
|
isResizing.value = false
|
||||||
resizeTimer = null
|
resizeTimer = null
|
||||||
}, 500)
|
}, 150)
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.glass-card {
|
||||||
|
opacity: 1 !important;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
#page-card {
|
#page-card {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -119,62 +88,7 @@ watch(heightCard, () => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#card-body :deep(.q-scrollarea__content ) {
|
#card-body :deep(.q-scrollarea__content) {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.q-scrollarea {
|
|
||||||
position: relative;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-scrollarea::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 8px;
|
|
||||||
background: linear-gradient(to bottom,
|
|
||||||
rgba(0,0,0,0.12) 0%,
|
|
||||||
rgba(0,0,0,0.08) 50%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
pointer-events: none;
|
|
||||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-8px);
|
|
||||||
will-change: opacity, transform;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-scrollarea::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 8px;
|
|
||||||
background: linear-gradient(to top,
|
|
||||||
rgba(0,0,0,0.12) 0%,
|
|
||||||
rgba(0,0,0,0.08) 50%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
pointer-events: none;
|
|
||||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(8px);
|
|
||||||
will-change: opacity, transform;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-scrollarea.shadow-top::before {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-scrollarea.shadow-bottom::after {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
107
src/components/pnShadowScroll.vue
Normal file
107
src/components/pnShadowScroll.vue
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="{'fix-scroll-area-content': hideShadows }">
|
||||||
|
<q-scroll-area
|
||||||
|
:style="{ height: height + 'px' }"
|
||||||
|
class="w100 q-pa-none q-ma-none"
|
||||||
|
@scroll="onScroll"
|
||||||
|
:class=" {
|
||||||
|
'shadow-top': hasScrolled,
|
||||||
|
'shadow-bottom': hasScrolledBottom
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot/>
|
||||||
|
<div class="q-pa-sm"/>
|
||||||
|
</q-scroll-area>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
height: number
|
||||||
|
hideShadows: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const hasScrolled = ref(false)
|
||||||
|
const hasScrolledBottom = ref(false)
|
||||||
|
|
||||||
|
interface ScrollInfo {
|
||||||
|
verticalPosition: number;
|
||||||
|
verticalPercentage: number;
|
||||||
|
verticalSize: number;
|
||||||
|
verticalContainerSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScroll(info: ScrollInfo) {
|
||||||
|
hasScrolled.value = info.verticalPosition > 0
|
||||||
|
const scrollEnd = info.verticalPosition + info.verticalContainerSize >= info.verticalSize - 1
|
||||||
|
hasScrolledBottom.value = !scrollEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.q-scrollarea {
|
||||||
|
position: relative;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-scrollarea::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
rgba(0,0,0,0.12) 0%,
|
||||||
|
rgba(0,0,0,0.08) 50%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
will-change: opacity, transform;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-scrollarea::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(to top,
|
||||||
|
rgba(0,0,0,0.12) 0%,
|
||||||
|
rgba(0,0,0,0.08) 50%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
will-change: opacity, transform;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fix-scroll-area-content:deep(.q-scrollarea::before) {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fix-scroll-area-content:deep(.q-scrollarea::after) {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-scrollarea.shadow-top::before {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-scrollarea.shadow-bottom::after {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
85
src/components/pnSmallDialog.vue
Normal file
85
src/components/pnSmallDialog.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<q-dialog
|
||||||
|
v-model="modelValue"
|
||||||
|
>
|
||||||
|
<q-card class="q-pa-none q-ma-none w100 no-scroll" align="center">
|
||||||
|
<q-card-section>
|
||||||
|
<q-avatar :color :icon size="60px" font-size="45px" text-color="white"/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section
|
||||||
|
class="text-h6 text-bold q-pt-none wrap no-scroll"
|
||||||
|
style="overflow-wrap: break-word"
|
||||||
|
>
|
||||||
|
{{ $t(title)}}
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section v-if="message1">
|
||||||
|
{{ $t(message1)}}
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section v-if="message2">
|
||||||
|
{{ $t(message2)}}
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="center" vertical>
|
||||||
|
<div class="flex no-wrap w100 justify-center q-gutter-x-md">
|
||||||
|
<q-btn
|
||||||
|
v-if="auxBtnLabel"
|
||||||
|
:label="$t(auxBtnLabel)"
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
v-close-popup
|
||||||
|
rounded
|
||||||
|
class="w50"
|
||||||
|
@click="emit('clickAuxBtn')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
:label="$t(mainBtnLabel)"
|
||||||
|
:color="color"
|
||||||
|
v-close-popup
|
||||||
|
rounded
|
||||||
|
:class="auxBtnLabel ? 'w50' : 'w80'"
|
||||||
|
@click="emit('clickMainBtn')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
class="w80 q-mt-md q-mb-sm" flat
|
||||||
|
v-close-popup rounded
|
||||||
|
no-caps
|
||||||
|
@click="emit('close')"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
{{$t('close')}}
|
||||||
|
<q-icon name="close"/>
|
||||||
|
</div>
|
||||||
|
</q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
icon?: string
|
||||||
|
color?: string
|
||||||
|
title: string
|
||||||
|
message1?: string
|
||||||
|
message2?: string
|
||||||
|
mainBtnLabel: string
|
||||||
|
auxBtnLabel?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const modelValue = defineModel<boolean>({
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'clickMainBtn',
|
||||||
|
'clickAuxBtn',
|
||||||
|
'close'
|
||||||
|
])
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
@@ -1,22 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-icon
|
<span
|
||||||
v-if="priority === 'important' || priority === 1"
|
v-if="taskPriority"
|
||||||
name="mdi-alert-circle-outline"
|
:class="'text-' + taskPriority.color"
|
||||||
color="warning"
|
style="display: inline-flex; align-items: center;"
|
||||||
class="q-px-xs"
|
>
|
||||||
/>
|
<q-icon
|
||||||
<q-icon
|
name="mdi-flag-triangle"
|
||||||
v-if="priority === 'critical' || priority === 2"
|
class="q-px-xs"
|
||||||
name="mdi-fire"
|
/>
|
||||||
color="negative"
|
<span v-if="label" >
|
||||||
class="q-px-xs"
|
{{ $t(taskPriority.label)}}
|
||||||
/>
|
</span>
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
priority?: 'normal' | 0 | 'important' | 1 | 'critical' | 2
|
priority?: 'normal' | 0 | 'important' | 1 | 'critical' | 2
|
||||||
}>()
|
label?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
label: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const taskPriority = computed(() => {
|
||||||
|
switch (props.priority) {
|
||||||
|
case 1:
|
||||||
|
case 'important':
|
||||||
|
return { label: 'task_priority_important', color: 'amber' }
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
case 'critical':
|
||||||
|
return { label: 'task_priority_critical', color: 'red' }
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex column items-center q-pa-lg">
|
|
||||||
<pn-image-selector
|
|
||||||
v-model="modelValue.logo"
|
|
||||||
:size="100"
|
|
||||||
:iconsize="80"
|
|
||||||
class="q-pb-lg"
|
|
||||||
/>
|
|
||||||
<div class="q-gutter-y-lg w100">
|
|
||||||
<q-input
|
|
||||||
v-model="modelValue.name"
|
|
||||||
no-error-icon
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class = "w100 fix-bottom-padding"
|
|
||||||
:label="$t('project_card__project_name')"
|
|
||||||
:rules="[rules.name]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-input
|
|
||||||
v-model="modelValue.description"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
autogrow
|
|
||||||
class="w100"
|
|
||||||
:label="$t('project_card__project_description')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-checkbox
|
|
||||||
v-if="modelValue.logo"
|
|
||||||
v-model="modelValue.logo_as_bg"
|
|
||||||
class="w100"
|
|
||||||
dense
|
|
||||||
>
|
|
||||||
{{ $t('project_card__image_use_as_background_chats') }}
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { watch, computed } from 'vue'
|
|
||||||
import type { ProjectParams } from 'src/types'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
const { t }= useI18n()
|
|
||||||
|
|
||||||
const modelValue = defineModel<ProjectParams>({ required: true })
|
|
||||||
const emit = defineEmits(['valid'])
|
|
||||||
const rulesErrorMessage = {
|
|
||||||
name: t('project_card__error_name')
|
|
||||||
}
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
name: (val :ProjectParams['name']) => !!val?.trim() || rulesErrorMessage['name']
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = computed(() => {
|
|
||||||
const checkName = rules.name(modelValue.value.name)
|
|
||||||
return { name: checkName && (checkName !== rulesErrorMessage['name']) }
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(isValid, (newVal) => {
|
|
||||||
const allValid = Object.values(newVal).every(v => v)
|
|
||||||
emit('valid', allValid)
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.q-field--with-bottom.fix-bottom-padding {
|
|
||||||
padding-bottom: 0 !important
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
393
src/components/taskBlock.vue
Normal file
393
src/components/taskBlock.vue
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
<template>
|
||||||
|
<pn-page-card>
|
||||||
|
<template #title>
|
||||||
|
{{ $t(title) }}
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<q-btn
|
||||||
|
rounded color="primary"
|
||||||
|
class="w100 q-mt-md q-mb-xs"
|
||||||
|
@click = "emit('update', newFiles)"
|
||||||
|
>
|
||||||
|
{{ $t(btnText) }}
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
<pn-scroll-list>
|
||||||
|
<div class="flex column items-center q-pa-md q-pb-sm">
|
||||||
|
<div class="q-gutter-y-lg w100">
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
v-model="modelValue.chat_id"
|
||||||
|
:options="displayChats"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
class="w100"
|
||||||
|
:label = "$t('task_block__attach_chat')"
|
||||||
|
option-value="id"
|
||||||
|
option-label="name"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="mdi-chat-outline"/>
|
||||||
|
</template>
|
||||||
|
<template #option="scope">
|
||||||
|
<q-item v-bind="scope.itemProps">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar rounded size="md">
|
||||||
|
<template v-if="scope.opt.id">
|
||||||
|
<pn-auto-avatar
|
||||||
|
:img="scope.opt.logo"
|
||||||
|
:name="scope.opt.name"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<q-icon size="32px" color="grey" name="mdi-cancel"/>
|
||||||
|
</template>
|
||||||
|
</q-avatar>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ scope.opt.name }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model.trim="modelValue.name"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
class = "w100 fix-bottom-padding q-pt-sm"
|
||||||
|
:rules="[rules.name]"
|
||||||
|
no-error-icon
|
||||||
|
label-slot
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
{{ $t('task_block__name') }}
|
||||||
|
<span class="text-red">*</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model="modelValue.description"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
autogrow
|
||||||
|
class="w100 q-pt-sm"
|
||||||
|
:label="$t('task_block__description')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<pn-file-uploader
|
||||||
|
v-model:exist-files ="modelValue.files"
|
||||||
|
v-model:new-files ="newFiles"
|
||||||
|
:existFileData="files"
|
||||||
|
:label="$t('meeting_block__attach_files')"
|
||||||
|
class="q-pt-sm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex column">
|
||||||
|
<div class="text-caption">
|
||||||
|
{{ $t('task_block__plan') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex no-wrap justify-between q-gutter-x-md">
|
||||||
|
<q-input
|
||||||
|
v-model="taskDate"
|
||||||
|
dense filled
|
||||||
|
mask="##/##/####"
|
||||||
|
:rules=[rules.date]
|
||||||
|
no-error-icon
|
||||||
|
:label="$t('task_block__date')"
|
||||||
|
hide-bottom-space
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="event" class="cursor-pointer">
|
||||||
|
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||||
|
<q-date
|
||||||
|
v-model="taskDate"
|
||||||
|
mask="DD/MM/YYYY"
|
||||||
|
class="relative-position"
|
||||||
|
:options="d => d >= date.formatDate(Date.now(), 'YYYY/MM/DD')"
|
||||||
|
:navigation-min-year-month="date.formatDate(Date.now(), 'YYYY/MM')"
|
||||||
|
>
|
||||||
|
<div class="absolute" style="top: 0; right: 0;">
|
||||||
|
<q-btn
|
||||||
|
v-close-popup
|
||||||
|
round flat
|
||||||
|
color="white"
|
||||||
|
icon="mdi-close"
|
||||||
|
class="q-ma-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-date>
|
||||||
|
</q-popup-proxy>
|
||||||
|
</q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model="taskTime"
|
||||||
|
dense filled
|
||||||
|
mask="time"
|
||||||
|
:rules="[rules.time]"
|
||||||
|
no-error-icon
|
||||||
|
:label="$t('task_block__time')"
|
||||||
|
hide-bottom-space
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="access_time" class="cursor-pointer">
|
||||||
|
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||||
|
<q-time
|
||||||
|
v-model="taskTime"
|
||||||
|
mask="HH:mm"
|
||||||
|
format24h
|
||||||
|
class="relative-position"
|
||||||
|
>
|
||||||
|
<div class="absolute" style="top: 0; right: 0;">
|
||||||
|
<q-btn
|
||||||
|
v-close-popup
|
||||||
|
round flat
|
||||||
|
color="white"
|
||||||
|
icon="mdi-close"
|
||||||
|
class="q-ma-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-time>
|
||||||
|
</q-popup-proxy>
|
||||||
|
</q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
v-model="modelValue.assigned_to"
|
||||||
|
:options="displayUsers"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
class="w100 file-input-fix q-pt-sm"
|
||||||
|
:label = "$t('task_block__assigned_to')"
|
||||||
|
option-value="id"
|
||||||
|
option-label="displayName"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="mdi-account-arrow-left-outline"/>
|
||||||
|
</template>
|
||||||
|
<template #option="scope">
|
||||||
|
<q-item v-bind="scope.itemProps">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<pn-auto-avatar
|
||||||
|
:img="scope.opt.photo"
|
||||||
|
:name="scope.opt.displayName"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ scope.opt.displayName }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
v-model="modelValue.observers"
|
||||||
|
:options="displayUsers"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
class="w100 file-input-fix q-pt-sm"
|
||||||
|
:label = "$t('task_block__observers')"
|
||||||
|
option-value="id"
|
||||||
|
option-label="displayName"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
use-chips
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="mdi-account-eye-outline"/>
|
||||||
|
</template>
|
||||||
|
<template #option="scope">
|
||||||
|
<q-item v-bind="scope.itemProps">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar round size="md">
|
||||||
|
<img v-if="scope.opt.photo" :src="scope.opt.photo"/>
|
||||||
|
<pn-auto-avatar v-else :name="scope.opt.name"/>
|
||||||
|
</q-avatar>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ scope.opt.displayName }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex column">
|
||||||
|
|
||||||
|
<div class="q-py-none q-my-none text-caption flex items-baseline">
|
||||||
|
<span>{{$t('task_block__priority')}}</span>
|
||||||
|
<q-icon
|
||||||
|
name="mdi-flag-triangle"
|
||||||
|
class="q-ml-xs"
|
||||||
|
:color="getToogleColor()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-btn-toggle
|
||||||
|
v-model="modelValue.priority"
|
||||||
|
no-caps
|
||||||
|
:toggle-color="getToogleColor()"
|
||||||
|
spread
|
||||||
|
unelevated
|
||||||
|
dense
|
||||||
|
:options="priority"
|
||||||
|
class="w100"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="item in priority"
|
||||||
|
:key="item.id" #[item.slot]
|
||||||
|
>
|
||||||
|
<div class="row items-center no-wrap gap-xs text-weight-regular">
|
||||||
|
<span
|
||||||
|
:class="modelValue.priority === item.value ? '' : 'text-grey'"
|
||||||
|
>
|
||||||
|
{{ $t(item.translationKey) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-btn-toggle>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</pn-scroll-list>
|
||||||
|
</pn-page-card>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, onMounted } from 'vue'
|
||||||
|
import type { TaskParams } from 'types/Task'
|
||||||
|
import { useChatsStore } from 'stores/chats'
|
||||||
|
import { useUsersStore } from 'stores/users'
|
||||||
|
import { useFilesStore } from 'stores/files'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { date } from 'quasar'
|
||||||
|
const { t } = useI18n()
|
||||||
|
const filesStore = useFilesStore()
|
||||||
|
const files = filesStore.getFiles
|
||||||
|
const newFiles=ref<File[]>([])
|
||||||
|
|
||||||
|
const modelValue = defineModel<TaskParams>({
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
title: string,
|
||||||
|
btnText: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['update'])
|
||||||
|
|
||||||
|
const chatsStore = useChatsStore()
|
||||||
|
const chats = chatsStore.getChats
|
||||||
|
const displayChats = computed(() => [
|
||||||
|
...chats.map(el => ({
|
||||||
|
id: el.id,
|
||||||
|
name: el.name,
|
||||||
|
logo: el.logo
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
id: null,
|
||||||
|
name: t('task_block__no_chat'),
|
||||||
|
logo: ''
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
const users = usersStore.getUsers
|
||||||
|
|
||||||
|
const displayUsers = computed(() => {
|
||||||
|
return users
|
||||||
|
.map(el => ({ ...el, displayName: usersStore.userNameById(el.id) }))
|
||||||
|
})
|
||||||
|
|
||||||
|
const taskDate = computed({
|
||||||
|
get: () => date.formatDate(modelValue.value.plan_date * 1000, 'DD/MM/YYYY'),
|
||||||
|
set: (d) => updateDateTime(d, taskTime.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const taskTime = computed({
|
||||||
|
get: () => date.formatDate(modelValue.value.plan_date * 1000, 'HH:mm'),
|
||||||
|
set: (t) => updateDateTime(taskDate.value, t)
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateDateTime (dateStr: string, timeStr: string) {
|
||||||
|
if (dateStr.length === 10 && timeStr.length === 5) {
|
||||||
|
const newDate = date.extractDate(`${dateStr} ${timeStr}`, 'DD/MM/YYYY HH:mm')
|
||||||
|
if (!isNaN(newDate.getTime())) {
|
||||||
|
modelValue.value.plan_date = newDate.getTime() / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const priority = [
|
||||||
|
{ id: 1, slot: 's1', label: '', translationKey: 'task_block__priority_normal', value: 0, props: { color: 'green' } },
|
||||||
|
{ id: 2, slot: 's2', label: '', translationKey: 'task_block__priority_important', value: 1, props: { color: 'amber' } },
|
||||||
|
{ id: 3, slot: 's3', label: '', translationKey: 'task_block__priority_critical', value: 2, props: { color: 'red' } }
|
||||||
|
]
|
||||||
|
|
||||||
|
function getToogleColor () {
|
||||||
|
const priorityItem = priority.find(el => el.value === modelValue.value.priority)
|
||||||
|
return priorityItem
|
||||||
|
? priorityItem.props.color
|
||||||
|
: 'primary'
|
||||||
|
}
|
||||||
|
|
||||||
|
const rulesErrorMessage = {
|
||||||
|
name: t('task_block__error_name'),
|
||||||
|
date: t('task_block__error_date'),
|
||||||
|
time: t('task_block__error_time')
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: (val: TaskParams['name']) => !!val?.trim() || rulesErrorMessage['name'],
|
||||||
|
date: () => (!!modelValue.value.plan_date && modelValue.value.plan_date > Date.now() / 1000) || rulesErrorMessage['date'],
|
||||||
|
time: () => (!!modelValue.value.plan_date && modelValue.value.plan_date > Date.now() / 1000) || rulesErrorMessage['time']
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFormValid = computed(() => {
|
||||||
|
const validations = {
|
||||||
|
name: rules.name(modelValue.value.name) === true,
|
||||||
|
date: rules.date() === true,
|
||||||
|
time: rules.time() === true
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(validations).every(Boolean)
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialTask = ref({} as TaskParams)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!modelValue.value.assigned_to) modelValue.value.assigned_to = usersStore.myId.id
|
||||||
|
initialTask.value = { ...modelValue.value }
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fix-bottom-padding.q-field--with-bottom {
|
||||||
|
padding-bottom: 0 !important
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-fix :deep(.q-field__append) {
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-fix :deep(.q-field__prepend) {
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,50 +1,146 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar color="primary" rounded text-color="white" icon="mdi-tray-arrow-down" size="md" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label class="text-bold flex items-center">
|
<q-item-label
|
||||||
|
v-if="taskStatus"
|
||||||
|
class="text-caption flex items-center w100"
|
||||||
|
:class="'text-' + taskStatus.color"
|
||||||
|
>
|
||||||
|
<q-icon :name="taskStatus.icon"/>
|
||||||
<span>
|
<span>
|
||||||
{{item.name}}
|
{{ $t(taskStatus.text) }}
|
||||||
</span>
|
</span>
|
||||||
<pn-task-priority-icon v-if="item?.priority" :priority="item.priority"/>
|
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label caption class="flex items-center">
|
|
||||||
<span v-if="item.attach && item.files.length !== 0" class="q-mr-sm flex items-center">
|
<q-item-label lines="2">
|
||||||
|
|
||||||
|
<span class="text-bold">{{item.name}}</span>
|
||||||
|
</q-item-label>
|
||||||
|
|
||||||
|
<q-item-label caption>
|
||||||
|
<div class="flex row items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<pn-auto-avatar
|
||||||
|
v-if="item.created_by !== usersStore.myId.id"
|
||||||
|
:img="usersStore.userById(item.created_by)?.photo"
|
||||||
|
:name="usersStore.userNameById(item.created_by)"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<span v-else>{{$t('task_item__from_me')}}</span>
|
||||||
|
<q-icon name="mdi-chevron-right" size="xs" class="text-grey"/>
|
||||||
|
<pn-auto-avatar
|
||||||
|
v-if="item.assigned_to && item.assigned_to !== usersStore.myId.id"
|
||||||
|
:img="usersStore.userById(item.assigned_to)?.photo"
|
||||||
|
:name="usersStore.userNameById(item.assigned_to)"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<span v-else-if="item.created_by !== item.assigned_to">{{$t('task_item__to_me')}}</span>
|
||||||
|
<span v-else>{{$t('task_item__to_me_from_me')}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-item-label>
|
||||||
|
|
||||||
|
<q-item-label caption>
|
||||||
|
<div
|
||||||
|
v-if="(
|
||||||
|
(Array.isArray(item.files) ? item.files.length : 0) +
|
||||||
|
(Array.isArray(item.close_files) ? item.close_files.length : 0)) !== 0
|
||||||
|
"
|
||||||
|
class="q-mr-sm flex items-center"
|
||||||
|
>
|
||||||
<q-icon name="mdi-paperclip"/>
|
<q-icon name="mdi-paperclip"/>
|
||||||
<span>{{ item.files.length }}</span>
|
{{ item.files.length + item.close_files.length }}
|
||||||
</span>
|
</div>
|
||||||
<span class="flex items-center">
|
<span
|
||||||
<q-icon name="mdi-chat-outline"/>
|
v-if="item.chat_id && chatsStore.chatById(item.chat_id)"
|
||||||
{{ item.owner_id }}
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<q-icon name="mdi-chat-outline" class="q-mr-xs"/>
|
||||||
|
{{ chatsStore.chatById(item.chat_id)?.name }}
|
||||||
</span>
|
</span>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section side class="flex column">
|
<q-item-section
|
||||||
<q-item-label v-if="item.plan_date" caption>
|
v-if="item.status === 1"
|
||||||
{{ dayjs(item.plan_date).format('ddd MMM') }}
|
side caption
|
||||||
</q-item-label>
|
:class="isLowTime ? 'text-red' : ''"
|
||||||
<q-item-label v-if="item.date_end" caption>
|
>
|
||||||
{{ dayjs(item.plan_date).format('hh:mm') }}
|
<q-item-label class="flex no-wrap items-center" caption>
|
||||||
|
<q-icon name="mdi-clock-outline" class="q-mr-xs"/>
|
||||||
|
<span>{{ date.formatDate(item.plan_date * 1000, 'D MMM') }}</span>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label caption>
|
<q-item-label caption>
|
||||||
<div class="flex items-center">
|
<span>{{ date.formatDate(item.plan_date * 1000, 'HH:mm') }}</span>
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label caption>
|
||||||
|
<pn-task-priority-icon v-if="item?.priority" :priority="item.priority"/>
|
||||||
</div>
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section
|
||||||
|
v-if="item.close_date"
|
||||||
|
side caption
|
||||||
|
>
|
||||||
|
<q-item-label class="flex no-wrap items-center">
|
||||||
|
<q-icon name="mdi-clock" class="q-mr-xs"/>
|
||||||
|
<span>{{ date.formatDate(item.close_date, 'D MMM') }}</span>
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label>
|
||||||
|
<span>{{ date.formatDate(item.close_date, 'HH:mm') }}</span>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useUsersStore } from 'stores/users'
|
||||||
|
import { useChatsStore } from 'stores/chats'
|
||||||
import type { Task } from 'types/Task'
|
import type { Task } from 'types/Task'
|
||||||
import dayjs from 'dayjs'
|
import { date } from 'quasar'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
item: Task
|
item: Task
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
const chatsStore = useChatsStore()
|
||||||
|
|
||||||
|
const isOverdue = computed(() => {
|
||||||
|
return props.item.close_date
|
||||||
|
? +props.item.close_date > props.item.plan_date
|
||||||
|
: props.item.plan_date * 1000 < Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLowTime = computed(() => props.item.plan_date * 1000 - Date.now() < 24 * 60 * 60 * 1000 && (props.item.plan_date * 1000 - Date.now() > 0))
|
||||||
|
|
||||||
|
const taskStatus = computed(() => {
|
||||||
|
switch (props.item.status) {
|
||||||
|
case 1:
|
||||||
|
return isOverdue.value
|
||||||
|
? {
|
||||||
|
text: 'task_item__task_overdue',
|
||||||
|
color: 'negative',
|
||||||
|
icon: 'mdi-alert-circle-outline'
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
return {
|
||||||
|
text: !isOverdue.value ? 'task_item__task_done' : 'task_item__task_done_overdue',
|
||||||
|
color: 'green',
|
||||||
|
icon: 'mdi-check-circle-outline'
|
||||||
|
}
|
||||||
|
|
||||||
|
case 6:
|
||||||
|
return {
|
||||||
|
text: 'task_item__task_cancel',
|
||||||
|
color: 'negative',
|
||||||
|
icon: 'mdi-close-circle-outline'
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ body {
|
|||||||
.orline:after {
|
.orline:after {
|
||||||
content: "";
|
content: "";
|
||||||
flex: 1 1;
|
flex: 1 1;
|
||||||
border-bottom: 1px solid;
|
border-bottom: 1px solid grey;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
125
src/helpers/files-functions.ts
Normal file
125
src/helpers/files-functions.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
type TranslateFunction = (key: string) => string
|
||||||
|
|
||||||
|
const fileSize = (value: number, t: TranslateFunction): string => {
|
||||||
|
if (value === 0) return '-'
|
||||||
|
|
||||||
|
const units = ['B', 'kB', 'MB', 'GB', 'TB'] as const
|
||||||
|
const i = Math.floor(Math.log(value) / Math.log(1024))
|
||||||
|
|
||||||
|
// Гарантируем, что индекс всегда будет в допустимых пределах
|
||||||
|
const index = Math.max(0, Math.min(i, units.length - 1))
|
||||||
|
|
||||||
|
const result = (value / Math.pow(1024, index)).toFixed(2)
|
||||||
|
|
||||||
|
// Безопасное получение значения из readonly-массива
|
||||||
|
const unitKey = units[index]
|
||||||
|
|
||||||
|
return `${result} ${t(unitKey!)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface ParsedFile {
|
||||||
|
name: string
|
||||||
|
ext: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFileName(filename: string): ParsedFile {
|
||||||
|
const lastDotIndex = filename.lastIndexOf('.')
|
||||||
|
|
||||||
|
if (lastDotIndex === -1) {
|
||||||
|
return { name: filename, ext: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: filename.slice(0, lastDotIndex),
|
||||||
|
ext: filename.slice(lastDotIndex + 1).toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileIcon {
|
||||||
|
type: string
|
||||||
|
icon: string
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileIcon(filename: string): FileIcon {
|
||||||
|
const ext = parseFileName(filename).ext
|
||||||
|
|
||||||
|
switch (ext) {
|
||||||
|
case 'doc':
|
||||||
|
case 'docx':
|
||||||
|
return { type: 'doc', icon: 'pn-icon-file-doc', color: '#2B579A' }
|
||||||
|
|
||||||
|
case 'xls':
|
||||||
|
case 'xlsx':
|
||||||
|
case 'csv':
|
||||||
|
return { type: 'xls', icon: 'pn-icon-file-xls', color: '#217346' }
|
||||||
|
|
||||||
|
case 'vsd':
|
||||||
|
case 'vsdx':
|
||||||
|
return { type: 'vsd', icon: 'pn-icon-file-vsd', color: '#3955A3' }
|
||||||
|
|
||||||
|
case 'ppt':
|
||||||
|
case 'pptx':
|
||||||
|
return { type: 'ppt', icon: 'pn-icon-file-ppt', color: '#D24726' }
|
||||||
|
|
||||||
|
case 'pdf':
|
||||||
|
return { type: 'pdf', icon: 'pn-icon-file-pdf', color: '#D0021B' }
|
||||||
|
|
||||||
|
case 'png':
|
||||||
|
case 'jpg':
|
||||||
|
case 'jpeg':
|
||||||
|
case 'gif':
|
||||||
|
case 'bmp':
|
||||||
|
case 'svg':
|
||||||
|
return { type: 'img', icon: 'pn-icon-file-img', color: '#4CAF50' }
|
||||||
|
|
||||||
|
case 'mp3':
|
||||||
|
case 'wav':
|
||||||
|
case 'ogg':
|
||||||
|
return { type: 'music', icon: 'pn-icon-file-audio', color: '#FF9800' }
|
||||||
|
|
||||||
|
case 'mp4':
|
||||||
|
case 'avi':
|
||||||
|
case 'mov':
|
||||||
|
case 'mkv':
|
||||||
|
return { type: 'video', icon: 'pn-icon-file-video', color: '#9C27B0' }
|
||||||
|
|
||||||
|
case 'js':
|
||||||
|
case 'ts':
|
||||||
|
case 'html':
|
||||||
|
case 'css':
|
||||||
|
case 'json':
|
||||||
|
case 'xml':
|
||||||
|
return { type: 'code', icon: 'pn-icon-file-code', color: '#999' }
|
||||||
|
|
||||||
|
case 'txt':
|
||||||
|
return { type: 'txt', icon: 'pn-icon-file-txt', color: '#757575' }
|
||||||
|
|
||||||
|
case 'zip':
|
||||||
|
case 'rar':
|
||||||
|
case '7z':
|
||||||
|
case 'tar':
|
||||||
|
case 'gz':
|
||||||
|
return { type: 'archive', icon: 'pn-icon-file-archive', color: '#F78E1E' }
|
||||||
|
|
||||||
|
case 'skp':
|
||||||
|
return { type: 'skp', icon: 'pn-icon-file-skp', color: '#CC0000' }
|
||||||
|
|
||||||
|
case 'dwg':
|
||||||
|
case 'dxf':
|
||||||
|
return { type: 'cad', icon: 'pn-icon-file-dwg', color: '#00579D' }
|
||||||
|
|
||||||
|
case 'ttf':
|
||||||
|
return { type: 'font', icon: 'pn-icon-file-ttf', color: '#607D8B' }
|
||||||
|
|
||||||
|
default:
|
||||||
|
return { type: 'common', icon: 'pn-icon-file-default', color: '#9E9E9E' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
fileSize,
|
||||||
|
parseFileName,
|
||||||
|
fileIcon
|
||||||
|
}
|
||||||
98
src/helpers/helpers.ts
Normal file
98
src/helpers/helpers.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
function isDirty(
|
||||||
|
obj1: Record<string, unknown> | null | undefined,
|
||||||
|
obj2: Record<string, unknown> | null | undefined
|
||||||
|
): boolean {
|
||||||
|
const actualObj1 = obj1 ?? {}
|
||||||
|
const actualObj2 = obj2 ?? {}
|
||||||
|
|
||||||
|
const filteredObj1 = filterIgnored(actualObj1)
|
||||||
|
const filteredObj2 = filterIgnored(actualObj2)
|
||||||
|
|
||||||
|
const allKeys = new Set([...Object.keys(filteredObj1), ...Object.keys(filteredObj2)])
|
||||||
|
|
||||||
|
for (const key of allKeys) {
|
||||||
|
const hasKey1 = Object.hasOwn(filteredObj1, key)
|
||||||
|
const hasKey2 = Object.hasOwn(filteredObj2, key)
|
||||||
|
|
||||||
|
// Различие в наличии ключа
|
||||||
|
if (hasKey1 !== hasKey2) return true
|
||||||
|
|
||||||
|
if (hasKey1 && hasKey2) {
|
||||||
|
const val1 = filteredObj1[key]
|
||||||
|
const val2 = filteredObj2[key]
|
||||||
|
|
||||||
|
// Сравнение массивов
|
||||||
|
if (Array.isArray(val1) && Array.isArray(val2)) {
|
||||||
|
if (val1.length !== val2.length) return true
|
||||||
|
const set2 = new Set(val2)
|
||||||
|
if (!val1.every(item => set2.has(item))) return true
|
||||||
|
}
|
||||||
|
// Один массив, другой - нет
|
||||||
|
else if (Array.isArray(val1) || Array.isArray(val2)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Сравнение строк
|
||||||
|
else if (typeof val1 === 'string' && typeof val2 === 'string') {
|
||||||
|
if (val1.trim() !== val2.trim()) return true
|
||||||
|
}
|
||||||
|
// Сравнение примитивов
|
||||||
|
else if (val1 !== val2) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterIgnored(obj: Record<string, unknown>): Record<string, string | number | boolean | (string | number)[]> {
|
||||||
|
const filtered: Record<string, string | number | boolean | (string | number)[]> = {}
|
||||||
|
|
||||||
|
for (const key in obj) {
|
||||||
|
const value = obj[key]
|
||||||
|
|
||||||
|
// Обработка массивов
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// Отбрасываем пустые массивы
|
||||||
|
if (value.length === 0) continue
|
||||||
|
|
||||||
|
// Фильтруем массивы с некорректными элементами
|
||||||
|
if (value.every(item =>
|
||||||
|
typeof item === 'string' ||
|
||||||
|
typeof item === 'number'
|
||||||
|
)) {
|
||||||
|
filtered[key] = value
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка примитивов
|
||||||
|
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка строк
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (trimmed === '') continue
|
||||||
|
filtered[key] = trimmed
|
||||||
|
}
|
||||||
|
// Обработка чисел и boolean
|
||||||
|
else if (value !== 0 && value !== false) {
|
||||||
|
filtered[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIntString (s: string | string[] | undefined) :number | null {
|
||||||
|
if (typeof s !== 'string') return null
|
||||||
|
const regex = /^[+-]?\d+$/
|
||||||
|
return regex.test(s) ? Number(s) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
isDirty,
|
||||||
|
parseIntString
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
// This is just an example,
|
|
||||||
// so you can safely delete all default props below
|
|
||||||
|
|
||||||
export default {
|
|
||||||
login__forgot_password: 'Forgot password?',
|
|
||||||
login__login: 'Login',
|
|
||||||
login__password: 'Password',
|
|
||||||
login__sign_in: 'Sign in',
|
|
||||||
login__sign_up: 'Register',
|
|
||||||
login__welcome: 'Welcome!',
|
|
||||||
project__companies: 'Companies',
|
|
||||||
project__persons: 'Persons',
|
|
||||||
project__chats: 'Chats',
|
|
||||||
projects__projects: 'Projects',
|
|
||||||
projects__account: 'Account',
|
|
||||||
failed: 'Action failed',
|
|
||||||
success: 'Action was successful'
|
|
||||||
}
|
|
||||||
@@ -1 +1 @@
|
|||||||
export default { EN: 'EN', RU: 'RU', continue: 'Continue', back: 'Back', close: 'Close', month: 'month', months: 'months', slogan: 'Work together - it\'s magic!', under_construction: 'Under construction.', B: 'B', kB: 'kB', MB: 'MB', GB: 'GB', TB: 'TB', main__chats: 'Chats', main__tasks: 'Tasks', main__meetings: 'Meetings', main__files: 'Files', main__users: 'Contacts', chats__search: 'Search', tasks__search: 'Search', tasks__filters: 'Filters', tasks__filters_types: 'Task types', tasks__filters_in: 'Assigned to me', tasks__filters_out: 'Assigned by me', tasks__filters_watch: 'Following', tasks__filters_priority: 'Task priority', tasks__filters_priority_normal: 'Normal', tasks__filters_priority_important: 'Important', tasks__filters_priority_critical: 'Critical', task_add__title: 'Create task', task_add__name: 'Title', task_add__description: 'Description', task_add__plan_date: 'Planed date', task_add__priority: 'Priority', task_add__priority_normal: 'Normal', task_add__priority_important: 'Important', task_add__priority_critical: 'Critical', task_add__attached_chat: 'Attached chat', task_add__attach_files: 'Attached files', task_add__assigned_to: 'Assignee', task_add__watch: 'Followers', meetings__search: 'Search', meetings__previous: 'Previous', meetings__previous_hide: 'Hide', meeting_create__title_card: 'Create meeting', meeting_edit__title_card: 'Edit meeting', meeting_view__title_card: 'Meeting card', meeting_info__name: 'Title', meeting_info__description: 'Description', meeting_info__date: 'Date', meeting_info__time: 'Time', meeting_info__attach_chat: 'Attach to chat', meeting_info__participants: 'Participants', meeting_info__attach_files: 'Files', meeting_info__canceled: 'Canceled', meeting_info__dialog_cancel_title: 'Cancel the meeting?', meeting_info__dialog_cancel_ok: 'Confirm', meeting_info__dialog_cancel_delete: 'Delete', meeting_info__dialog_restore_title: 'Restore the meeting?', meeting_info__dialog_restore_ok: 'Confirm', files__search: 'Search', files__filters: 'Filters', files__filters_extension: 'Extensions (types)', files__filters_source: 'Source', files__filters_source_chats: 'chat', files__filters_source_tasks: 'task', files__filters_source_meetings: 'meeting', files__filters_by: 'Author', files__filters_size: 'Size', files__filters_size_small: 'small (less than 5MB)', files__filters_size_middle: 'middle (5-25MB)', files__filters_size_big: 'big (25-100MB)', files__filters_size_very_big: 'very big (more 100MB)', files_filters_reset: 'Reset filters', header__my_projects: 'My projects', header__all_projects: 'All projects', users__search: 'Search', user_card__title: 'User card', user_card__name: 'Name', user_card__phone: 'Phone', user_card__email: 'Email', user_card__position: 'Position', settings__title: 'Settings', settings__language: 'Language', settings__font_size: 'Font size' }
|
export default { EN: 'EN', RU: 'RU', '': '', error404: 'Oops. Nothing here…', continue: 'Continue', back: 'Back', close: 'Close', month: 'month', months: 'months', slogan: 'Work together - it\'s magic!', under_construction: 'Under construction.', B: 'B', kB: 'kB', MB: 'MB', GB: 'GB', TB: 'TB', main__chats: 'Chats', main__tasks: 'Tasks', main__meetings: 'Meetings', main__files: 'Files', main__users: 'Contacts', chats__search: 'Search', tasks__search: 'Search', tasks__filters: 'Filters', tasks__filters_by_participant: 'Task types', tasks__filters_to_me: 'Assigned to me', tasks__filters_from_me: 'Assigned by me', tasks__filters_observers: 'Following', tasks__filters_not_involved: 'Not involved', tasks__filters_by_priority: 'Task priority', tasks__filters_priority_normal: 'Normal', tasks__filters_priority_important: 'Important', tasks__filters_priority_critical: 'Critical', tasks__filters_continue: 'Continue', tasks__filters_reset: 'Reset filters', tasks__show_archive: 'Show archive', tasks__hide_archive: 'Hide archive', tasks__dialog_cancel_title: 'Cancel the task?', tasks__dialog_cancel_delete: 'Delete', tasks__dialog_cancel_ok: 'Reject', task_create__title_card: 'Create task', task_create__btn: 'Create', task_edit__title_card: 'Edit task', task_edit__btn: 'Apply', task_view__title_card: 'Task card', task_view__go_to_chat: 'to chat', task_view__btn_cancel_task: 'Reject', task_view__btn_close_task: 'Finished', task_view__dialog_task_done_title: 'Complete Task', task_view__dialog_task_done_comment: 'Comment', task_view__dialog_task_done_btn: 'Complete', task_view__dialog_task_cancel_title: 'Reject Task', task_view__dialog_task_cancel_comment: 'Reason', task_view__dialog_task_cancel_btn: 'Reject', task_view__dialog_task_files: 'Files', task_view__error_comment: 'Require', task_block__name: 'Title', task_block__description: 'Description', task_block__plan: 'Planed', task_block__date: 'Date', task_block__time: 'Time', task_block__priority: 'Priority', task_block__priority_normal: 'Normal', task_block__priority_important: 'Important', task_block__priority_critical: 'Critical', task_block__attached_chat: 'Attached chat', task_block__attach_files: 'Attached files', task_block__assigned_to: 'Assignee', task_block__observers: 'Followers', task_block__attach_chat: 'Attached chat', task_block__no_chat: 'No chat', task_block__error_name: 'Please type something', task_block__error_date: 'Incorrect date', task_block__error_time: 'Incorrect time', task_item__from_me: 'Me', task_item__to_me: 'Me', task_item__to_me_from_me: 'Me', task_item__task_overdue: 'Overdue', task_item__task_done: 'Done!', task_item__task_done_overdue: 'Done (overdue)', task_item__task_cancel: 'Canceled', task_priority_important: 'Important', task_priority_critical: 'Critical', meetings__search: 'Search', meetings__previous: 'Previous', meetings__previous_hide: 'Hide', meeting_create__title_card: 'Create meeting', meeting_edit__title_card: 'Edit meeting', meeting_edit__btn: 'Apply', meeting_view__title_card: 'Meeting card', meeting_view__go_to_chat: 'to chat', meeting_page__canceled: 'Canceled', meeting_page__dialog_cancel_title: 'Cancel the meeting?', meeting_page__dialog_cancel_ok: 'Confirm', meeting_page__dialog_cancel_delete: 'Delete', meeting_page__dialog_restore_title: 'Restore the meeting?', meeting_page__dialog_restore_ok: 'Confirm', meeting_block__name: 'Title', meeting_block__description: 'Description', meeting_block__date: 'Date', meeting_block__time: 'Time', meeting_block__place: 'Venue', meeting_block__attach_chat: 'Attached chat', meeting_block__participants: 'Participants', meeting_block__attach_files: 'Files', meeting_block__error_name: 'Please type something', meeting_block__error_date: 'Incorrect date', meeting_block__error_time: 'Incorrect time', meeting_block__no_chat: 'No chat', files__search: 'Search', files__filters: 'Filters', files__filters_extension: 'Extensions (types)', files__filters_source: 'Source', files__filters_source_chats: 'chat', files__filters_source_tasks: 'task', files__filters_source_meetings: 'meeting', files__filters_by: 'Author', files__filters_size: 'Size', files__filters_size_small: 'small (less than 5MB)', files__filters_size_middle: 'middle (5-25MB)', files__filters_size_big: 'big (25-100MB)', files__filters_size_very_big: 'very big (more 100MB)', files__filters_continue: 'Continue', files__filters_reset: 'Reset filters', header__my_projects: 'My projects', header__all_projects: 'All projects', header__projects: 'Projects', users__search: 'Search', user_card__title: 'User card', user_card__name: 'Name', user_card__phone: 'Phone', user_card__email: 'Email', user_card__position: 'Position', settings__title: 'Settings', settings__language: 'Language', settings__font_size: 'Font size', file_upload__comment: 'Single file select for upload only (Android)' }
|
||||||
File diff suppressed because one or more lines are too long
@@ -6,22 +6,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-h2" style="opacity:.4">
|
<div class="text-h2" style="opacity:.4">
|
||||||
Oops. Nothing here...
|
{{ $t('error404') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-btn
|
|
||||||
class="q-mt-xl"
|
|
||||||
color="white"
|
|
||||||
text-color="blue"
|
|
||||||
unelevated
|
|
||||||
to="/"
|
|
||||||
label="Go Home"
|
|
||||||
no-caps
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
//
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -40,15 +40,7 @@
|
|||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<div class="flex column items-center">
|
<div class="flex column items-center">
|
||||||
<q-icon :name="tab.icon" :size="maxTabWidth < baseTabWidth ? 'sm' : 'md'">
|
<q-icon :name="tab.icon" :size="maxTabWidth < baseTabWidth ? 'sm' : 'md'"/>
|
||||||
<q-badge
|
|
||||||
color="brand" align="top"
|
|
||||||
rounded floating
|
|
||||||
style="font-style: normal;"
|
|
||||||
>
|
|
||||||
{{ currentProject?.[tab.name as keyof typeof currentProject] ?? 0 }}
|
|
||||||
</q-badge>
|
|
||||||
</q-icon>
|
|
||||||
<div
|
<div
|
||||||
class="text-caption flex justify-center"
|
class="text-caption flex justify-center"
|
||||||
:style="{ width: (baseTabWidth - 32) + 'px'}"
|
:style="{ width: (baseTabWidth - 32) + 'px'}"
|
||||||
@@ -71,14 +63,11 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onBeforeMount, computed } from 'vue'
|
import { ref, onBeforeMount, computed } from 'vue'
|
||||||
import { useProjectsStore } from 'stores/projects'
|
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import projectPageHeader from 'pages/main/HeaderPage.vue'
|
import projectPageHeader from 'pages/main/HeaderPage.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const projectsStore = useProjectsStore()
|
|
||||||
const currentProject = computed(() => projectsStore.currentProjectId)
|
|
||||||
|
|
||||||
const tabs = ref([
|
const tabs = ref([
|
||||||
{name: 'files', label: 'main__files', icon: 'mdi-file-multiple-outline', to: { name: 'files'}, width: 0 },
|
{name: 'files', label: 'main__files', icon: 'mdi-file-multiple-outline', to: { name: 'files'}, width: 0 },
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<meeting-block
|
||||||
<template #title>
|
v-model="newMeeting"
|
||||||
{{$t('meeting_create__title_card')}}
|
title="meeting_create__title_card"
|
||||||
<q-btn
|
btnText="meeting_create__btn"
|
||||||
v-if="(Object.keys(meetingMod).length !== 0)"
|
@update="addMeeting"
|
||||||
@click = "addCompany(meetingMod)"
|
/>
|
||||||
flat round
|
|
||||||
icon="mdi-check"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<pn-scroll-list>
|
|
||||||
<meeting-block v-model="meetingMod"/>
|
|
||||||
</pn-scroll-list>
|
|
||||||
</pn-page-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -25,19 +17,19 @@
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const meetingsStore = useMeetingsStore()
|
const meetingsStore = useMeetingsStore()
|
||||||
|
|
||||||
const meetingMod = ref(<MeetingParams>{
|
const newMeeting = ref(<MeetingParams>{
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
place: '',
|
place: '',
|
||||||
meet_date: Date.now(),
|
meet_date: (Date.now() + 1000*60*60*24) / 1000, // fix incorrect start date and time -> move 24h
|
||||||
chat_attach: null,
|
chat_id: null,
|
||||||
participants: [],
|
participants: [],
|
||||||
files: [],
|
files: [],
|
||||||
is_cancel: false
|
is_cancel: false
|
||||||
})
|
})
|
||||||
|
|
||||||
async function addCompany (data: MeetingParams) {
|
async function addMeeting (newFiles: File[]) {
|
||||||
await meetingsStore.add(data)
|
await meetingsStore.add(newMeeting.value, newFiles)
|
||||||
router.go(-1)
|
router.go(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,236 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<meeting-block
|
||||||
<template #title>
|
v-if="meetingMod"
|
||||||
<div class="flex items-center justify-between col-grow">
|
v-model="meetingMod"
|
||||||
<div>
|
title="meeting_edit__title_card"
|
||||||
{{ $t('meeting_add__title') }}
|
btnText="meeting_edit__btn"
|
||||||
</div>
|
@update="updateMeeting"
|
||||||
<q-btn
|
/>
|
||||||
@click = "createMeeting()"
|
|
||||||
flat round
|
|
||||||
icon="mdi-check"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<pn-scroll-list>
|
|
||||||
<div class="flex column items-center q-ma-lg">
|
|
||||||
<div class="q-gutter-y-lg w100">
|
|
||||||
<q-input
|
|
||||||
v-model.trim="task.name"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
autogrow
|
|
||||||
:label = "$t('task_add__name')"
|
|
||||||
class="bold-input w100"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-input
|
|
||||||
v-model.trim="task.description"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
autogrow
|
|
||||||
class = "w100"
|
|
||||||
:label = "$t('task_add__description')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-file
|
|
||||||
v-model="task.files"
|
|
||||||
:label="$t('task_add__attach_files')"
|
|
||||||
outlined
|
|
||||||
use-chips
|
|
||||||
multiple
|
|
||||||
dense
|
|
||||||
class="file-input-fix"
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<q-icon name="attach_file"/>
|
|
||||||
</template>
|
|
||||||
</q-file>
|
|
||||||
|
|
||||||
<q-select
|
|
||||||
v-if="companies"
|
|
||||||
v-model="task.company"
|
|
||||||
:options="companies"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class="w100"
|
|
||||||
:label = "$t('task_add__assigned_to')"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-account-arrow-left-outline"/>
|
|
||||||
</template>
|
|
||||||
<template #option="scope">
|
|
||||||
<q-item v-bind="scope.itemProps">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar rounded size="md">
|
|
||||||
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
|
|
||||||
<pn-auto-avatar v-else :name="scope.opt.name"/>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>{{ scope.opt.name }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
<template #selected>
|
|
||||||
{{ JSON.parse(JSON.stringify(task.company)).name }}
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
|
|
||||||
<q-select
|
|
||||||
v-if="companies"
|
|
||||||
v-model="task.company"
|
|
||||||
:options="companies"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class="w100"
|
|
||||||
:label = "$t('task_add__watch')"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-account-eye-outline"/>
|
|
||||||
</template>
|
|
||||||
<template #option="scope">
|
|
||||||
<q-item v-bind="scope.itemProps">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar rounded size="md">
|
|
||||||
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
|
|
||||||
<pn-auto-avatar v-else :name="scope.opt.name"/>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>{{ scope.opt.name }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
<template #selected>
|
|
||||||
{{ JSON.parse(JSON.stringify(task.company)).name }}
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
|
|
||||||
<q-input filled v-model="task.date" dense :label="$t('task_add__plan_date')">
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="event" class="cursor-pointer">
|
|
||||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
|
||||||
<q-date v-model="task.date" mask="YYYY-MM-DD HH:mm">
|
|
||||||
<div class="row items-center justify-end">
|
|
||||||
<q-btn v-close-popup label="Close" color="primary" flat></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-date>
|
|
||||||
</q-popup-proxy>
|
|
||||||
</q-icon>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #append>
|
|
||||||
<q-icon name="access_time" class="cursor-pointer">
|
|
||||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
|
||||||
<q-time v-model="task.date" mask="YYYY-MM-DD HH:mm" format24h>
|
|
||||||
<div class="row items-center justify-end">
|
|
||||||
<q-btn v-close-popup label="Close" color="primary" flat></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-time>
|
|
||||||
</q-popup-proxy>
|
|
||||||
</q-icon>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<div class="flex column">
|
|
||||||
|
|
||||||
<q-item-label header class="q-py-none q-my-none">
|
|
||||||
{{$t('task_add__priority')}}
|
|
||||||
</q-item-label>
|
|
||||||
|
|
||||||
<q-btn-toggle
|
|
||||||
v-model="task.priority"
|
|
||||||
no-caps
|
|
||||||
toggle-color="primary"
|
|
||||||
spread
|
|
||||||
unelevated
|
|
||||||
:options="priority"
|
|
||||||
class="q-mt-sm w100"
|
|
||||||
>
|
|
||||||
<template v-for="item in priority" :key="item.id" #[item.slot]>
|
|
||||||
<div class="row items-center no-wrap gap-xs text-weight-regular">
|
|
||||||
<span>
|
|
||||||
{{ $t(item.translationKey) }}
|
|
||||||
</span>
|
|
||||||
<pn-task-priority-icon :priority="item.value"/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</q-btn-toggle>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-select
|
|
||||||
v-if="companies"
|
|
||||||
v-model="task.company"
|
|
||||||
:options="companies"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class="w100"
|
|
||||||
:label = "$t('meeting_add__attached_chat')"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-chat-outline"/>
|
|
||||||
</template>
|
|
||||||
<template #option="scope">
|
|
||||||
<q-item v-bind="scope.itemProps">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar rounded size="md">
|
|
||||||
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
|
|
||||||
<pn-auto-avatar v-else :name="scope.opt.name"/>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>{{ scope.opt.name }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
<template #selected>
|
|
||||||
{{ JSON.parse(JSON.stringify(task.company)).name }}
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</pn-scroll-list>
|
|
||||||
</pn-page-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import meetingBlock from 'components/meetingBlock.vue'
|
||||||
|
import { useMeetingsStore } from 'stores/meetings'
|
||||||
|
import { parseIntString } from 'helpers/helpers'
|
||||||
|
import type { MeetingParams } from 'types/Meeting'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const meetingsStore = useMeetingsStore()
|
||||||
|
|
||||||
const task = ref({id: "p1", name: 'Кирюшкин Андрей', description: 'fdsfdsfsdfs', company: '', priority: 1, department: 'test', files: [], date: new Date(Date.now()).toLocaleString() })
|
const meetingId = computed(() => parseIntString(route.params.meetingId))
|
||||||
const priority = [
|
const meetingMod = ref<MeetingParams | null>(null)
|
||||||
{ id: 1, slot: 's1', label: '', translationKey: 'task_add__priority_normal', value: 0 },
|
|
||||||
{ id: 2, slot: 's2', label: '', translationKey: 'task_add__priority_important', value: 1 },
|
|
||||||
{ id: 3, slot: 's3', label: '', translationKey: 'task_add__priority_critical', value: 2 }
|
|
||||||
]
|
|
||||||
|
|
||||||
const companies = ref([
|
if (meetingsStore.isInit) {
|
||||||
{id: "com11", value: "com11", name: 'Рога и копытца1', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: [] },
|
meetingMod.value = meetingId.value
|
||||||
{id: "com21", name: 'ООО "Василек1"', logo: 'https://cdn.quasar.dev/img/avatar5.jpg', qtyPersons: 2, masked: true, unmasked: [] },
|
? { ...meetingsStore.meetingById(meetingId.value) } as MeetingParams
|
||||||
{id: "ch13", name: 'Откат и деньги1', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
|
: null
|
||||||
{id: "ch14", name: 'Откат и деньги2', logo: '', description: 'Договариваются о чем-то', qtyPersons: 5, masked: false, unmasked: [] },
|
}
|
||||||
{id: "com111", name: 'Рога и копытца2', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: []},
|
|
||||||
{id: "com211", name: 'ООО "Василек2"', logo: '', qtyPersons: 2, masked: true, unmasked: [] },
|
|
||||||
{id: "ch131", name: 'Откат и деньги3', logo: '', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
|
|
||||||
])
|
|
||||||
|
|
||||||
|
watch(() => meetingsStore.isInit, (isInit) => {
|
||||||
|
if (isInit && meetingId.value && !meetingMod.value) {
|
||||||
|
meetingMod.value = { ...meetingsStore.meetingById(meetingId.value) as MeetingParams }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function createMeeting () {
|
const updateMeeting = async (newFiles: File[]) => {
|
||||||
await router.push({ name: 'meetings' })
|
if (!meetingId.value || !meetingMod.value) return
|
||||||
}
|
await meetingsStore.update(meetingId.value, meetingMod.value)
|
||||||
|
await meetingsStore.updateParticipants(meetingId.value, meetingMod.value.participants)
|
||||||
|
if (newFiles.length !== 0) await meetingsStore.attachFiles(meetingId.value, newFiles)
|
||||||
|
router.go(-1)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.bold-input::v-deep .q-field__native {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-input-fix :deep(.q-field__append) {
|
|
||||||
height: auto !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,195 +1,133 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-card v-if="meeting">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center justify-between col-grow">
|
{{ $t('meeting_view__title_card') }}
|
||||||
<div>
|
<q-btn
|
||||||
{{ $t('meeting_add__title') }}
|
v-if="meeting.is_editable"
|
||||||
</div>
|
@click = "editMeeting"
|
||||||
<q-btn
|
flat round
|
||||||
@click = "createMeeting()"
|
icon="edit"
|
||||||
flat round
|
/>
|
||||||
icon="mdi-check"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<pn-scroll-list>
|
<pn-scroll-list>
|
||||||
<div class="flex column items-center q-ma-lg">
|
<div class="flex column items-center q-px-md q-gutter-y-md w100">
|
||||||
<div class="q-gutter-y-lg w100">
|
<div class="flex w100 items-center ellipsis text-caption text-grey no-wrap ellipsis q-mt-none" v-if="meeting.chat_id">
|
||||||
<q-input
|
<div class="flex items-center justify-start w100 no-wrap">
|
||||||
v-model.trim="task.name"
|
<q-icon name="mdi-chat-outline" class="q-mr-xs"/>
|
||||||
dense
|
<span class="ellipsis">{{ chatsStore.chatById(meeting.chat_id)?.name }}</span>
|
||||||
filled
|
|
||||||
autogrow
|
|
||||||
:label = "$t('task_add__name')"
|
|
||||||
class="bold-input w100"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-input
|
|
||||||
v-model.trim="task.description"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
autogrow
|
|
||||||
class = "w100"
|
|
||||||
:label = "$t('task_add__description')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-file
|
|
||||||
v-model="task.files"
|
|
||||||
:label="$t('task_add__attach_files')"
|
|
||||||
outlined
|
|
||||||
use-chips
|
|
||||||
multiple
|
|
||||||
dense
|
|
||||||
class="file-input-fix"
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<q-icon name="attach_file"/>
|
|
||||||
</template>
|
|
||||||
</q-file>
|
|
||||||
|
|
||||||
<q-select
|
|
||||||
v-if="companies"
|
|
||||||
v-model="task.company"
|
|
||||||
:options="companies"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class="w100"
|
|
||||||
:label = "$t('task_add__assigned_to')"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-account-arrow-left-outline"/>
|
|
||||||
</template>
|
|
||||||
<template #option="scope">
|
|
||||||
<q-item v-bind="scope.itemProps">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar rounded size="md">
|
|
||||||
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
|
|
||||||
<pn-auto-avatar v-else :name="scope.opt.name"/>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>{{ scope.opt.name }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
<template #selected>
|
|
||||||
{{ JSON.parse(JSON.stringify(task.company)).name }}
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
|
|
||||||
<q-select
|
|
||||||
v-if="companies"
|
|
||||||
v-model="task.company"
|
|
||||||
:options="companies"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class="w100"
|
|
||||||
:label = "$t('task_add__watch')"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-account-eye-outline"/>
|
|
||||||
</template>
|
|
||||||
<template #option="scope">
|
|
||||||
<q-item v-bind="scope.itemProps">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar rounded size="md">
|
|
||||||
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
|
|
||||||
<pn-auto-avatar v-else :name="scope.opt.name"/>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>{{ scope.opt.name }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
<template #selected>
|
|
||||||
{{ JSON.parse(JSON.stringify(task.company)).name }}
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
|
|
||||||
<q-input filled v-model="task.date" dense :label="$t('task_add__plan_date')">
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="event" class="cursor-pointer">
|
|
||||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
|
||||||
<q-date v-model="task.date" mask="YYYY-MM-DD HH:mm">
|
|
||||||
<div class="row items-center justify-end">
|
|
||||||
<q-btn v-close-popup label="Close" color="primary" flat></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-date>
|
|
||||||
</q-popup-proxy>
|
|
||||||
</q-icon>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #append>
|
|
||||||
<q-icon name="access_time" class="cursor-pointer">
|
|
||||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
|
||||||
<q-time v-model="task.date" mask="YYYY-MM-DD HH:mm" format24h>
|
|
||||||
<div class="row items-center justify-end">
|
|
||||||
<q-btn v-close-popup label="Close" color="primary" flat></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-time>
|
|
||||||
</q-popup-proxy>
|
|
||||||
</q-icon>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<div class="flex column">
|
|
||||||
|
|
||||||
<q-item-label header class="q-py-none q-my-none">
|
|
||||||
{{$t('task_add__priority')}}
|
|
||||||
</q-item-label>
|
|
||||||
|
|
||||||
<q-btn-toggle
|
|
||||||
v-model="task.priority"
|
|
||||||
no-caps
|
|
||||||
toggle-color="primary"
|
|
||||||
spread
|
|
||||||
unelevated
|
|
||||||
:options="priority"
|
|
||||||
class="q-mt-sm w100"
|
|
||||||
>
|
|
||||||
<template v-for="item in priority" :key="item.id" #[item.slot]>
|
|
||||||
<div class="row items-center no-wrap gap-xs text-weight-regular">
|
|
||||||
<span>
|
|
||||||
{{ $t(item.translationKey) }}
|
|
||||||
</span>
|
|
||||||
<pn-task-priority-icon :priority="item.value"/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</q-btn-toggle>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<q-btn
|
||||||
<q-select
|
@click="goChat"
|
||||||
v-if="companies"
|
flat rounded no-caps dense
|
||||||
v-model="task.company"
|
color="primary"
|
||||||
:options="companies"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class="w100"
|
|
||||||
:label = "$t('meeting_add__attached_chat')"
|
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<div class="flex no-wrap items-center text-caption">
|
||||||
<q-icon name="mdi-chat-outline"/>
|
<span class="q-ml-sm">{{$t('meeting_view__go_to_chat')}}</span>
|
||||||
</template>
|
<q-icon name="mdi-chevron-right"/>
|
||||||
<template #option="scope">
|
</div>
|
||||||
<q-item v-bind="scope.itemProps">
|
</q-btn>
|
||||||
<q-item-section avatar>
|
</div>
|
||||||
<q-avatar rounded size="md">
|
|
||||||
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
|
<div class="w100" style="border-bottom: 1px solid #eee;">
|
||||||
<pn-auto-avatar v-else :name="scope.opt.name"/>
|
<div class="flex w100 justify-between text-h6 text-bold">
|
||||||
</q-avatar>
|
<div>
|
||||||
</q-item-section>
|
<span class="text-caption text-grey">
|
||||||
<q-item-section>
|
{{ date.formatDate(meeting.meet_date * 1000, 'ddd') }}
|
||||||
<q-item-label>{{ scope.opt.name }}</q-item-label>
|
</span>
|
||||||
</q-item-section>
|
{{ date.formatDate(meeting.meet_date * 1000, 'DD MMMM') }}
|
||||||
</q-item>
|
<span v-if="showYear">
|
||||||
</template>
|
{{ date.formatDate(meeting.meet_date * 1000, 'YYYY') }}
|
||||||
<template #selected>
|
</span>
|
||||||
{{ JSON.parse(JSON.stringify(task.company)).name }}
|
</div>
|
||||||
</template>
|
<div>
|
||||||
</q-select>
|
{{ date.formatDate(meeting.meet_date * 1000, 'HH:mm') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w100 items-center text-caption text-grey no-wrap" v-if="meeting.place">
|
||||||
|
<q-icon name="mdi-map-marker-outline" class="q-mr-xs"/>
|
||||||
|
<span>{{ meeting.place }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="text-bold flex w100 self-start"
|
||||||
|
style="white-space: pre-line"
|
||||||
|
>
|
||||||
|
{{ meeting.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="meeting.description"
|
||||||
|
class="flex w100 self-start q-mt-none"
|
||||||
|
style="white-space: pre-line"
|
||||||
|
>
|
||||||
|
{{ meeting.description }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="meeting.files&&meeting.files.length!==0"
|
||||||
|
class="flex w100 no-wrap items-start"
|
||||||
|
>
|
||||||
|
<div class="flex column w100">
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in meeting.files"
|
||||||
|
:key="idx"
|
||||||
|
class="flex items-center text-caption"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
:name="getFileIcon(item).icon"
|
||||||
|
:style="{color: getFileIcon(item).color}"
|
||||||
|
size="sm"
|
||||||
|
class="q-mr-sm"
|
||||||
|
/>
|
||||||
|
{{ filesStore.fileById(item)?.filename }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="meeting.participants && meeting.participants.length!==0"
|
||||||
|
class="flex w100 no-wrap items-center"
|
||||||
|
>
|
||||||
|
<div class="flex w100 column">
|
||||||
|
<div class="flex row no-wrap q-mb-sm">
|
||||||
|
<q-btn
|
||||||
|
flat dense rounded
|
||||||
|
@click="isParticipantsShow = !isParticipantsShow"
|
||||||
|
>
|
||||||
|
<pn-chain-avatar :users="meeting.participants" :overlap=10 :maxDisplayUsers=3 />
|
||||||
|
<q-icon
|
||||||
|
name="mdi-chevron-down"
|
||||||
|
size="sm"
|
||||||
|
color="grey"
|
||||||
|
style="transition: transform 0.3s ease;"
|
||||||
|
:class="{ 'rotate-180': isParticipantsShow}"
|
||||||
|
/>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
<transition
|
||||||
|
appear
|
||||||
|
enter-active-class="animated fadeIn"
|
||||||
|
leave-active-class="animated fadeOut"
|
||||||
|
>
|
||||||
|
<div class="flex column w100 q-ml-xs" v-if="isParticipantsShow">
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in meeting.participants"
|
||||||
|
:key="idx"
|
||||||
|
class="flex items-center q-py-xs cursor-pointer"
|
||||||
|
@click="goUserInfo(item)"
|
||||||
|
>
|
||||||
|
<pn-auto-avatar
|
||||||
|
:img="usersStore.userById(item)?.photo"
|
||||||
|
:name="usersStore.userNameById(item)"
|
||||||
|
size="sm"
|
||||||
|
class="q-mr-sm"
|
||||||
|
/>
|
||||||
|
{{ usersStore.userNameById(item) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
@@ -197,40 +135,74 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { computed, inject, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useMeetingsStore } from 'stores/meetings'
|
||||||
|
import { useChatsStore } from 'stores/chats'
|
||||||
|
import { useUsersStore } from 'stores/users'
|
||||||
|
import { useFilesStore } from 'stores/files'
|
||||||
|
import { fileIcon } from 'helpers/files-functions'
|
||||||
|
import { parseIntString } from 'helpers/helpers'
|
||||||
|
import { date } from 'quasar'
|
||||||
|
import type { WebApp } from '@twa-dev/types'
|
||||||
|
const tg = inject('tg') as WebApp
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const task = ref({id: "p1", name: 'Кирюшкин Андрей', description: 'fdsfdsfsdfs', company: '', priority: 1, department: 'test', files: [], date: new Date(Date.now()).toLocaleString() })
|
const meetingsStore = useMeetingsStore()
|
||||||
const priority = [
|
const meetingId = parseIntString(route.params.meetingId)
|
||||||
{ id: 1, slot: 's1', label: '', translationKey: 'task_add__priority_normal', value: 0 },
|
const meeting = computed(() => meetingId ? meetingsStore.meetingById(meetingId) : null)
|
||||||
{ id: 2, slot: 's2', label: '', translationKey: 'task_add__priority_important', value: 1 },
|
|
||||||
{ id: 3, slot: 's3', label: '', translationKey: 'task_add__priority_critical', value: 2 }
|
|
||||||
]
|
|
||||||
|
|
||||||
const companies = ref([
|
const chatsStore = useChatsStore()
|
||||||
{id: "com11", value: "com11", name: 'Рога и копытца1', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: [] },
|
const usersStore = useUsersStore()
|
||||||
{id: "com21", name: 'ООО "Василек1"', logo: 'https://cdn.quasar.dev/img/avatar5.jpg', qtyPersons: 2, masked: true, unmasked: [] },
|
const filesStore = useFilesStore()
|
||||||
{id: "ch13", name: 'Откат и деньги1', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
|
|
||||||
{id: "ch14", name: 'Откат и деньги2', logo: '', description: 'Договариваются о чем-то', qtyPersons: 5, masked: false, unmasked: [] },
|
|
||||||
{id: "com111", name: 'Рога и копытца2', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: []},
|
|
||||||
{id: "com211", name: 'ООО "Василек2"', logo: '', qtyPersons: 2, masked: true, unmasked: [] },
|
|
||||||
{id: "ch131", name: 'Откат и деньги3', logo: '', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
|
|
||||||
])
|
|
||||||
|
|
||||||
|
const isParticipantsShow = ref<boolean>(false)
|
||||||
|
|
||||||
|
async function editMeeting () {
|
||||||
|
await router.push({ name: 'meeting_edit', params: { meetingId }})
|
||||||
|
}
|
||||||
|
|
||||||
|
const showYear = computed(() =>
|
||||||
|
meeting.value && meeting.value.meet_date &&
|
||||||
|
(date.formatDate(Date.now(), 'YYYY') !==
|
||||||
|
date.formatDate(meeting.value.meet_date * 1000, 'YYYY'))
|
||||||
|
)
|
||||||
|
|
||||||
|
function goChat () {
|
||||||
|
if (meeting.value && meeting.value.chat_id) {
|
||||||
|
const chat = chatsStore.chatById(meeting.value.chat_id)
|
||||||
|
if (chat) {
|
||||||
|
const invite = chat.invite_link
|
||||||
|
tg.openTelegramLink(invite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileIcon (id: number) {
|
||||||
|
const file = filesStore.fileById(id)
|
||||||
|
return file
|
||||||
|
? fileIcon(file.filename)
|
||||||
|
: { color: '', icon: ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function goUserInfo (id: number) {
|
||||||
|
await router.push({ name: 'user_info', params: { userId: id }})
|
||||||
|
}
|
||||||
|
|
||||||
async function createMeeting () {
|
|
||||||
await router.push({ name: 'meetings' })
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.bold-input::v-deep .q-field__native {
|
.fix-bottom-padding.q-field--with-bottom {
|
||||||
font-weight: bold;
|
padding-bottom: 0 !important
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-input-fix :deep(.q-field__append) {
|
.file-input-fix :deep(.q-field__append) {
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-input-fix :deep(.q-field__prepend) {
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,236 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<task-block
|
||||||
<template #title>
|
v-model="newTask"
|
||||||
<div class="flex items-center justify-between col-grow">
|
title="task_create__title_card"
|
||||||
<div>
|
btnText="task_create__btn"
|
||||||
{{ $t('task_add__title') }}
|
@update="addTask"
|
||||||
</div>
|
/>
|
||||||
<q-btn
|
|
||||||
@click = "createTask()"
|
|
||||||
flat round
|
|
||||||
icon="mdi-check"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<pn-scroll-list>
|
|
||||||
<div class="flex column items-center q-ma-lg">
|
|
||||||
<div class="q-gutter-y-lg w100">
|
|
||||||
<q-input
|
|
||||||
v-model.trim="task.name"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
autogrow
|
|
||||||
:label = "$t('task_add__name')"
|
|
||||||
class="bold-input w100"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-input
|
|
||||||
v-model.trim="task.description"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
autogrow
|
|
||||||
class = "w100"
|
|
||||||
:label = "$t('task_add__description')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-file
|
|
||||||
v-model="task.files"
|
|
||||||
:label="$t('task_add__attach_files')"
|
|
||||||
outlined
|
|
||||||
use-chips
|
|
||||||
multiple
|
|
||||||
dense
|
|
||||||
class="file-input-fix"
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<q-icon name="attach_file"/>
|
|
||||||
</template>
|
|
||||||
</q-file>
|
|
||||||
|
|
||||||
<q-select
|
|
||||||
v-if="companies"
|
|
||||||
v-model="task.company"
|
|
||||||
:options="companies"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class="w100"
|
|
||||||
:label = "$t('task_add__assigned_to')"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-account-arrow-left-outline"/>
|
|
||||||
</template>
|
|
||||||
<template #option="scope">
|
|
||||||
<q-item v-bind="scope.itemProps">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar rounded size="md">
|
|
||||||
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
|
|
||||||
<pn-auto-avatar v-else :name="scope.opt.name"/>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>{{ scope.opt.name }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
<template #selected>
|
|
||||||
{{ JSON.parse(JSON.stringify(task.company)).name }}
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
|
|
||||||
<q-select
|
|
||||||
v-if="companies"
|
|
||||||
v-model="task.company"
|
|
||||||
:options="companies"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class="w100"
|
|
||||||
:label = "$t('task_add__watch')"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-account-eye-outline"/>
|
|
||||||
</template>
|
|
||||||
<template #option="scope">
|
|
||||||
<q-item v-bind="scope.itemProps">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar rounded size="md">
|
|
||||||
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
|
|
||||||
<pn-auto-avatar v-else :name="scope.opt.name"/>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>{{ scope.opt.name }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
<template #selected>
|
|
||||||
{{ JSON.parse(JSON.stringify(task.company)).name }}
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
|
|
||||||
<q-input filled v-model="task.date" dense :label="$t('task_add__plan_date')">
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="event" class="cursor-pointer">
|
|
||||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
|
||||||
<q-date v-model="task.date" mask="YYYY-MM-DD HH:mm">
|
|
||||||
<div class="row items-center justify-end">
|
|
||||||
<q-btn v-close-popup label="Close" color="primary" flat></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-date>
|
|
||||||
</q-popup-proxy>
|
|
||||||
</q-icon>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #append>
|
|
||||||
<q-icon name="access_time" class="cursor-pointer">
|
|
||||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
|
||||||
<q-time v-model="task.date" mask="YYYY-MM-DD HH:mm" format24h>
|
|
||||||
<div class="row items-center justify-end">
|
|
||||||
<q-btn v-close-popup label="Close" color="primary" flat></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-time>
|
|
||||||
</q-popup-proxy>
|
|
||||||
</q-icon>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<div class="flex column">
|
|
||||||
|
|
||||||
<q-item-label header class="q-py-none q-my-none">
|
|
||||||
{{$t('task_add__priority')}}
|
|
||||||
</q-item-label>
|
|
||||||
|
|
||||||
<q-btn-toggle
|
|
||||||
v-model="task.priority"
|
|
||||||
no-caps
|
|
||||||
toggle-color="primary"
|
|
||||||
spread
|
|
||||||
unelevated
|
|
||||||
:options="priority"
|
|
||||||
class="q-mt-sm w100"
|
|
||||||
>
|
|
||||||
<template v-for="item in priority" :key="item.id" #[item.slot]>
|
|
||||||
<div class="row items-center no-wrap gap-xs text-weight-regular">
|
|
||||||
<span>
|
|
||||||
{{ $t(item.translationKey) }}
|
|
||||||
</span>
|
|
||||||
<pn-task-priority-icon :priority="item.value"/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</q-btn-toggle>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-select
|
|
||||||
v-if="companies"
|
|
||||||
v-model="task.company"
|
|
||||||
:options="companies"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class="w100"
|
|
||||||
:label = "$t('task_add__attached_chat')"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-chat-outline"/>
|
|
||||||
</template>
|
|
||||||
<template #option="scope">
|
|
||||||
<q-item v-bind="scope.itemProps">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar rounded size="md">
|
|
||||||
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
|
|
||||||
<pn-auto-avatar v-else :name="scope.opt.name"/>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>{{ scope.opt.name }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
<template #selected>
|
|
||||||
{{ JSON.parse(JSON.stringify(task.company)).name }}
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</pn-scroll-list>
|
|
||||||
</pn-page-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import taskBlock from 'components/taskBlock.vue'
|
||||||
|
import { useTasksStore } from 'stores/tasks'
|
||||||
|
import type { TaskParams } from 'types/Task'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const tasksStore = useTasksStore()
|
||||||
|
|
||||||
const task = ref({id: "p1", name: 'Кирюшкин Андрей', description: 'fdsfdsfsdfs', company: '', priority: 1, department: 'test', files: [], date: new Date(Date.now()).toLocaleString() })
|
const newTask = ref(<TaskParams>{
|
||||||
const priority = [
|
name: '',
|
||||||
{ id: 1, slot: 's1', label: '', translationKey: 'task_add__priority_normal', value: 0 },
|
description: '',
|
||||||
{ id: 2, slot: 's2', label: '', translationKey: 'task_add__priority_important', value: 1 },
|
assigned_to: null,
|
||||||
{ id: 3, slot: 's3', label: '', translationKey: 'task_add__priority_critical', value: 2 }
|
priority: 0,
|
||||||
]
|
status: 1,
|
||||||
|
plan_date: (Date.now() + 1000*60*60*24) / 1000, // fix incorrect start date and time -> move 24h
|
||||||
const companies = ref([
|
observers: [],
|
||||||
{id: "com11", value: "com11", name: 'Рога и копытца1', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: [] },
|
files: [],
|
||||||
{id: "com21", name: 'ООО "Василек1"', logo: 'https://cdn.quasar.dev/img/avatar5.jpg', qtyPersons: 2, masked: true, unmasked: [] },
|
chat_id: null,
|
||||||
{id: "ch13", name: 'Откат и деньги1', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
|
close_files: [],
|
||||||
{id: "ch14", name: 'Откат и деньги2', logo: '', description: 'Договариваются о чем-то', qtyPersons: 5, masked: false, unmasked: [] },
|
close_comment: '',
|
||||||
{id: "com111", name: 'Рога и копытца2', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: []},
|
})
|
||||||
{id: "com211", name: 'ООО "Василек2"', logo: '', qtyPersons: 2, masked: true, unmasked: [] },
|
|
||||||
{id: "ch131", name: 'Откат и деньги3', logo: '', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
|
async function addTask () {
|
||||||
])
|
await tasksStore.add(newTask.value)
|
||||||
|
router.go(-1)
|
||||||
|
|
||||||
async function createTask () {
|
|
||||||
await router.push({ name: 'tasks' })
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.bold-input::v-deep .q-field__native {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-input-fix :deep(.q-field__append) {
|
</script>
|
||||||
height: auto !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
34
src/pages/TaskEditPage.vue
Normal file
34
src/pages/TaskEditPage.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<task-block
|
||||||
|
v-model="taskMod"
|
||||||
|
title="task_edit__title_card"
|
||||||
|
btnText="task_edit__btn"
|
||||||
|
@update=updateTask()
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import taskBlock from 'components/taskBlock.vue'
|
||||||
|
import { useTasksStore } from 'stores/tasks'
|
||||||
|
import type { TaskParams } from 'types/Task'
|
||||||
|
import { parseIntString } from 'src/helpers/helpers'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const tasksStore = useTasksStore()
|
||||||
|
const taskId = parseIntString(route.params.taskId)
|
||||||
|
const initialTask = taskId && tasksStore.taskById(taskId)
|
||||||
|
|
||||||
|
const taskMod = ref({
|
||||||
|
...initialTask
|
||||||
|
} as TaskParams)
|
||||||
|
|
||||||
|
async function updateTask () {
|
||||||
|
if (!taskId || !initialTask) return
|
||||||
|
await tasksStore.update(taskId, taskMod.value)
|
||||||
|
router.go(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -1,93 +1,327 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-card v-if="task">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center justify-between col-grow">
|
{{ $t('task_view__title_card') }}
|
||||||
<div>
|
<q-btn
|
||||||
{{ $t('settings__title') }}
|
v-if="task.is_editable"
|
||||||
|
@click = "editTask"
|
||||||
|
flat round
|
||||||
|
icon="edit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<pn-scroll-list>
|
||||||
|
<div class="flex column items-center q-pa-md q-gutter-y-md w100">
|
||||||
|
<div class="flex w100 items-center ellipsis text-caption text-grey no-wrap ellipsis q-mt-none" v-if="task.chat_id">
|
||||||
|
<div class="flex items-center justify-start w100 no-wrap">
|
||||||
|
<q-icon name="mdi-chat-outline" class="q-mr-xs"/>
|
||||||
|
<span class="ellipsis">{{ chatsStore.chatById(task.chat_id)?.name }}</span>
|
||||||
|
</div>
|
||||||
|
<q-btn
|
||||||
|
@click="goChat"
|
||||||
|
flat rounded no-caps dense
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<div class="flex no-wrap items-center text-caption">
|
||||||
|
<span class="q-ml-sm">{{$t('meeting_view__go_to_chat')}}</span>
|
||||||
|
<q-icon name="mdi-chevron-right"/>
|
||||||
|
</div>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w100" style="border-bottom: 1px solid #eee;">
|
||||||
|
<div class="flex w100 justify-between text-caption">
|
||||||
|
<div class="flex items-baseline text-bold">
|
||||||
|
<q-icon name="mdi-clock-outline" class="q-mr-xs"/>
|
||||||
|
<span class="q-mr-sm text-weight-regular">
|
||||||
|
{{ date.formatDate(task.plan_date * 1000, 'ddd')}}
|
||||||
|
</span>
|
||||||
|
<div class="text-body1 flex items-end">
|
||||||
|
{{ date.formatDate(task.plan_date * 1000, !showYear ? 'DD MMMM' : 'DD MMMM YYYY') }}
|
||||||
|
<span class="q-ml-xs">
|
||||||
|
{{ date.formatDate(task.plan_date * 1000, 'HH:mm') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pn-task-priority-icon v-if="task.priority" :priority="task.priority" label/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center q-my-sm">
|
||||||
|
<div
|
||||||
|
v-if="task.created_by !== usersStore.myId.id"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<pn-auto-avatar
|
||||||
|
:img="usersStore.userById(task.created_by)?.photo"
|
||||||
|
:name="usersStore.userNameById(task.created_by)"
|
||||||
|
size="sm"
|
||||||
|
class="q-mr-xs"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ usersStore.userNameById(task.created_by) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span v-else>{{$t('task_item__from_me')}}</span>
|
||||||
|
<q-icon name="mdi-chevron-right" size="xs" class="text-grey"/>
|
||||||
|
<div
|
||||||
|
v-if="task.assigned_to && task.assigned_to !== usersStore.myId.id"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<pn-auto-avatar
|
||||||
|
:img="usersStore.userById(task.assigned_to)?.photo"
|
||||||
|
:name="usersStore.userNameById(task.assigned_to)"
|
||||||
|
size="sm"
|
||||||
|
class="q-mr-xs"
|
||||||
|
/>
|
||||||
|
{{ usersStore.userNameById(task.assigned_to) }}
|
||||||
|
</div>
|
||||||
|
<span v-else-if="task.created_by !== task.assigned_to">{{$t('task_item__to_me')}}</span>
|
||||||
|
<span v-else>{{$t('task_item__to_me_from_me')}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-bold flex w100 self-start">
|
||||||
|
{{ task.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="task.description"
|
||||||
|
class="flex w100 self-start"
|
||||||
|
style="white-space: pre-line"
|
||||||
|
>
|
||||||
|
{{ task.description }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="task.files&&task.files.length!==0"
|
||||||
|
class="flex w100 no-wrap items-start"
|
||||||
|
>
|
||||||
|
<q-icon name="attach_file" class="q-mr-xs"/>
|
||||||
|
<div class="flex column w100">
|
||||||
|
<span
|
||||||
|
v-for="(item, idx) in task.files"
|
||||||
|
:key="idx"
|
||||||
|
class="text-caption q-pl-sm"
|
||||||
|
style="border-left: solid 1px grey"
|
||||||
|
>
|
||||||
|
{{ filesStore.fileById(item)?.filename }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="task.files&&task.files.length!==0"
|
||||||
|
class="flex w100 no-wrap items-start"
|
||||||
|
>
|
||||||
|
<div class="flex column w100">
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in task.files"
|
||||||
|
:key="idx"
|
||||||
|
class="flex items-center text-caption"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
:name="getFileIcon(item).icon"
|
||||||
|
:style="{color: getFileIcon(item).color}"
|
||||||
|
size="sm"
|
||||||
|
class="q-mr-sm"
|
||||||
|
/>
|
||||||
|
{{ filesStore.fileById(item)?.filename }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
|
||||||
<pn-scroll-list>
|
|
||||||
<q-list separator>
|
|
||||||
<q-item>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar color="primary" rounded text-color="white" icon="mdi-translate" size="md" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<span>{{ $t('settings__language') }}</span>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-select
|
|
||||||
class="fix-input-right text-body1"
|
|
||||||
v-model="locale"
|
|
||||||
:options="localeOptions"
|
|
||||||
dense
|
|
||||||
borderless
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
hide-bottom-space
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar color="primary" rounded text-color="white" icon="mdi-format-size" size="md" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<span>{{ $t('settings__font_size') }}</span>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<q-btn
|
|
||||||
@click="textSizeStore.decreaseFontSize()"
|
|
||||||
color="negative" flat
|
|
||||||
icon="mdi-format-font-size-decrease"
|
|
||||||
class="q-pa-sm q-mx-xs"
|
|
||||||
:disable="currentTextSize <= minTextSize"
|
|
||||||
/>
|
|
||||||
<q-btn
|
|
||||||
@click="textSizeStore.increaseFontSize()"
|
|
||||||
color="positive" flat
|
|
||||||
icon="mdi-format-font-size-increase"
|
|
||||||
class="q-pa-sm q-mx-xs"
|
|
||||||
:disable="currentTextSize >= maxTextSize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex no-wrap w100 justify-center q-gutter-x-md q-mt-md q-mb-xs">
|
||||||
|
<q-btn
|
||||||
|
:label="$t('task_view__btn_cancel_task')"
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
rounded
|
||||||
|
class="w50"
|
||||||
|
@click="showCloseTaskDialog = true; typeCloseTaskDialog = 'cancel'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
:label="$t('task_view__btn_close_task')"
|
||||||
|
color="primary"
|
||||||
|
rounded
|
||||||
|
class="w50"
|
||||||
|
@click="showCloseTaskDialog = true; typeCloseTaskDialog = 'done'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</pn-page-card>
|
</pn-page-card>
|
||||||
|
<pn-bottom-sheet-dialog
|
||||||
|
:title="$t(closeTaskDialog.title)"
|
||||||
|
v-model="showCloseTaskDialog"
|
||||||
|
>
|
||||||
|
<div class="w100 q-gutter-y-lg flex column">
|
||||||
|
<q-input
|
||||||
|
v-model="closeComment.comment"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
autogrow
|
||||||
|
autofocus
|
||||||
|
:rules="[rules.comment]"
|
||||||
|
class = "w100 fix-bottom-padding q-pt-sm"
|
||||||
|
no-error-icon
|
||||||
|
label-slot
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
{{ $t( closeTaskDialog.commentLabel) }}
|
||||||
|
<span class="text-red">*</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<pn-file-uploader
|
||||||
|
v-model:exist-files ="closeComment.existFiles"
|
||||||
|
v-model:new-files ="closeComment.newFiles"
|
||||||
|
:label="$t('task_view__dialog_task_files')"
|
||||||
|
class="q-pt-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<q-btn
|
||||||
|
rounded
|
||||||
|
class="w100"
|
||||||
|
:color="closeTaskDialog.color"
|
||||||
|
@click="closeTask"
|
||||||
|
>
|
||||||
|
{{$t(closeTaskDialog.btnText)}}
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
</pn-bottom-sheet-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, inject, ref, watch } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useTasksStore } from 'stores/tasks'
|
||||||
|
import { useChatsStore } from 'stores/chats'
|
||||||
|
import { useUsersStore } from 'stores/users'
|
||||||
|
import { useFilesStore } from 'stores/files'
|
||||||
|
import { fileIcon } from 'helpers/files-functions'
|
||||||
|
import { parseIntString } from 'helpers/helpers'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { watch, ref } from 'vue'
|
import { date } from 'quasar'
|
||||||
import { useTextSizeStore } from 'src/stores/textSize'
|
import type { WebApp } from '@twa-dev/types'
|
||||||
|
import type { TaskParams } from 'types/Task'
|
||||||
|
const tg = inject('tg') as WebApp
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const { locale } = useI18n()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
const savedLocale = localStorage.getItem('locale') || 'en-US'
|
|
||||||
locale.value = savedLocale
|
|
||||||
|
|
||||||
const localeOptions = ref([
|
const tasksStore = useTasksStore()
|
||||||
{ value: 'en-US', label: 'English' },
|
const taskId = parseIntString(route.params.taskId)
|
||||||
{ value: 'ru-RU', label: 'Русский' }
|
const task = computed(() => taskId && tasksStore.taskById(taskId))
|
||||||
])
|
|
||||||
|
|
||||||
watch(locale, (newLocale) => {
|
const chatsStore = useChatsStore()
|
||||||
localStorage.setItem('locale', newLocale)
|
const usersStore = useUsersStore()
|
||||||
|
const filesStore = useFilesStore()
|
||||||
|
|
||||||
|
async function editTask () {
|
||||||
|
await router.push({ name: 'task_edit', params: { taskId }})
|
||||||
|
}
|
||||||
|
|
||||||
|
const showYear = computed(() =>
|
||||||
|
task.value && task.value.plan_date &&
|
||||||
|
(date.formatDate(Date.now(), 'YYYY') !==
|
||||||
|
date.formatDate(task.value.plan_date * 1000, 'YYYY'))
|
||||||
|
)
|
||||||
|
|
||||||
|
function goChat () {
|
||||||
|
if (task.value && task.value.chat_id) {
|
||||||
|
const chat = chatsStore.chatById(task.value.chat_id)
|
||||||
|
if (chat) {
|
||||||
|
const invite = chat.invite_link
|
||||||
|
tg.openTelegramLink(invite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileIcon (id: number) {
|
||||||
|
const file = filesStore.fileById(id)
|
||||||
|
return file
|
||||||
|
? fileIcon(file.filename)
|
||||||
|
: { color: '', icon: ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showCloseTaskDialog = ref(false)
|
||||||
|
const typeCloseTaskDialog = ref<'done'| 'cancel' | null>(null)
|
||||||
|
const closeTaskDialog = computed(() =>{
|
||||||
|
switch (typeCloseTaskDialog.value) {
|
||||||
|
case 'done':
|
||||||
|
return {
|
||||||
|
title: 'task_view__dialog_task_done_title',
|
||||||
|
commentLabel: 'task_view__dialog_task_done_comment',
|
||||||
|
btnText: 'task_view__dialog_task_done_btn',
|
||||||
|
color: 'primary'
|
||||||
|
}
|
||||||
|
case 'cancel':
|
||||||
|
return {
|
||||||
|
title: 'task_view__dialog_task_cancel_title',
|
||||||
|
commentLabel: 'task_view__dialog_task_cancel_comment',
|
||||||
|
btnText: 'task_view__dialog_task_cancel_btn',
|
||||||
|
color: 'negative'
|
||||||
|
}
|
||||||
|
case null:
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
title: '',
|
||||||
|
commentLabel: '',
|
||||||
|
btnText: '',
|
||||||
|
color: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const textSizeStore = useTextSizeStore()
|
interface Comment {
|
||||||
const currentTextSize = textSizeStore.currentFontSize
|
comment: string
|
||||||
const maxTextSize = textSizeStore.maxFontSize
|
existFiles: File[]
|
||||||
const minTextSize = textSizeStore.minFontSize
|
newFiles: File[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultCloseComment = {
|
||||||
|
comment: '',
|
||||||
|
existFiles: [],
|
||||||
|
newFiles: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeComment = ref<Comment>({ ...defaultCloseComment })
|
||||||
|
|
||||||
|
function closeTask () {
|
||||||
|
showCloseTaskDialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch (showCloseTaskDialog, () => closeComment.value = { ...defaultCloseComment })
|
||||||
|
|
||||||
|
const rulesErrorMessage = {
|
||||||
|
comment: t('task_view__error_comment')
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
comment: (val: TaskParams['close_comment']) => !!val?.trim() || rulesErrorMessage['comment']
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.fix-input-right :deep(.q-field__native) {
|
.fix-bottom-padding.q-field--with-bottom {
|
||||||
justify-content: end;
|
padding-bottom: 0 !important
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-fix :deep(.q-field__append) {
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-fix :deep(.q-field__prepend) {
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fix-card-width {
|
||||||
|
width: var(--body-width) !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,37 +2,35 @@
|
|||||||
<pn-page-card>
|
<pn-page-card>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center justify-between col-grow">
|
<div class="flex items-center justify-between col-grow">
|
||||||
<div>
|
{{ $t('user_card__title') }}
|
||||||
{{ $t('user_card__title') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<pn-scroll-list>
|
<pn-scroll-list>
|
||||||
<div
|
<div
|
||||||
v-if="user"
|
v-if="user"
|
||||||
class="flex column items-center q-pa-lg"
|
class="flex column items-center q-pa-md q-pb-sm"
|
||||||
>
|
>
|
||||||
|
<pn-auto-avatar
|
||||||
<q-avatar size="100px">
|
:img="user.photo"
|
||||||
<q-img v-if="user.photo" :src="user.photo"/>
|
:name="tname"
|
||||||
<pn-auto-avatar v-else :name="tname"/>
|
size="100px"
|
||||||
</q-avatar>
|
class="q-mr-sm"
|
||||||
|
/>
|
||||||
<div class="flex row items-start justify-center no-wrap q-pb-lg">
|
<div class="flex row items-start justify-center no-wrap q-pb-lg">
|
||||||
<div class="flex column justify-center">
|
<div class="flex column justify-center">
|
||||||
<div class="text-bold q-pr-xs text-center" align="center" v-if="tname">{{ tname }}</div>
|
<div class="text-bold q-pr-xs text-center" align="center" v-if="tname">{{ tname }}</div>
|
||||||
<div caption class="text-blue text-caption" align="center" v-if="user.username">@{{ user.username }}</div>
|
<div caption class="text-blue text-caption" align="center" v-if="user.username">@{{ user.username }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-around w100 q-pb-lg">
|
<div class="flex justify-around w100 q-pb-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-for="item in userActions"
|
v-for="item in userActions"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@click="item.f"
|
@click="item.f()"
|
||||||
round
|
round
|
||||||
:icon="item.icon"
|
:icon="item.icon"
|
||||||
|
:disable = "item.disable"
|
||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,6 +42,7 @@
|
|||||||
v-show="item"
|
v-show="item"
|
||||||
:model-value="displayUser[key]"
|
:model-value="displayUser[key]"
|
||||||
dense
|
dense
|
||||||
|
readonly
|
||||||
filled
|
filled
|
||||||
class="w100"
|
class="w100"
|
||||||
:label = "$t('user_card__' + key)"
|
:label = "$t('user_card__' + key)"
|
||||||
@@ -60,21 +59,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed, inject } from 'vue'
|
import { computed, inject } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useUsersStore } from 'stores/users'
|
import { useUsersStore } from 'stores/users'
|
||||||
import type { User } from 'types/User'
|
import { parseIntString } from 'src/helpers/helpers'
|
||||||
import { parseIntString } from 'boot/helpers'
|
|
||||||
import type { WebApp } from '@twa-dev/types'
|
import type { WebApp } from '@twa-dev/types'
|
||||||
|
|
||||||
const tg = inject('tg') as WebApp
|
const tg = inject('tg') as WebApp
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const usersStore = useUsersStore()
|
const usersStore = useUsersStore()
|
||||||
|
|
||||||
const user = ref<User>()
|
|
||||||
const userId = parseIntString(route.params.userId)
|
const userId = parseIntString(route.params.userId)
|
||||||
|
const user = computed(() => userId && usersStore.userById(userId))
|
||||||
|
|
||||||
const tname = computed(() => {
|
const tname = computed(() => {
|
||||||
return (!user.value)
|
return (!user.value)
|
||||||
@@ -101,23 +98,11 @@
|
|||||||
position: userPosition.value
|
position: userPosition.value
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const userActions = [
|
const userActions = computed(() =>[
|
||||||
{ id: 0, icon: 'mdi-chat-outline', f: messageUser },
|
{ id: 0, icon: 'mdi-chat-outline', f: messageUser, disable: !(user.value && user.value.username) },
|
||||||
{ id: 1, icon: 'mdi-phone-outline', f: callUser },
|
{ id: 1, icon: 'mdi-phone-outline', f: callUser, disable: tg.platform !== 'ios' && tg.platform !== 'android' },
|
||||||
{ id: 2, icon: 'mdi-share-variant-outline', f: shareUser }
|
{ id: 2, icon: 'mdi-share-variant-outline', f: shareUser, disable: false }
|
||||||
]
|
])
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (userId && usersStore.userById(userId)) {
|
|
||||||
user.value = usersStore.userById(userId)
|
|
||||||
} else {
|
|
||||||
await abort()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function abort () {
|
|
||||||
await router.replace({ name: 'files' })
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateVCard () {
|
function generateVCard () {
|
||||||
if (!user.value) return ''
|
if (!user.value) return ''
|
||||||
@@ -137,7 +122,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function messageUser () {
|
function messageUser () {
|
||||||
const telegramUrl = 'https://t.me/' + (!user.value ? '' : (user.value.username ?? user.value.telegram_id))
|
console.log((!user.value ? '' : (user.value.username)))
|
||||||
|
const telegramUrl = 'https://t.me/' + (!user.value ? '' : (user.value.username ?? undefined))
|
||||||
|
|
||||||
if (tg?.platform !== 'unknown') {
|
if (tg?.platform !== 'unknown') {
|
||||||
tg?.openLink(telegramUrl, { try_instant_view: true })
|
tg?.openLink(telegramUrl, { try_instant_view: true })
|
||||||
@@ -147,21 +133,37 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function callUser () {
|
function callUser () {
|
||||||
const telegramUrl = 'tg://call?peer_id=' + (user.value ? user.value.telegram_id : '')
|
if (user.value) window.open('tel:' + normalizePhoneString(user.value.phone))
|
||||||
|
|
||||||
if (tg?.platform !== 'unknown') {
|
|
||||||
tg?.openLink(telegramUrl, { try_instant_view: true })
|
|
||||||
} else {
|
|
||||||
window.open(telegramUrl, '_blank')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function shareUser () {
|
function shareUser () {
|
||||||
|
tg.openTelegramLink('tg://resolve?domain=ready_or_not_2025_bot&startchannel&admin=')
|
||||||
// не работает, отправляет текст
|
// не работает, отправляет текст
|
||||||
const tgShareUrl = 'https://t.me/share/url?url= &text=' + encodeURIComponent(generateVCard())
|
// const tgShareUrl = 'https://t.me/share/url?url= &text=' + encodeURIComponent(generateVCard())
|
||||||
tg.openTelegramLink(tgShareUrl)
|
// tg.openTelegramLink(tgShareUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePhoneString (input: string): string | null {
|
||||||
|
const phonePattern = /\+?[\d\s\-()]+/g
|
||||||
|
const matches = input.match(phonePattern) || []
|
||||||
|
|
||||||
|
for (const match of matches) {
|
||||||
|
let cleaned = match.replace(/[^\d+]/g, '')
|
||||||
|
const digitCount = cleaned.replace(/\D/g, '').length
|
||||||
|
if (digitCount < 5) continue
|
||||||
|
|
||||||
|
if (cleaned.startsWith('+')) {
|
||||||
|
cleaned = '+' + cleaned.slice(1).replace(/\D/g, '')
|
||||||
|
} else {
|
||||||
|
cleaned = cleaned.replace(/\D/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -27,10 +27,11 @@
|
|||||||
@click="goChat(item.invite_link)"
|
@click="goChat(item.invite_link)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar rounded>
|
<pn-auto-avatar
|
||||||
<q-img v-if="item.logo" :src="item.logo"/>
|
:img="item.logo"
|
||||||
<pn-auto-avatar v-else :name="item.name"/>
|
:name="item.name"
|
||||||
</q-avatar>
|
type="rounded"
|
||||||
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label lines="1" class="text-bold">
|
<q-item-label lines="1" class="text-bold">
|
||||||
@@ -64,12 +65,12 @@
|
|||||||
const search = ref('')
|
const search = ref('')
|
||||||
const chatsStore = useChatsStore()
|
const chatsStore = useChatsStore()
|
||||||
|
|
||||||
const chats = computed(() => chatsStore.chats)
|
const chats = chatsStore.getChats
|
||||||
|
|
||||||
const displayChats = computed(() => {
|
const displayChats = computed(() => {
|
||||||
if (!search.value || !(search.value && search.value.trim())) return chats.value
|
if (!search.value || !(search.value && search.value.trim())) return chats
|
||||||
const searchValue = search.value.trim().toLowerCase()
|
const searchValue = search.value.trim().toLowerCase()
|
||||||
const arrOut = chats.value
|
const arrOut = chats
|
||||||
.filter(el =>
|
.filter(el =>
|
||||||
(el.name && el.name.toLowerCase().includes(searchValue)) ||
|
(el.name && el.name.toLowerCase().includes(searchValue)) ||
|
||||||
(el.description && el.description.toLowerCase().includes(searchValue))
|
(el.description && el.description.toLowerCase().includes(searchValue))
|
||||||
|
|||||||
@@ -73,20 +73,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-slide-transition>
|
</q-slide-transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<q-list separator>
|
<q-list separator>
|
||||||
<q-item
|
<q-item
|
||||||
v-for="item in displayFiles"
|
v-for="item in displayFiles"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:clickable="false"
|
@click="goFile(item)"
|
||||||
|
v-ripple
|
||||||
|
clickable
|
||||||
>
|
>
|
||||||
<q-item-section avatar class="items-center" >
|
<q-item-section avatar class="items-center" >
|
||||||
<q-avatar rounded>
|
<q-avatar>
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="fileIcon(item.filename).icon"
|
:name="fileIcon(item.filename).icon"
|
||||||
:style="{color: fileIcon(item.filename).color}"
|
:style="{color: fileIcon(item.filename).color}"
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
</q-avatar>
|
</q-avatar>
|
||||||
|
<span
|
||||||
|
class="text-caption text-grey"
|
||||||
|
style="line-height: 0.25rem;"
|
||||||
|
>
|
||||||
|
{{ parseFileName(item.filename).ext }}
|
||||||
|
</span>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label lines="1" class="text-bold">
|
<q-item-label lines="1" class="text-bold">
|
||||||
@@ -97,25 +106,25 @@
|
|||||||
<div class="second-line-item flex no-wrap items-center q-mr-sm">
|
<div class="second-line-item flex no-wrap items-center q-mr-sm">
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="item.parent_type === 0
|
:name="item.parent_type === 0
|
||||||
? 'mdi-message-outline'
|
? 'mdi-chat-outline'
|
||||||
: item.parent_type === 1
|
: item.parent_type === 1
|
||||||
? 'mdi-clipboard-outline'
|
? 'mdi-clipboard-outline'
|
||||||
: 'mdi-calendar-month-outline'
|
: 'mdi-calendar-month-outline'
|
||||||
"
|
"
|
||||||
class="q-mr-none"
|
class="q-mr-xs"
|
||||||
/>
|
/>
|
||||||
<span class="ellipsis">{{ fileFrom(item.chat_id) }}</span>
|
<div class="ellipsis">{{ fileFrom(item) }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="second-line-item flex no-wrap items-center">
|
<div class="second-line-item flex no-wrap items-center">
|
||||||
<q-icon name="mdi-account-outline" class="q-mr-none"/>
|
<q-icon name="mdi-account-outline" class="q-mr-xs"/>
|
||||||
<span class="ellipsis">{{ fileBy(item.published_by) }}</span>
|
<div class="ellipsis">{{ usersStore.userNameById(item.published_by) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label caption lines="1">
|
<q-item-label caption lines="1">
|
||||||
<div class = "flex justify-between items-center">
|
<div class = "flex justify-between items-center text-caption">
|
||||||
<span>{{ fileSize(item.size) }}</span>
|
<span>{{ fileSize(item.size, $t) }}</span>
|
||||||
<span>{{ fileDate(item.published) }}</span>
|
<span>{{ fileDate(item.published) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
@@ -124,122 +133,126 @@
|
|||||||
</q-list>
|
</q-list>
|
||||||
|
|
||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
|
|
||||||
<q-dialog
|
|
||||||
v-model="showFiltersDialog"
|
|
||||||
maximized
|
|
||||||
transition-show="slide-up"
|
|
||||||
transition-hide="slide-down"
|
|
||||||
>
|
|
||||||
<q-card
|
|
||||||
class="q-mt-xl top-rounded-card flex column fix-card-width no-scroll no-wrap"
|
|
||||||
style="border-top-left-radius: var(--top-raduis) !important; border-top-right-radius: var(--top-raduis) !important;">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="flex items-center no-wrap justify-between w100">
|
|
||||||
<div class="text-h6">{{ t('files__filters') }}</div>
|
|
||||||
<div>
|
|
||||||
<q-btn
|
|
||||||
v-if="!checkFiltersSelect"
|
|
||||||
@click="resetFilters"
|
|
||||||
flat
|
|
||||||
no-caps
|
|
||||||
dense
|
|
||||||
color="grey-6"
|
|
||||||
class="q-mr-lg"
|
|
||||||
>
|
|
||||||
{{ t('files_filters_reset')}}
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
:icon="checkFiltersSelect ? 'mdi-close' :'mdi-check'"
|
|
||||||
@click="showFiltersDialog=false"
|
|
||||||
flat round
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<div class="col-grow q-px-none q-ma-none">
|
|
||||||
<q-resize-observer @resize="onResize" />
|
|
||||||
<q-card-section
|
|
||||||
class="q-pt-none q-pb-md q-px-md q-ma-none scroll"
|
|
||||||
:style="{ height: 'calc(' + dialogSectionHeight + 'px' +' - 32px)' }"
|
|
||||||
>
|
|
||||||
<div class="q-pl-sm text-bold">{{ t('files__filters_extension') }}</div>
|
|
||||||
<div class="flex row">
|
|
||||||
<div
|
|
||||||
v-for="(item,idx) in fileExtExample"
|
|
||||||
:key="idx"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
:name="fileIcon(item).icon"
|
|
||||||
:style="{ color: fileIcon(item).color }"
|
|
||||||
@click = "updateFileExtFilters(fileIcon(item).type)"
|
|
||||||
:class="fileExtFilters.includes(fileIcon(item).type) ? 'active' : 'inactive'"
|
|
||||||
size="md"
|
|
||||||
class="q-pa-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="q-pl-sm q-mt-md text-bold">{{ t('files__filters_source') }}</div>
|
|
||||||
<div class="flex column">
|
|
||||||
<div
|
|
||||||
v-for="(item,idx) in fileSourceOptions"
|
|
||||||
:key="idx"
|
|
||||||
>
|
|
||||||
<q-checkbox
|
|
||||||
v-model="fileSourceFilters"
|
|
||||||
:val="item.value"
|
|
||||||
>
|
|
||||||
{{ t(item.label) }}
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="q-pl-sm q-mt-md text-bold">{{ t('files__filters_by') }}</div>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
v-model="fileByFilters"
|
|
||||||
:options="fileByOptions"
|
|
||||||
multiple
|
|
||||||
use-chips
|
|
||||||
dense
|
|
||||||
class="w100"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="q-pl-sm q-mt-md text-bold">{{ t('files__filters_size') }}</div>
|
|
||||||
<div class="flex column">
|
|
||||||
<div
|
|
||||||
v-for="(item,idx) in fileSizeOptions"
|
|
||||||
:key="idx"
|
|
||||||
>
|
|
||||||
<q-checkbox
|
|
||||||
v-model="fileSizeFilters"
|
|
||||||
:val="item.value"
|
|
||||||
:label = "t(item.label)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</q-card-section>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
</q-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<pn-bottom-sheet-dialog
|
||||||
|
title="files__filters"
|
||||||
|
v-model="showFiltersDialog"
|
||||||
|
>
|
||||||
|
<template #btnSlot>
|
||||||
|
<div>
|
||||||
|
<q-btn
|
||||||
|
v-if="!checkFiltersSelect"
|
||||||
|
@click="resetFilters"
|
||||||
|
flat
|
||||||
|
no-caps
|
||||||
|
dense
|
||||||
|
color="grey-6"
|
||||||
|
>
|
||||||
|
{{ $t('files__filters_reset')}}
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<q-btn
|
||||||
|
rounded
|
||||||
|
class="w100"
|
||||||
|
color="primary"
|
||||||
|
@click="showFiltersDialog = false"
|
||||||
|
>
|
||||||
|
{{$t('files__filters_continue')}}
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="q-pl-sm text-bold text-caption">
|
||||||
|
{{ $t('files__filters_extension') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex row">
|
||||||
|
<div
|
||||||
|
v-for="(item,idx) in fileExtExample"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
:name="fileIcon(item).icon"
|
||||||
|
:style="{ color: fileIcon(item).color }"
|
||||||
|
@click = "updateFileExtFilters(fileIcon(item).type)"
|
||||||
|
:class="filters.fileExt.includes(fileIcon(item).type) ? 'active' : 'inactive'"
|
||||||
|
size="md"
|
||||||
|
class="q-pa-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-pl-sm q-mt-md text-bold text-caption">
|
||||||
|
{{ $t('files__filters_source') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex column">
|
||||||
|
<div
|
||||||
|
v-for="(item,idx) in fileSourceOptions"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="filters.fileSource"
|
||||||
|
:val="item.value"
|
||||||
|
>
|
||||||
|
{{ $t(item.label) }}
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-pl-sm q-mt-md text-bold text-caption">
|
||||||
|
{{ $t('files__filters_by') }}
|
||||||
|
</div>
|
||||||
|
<q-select
|
||||||
|
v-model="filters.fileBy"
|
||||||
|
:options="fileByOptions"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
class="w100 q-pt-sm"
|
||||||
|
option-value="id"
|
||||||
|
option-label="displayName"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
use-chips
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="q-pl-sm q-mt-md text-bold text-caption">
|
||||||
|
{{ $t('files__filters_size') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex column">
|
||||||
|
<div
|
||||||
|
v-for="(item,idx) in fileSizeOptions"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="filters.fileSize"
|
||||||
|
:val="item.value"
|
||||||
|
:label = "$t(item.label)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</pn-bottom-sheet-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, inject } from 'vue'
|
||||||
import { useFilesStore } from 'stores/files'
|
import { useFilesStore } from 'stores/files'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useUsersStore } from 'stores/users'
|
||||||
|
import { useChatsStore } from 'stores/chats'
|
||||||
import { date } from 'quasar'
|
import { date } from 'quasar'
|
||||||
import type { File } from 'types/File'
|
import { parseFileName, fileIcon, fileSize } from 'helpers/files-functions'
|
||||||
const { t }= useI18n()
|
import { useRouter } from 'vue-router'
|
||||||
|
import type { FileLink } from 'types/FileLink'
|
||||||
|
import type { WebApp } from '@twa-dev/types'
|
||||||
|
const tg = inject('tg') as WebApp
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const filesStore = useFilesStore()
|
const filesStore = useFilesStore()
|
||||||
|
const chatsStore = useChatsStore()
|
||||||
|
|
||||||
const files = computed(() => filesStore.files)
|
const files = filesStore.getFiles
|
||||||
|
|
||||||
const showCalendar = ref<boolean>(false)
|
const showCalendar = ref<boolean>(false)
|
||||||
const showFiltersDialog = ref(false)
|
const showFiltersDialog = ref(false)
|
||||||
@@ -248,42 +261,43 @@
|
|||||||
|
|
||||||
const displayFiles = computed(() => {
|
const displayFiles = computed(() => {
|
||||||
|
|
||||||
return files.value
|
return files
|
||||||
.filter(searchFiles)
|
.filter(searchFiles)
|
||||||
.filter(fileExt)
|
.filter(fileExt)
|
||||||
.filter(fileSource)
|
.filter(fileSource)
|
||||||
.filter(fileSize)
|
.filter(fileSize)
|
||||||
|
.filter(fileBy)
|
||||||
.filter(checkDateInterval)
|
.filter(checkDateInterval)
|
||||||
|
|
||||||
function searchFiles (el: File) {
|
function searchFiles (el: FileLink) {
|
||||||
if (!search.value || !(search.value && search.value.trim())) return true
|
if (!search.value || !(search.value && search.value.trim())) return true
|
||||||
const searchValue = search.value.trim().toLowerCase()
|
const searchValue = search.value.trim().toLowerCase()
|
||||||
return el.filename.toLowerCase().includes(searchValue)
|
return el.filename.toLowerCase().includes(searchValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkDateInterval (el: File) {
|
function checkDateInterval (el: FileLink) {
|
||||||
if (!datesRange.value) return true
|
if (!datesRange.value) return true
|
||||||
const from = date.extractDate(datesRange.value.from, 'YYYY/MM/DD').getTime()
|
const from = date.extractDate(datesRange.value.from, 'YYYY/MM/DD').getTime()
|
||||||
const to = date.extractDate(datesRange.value.to, 'YYYY/MM/DD').getTime() + 86399999
|
const to = date.extractDate(datesRange.value.to, 'YYYY/MM/DD').getTime() + 86399999
|
||||||
return (from < el.published) && ( to >= el.published)
|
return (from < el.published * 1000) && ( to >= el.published * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileExt (el: File) {
|
function fileExt (el: FileLink) {
|
||||||
if (fileExtFilters.value.length === 0) return true
|
if (filters.value.fileExt.length === 0) return true
|
||||||
const type = fileIcon(el.filename).type
|
const type = fileIcon(el.filename).type
|
||||||
return fileExtFilters.value.includes(type)
|
return filters.value.fileExt.includes(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileSource (el: File) {
|
function fileSource (el: FileLink) {
|
||||||
if (fileSourceFilters.value.length === 0) return true
|
if (filters.value.fileSource.length === 0) return true
|
||||||
return fileSourceFilters.value.includes(el.parent_type)
|
return filters.value.fileSource.includes(el.parent_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileSize (el: File) {
|
function fileSize (el: FileLink) {
|
||||||
if (fileSizeFilters.value.length === 0) return true
|
if (filters.value.fileSize.length === 0) return true
|
||||||
|
|
||||||
const fileSize = el.size
|
const fileSize = el.size
|
||||||
const sortedFilters = [...fileSizeFilters.value].sort((a, b) => a - b)
|
const sortedFilters = [...filters.value.fileSize].sort((a, b) => a - b)
|
||||||
|
|
||||||
const ranges = fileSizeOptions.map((option, index) => ({
|
const ranges = fileSizeOptions.map((option, index) => ({
|
||||||
min: index === 0 ? 0 : fileSizeOptions[index - 1]!.value,
|
min: index === 0 ? 0 : fileSizeOptions[index - 1]!.value,
|
||||||
@@ -297,186 +311,89 @@
|
|||||||
(range.max === Infinity ? true : fileSize <= range.max))
|
(range.max === Infinity ? true : fileSize <= range.max))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fileBy (el: FileLink) {
|
||||||
|
if (filters.value.fileBy.length === 0) return true
|
||||||
|
return filters.value.fileBy.includes(el.published_by)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const fileExtFilters = ref<string[]>([])
|
interface Filters {
|
||||||
|
fileSource: number[]
|
||||||
|
fileSize: number[]
|
||||||
|
fileBy: number[]
|
||||||
|
fileExt: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultFilters = {
|
||||||
|
fileSource: [],
|
||||||
|
fileSize: [],
|
||||||
|
fileBy: [],
|
||||||
|
fileExt: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = ref<Filters>({ ...defaultFilters })
|
||||||
|
|
||||||
|
const checkFiltersSelect = computed(() => (
|
||||||
|
Object.values(filters.value).every(el => el.length === 0)
|
||||||
|
))
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
(Object.keys(filters.value) as (keyof Filters)[]).forEach(key => filters.value[key] = [])
|
||||||
|
}
|
||||||
|
|
||||||
const fileExtExample = [
|
const fileExtExample = [
|
||||||
'1.common', '2.doc', '3.xls', '4.vsd', '5.ppt', '6.pdf', '7.png', '8.mp3', '9.mp4', '10.js', '11.txt', '12.zip', '13.skp', '14.dwg', '15.ttf'
|
'1.common', '2.doc', '3.xls', '4.vsd', '5.ppt', '6.pdf', '7.png', '8.mp3', '9.mp4', '10.js', '11.txt', '12.zip', '13.skp', '14.dwg', '15.ttf'
|
||||||
]
|
]
|
||||||
|
|
||||||
function updateFileExtFilters (select: string) {
|
function updateFileExtFilters (select: string) {
|
||||||
if (fileExtFilters.value.includes(select)) {
|
if (filters.value.fileExt.includes(select)) {
|
||||||
const idx = fileExtFilters.value.indexOf(select)
|
const idx = filters.value.fileExt.indexOf(select)
|
||||||
fileExtFilters.value.splice(idx, 1)
|
filters.value.fileExt.splice(idx, 1)
|
||||||
} else fileExtFilters.value.push(select)
|
} else filters.value.fileExt.push(select)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileSourceFilters = ref<number[]>([])
|
|
||||||
const fileSourceOptions = [
|
const fileSourceOptions = [
|
||||||
{ id: 1, icon: 'mdi-message-outline', value: 0, label: 'files__filters_source_chats' },
|
{ id: 1, icon: 'mdi-chat-outline', value: 0, label: 'files__filters_source_chats' },
|
||||||
{ id: 2, icon: 'mdi-clipboard-outline', value: 1, label: 'files__filters_source_tasks' },
|
{ id: 2, icon: 'mdi-clipboard-outline', value: 1, label: 'files__filters_source_tasks' },
|
||||||
{ id: 3, icon: 'mdi-calendar-month-outline', value: 2, label: 'files__filters_source_meetings' }
|
{ id: 3, icon: 'mdi-calendar-month-outline', value: 2, label: 'files__filters_source_meetings' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const fileSizeFilters = ref<number[]>([])
|
|
||||||
const fileSizeOptions = [
|
const fileSizeOptions = [
|
||||||
{ id: 1, value: 5242880, label: 'files__filters_size_small' }, // 5MB
|
{ id: 1, value: 5242880, label: 'files__filters_size_small' }, // 5MB
|
||||||
{ id: 2, value: 26214400, label: 'files__filters_size_middle' }, // 25MB
|
{ id: 2, value: 26214400, label: 'files__filters_size_middle' }, // 25MB
|
||||||
{ id: 3, value: 104857600, label: 'files__filters_size_big' }, // 100MB
|
{ id: 3, value: 104857600, label: 'files__filters_size_big' }, // 100MB
|
||||||
{ id: 4, value: Infinity, label: 'files__filters_size_very_big' } // more 100 MB
|
{ id: 4, value: Infinity, label: 'files__filters_size_very_big' } // more 100 MB
|
||||||
]
|
]
|
||||||
|
|
||||||
const fileByFilters = ref<number[]>([]) // user ids
|
|
||||||
const fileByOptions = <object>[] // temp obj, need users store!!!
|
|
||||||
|
|
||||||
const checkFiltersSelect = computed(() => (
|
|
||||||
(fileExtFilters.value.length === 0) &&
|
|
||||||
(fileByFilters.value.length === 0) &&
|
|
||||||
(fileSourceFilters.value.length === 0) &&
|
|
||||||
(fileSizeFilters.value.length === 0)
|
|
||||||
))
|
|
||||||
|
|
||||||
const resetFilters = () => {
|
|
||||||
fileExtFilters.value = []
|
|
||||||
fileByFilters.value= []
|
|
||||||
fileSourceFilters.value = []
|
|
||||||
fileSizeFilters.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
const filesDates = computed(() => displayFiles.value.map(el => date.formatDate(el.published, 'YYYY/MM/DD')))
|
|
||||||
|
|
||||||
interface ParsedFile {
|
|
||||||
name: string
|
|
||||||
ext: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseFileName(filename: string): ParsedFile {
|
|
||||||
const lastDotIndex = filename.lastIndexOf('.')
|
|
||||||
|
|
||||||
if (lastDotIndex === -1) {
|
|
||||||
return { name: filename, ext: '' }
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: filename.slice(0, lastDotIndex),
|
|
||||||
ext: filename.slice(lastDotIndex + 1).toLowerCase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FileIcon {
|
|
||||||
type: string
|
|
||||||
icon: string
|
|
||||||
color: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileIcon(filename: string): FileIcon {
|
|
||||||
const ext = parseFileName(filename).ext;
|
|
||||||
|
|
||||||
switch (ext) {
|
|
||||||
case 'doc':
|
|
||||||
case 'docx':
|
|
||||||
return { type: 'doc', icon: 'pn-icon-file-doc', color: '#2B579A' }
|
|
||||||
|
|
||||||
case 'xls':
|
|
||||||
case 'xlsx':
|
|
||||||
case 'csv':
|
|
||||||
return { type: 'xls', icon: 'pn-icon-file-xls', color: '#217346' }
|
|
||||||
|
|
||||||
case 'vsd':
|
|
||||||
case 'vsdx':
|
|
||||||
return { type: 'vsd', icon: 'pn-icon-file-vsd', color: '#3955A3' }
|
|
||||||
|
|
||||||
case 'ppt':
|
|
||||||
case 'pptx':
|
|
||||||
return { type: 'ppt', icon: 'pn-icon-file-ppt', color: '#D24726' }
|
|
||||||
|
|
||||||
case 'pdf':
|
|
||||||
return { type: 'pdf', icon: 'pn-icon-file-pdf', color: '#D0021B' }
|
|
||||||
|
|
||||||
case 'png':
|
|
||||||
case 'jpg':
|
|
||||||
case 'jpeg':
|
|
||||||
case 'gif':
|
|
||||||
case 'bmp':
|
|
||||||
case 'svg':
|
|
||||||
return { type: 'img', icon: 'pn-icon-file-img', color: '#4CAF50' }
|
|
||||||
|
|
||||||
case 'mp3':
|
|
||||||
case 'wav':
|
|
||||||
case 'ogg':
|
|
||||||
return { type: 'music', icon: 'pn-icon-file-audio', color: '#FF9800' }
|
|
||||||
|
|
||||||
case 'mp4':
|
|
||||||
case 'avi':
|
|
||||||
case 'mov':
|
|
||||||
case 'mkv':
|
|
||||||
return { type: 'video', icon: 'pn-icon-file-video', color: '#9C27B0' }
|
|
||||||
|
|
||||||
case 'js':
|
|
||||||
case 'ts':
|
|
||||||
case 'html':
|
|
||||||
case 'css':
|
|
||||||
case 'json':
|
|
||||||
case 'xml':
|
|
||||||
return { type: 'code', icon: 'pn-icon-file-code', color: '#999' }
|
|
||||||
|
|
||||||
case 'txt':
|
|
||||||
return { type: 'txt', icon: 'pn-icon-file-txt', color: '#757575' }
|
|
||||||
|
|
||||||
case 'zip':
|
|
||||||
case 'rar':
|
|
||||||
case '7z':
|
|
||||||
case 'tar':
|
|
||||||
case 'gz':
|
|
||||||
return { type: 'archive', icon: 'pn-icon-file-archive', color: '#F78E1E' }
|
|
||||||
|
|
||||||
case 'skp':
|
|
||||||
return { type: 'skp', icon: 'pn-icon-file-skp', color: '#CC0000' }
|
|
||||||
|
|
||||||
case 'dwg':
|
|
||||||
case 'dxf':
|
|
||||||
return { type: 'cad', icon: 'pn-icon-file-dwg', color: '#00579D' }
|
|
||||||
|
|
||||||
case 'ttf':
|
|
||||||
return { type: 'font', icon: 'pn-icon-file-ttf', color: '#607D8B' }
|
|
||||||
|
|
||||||
default:
|
|
||||||
return { type: 'common', icon: 'pn-icon-file-default', color: '#9E9E9E' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileFrom (value: number): string {
|
const usersStore = useUsersStore()
|
||||||
return String(value)
|
const users = computed(() => usersStore.users)
|
||||||
}
|
const fileByOptions = computed(() => {
|
||||||
|
return users.value
|
||||||
|
.map(el => ({ ...el, displayName: usersStore.userNameById(el.id) }))
|
||||||
|
})
|
||||||
|
|
||||||
function fileBy (value: number): string {
|
const filesDates = computed(() => displayFiles.value.map(el => date.formatDate(el.published * 1000, 'YYYY/MM/DD')))
|
||||||
return String(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileSize (value: number): string {
|
|
||||||
if (value === 0) return '-'
|
|
||||||
|
|
||||||
const units = ['B', 'kB', 'MB', 'GB', 'TB']
|
function fileFrom (file: FileLink) {
|
||||||
const i = Math.floor(Math.log(value) / Math.log(1024))
|
|
||||||
const index = Math.min(i, units.length - 1)
|
|
||||||
|
|
||||||
const result = (value / Math.pow(1024, index)).toFixed(2)
|
if (file.parent_type === 0) {
|
||||||
return `${result} ${t(units[index]??'')}`
|
const chat = chatsStore.chatById(file.chat_id)
|
||||||
|
if (chat) return chat.name
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(file.chat_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileDate (value: number): string {
|
function fileDate (time: number) {
|
||||||
return date.formatDate(value, 'DD-MM-YY')
|
return date.formatDate(time*1000, 'DD.MM.YYYY HH:mm')
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialogSectionHeight = ref<number>(0)
|
const router = useRouter()
|
||||||
|
async function goFile(file: FileLink) {
|
||||||
interface sizeParams {
|
if (file.parent_type === 0) tg.openTelegramLink('https://t.me/c/' + file.telegram_chat_id + '/' + file.message_id)
|
||||||
height: number,
|
if (file.parent_type === 1) await router.push({ name: 'task_info', params: { taskId: file.parent_id }})
|
||||||
width: number
|
if (file.parent_type === 2) await router.push({ name: 'meeting_info', params: { meetingId: file.parent_id }})
|
||||||
}
|
|
||||||
|
|
||||||
function onResize (size: sizeParams) {
|
|
||||||
dialogSectionHeight.value = size.height
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -27,20 +27,23 @@
|
|||||||
@click="toggleExpand"
|
@click="toggleExpand"
|
||||||
key="expanded"
|
key="expanded"
|
||||||
>
|
>
|
||||||
<q-avatar rounded>
|
<pn-auto-avatar
|
||||||
<img v-if="project.logo" :src="project.logo" style="object-fit: cover;"/>
|
:img="project.logo"
|
||||||
<pn-auto-avatar v-else :name="project.name"/>
|
:name="project.name"
|
||||||
</q-avatar>
|
type="rounded"
|
||||||
|
/>
|
||||||
<div class="flex column text-white fit">
|
<div class="flex column text-white fit">
|
||||||
<div
|
<div
|
||||||
class="text-h6 q-pl-sm"
|
class="text-h6 q-pl-sm"
|
||||||
:style="{ maxWidth: '-webkit-fill-available', whiteSpace: 'normal' }"
|
style="max-width: -webkit-fill-available; white-space: pre-line"
|
||||||
>
|
>
|
||||||
{{ project.name }}
|
{{ project.name }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-caption q-pl-sm" :style="{ maxWidth: '-webkit-fill-available', whiteSpace: 'normal' }">
|
<div
|
||||||
|
class="text-caption q-pl-sm"
|
||||||
|
style="max-width: -webkit-fill-available; white-space: pre-line"
|
||||||
|
>
|
||||||
{{ project.description }}
|
{{ project.description }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,14 +52,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
|
v-if="projects.length > 1"
|
||||||
@click="drawer=!drawer"
|
@click="drawer=!drawer"
|
||||||
:class="drawer ? 'text-grey' : 'text-white'"
|
:class="drawer ? 'text-grey' : 'text-white'"
|
||||||
flat
|
flat
|
||||||
round
|
no-caps
|
||||||
icon="mdi-briefcase-outline"
|
dense
|
||||||
size="md"
|
|
||||||
class="q-ml-xl"
|
class="q-ml-xl"
|
||||||
/>
|
>
|
||||||
|
<span class="flex items-center">
|
||||||
|
{{ $t('header__projects') }}
|
||||||
|
<q-icon name="mdi-chevron-right"/>
|
||||||
|
</span>
|
||||||
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
<q-drawer
|
<q-drawer
|
||||||
v-model="drawer"
|
v-model="drawer"
|
||||||
@@ -89,14 +97,21 @@
|
|||||||
@click="changeProject(item.id)"
|
@click="changeProject(item.id)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar rounded >
|
<pn-auto-avatar
|
||||||
<q-img v-if="item.logo" :src="item.logo" fit="cover" style="height: 40px"/>
|
:img="item.logo"
|
||||||
<pn-auto-avatar v-else :name="item.name"/>
|
:name="item.name"
|
||||||
</q-avatar>
|
type="rounded"
|
||||||
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section :class="item.id === currentProjectId ? 'text-primary !important' : ''">
|
<q-item-section :class="item.id === currentProjectId ? 'text-primary !important' : ''">
|
||||||
<q-item-label lines="1" class="text-bold">{{ item.name }}</q-item-label>
|
<q-item-label lines="1" class="text-bold">{{ item.name }}</q-item-label>
|
||||||
<q-item-label class="text-caption" lines="2">{{item.description}}</q-item-label>
|
<q-item-label
|
||||||
|
class="text-caption"
|
||||||
|
lines="2"
|
||||||
|
style="white-space: pre-line"
|
||||||
|
>
|
||||||
|
{{item.description}}
|
||||||
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
@@ -120,7 +135,7 @@
|
|||||||
|
|
||||||
|
|
||||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
||||||
const projects = computed(() => projectsStore.projects)
|
const projects = projectsStore.getProjects
|
||||||
const project = computed(() => {
|
const project = computed(() => {
|
||||||
const currentProject = currentProjectId.value && projectsStore.projectById(currentProjectId.value)
|
const currentProject = currentProjectId.value && projectsStore.projectById(currentProjectId.value)
|
||||||
|
|
||||||
@@ -144,9 +159,9 @@
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
function changeProject(id: number) {
|
async function changeProject(id: number) {
|
||||||
drawer.value = false
|
drawer.value = false
|
||||||
router.push({ name: route.name, params: { id }})
|
await router.push({ name: route.name, params: { id }})
|
||||||
}
|
}
|
||||||
|
|
||||||
interface sizeParams {
|
interface sizeParams {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
backgroundColor:
|
backgroundColor:
|
||||||
item.is_cancel
|
item.is_cancel
|
||||||
? '#999'
|
? '#999'
|
||||||
: item.meet_date < Date.now()
|
: item.meet_date * 1000 < Date.now()
|
||||||
? '#eee'
|
? '#eee'
|
||||||
: 'inherit',
|
: 'inherit',
|
||||||
border: item.is_cancel ? 'solid 1px #999' : 'inherit'
|
border: item.is_cancel ? 'solid 1px #999' : 'inherit'
|
||||||
@@ -131,12 +131,12 @@
|
|||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label v-if="item.is_cancel" lines="1" class="text-negative flex items-center text-caption">
|
<q-item-label v-if="item.is_cancel" lines="1" class="text-negative flex items-center text-caption">
|
||||||
<q-icon name="mdi-calendar-remove-outline" class="q-mr-none"/>
|
<q-icon name="mdi-calendar-remove-outline" class="q-mr-none"/>
|
||||||
{{ $t('meeting_info__canceled') }}
|
{{ $t('meeting_page__canceled') }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label lines="1" class="text-bold">
|
<q-item-label lines="1" class="text-bold">
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label lines="1" class="text-caption" v-if="item.place">
|
<q-item-label lines="1" class="text-caption flex items-center" v-if="item.place">
|
||||||
<q-icon name="mdi-map-marker-outline" class="q-mr-none"/>
|
<q-icon name="mdi-map-marker-outline" class="q-mr-none"/>
|
||||||
<span class="ellipsis">{{ item.place }}</span>
|
<span class="ellipsis">{{ item.place }}</span>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
@@ -144,13 +144,13 @@
|
|||||||
<q-item-label caption lines="1">
|
<q-item-label caption lines="1">
|
||||||
<div class="flex row no-wrap items-center w100">
|
<div class="flex row no-wrap items-center w100">
|
||||||
<div class="second-line-item flex no-wrap items-center q-mr-sm">
|
<div class="second-line-item flex no-wrap items-center q-mr-sm">
|
||||||
<q-icon name="mdi-account-tie-outline" class="q-mr-none"/>
|
<q-icon name="mdi-account-tie-outline" class="q-mr-xs"/>
|
||||||
<span class="ellipsis">{{ usersStore.userNameById(item.created_by) }}</span>
|
<span class="ellipsis">{{ usersStore.userNameById(item.created_by) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="second-line-item flex no-wrap items-center" v-if="item.chat_attach">
|
<div class="second-line-item flex no-wrap items-center" v-if="item.chat_id">
|
||||||
<q-icon name="mdi-message-outline" class="q-mr-none"/>
|
<q-icon name="mdi-chat-outline" class="q-mr-xs"/>
|
||||||
<span class="ellipsis">{{ chatsStore.chatById(item.chat_attach)?.name }}</span>
|
<span class="ellipsis">{{ chatsStore.chatById(item.chat_id)?.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
<span>{{ item.participants?.length }}</span>
|
<span>{{ item.participants?.length }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="text-caption flex items-center" v-if="item.files && item.length !== 0">
|
<span class="text-caption flex items-center" v-if="item.files && item.files.length !== 0">
|
||||||
<q-icon name="mdi-paperclip" color="grey" />
|
<q-icon name="mdi-paperclip" color="grey" />
|
||||||
<span>{{ item.files.length }}</span>
|
<span>{{ item.files.length }}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -194,90 +194,29 @@
|
|||||||
</q-page-sticky>
|
</q-page-sticky>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-dialog
|
<pn-small-dialog
|
||||||
v-model="showDialogDeleteMeeting"
|
v-model="showDialogDeleteMeeting"
|
||||||
|
icon="mdi-calendar-remove-outline"
|
||||||
|
color="negative"
|
||||||
|
title="meeting_page__dialog_cancel_title"
|
||||||
|
mainBtnLabel="meeting_page__dialog_cancel_ok"
|
||||||
|
auxBtnLabel="meeting_page__dialog_cancel_delete"
|
||||||
|
@clickMainBtn="onConfirmCancel()"
|
||||||
|
@clickAuxBtn="onConfirmDelete()"
|
||||||
|
@close="onCancel()"
|
||||||
@before-hide="onDialogBeforeHide()"
|
@before-hide="onDialogBeforeHide()"
|
||||||
>
|
/>
|
||||||
<pn-dialog-body
|
|
||||||
icon="mdi-calendar-remove-outline"
|
|
||||||
color="negative"
|
|
||||||
title="meeting_info__dialog_cancel_title"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
</template>
|
|
||||||
<template #actions>
|
|
||||||
<div class="flex no-wrap w100 justify-center q-gutter-x-md">
|
|
||||||
|
|
||||||
<q-btn
|
<pn-small-dialog
|
||||||
:label="$t('meeting_info__dialog_cancel_delete')"
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
v-close-popup
|
|
||||||
rounded
|
|
||||||
class="w50"
|
|
||||||
@click="onConfirmDelete()"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
:label="$t('meeting_info__dialog_cancel_ok')"
|
|
||||||
color="negative"
|
|
||||||
v-close-popup
|
|
||||||
rounded
|
|
||||||
class="w50"
|
|
||||||
@click="onConfirmCancel()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
class="w80 q-mt-md q-mb-sm" flat
|
|
||||||
v-close-popup rounded
|
|
||||||
no-caps
|
|
||||||
@click="onCancel()"
|
|
||||||
>
|
|
||||||
<div class="flex items-center">
|
|
||||||
{{$t('close')}}
|
|
||||||
<q-icon name="close"/>
|
|
||||||
</div>
|
|
||||||
</q-btn>
|
|
||||||
</template>
|
|
||||||
</pn-dialog-body>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<q-dialog
|
|
||||||
v-model="showDialogRestoreMeeting"
|
v-model="showDialogRestoreMeeting"
|
||||||
|
icon="mdi-calendar-refresh-outline"
|
||||||
|
color="green"
|
||||||
|
title="meeting_page__dialog_restore_title"
|
||||||
|
mainBtnLabel="meeting_page__dialog_restore_ok"
|
||||||
|
@clickMainBtn="onConfirmRestore()"
|
||||||
|
@close="onCancel()"
|
||||||
@before-hide="onDialogBeforeHide()"
|
@before-hide="onDialogBeforeHide()"
|
||||||
>
|
/>
|
||||||
<pn-dialog-body
|
|
||||||
icon="mdi-calendar-refresh-outline"
|
|
||||||
color="green"
|
|
||||||
title="meeting_info__dialog_restore_title"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
</template>
|
|
||||||
<template #actions>
|
|
||||||
<q-btn
|
|
||||||
:label="$t('meeting_info__dialog_restore_ok')"
|
|
||||||
color="green"
|
|
||||||
v-close-popup
|
|
||||||
rounded
|
|
||||||
class="w80 q-mb-md"
|
|
||||||
@click="onConfirmRestore()"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
class="w80 q-mb-sm" flat
|
|
||||||
v-close-popup rounded
|
|
||||||
no-caps
|
|
||||||
@click="onCancel()"
|
|
||||||
>
|
|
||||||
<div class="flex items-center">
|
|
||||||
{{$t('close')}}
|
|
||||||
<q-icon name="close"/>
|
|
||||||
</div>
|
|
||||||
</q-btn>
|
|
||||||
</template>
|
|
||||||
</pn-dialog-body>
|
|
||||||
</q-dialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -315,12 +254,12 @@
|
|||||||
time: string
|
time: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const meetings = computed(() => meetingsStore.meetings)
|
const meetings = meetingsStore.getMeetings
|
||||||
const meetingsDates = computed(() => displayMeetingsAll.value.map(el => date.formatDate(el.meet_date, 'YYYY/MM/DD')))
|
const meetingsDates = computed(() => displayMeetingsAll.value.map(el => date.formatDate(el.meet_date * 1000, 'YYYY/MM/DD')))
|
||||||
|
|
||||||
const displayMeetingsAll = computed(() => {
|
const displayMeetingsAll = computed(() => {
|
||||||
|
|
||||||
return meetings.value
|
return meetings
|
||||||
.filter(searchMeetings)
|
.filter(searchMeetings)
|
||||||
.filter(checkDateInterval)
|
.filter(checkDateInterval)
|
||||||
.sort(sortByTime)
|
.sort(sortByTime)
|
||||||
@@ -339,7 +278,7 @@
|
|||||||
if (!datesRange.value) return true
|
if (!datesRange.value) return true
|
||||||
const from = date.extractDate(datesRange.value.from, 'YYYY/MM/DD').getTime()
|
const from = date.extractDate(datesRange.value.from, 'YYYY/MM/DD').getTime()
|
||||||
const to = date.extractDate(datesRange.value.to, 'YYYY/MM/DD').getTime() + 86399999
|
const to = date.extractDate(datesRange.value.to, 'YYYY/MM/DD').getTime() + 86399999
|
||||||
return (from < el.meet_date) && ( to >= el.meet_date)
|
return (from < el.meet_date * 1000) && ( to >= el.meet_date * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortByTime (a: Meeting, b: Meeting) {
|
function sortByTime (a: Meeting, b: Meeting) {
|
||||||
@@ -349,11 +288,11 @@
|
|||||||
function enrich (el: Meeting) {
|
function enrich (el: Meeting) {
|
||||||
return {
|
return {
|
||||||
...el,
|
...el,
|
||||||
monthYear: date.formatDate(el.meet_date, 'MMMM YYYY'),
|
monthYear: date.formatDate(el.meet_date * 1000, 'MMMM YYYY'),
|
||||||
day: date.formatDate(el.meet_date, 'D'),
|
day: date.formatDate(el.meet_date * 1000, 'D'),
|
||||||
isWeekend: (date.formatDate(el.meet_date, 'E') === '6'|| date.formatDate(el.meet_date, 'E') === '7'),
|
isWeekend: (date.formatDate(el.meet_date * 1000, 'E') === '6'|| date.formatDate(el.meet_date * 1000, 'E') === '7'),
|
||||||
dayOfWeek: date.formatDate(el.meet_date, 'ddd'),
|
dayOfWeek: date.formatDate(el.meet_date * 1000, 'ddd'),
|
||||||
time: date.formatDate(el.meet_date, 'HH:mm')
|
time: date.formatDate(el.meet_date * 1000, 'HH:mm')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -361,7 +300,7 @@
|
|||||||
const displayTime = ref(Date.now())
|
const displayTime = ref(Date.now())
|
||||||
|
|
||||||
const currentMeetings = computed(() =>
|
const currentMeetings = computed(() =>
|
||||||
displayMeetingsAll.value.filter(el => el.meet_date >= displayTime.value)
|
displayMeetingsAll.value.filter(el => el.meet_date * 1000 >= displayTime.value)
|
||||||
)
|
)
|
||||||
|
|
||||||
const countPreviousMeetings = computed(() =>
|
const countPreviousMeetings = computed(() =>
|
||||||
@@ -369,13 +308,13 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
const showReset = computed(() =>
|
const showReset = computed(() =>
|
||||||
currentMeetings.value.length !== displayMeetingsAll.value.filter(el => el.meet_date >= Date.now()).length
|
currentMeetings.value.length !== displayMeetingsAll.value.filter(el => el.meet_date * 1000 >= Date.now()).length
|
||||||
)
|
)
|
||||||
|
|
||||||
function showPreviousMeetings () {
|
function showPreviousMeetings () {
|
||||||
if (countPreviousMeetings.value === 0) return
|
if (countPreviousMeetings.value === 0) return
|
||||||
const newIndex = Math.max(0, countPreviousMeetings.value - 5)
|
const newIndex = Math.max(0, countPreviousMeetings.value - 5)
|
||||||
displayTime.value = displayMeetingsAll.value[newIndex]?.meet_date || Date.now()
|
displayTime.value = (displayMeetingsAll.value[newIndex] && displayMeetingsAll.value[newIndex]?.meet_date * 1000) || Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetDisplayPreviousMeetings () {
|
function resetDisplayPreviousMeetings () {
|
||||||
@@ -398,7 +337,7 @@
|
|||||||
let currentGroup: ResultItem
|
let currentGroup: ResultItem
|
||||||
|
|
||||||
currentMeetings.value.forEach((el: enrichMeeting) => {
|
currentMeetings.value.forEach((el: enrichMeeting) => {
|
||||||
const month = date.formatDate(el.meet_date, 'MMMM YYYY')
|
const month = date.formatDate(el.meet_date * 1000, 'MMMM YYYY')
|
||||||
|
|
||||||
if (month !== currentMonth) {
|
if (month !== currentMonth) {
|
||||||
currentMonth = month
|
currentMonth = month
|
||||||
|
|||||||
@@ -11,8 +11,18 @@
|
|||||||
size="lg"
|
size="lg"
|
||||||
:color="showCalendar ? 'primary' : 'grey'"
|
:color="showCalendar ? 'primary' : 'grey'"
|
||||||
@click="showCalendar = !showCalendar"
|
@click="showCalendar = !showCalendar"
|
||||||
/>
|
>
|
||||||
|
<div>
|
||||||
|
<q-badge
|
||||||
|
color="red"
|
||||||
|
rounded
|
||||||
|
floating
|
||||||
|
transparent
|
||||||
|
style="position: relative; top: -16px; margin-left: -12px"
|
||||||
|
:style="{ opacity: datesRange ? 0.8 : 0 }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-btn>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="search"
|
v-model="search"
|
||||||
clearable
|
clearable
|
||||||
@@ -27,11 +37,11 @@
|
|||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="showFilters = true"
|
@click="showFiltersDialog = true"
|
||||||
icon="mdi-filter-outline"
|
icon="mdi-filter-outline"
|
||||||
dense round flat
|
dense round flat
|
||||||
size="lg"
|
size="lg"
|
||||||
:color="showFilters ? 'primary' : 'grey'"
|
:color="showFiltersDialog ? 'primary' : 'grey'"
|
||||||
class="q-mr-xs"
|
class="q-mr-xs"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -41,38 +51,69 @@
|
|||||||
floating
|
floating
|
||||||
transparent
|
transparent
|
||||||
style="position: relative; top: -16px; margin-left: -12px"
|
style="position: relative; top: -16px; margin-left: -12px"
|
||||||
:style="{ opacity: selectedFilters.length !== 0 ? 0.8 : 0 }"
|
:style="{ opacity: !checkFiltersSelect ? 0.8 : 0 }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<q-slide-transition>
|
<q-slide-transition>
|
||||||
<div v-show="showCalendar">
|
<div v-show="showCalendar">
|
||||||
<q-date
|
<q-date
|
||||||
class="w100 fix-calendar q-mb-sm q-mt-xs"
|
class="w100 fix-calendar q-mb-sm q-mt-xs"
|
||||||
first-day-of-week="1"
|
first-day-of-week="1"
|
||||||
v-model="date"
|
v-model="datesRange"
|
||||||
|
range
|
||||||
|
flat
|
||||||
|
:events="tasksDates"
|
||||||
|
event-color="brand"
|
||||||
|
today-btn
|
||||||
minimal
|
minimal
|
||||||
dense
|
dense
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-slide-transition>
|
</q-slide-transition>
|
||||||
</template>
|
</template>
|
||||||
|
<div class="w100 flex justify-end q-px-md">
|
||||||
<q-list bordered separator>
|
<q-btn flat dense no-caps @click="showArchiveTasks=!showArchiveTasks">
|
||||||
<q-item
|
<span class="text-caption text-grey">
|
||||||
v-for="item in displayTasks"
|
{{
|
||||||
:key="item.id"
|
$t(!showArchiveTasks ? 'tasks__show_archive' : 'tasks__hide_archive') +
|
||||||
clickable v-ripple
|
(hiddenTask !== 0 ? ' (+' + hiddenTask +')' : '')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
<template v-for="item in displayTasks" :key="item.id">
|
||||||
|
<q-slide-item
|
||||||
|
@right="handleSlideRight($event, item.id)"
|
||||||
|
@left="handleSlideLeft($event, item.id)"
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
@click="goTask(item.id)"
|
@click="goTask(item.id)"
|
||||||
|
right-color="red"
|
||||||
|
left-color="green"
|
||||||
>
|
>
|
||||||
<task-item :item/>
|
<template #right v-if="item.status !== 6">
|
||||||
</q-item>
|
<q-icon size="lg" name="mdi-clipboard-remove-outline"/>
|
||||||
</q-list>
|
</template>
|
||||||
<div class="q-py-lg"/> <!-- fix hide scroll area by tabs -->
|
<template #left v-if="item.status === 6">
|
||||||
|
<q-icon size="lg" name="mdi-clipboard-play-outline"/>
|
||||||
|
</template>
|
||||||
|
<q-item
|
||||||
|
:key="item.id"
|
||||||
|
:style = "{
|
||||||
|
backgroundColor:
|
||||||
|
item.status === 6
|
||||||
|
? '#999'
|
||||||
|
: 'inherit',
|
||||||
|
border: item.status === 6 ? 'solid 1px #999' : 'inherit'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<task-item :item/>
|
||||||
|
</q-item>
|
||||||
|
</q-slide-item>
|
||||||
|
</template>
|
||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
|
|
||||||
<q-page-sticky
|
<q-page-sticky
|
||||||
position="bottom-right"
|
position="bottom-right"
|
||||||
:offset="[0, 18]"
|
:offset="[0, 18]"
|
||||||
@@ -93,124 +134,225 @@
|
|||||||
</q-page-sticky>
|
</q-page-sticky>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-dialog
|
<pn-small-dialog
|
||||||
v-model="showFilters"
|
v-model="showDialogDeleteTask"
|
||||||
position="bottom"
|
icon="mdi-clipboard-remove-outline"
|
||||||
|
color="negative"
|
||||||
|
title="tasks__dialog_cancel_title"
|
||||||
|
mainBtnLabel="tasks__dialog_cancel_ok"
|
||||||
|
auxBtnLabel="tasks__dialog_cancel_delete"
|
||||||
|
@clickMainBtn="onConfirmCancel()"
|
||||||
|
@clickAuxBtn="onConfirmDelete()"
|
||||||
|
@close="onCancel()"
|
||||||
|
@before-hide="onDialogBeforeHide()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<pn-small-dialog
|
||||||
|
v-model="showDialogRestoreTask"
|
||||||
|
icon="mdi-clipboard-play-outline"
|
||||||
|
color="green"
|
||||||
|
title="tasks__dialog_restore_title"
|
||||||
|
mainBtnLabel="tasks__dialog_restore_ok"
|
||||||
|
@clickMainBtn="onConfirmRestore()"
|
||||||
|
@close="onCancel()"
|
||||||
|
@before-hide="onDialogBeforeHide()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<pn-bottom-sheet-dialog
|
||||||
|
title="tasks__filters"
|
||||||
|
v-model="showFiltersDialog"
|
||||||
>
|
>
|
||||||
<div class="w100 filter-panel top-rounded-card bg-white q-pa-md">
|
<template #btnSlot>
|
||||||
<div class="flex justify-between items-center">
|
<div>
|
||||||
<span class="text-h6">{{ $t('tasks__filters') }}</span>
|
<q-btn
|
||||||
<q-btn
|
v-if="!checkFiltersSelect"
|
||||||
@click="showFilters = false"
|
@click="resetFilters"
|
||||||
flat dense round
|
flat
|
||||||
size="md"
|
no-caps
|
||||||
icon="close"
|
dense
|
||||||
/>
|
color="grey-6"
|
||||||
|
>
|
||||||
|
{{ $t('tasks__filters_reset')}}
|
||||||
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
<q-list class="w100">
|
</template>
|
||||||
<template v-for="filter in filters" :key="filter.id">
|
|
||||||
<q-item-label header v-if="filter.header">{{$t(filter.label)}}</q-item-label>
|
<template #footer>
|
||||||
<q-item v-else class="q-px-none">
|
<q-btn
|
||||||
<q-item-section side>
|
rounded
|
||||||
<q-checkbox
|
class="w100"
|
||||||
v-model="selectedFilters"
|
color="primary"
|
||||||
:val="filter.id"
|
@click="showFiltersDialog = false"
|
||||||
dense
|
>
|
||||||
class="q-px-sm"
|
{{$t('tasks__filters_continue')}}
|
||||||
/>
|
</q-btn>
|
||||||
</q-item-section>
|
</template>
|
||||||
<q-item-section>
|
|
||||||
<q-item-label class="flex items-center">
|
<div class="q-pl-sm text-bold text-caption">
|
||||||
<span>
|
{{ $t('tasks__filters_by_participant') }}
|
||||||
{{ $t(filter.label) }}
|
|
||||||
</span>
|
|
||||||
<pn-task-priority-icon :priority="filter.priority"/>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
</q-list>
|
|
||||||
</div>
|
</div>
|
||||||
</q-dialog>
|
|
||||||
|
<div class="flex column">
|
||||||
|
<div
|
||||||
|
v-for="(item,idx) in taskParticipantsOptions"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="filters.byParticipants"
|
||||||
|
:val="item.value"
|
||||||
|
>
|
||||||
|
{{ $t(item.label) }}
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-pl-sm q-mt-md text-bold text-caption">
|
||||||
|
{{ $t('tasks__filters_by_priority') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex column">
|
||||||
|
<div
|
||||||
|
v-for="(item,idx) in taskPriorityOptions"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="filters.byPriority"
|
||||||
|
:val="item.value"
|
||||||
|
>
|
||||||
|
{{ $t(item.label) }}
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</pn-bottom-sheet-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onActivated, onDeactivated, onBeforeUnmount } from 'vue'
|
import { ref, computed, onActivated, onDeactivated, onBeforeUnmount } from 'vue'
|
||||||
import { useTasksStore } from 'stores/tasks'
|
import { useTasksStore } from 'stores/tasks'
|
||||||
|
import { useUsersStore } from 'stores/users'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import taskItem from 'components/taskItem.vue'
|
import taskItem from 'components/taskItem.vue'
|
||||||
import type { Task } from 'types/Task'
|
import type { Task } from 'types/Task'
|
||||||
|
import { date } from 'quasar'
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const showCalendar = ref<boolean>(false)
|
const showCalendar = ref<boolean>(false)
|
||||||
const date = ref(Date.now())
|
const datesRange = ref<null | { from: string, to: string }>(null)
|
||||||
|
const deleteTaskId = ref<number | undefined>(undefined)
|
||||||
|
const restoreTaskId = ref<number | undefined>(undefined)
|
||||||
|
const showDialogDeleteTask = ref<boolean>(false)
|
||||||
|
const showDialogRestoreTask = ref<boolean>(false)
|
||||||
|
const showFiltersDialog = ref(false)
|
||||||
|
const showArchiveTasks = ref(false)
|
||||||
|
const currentSlideEvent = ref<SlideEvent | null>(null)
|
||||||
|
const closedByUserAction = ref(false)
|
||||||
const tasksStore = useTasksStore()
|
const tasksStore = useTasksStore()
|
||||||
|
const usersStore = useUsersStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const tasks = computed(() => tasksStore.tasks)
|
interface SlideEvent {
|
||||||
|
reset: () => void
|
||||||
// filter
|
|
||||||
const showFilters = ref(false)
|
|
||||||
const selectedFilters = ref([])
|
|
||||||
|
|
||||||
const filters = [
|
|
||||||
{ id: 'taskType', label: 'tasks__filters_types', header: true },
|
|
||||||
{ id: 'taskIn', label: 'tasks__filters_in' },
|
|
||||||
{ id: 'taskOut', label: 'tasks__filters_out' },
|
|
||||||
{ id: 'taskWatch', label: 'tasks__filters_watch' },
|
|
||||||
{ id: 'taskPriority', label: 'tasks__filters_priority', header: true },
|
|
||||||
{ id: 'taskPriorityNormal', label: 'tasks__filters_priority_normal' },
|
|
||||||
{ id: 'taskPriorityImportant', label: 'tasks__filters_priority_important', priority: 1 },
|
|
||||||
{ id: 'taskPriorityCritical', label: 'tasks__filters_priority_critical', priority: 2 }
|
|
||||||
]
|
|
||||||
|
|
||||||
const displayTasks = computed(() => {
|
|
||||||
let filteredTasks = [...tasks.value]
|
|
||||||
|
|
||||||
if (selectedFilters.value.length > 0) {
|
|
||||||
|
|
||||||
const filterFunctions = selectedFilters.value.map((filterId: string) => {
|
|
||||||
switch(filterId) {
|
|
||||||
case 'taskPriorityNormal':
|
|
||||||
return (task: Task) => task.priority === 0 || !task.priority
|
|
||||||
|
|
||||||
case 'taskPriorityImportant':
|
|
||||||
return (task: Task) => task.priority === 1
|
|
||||||
|
|
||||||
case 'taskPriorityCritical':
|
|
||||||
return (task: Task) => task.priority === 2
|
|
||||||
|
|
||||||
/* case 'taskIn':
|
|
||||||
return (task: Task) => task.type === 'in'
|
|
||||||
|
|
||||||
case 'taskOut':
|
|
||||||
return (task: Task) => task.type === 'out'
|
|
||||||
|
|
||||||
case 'taskWatch':
|
|
||||||
return (task: Task) => task.type === 'watch' */
|
|
||||||
|
|
||||||
default:
|
|
||||||
return () => true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
filteredTasks = filteredTasks.filter(task =>
|
|
||||||
filterFunctions.every(filterFn => filterFn(task))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search.value?.trim()) {
|
const tasks = tasksStore.getTasks
|
||||||
const searchValue = search.value.trim().toLowerCase()
|
const tasksDates = computed(() => tasks.map(el => date.formatDate(el.plan_date * 1000, 'YYYY/MM/DD')))
|
||||||
filteredTasks = filteredTasks.filter(task =>
|
|
||||||
task.name.toLowerCase().includes(searchValue) ||
|
const displayTasks = computed(() => {
|
||||||
(task.description && task.description.toLowerCase().includes(searchValue))
|
return filteredTasks.value.filter(archiveTasks)
|
||||||
|
|
||||||
|
function archiveTasks (el: Task) {
|
||||||
|
if (showArchiveTasks.value) return true
|
||||||
|
return (
|
||||||
|
el.close_date < Date.now() - 7 * 24 * 60 * 60 * 1000 // показыать закрытые менее недели назад
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return filteredTasks
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const hiddenTask = computed(() => filteredTasks.value.length - displayTasks.value.length)
|
||||||
|
|
||||||
|
const filteredTasks = computed(() => {
|
||||||
|
|
||||||
|
return tasks
|
||||||
|
.filter(searchTasks)
|
||||||
|
.filter(checkDateInterval)
|
||||||
|
.filter(byParticipants)
|
||||||
|
.filter(byPriority)
|
||||||
|
|
||||||
|
function searchTasks (el: Task) {
|
||||||
|
if (!search.value || !(search.value && search.value.trim())) return true
|
||||||
|
const searchValue = search.value.trim().toLowerCase()
|
||||||
|
return (
|
||||||
|
el.name.toLowerCase().includes(searchValue) ||
|
||||||
|
el.description && el.description.toLowerCase().includes(searchValue)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkDateInterval (el: Task) {
|
||||||
|
if (!datesRange.value) return true
|
||||||
|
const from = date.extractDate(datesRange.value.from, 'YYYY/MM/DD').getTime()
|
||||||
|
const to = date.extractDate(datesRange.value.to, 'YYYY/MM/DD').getTime() + 86399999
|
||||||
|
return (from < el.plan_date * 1000) && ( to >= el.plan_date * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function byParticipants(task: Task): boolean {
|
||||||
|
const selected = filters.value.byParticipants
|
||||||
|
if (selected.length === 0) return true
|
||||||
|
|
||||||
|
const userId = usersStore.myId.id
|
||||||
|
return selected.some(opt => {
|
||||||
|
switch (opt) {
|
||||||
|
case 1: return task.assigned_to === userId
|
||||||
|
case 2: return task.created_by === userId
|
||||||
|
case 3: return task.observers.includes(userId)
|
||||||
|
case 4: return task.assigned_to !== userId
|
||||||
|
&& task.created_by !== userId
|
||||||
|
&& !task.observers.includes(userId)
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function byPriority (el: Task) {
|
||||||
|
if (filters.value.byPriority.length === 0) return true
|
||||||
|
return filters.value.byPriority.includes(el.priority)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
interface Filters {
|
||||||
|
byParticipants: number[]
|
||||||
|
byPriority: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultFilters = {
|
||||||
|
byParticipants: [],
|
||||||
|
byPriority: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = ref<Filters>({ ...defaultFilters })
|
||||||
|
|
||||||
|
const checkFiltersSelect = computed(() => (
|
||||||
|
Object.values(filters.value).every(el => el.length === 0)
|
||||||
|
))
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
(Object.keys(filters.value) as (keyof Filters)[]).forEach(key => filters.value[key] = [])
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskParticipantsOptions = [
|
||||||
|
{ id: 1, value: 1, label: 'tasks__filters_to_me' },
|
||||||
|
{ id: 2, value: 2, label: 'tasks__filters_from_me' },
|
||||||
|
{ id: 3, value: 3, label: 'tasks__filters_observers' },
|
||||||
|
{ id: 4, value: 4, label: 'tasks__filters_not_involved' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const taskPriorityOptions = [
|
||||||
|
{ id: 0, value: 0, label: 'tasks__filters_priority_normal' },
|
||||||
|
{ id: 1, value: 1, label: 'tasks__filters_priority_important' },
|
||||||
|
{ id: 2, value: 2, label: 'tasks__filters_priority_critical' }
|
||||||
|
]
|
||||||
|
|
||||||
async function goTask (taskId: number) {
|
async function goTask (taskId: number) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300))
|
||||||
await router.push({ name: 'task_info', params: { taskId }})
|
await router.push({ name: 'task_info', params: { taskId }})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +360,57 @@
|
|||||||
await router.push({ name: 'task_add'})
|
await router.push({ name: 'task_add'})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSlideRight (event: SlideEvent, id: number) {
|
||||||
|
currentSlideEvent.value = event
|
||||||
|
showDialogDeleteTask.value = true
|
||||||
|
deleteTaskId.value = id
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSlideLeft (event: SlideEvent, id: number) {
|
||||||
|
currentSlideEvent.value = event
|
||||||
|
showDialogRestoreTask.value = true
|
||||||
|
restoreTaskId.value = id
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDialogBeforeHide () {
|
||||||
|
if (!closedByUserAction.value) {
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
closedByUserAction.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancel() {
|
||||||
|
closedByUserAction.value = true
|
||||||
|
if (currentSlideEvent.value) {
|
||||||
|
currentSlideEvent.value.reset()
|
||||||
|
currentSlideEvent.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onConfirmDelete() {
|
||||||
|
closedByUserAction.value = true
|
||||||
|
if (deleteTaskId.value) {
|
||||||
|
await tasksStore.remove(deleteTaskId.value)
|
||||||
|
}
|
||||||
|
currentSlideEvent.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onConfirmCancel() {
|
||||||
|
closedByUserAction.value = true
|
||||||
|
if (deleteTaskId.value) {
|
||||||
|
await tasksStore.setCancelStatus(deleteTaskId.value)
|
||||||
|
}
|
||||||
|
currentSlideEvent.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onConfirmRestore() {
|
||||||
|
closedByUserAction.value = true
|
||||||
|
if (restoreTaskId.value) {
|
||||||
|
await tasksStore.setRestoreStatus(restoreTaskId.value)
|
||||||
|
}
|
||||||
|
currentSlideEvent.value = null
|
||||||
|
}
|
||||||
|
|
||||||
// fix fab jumping
|
// fix fab jumping
|
||||||
const showFab = ref(false)
|
const showFab = ref(false)
|
||||||
const timerId = ref<ReturnType<typeof setTimeout> | null>(null)
|
const timerId = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||||
@@ -245,8 +438,12 @@
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
/* fix mini border after slide */
|
/* fix mini border after slide */
|
||||||
:deep(.q-slide-item__right)
|
:deep(.q-slide-item__right) {
|
||||||
{
|
align-self: center;
|
||||||
|
height: 98%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.q-slide-item__left) {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
height: 98%;
|
height: 98%;
|
||||||
}
|
}
|
||||||
@@ -260,10 +457,4 @@
|
|||||||
.fix-calendar :deep(.q-date__calendar-days-container) {
|
.fix-calendar :deep(.q-date__calendar-days-container) {
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-panel {
|
|
||||||
max-height: 70vh;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -27,26 +27,26 @@
|
|||||||
@click="goUserInfo(item.id)"
|
@click="goUserInfo(item.id)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar>
|
<pn-auto-avatar
|
||||||
<q-img v-if="item.photo" :src="item.photo" fit="cover"/>
|
:img="item.photo"
|
||||||
<pn-auto-avatar v-else :name="item.section1"/>
|
:name="item.section1"
|
||||||
</q-avatar>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section q-item-section>
|
||||||
<q-item-label lines="1" class="text-bold" v-if="item.section1">
|
<q-item-label lines="1" class="text-bold" v-if="item.section1">
|
||||||
{{item.section1}}
|
{{item.section1}}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label lines="1" caption v-if="item.section3">
|
<q-item-label lines="1" caption v-if="item.section3">
|
||||||
{{item.section3}}
|
{{item.section3}}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label caption lines="2">
|
<q-item-label caption lines="2">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<q-icon name="telegram" v-if="item.section2_1 || item.section2_2" class="q-pr-xs" style="color: #27a7e7"/>
|
<q-icon name="telegram" v-if="item.section2_1 || item.section2_2" class="q-pr-xs" style="color: #27a7e7"/>
|
||||||
<div v-if="item.section2_1" class="q-mr-sm text-bold">{{item.section2_1}}</div>
|
<div v-if="item.section2_1" class="q-mr-sm text-bold">{{item.section2_1}}</div>
|
||||||
<div class="text-blue" v-if="item.section2_2">{{'@' + item.section2_2}}</div>
|
<div class="text-blue" v-if="item.section2_2">{{'@' + item.section2_2}}</div>
|
||||||
</div>
|
</div>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
|
|||||||
@@ -24,26 +24,42 @@ export default defineRouter(function (/* { store, ssrContext } */) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Router.beforeEach(async (to) => {
|
Router.beforeEach(async (to) => {
|
||||||
console.log(to)
|
|
||||||
if (to.name === 'settings') return;
|
if (to.name === 'settings') return
|
||||||
const projectsStore = useProjectsStore()
|
if (to.name === '404') return
|
||||||
|
|
||||||
|
const projectsStore = useProjectsStore()
|
||||||
|
console.log('router mount', projectsStore.startRouteInfo)
|
||||||
|
|
||||||
|
if (projectsStore.startRouteInfo && to.path === '/') {
|
||||||
|
const { id, taskId, meetingId } = projectsStore.startRouteInfo
|
||||||
|
projectsStore.setStartRouteInfo(null)
|
||||||
|
|
||||||
|
if (!projectsStore.isInit) await projectsStore.init()
|
||||||
|
const project = projectsStore.projectById(id)
|
||||||
|
|
||||||
|
if (!project) return { name: '404' }
|
||||||
|
|
||||||
|
return taskId
|
||||||
|
? { name: 'task_info', params: { id, taskId } }
|
||||||
|
: meetingId
|
||||||
|
? { name: 'meeting_info', params: { id, meetingId } }
|
||||||
|
: { name: 'files', params: { id } }
|
||||||
|
}
|
||||||
|
|
||||||
if (to.params.id) {
|
if (to.params.id) {
|
||||||
const projectId = Number(to.params.id)
|
const projectId = Number(to.params.id)
|
||||||
|
|
||||||
if (!projectsStore.isInit) await projectsStore.init()
|
if (!projectsStore.isInit) await projectsStore.init()
|
||||||
|
|
||||||
const project = projectsStore.projectById(projectId)
|
const project = projectsStore.projectById(projectId)
|
||||||
if (!project) return { name: 'page404' }
|
if (!project) return { name: '404' }
|
||||||
|
|
||||||
if (projectsStore.currentProjectId !== projectId) {
|
if (projectsStore.currentProjectId !== projectId) {
|
||||||
projectsStore.setCurrentProjectId(projectId)
|
projectsStore.setCurrentProjectId(projectId)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (!projectsStore.startProjectId) return { name: 'page404' }
|
return { name: '404' }
|
||||||
projectsStore.setCurrentProjectId(projectsStore.startProjectId)
|
|
||||||
return { name: 'files', params: { id: projectsStore.startProjectId }}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -61,7 +77,7 @@ export default defineRouter(function (/* { store, ssrContext } */) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Router.afterEach((to) => {
|
Router.afterEach((to) => {
|
||||||
const BackButton = window.Telegram?.WebApp?.BackButton;
|
const BackButton = window.Telegram?.WebApp?.BackButton
|
||||||
if (BackButton) {
|
if (BackButton) {
|
||||||
// Управление видимостью
|
// Управление видимостью
|
||||||
if (to.meta.hideBackButton) {
|
if (to.meta.hideBackButton) {
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/project/:id(\\d+)/task/add',
|
path: '/project/:id(\\d+)/task/add',
|
||||||
component: () => import('pages/TaskAddPage.vue')
|
component: () => import('pages/TaskAddPage.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'task_edit',
|
||||||
|
path: '/project/:id(\\d+)/task/:taskId(\\d+)/edit',
|
||||||
|
component: () => import('pages/TaskEditPage.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'task_info',
|
name: 'task_info',
|
||||||
path: '/project/:id(\\d+)/task/:taskId(\\d+)',
|
path: '/project/:id(\\d+)/task/:taskId(\\d+)',
|
||||||
@@ -60,7 +65,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'meeting_edit',
|
name: 'meeting_edit',
|
||||||
path: '/project/:id(\\d+)/meeting/:meetingId(\\d+)',
|
path: '/project/:id(\\d+)/meeting/:meetingId(\\d+)/edit',
|
||||||
component: () => import('pages/MeetingEditPage.vue'),
|
component: () => import('pages/MeetingEditPage.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -81,6 +86,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: '404',
|
||||||
path: '/:catchAll(.*)*',
|
path: '/:catchAll(.*)*',
|
||||||
component: () => import('pages/ErrorNotFound.vue'),
|
component: () => import('pages/ErrorNotFound.vue'),
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/stores/auth.ts
Normal file
21
src/stores/auth.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { api } from 'boot/axios'
|
||||||
|
import type { WebApp } from '@twa-dev/types'
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
const isInit = ref(false)
|
||||||
|
const telegramUserData = ref()
|
||||||
|
|
||||||
|
async function init (tg: WebApp) {
|
||||||
|
await api.post('/auth?' + tg?.initData)
|
||||||
|
telegramUserData.value = tg?.initDataUnsafe.user
|
||||||
|
isInit.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isInit,
|
||||||
|
telegramUserData,
|
||||||
|
init
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -13,8 +13,8 @@ export const useChatsStore = defineStore('chats', () => {
|
|||||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
||||||
|
|
||||||
async function init () {
|
async function init () {
|
||||||
const response = await api.get('/project/' + currentProjectId.value + '/chat')
|
const { data } = await api.get('/project/' + currentProjectId.value + '/chat')
|
||||||
const chatsAPI = response.data.data
|
const chatsAPI = data.data
|
||||||
chats.value.push(...chatsAPI)
|
chats.value.push(...chatsAPI)
|
||||||
isInit.value = true
|
isInit.value = true
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,8 @@ export const useChatsStore = defineStore('chats', () => {
|
|||||||
isInit.value = false
|
isInit.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getChats = computed(() => chats.value)
|
||||||
|
|
||||||
function chatById (id: number) {
|
function chatById (id: number) {
|
||||||
return chats.value.find(el =>el.id === id)
|
return chats.value.find(el =>el.id === id)
|
||||||
}
|
}
|
||||||
@@ -33,6 +35,7 @@ export const useChatsStore = defineStore('chats', () => {
|
|||||||
isInit,
|
isInit,
|
||||||
init,
|
init,
|
||||||
reset,
|
reset,
|
||||||
|
getChats,
|
||||||
chatById
|
chatById
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ import { ref, computed } from 'vue'
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { api } from 'boot/axios'
|
import { api } from 'boot/axios'
|
||||||
import { useProjectsStore } from 'stores/projects'
|
import { useProjectsStore } from 'stores/projects'
|
||||||
import type { File } from 'types/File'
|
import type { FileLink } from 'types/FileLink'
|
||||||
|
|
||||||
export const useFilesStore = defineStore('files', () => {
|
export const useFilesStore = defineStore('files', () => {
|
||||||
|
|
||||||
const files = ref<File[]>([])
|
const files = ref<FileLink[]>([])
|
||||||
const isInit = ref<boolean>(false)
|
const isInit = ref<boolean>(false)
|
||||||
|
|
||||||
const projectsStore = useProjectsStore()
|
const projectsStore = useProjectsStore()
|
||||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
||||||
|
|
||||||
async function init () {
|
async function init () {
|
||||||
const response = await api.get('/project/' + currentProjectId.value + '/file')
|
const { data } = await api.get('/project/' + currentProjectId.value + '/file')
|
||||||
const filesAPI = response.data.data
|
const filesAPI = data.data
|
||||||
files.value.push(...filesAPI)
|
files.value.push(...filesAPI)
|
||||||
isInit.value = true
|
isInit.value = true
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,7 @@ export const useFilesStore = defineStore('files', () => {
|
|||||||
return (await response).data.data
|
return (await response).data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFiles = computed(() => files.value)
|
||||||
|
|
||||||
function fileById (id: number) {
|
function fileById (id: number) {
|
||||||
return files.value.find(el =>el.id === id)
|
return files.value.find(el =>el.id === id)
|
||||||
@@ -46,6 +47,7 @@ export const useFilesStore = defineStore('files', () => {
|
|||||||
reset,
|
reset,
|
||||||
fileUrl,
|
fileUrl,
|
||||||
remove,
|
remove,
|
||||||
|
getFiles,
|
||||||
fileById
|
fileById
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export const useMeetingsStore = defineStore('meetings', () => {
|
|||||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
||||||
|
|
||||||
async function init () {
|
async function init () {
|
||||||
const response = await api.get('/project/' + currentProjectId.value + '/meeting')
|
const { data } = await api.get('/project/' + currentProjectId.value + '/meeting')
|
||||||
const meetingsAPI = response.data.data
|
const meetingsAPI = data.data
|
||||||
meetings.value.push(...meetingsAPI)
|
meetings.value.push(...meetingsAPI)
|
||||||
isInit.value = true
|
isInit.value = true
|
||||||
}
|
}
|
||||||
@@ -24,44 +24,57 @@ export const useMeetingsStore = defineStore('meetings', () => {
|
|||||||
isInit.value = false
|
isInit.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function add (meetingData: MeetingParams) {
|
async function add (meetingData: MeetingParams, newFiles: File[]) {
|
||||||
const response = await api.post('/project/' + currentProjectId.value + '/meeting', meetingData)
|
const { data } = await api.post('/project/' + currentProjectId.value + '/meeting', meetingData)
|
||||||
const newMeetingAPI = response.data.data
|
const newMeetingAPI = data.data
|
||||||
meetings.value.push(newMeetingAPI)
|
meetings.value.push(newMeetingAPI)
|
||||||
return newMeetingAPI
|
const id = newMeetingAPI.id
|
||||||
|
await updateParticipants(id, meetingData.participants)
|
||||||
|
if (newFiles.length !== 0) await attachFiles(id, newFiles)
|
||||||
|
return newMeetingAPI //not include files and participants!!
|
||||||
}
|
}
|
||||||
|
|
||||||
async function update (meetingId: number, meetingData: MeetingParams) {
|
async function update (meetingId: number, meetingData: MeetingParams) {
|
||||||
const response = await api.put('/project/' + currentProjectId.value + '/meeting/' + meetingId, meetingData)
|
const { data } = await api.put('/project/' + currentProjectId.value + '/meeting/' + meetingId, meetingData)
|
||||||
const meetingAPI = response.data.data
|
const meetingAPI = data.data
|
||||||
const idx = meetings.value.findIndex(item => item.id === meetingAPI.id)
|
const idx = meetings.value.findIndex(item => item.id === meetingAPI.id)
|
||||||
if (meetings.value[idx]) Object.assign(meetings.value[idx], meetingAPI)
|
if (meetings.value[idx]) Object.assign(meetings.value[idx], meetingAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateParticipants (meetingId: number, participants: number[]) {
|
async function updateParticipants (meetingId: number, participants: number[]) {
|
||||||
const response = await api.put('/project/' + currentProjectId.value + '/meeting/' + meetingId + '/participant', participants)
|
const { data } = await api.put('/project/' + currentProjectId.value + '/meeting/' + meetingId + '/participant', participants)
|
||||||
const participantsAPI = response.data.data
|
const participantsAPI = data.data
|
||||||
const idx = meetings.value.findIndex(item => item.id === meetingId)
|
const idx = meetings.value.findIndex(item => item.id === meetingId)
|
||||||
if (meetings.value[idx]) meetings.value[idx].participants = participantsAPI
|
if (meetings.value[idx]) meetings.value[idx].participants = participantsAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
async function attachFiles (meetingId: number, files: number[]) {
|
async function attachFiles (meetingId: number, files: File[]) {
|
||||||
const response = await api.put('/project/' + currentProjectId.value + '/meeting/' + meetingId + '/attach', files)
|
const formData = new FormData()
|
||||||
const filesAPI = response.data.data
|
files.forEach(file => formData.append('files[]', file))
|
||||||
|
const { data } = await api.post(
|
||||||
|
'/project/' + currentProjectId.value + '/meeting/' + meetingId + '/attach',
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const filesAPI = data.data
|
||||||
const idx = meetings.value.findIndex(item => item.id === meetingId)
|
const idx = meetings.value.findIndex(item => item.id === meetingId)
|
||||||
if (meetings.value[idx]) meetings.value[idx].files = filesAPI
|
if (meetings.value[idx]) meetings.value[idx].files = filesAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setCancelStatus (meetingId: number, status: boolean) {
|
async function setCancelStatus (meetingId: number, status: boolean) {
|
||||||
const response = await api.put('/project/' + currentProjectId.value + '/meeting/' + meetingId, { is_cancel: status })
|
const { data } = await api.put('/project/' + currentProjectId.value + '/meeting/' + meetingId, { is_cancel: status })
|
||||||
const meetingAPI = response.data.data
|
const meetingAPI = data.data
|
||||||
const idx = meetings.value.findIndex(item => item.id === meetingAPI.id)
|
const idx = meetings.value.findIndex(item => item.id === meetingAPI.id)
|
||||||
if (meetings.value[idx]) Object.assign(meetings.value[idx], meetingAPI)
|
if (meetings.value[idx]) Object.assign(meetings.value[idx], meetingAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove (meetingId: number) {
|
async function remove (meetingId: number) {
|
||||||
const response = await api.delete('/project/' + currentProjectId.value + '/meeting/' + meetingId)
|
const { data } = await api.delete('/project/' + currentProjectId.value + '/meeting/' + meetingId)
|
||||||
const meetingAPIid = response.data.data.id
|
const meetingAPIid = data.data.id
|
||||||
const idx = meetings.value.findIndex(item => item.id === meetingAPIid)
|
const idx = meetings.value.findIndex(item => item.id === meetingAPIid)
|
||||||
meetings.value.splice(idx, 1)
|
meetings.value.splice(idx, 1)
|
||||||
}
|
}
|
||||||
@@ -70,6 +83,9 @@ export const useMeetingsStore = defineStore('meetings', () => {
|
|||||||
return meetings.value.find(el => el.id === id)
|
return meetings.value.find(el => el.id === id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getters
|
||||||
|
const getMeetings = computed(() => meetings.value)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
meetings,
|
meetings,
|
||||||
isInit,
|
isInit,
|
||||||
@@ -81,6 +97,7 @@ export const useMeetingsStore = defineStore('meetings', () => {
|
|||||||
attachFiles,
|
attachFiles,
|
||||||
setCancelStatus,
|
setCancelStatus,
|
||||||
remove,
|
remove,
|
||||||
|
getMeetings,
|
||||||
meetingById
|
meetingById
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { ref, watch } from 'vue'
|
import { ref, watch, computed } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { api } from 'boot/axios'
|
import { api } from 'boot/axios'
|
||||||
|
|
||||||
|
import { useAuthStore } from 'stores/auth'
|
||||||
|
|
||||||
import { useFilesStore } from 'stores/files'
|
import { useFilesStore } from 'stores/files'
|
||||||
import { useTasksStore } from 'stores/tasks'
|
import { useTasksStore } from 'stores/tasks'
|
||||||
import { useMeetingsStore } from 'stores/meetings'
|
import { useMeetingsStore } from 'stores/meetings'
|
||||||
@@ -13,7 +15,6 @@ import type { Project } from 'types/Project'
|
|||||||
export const useProjectsStore = defineStore('projects', () => {
|
export const useProjectsStore = defineStore('projects', () => {
|
||||||
const projects = ref<Project[]>([])
|
const projects = ref<Project[]>([])
|
||||||
const currentProjectId = ref<number | null>(null)
|
const currentProjectId = ref<number | null>(null)
|
||||||
const startProjectId = ref<number | null>(null)
|
|
||||||
const isInit = ref<boolean>(false)
|
const isInit = ref<boolean>(false)
|
||||||
|
|
||||||
const filesStore = useFilesStore()
|
const filesStore = useFilesStore()
|
||||||
@@ -23,8 +24,8 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||||||
const chatsStore = useChatsStore()
|
const chatsStore = useChatsStore()
|
||||||
|
|
||||||
async function init () {
|
async function init () {
|
||||||
const response = await api.get('/project')
|
const { data } = await api.get('/project')
|
||||||
const projectsAPI = response.data.data
|
const projectsAPI = data.data
|
||||||
projects.value.push(...projectsAPI)
|
projects.value.push(...projectsAPI)
|
||||||
isInit.value = true
|
isInit.value = true
|
||||||
}
|
}
|
||||||
@@ -43,18 +44,21 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||||||
currentProjectId.value = id
|
currentProjectId.value = id
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStartProjectId (id: number | null) {
|
|
||||||
startProjectId.value = id
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
async function initStores () {
|
async function initStores () {
|
||||||
resetStores()
|
resetStores()
|
||||||
if (!filesStore.isInit) await filesStore.init()
|
|
||||||
if (!tasksStore.isInit) await tasksStore.init()
|
await Promise.all([
|
||||||
if (!meetingsStore.isInit) await meetingsStore.init()
|
filesStore.init(),
|
||||||
if (!usersStore.isInit) await usersStore.init()
|
tasksStore.init(),
|
||||||
if (!chatsStore.isInit) await chatsStore.init()
|
meetingsStore.init(),
|
||||||
|
usersStore.init(),
|
||||||
|
chatsStore.init()
|
||||||
|
])
|
||||||
|
|
||||||
|
usersStore.setMyId(authStore.telegramUserData.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetStores () {
|
function resetStores () {
|
||||||
@@ -65,6 +69,14 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||||||
chatsStore.reset()
|
chatsStore.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getProjects = computed(() => projects.value)
|
||||||
|
|
||||||
|
const startRouteInfo = ref<{ id: number; taskId?: number; meetingId?: number } | null>(null)
|
||||||
|
|
||||||
|
function setStartRouteInfo (info: { id: number; taskId?: number; meetingId?: number } | null) {
|
||||||
|
startRouteInfo.value = info
|
||||||
|
}
|
||||||
|
|
||||||
watch (currentProjectId, async (newId) => {
|
watch (currentProjectId, async (newId) => {
|
||||||
if (newId) await initStores(); else resetStores()
|
if (newId) await initStores(); else resetStores()
|
||||||
}, { flush: 'sync' })
|
}, { flush: 'sync' })
|
||||||
@@ -75,11 +87,12 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||||||
isInit,
|
isInit,
|
||||||
projects,
|
projects,
|
||||||
currentProjectId,
|
currentProjectId,
|
||||||
startProjectId,
|
|
||||||
projectById,
|
projectById,
|
||||||
setCurrentProjectId,
|
setCurrentProjectId,
|
||||||
setStartProjectId,
|
|
||||||
initStores,
|
initStores,
|
||||||
resetStores
|
resetStores,
|
||||||
|
getProjects,
|
||||||
|
startRouteInfo,
|
||||||
|
setStartRouteInfo
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -54,8 +54,6 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
console.error('Quasar Error load locale:', quasarLang, e)
|
console.error('Quasar Error load locale:', quasarLang, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const detectLocale = (): string => {
|
const detectLocale = (): string => {
|
||||||
const localeMap = {
|
const localeMap = {
|
||||||
@@ -113,10 +111,10 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/settings')
|
const { data } = await api.get('/settings')
|
||||||
settings.value = {
|
settings.value = {
|
||||||
fontSize: response.data.data.settings.fontSize || defaultSettings.fontSize,
|
fontSize: data.data.settings.fontSize || defaultSettings.fontSize,
|
||||||
locale: response.data.data.settings.locale || detectLocale()
|
locale: data.data.settings.locale || detectLocale()
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
settings.value.locale = detectLocale()
|
settings.value.locale = detectLocale()
|
||||||
|
|||||||
@@ -45,13 +45,37 @@ export const useTasksStore = defineStore('tasks', () => {
|
|||||||
if (tasks.value[idx]) tasks.value[idx].participants = observersAPI
|
if (tasks.value[idx]) tasks.value[idx].participants = observersAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
async function attachFiles (taskId: number, files: number[]) {
|
async function attachFiles (taskId: number, files: File[]) {
|
||||||
const response = await api.put('/project/' + currentProjectId.value + '/task/' + taskId + '/attach', files)
|
const formData = new FormData()
|
||||||
|
files.forEach(file => formData.append('files[]', file))
|
||||||
|
const response = await api.post(
|
||||||
|
'/project/' + currentProjectId.value + '/task/' + taskId + '/attach',
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
const filesAPI = response.data.data
|
const filesAPI = response.data.data
|
||||||
const idx = tasks.value.findIndex(item => item.id === taskId)
|
const idx = tasks.value.findIndex(item => item.id === taskId)
|
||||||
if (tasks.value[idx]) tasks.value[idx].files = filesAPI
|
if (tasks.value[idx]) tasks.value[idx].files = filesAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setCancelStatus (taskId: number) {
|
||||||
|
const response = await api.put('/project/' + currentProjectId.value + '/task/' + taskId, { status: 6 })
|
||||||
|
const taskAPI = response.data.data
|
||||||
|
const idx = tasks.value.findIndex(item => item.id === taskAPI.id)
|
||||||
|
if (tasks.value[idx]) Object.assign(tasks.value[idx], taskAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setRestoreStatus (taskId: number) {
|
||||||
|
const response = await api.put('/project/' + currentProjectId.value + '/task/' + taskId, { status: 0 })
|
||||||
|
const taskAPI = response.data.data
|
||||||
|
const idx = tasks.value.findIndex(item => item.id === taskAPI.id)
|
||||||
|
if (tasks.value[idx]) Object.assign(tasks.value[idx], taskAPI)
|
||||||
|
}
|
||||||
|
|
||||||
async function remove (taskId: number) {
|
async function remove (taskId: number) {
|
||||||
const response = await api.delete('/project/' + currentProjectId.value + '/task/' + taskId)
|
const response = await api.delete('/project/' + currentProjectId.value + '/task/' + taskId)
|
||||||
const taskAPIid = response.data.data.id
|
const taskAPIid = response.data.data.id
|
||||||
@@ -59,6 +83,8 @@ export const useTasksStore = defineStore('tasks', () => {
|
|||||||
tasks.value.splice(idx, 1)
|
tasks.value.splice(idx, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTasks = computed(() => tasks.value)
|
||||||
|
|
||||||
function taskById (id :number) {
|
function taskById (id :number) {
|
||||||
return tasks.value.find(el => el.id === id)
|
return tasks.value.find(el => el.id === id)
|
||||||
}
|
}
|
||||||
@@ -72,7 +98,10 @@ export const useTasksStore = defineStore('tasks', () => {
|
|||||||
update,
|
update,
|
||||||
updateObservers,
|
updateObservers,
|
||||||
attachFiles,
|
attachFiles,
|
||||||
|
setCancelStatus,
|
||||||
|
setRestoreStatus,
|
||||||
remove,
|
remove,
|
||||||
|
getTasks,
|
||||||
taskById
|
taskById
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,15 +6,21 @@ import type { User } from 'types/User'
|
|||||||
|
|
||||||
export const useUsersStore = defineStore('users', () => {
|
export const useUsersStore = defineStore('users', () => {
|
||||||
|
|
||||||
|
interface myId {
|
||||||
|
telegram_id: number
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
const users = ref<User[]>([])
|
const users = ref<User[]>([])
|
||||||
const isInit = ref<boolean>(false)
|
const isInit = ref<boolean>(false)
|
||||||
|
const myId = ref<myId>({ telegram_id: -1, id: -1 })
|
||||||
|
|
||||||
const projectsStore = useProjectsStore()
|
const projectsStore = useProjectsStore()
|
||||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
||||||
|
|
||||||
async function init () {
|
async function init () {
|
||||||
const response = await api.get('/project/' + currentProjectId.value + '/user')
|
const { data } = await api.get('/project/' + currentProjectId.value + '/user')
|
||||||
const usersAPI = response.data.data
|
const usersAPI = data.data
|
||||||
users.value.push(...usersAPI)
|
users.value.push(...usersAPI)
|
||||||
isInit.value = true
|
isInit.value = true
|
||||||
}
|
}
|
||||||
@@ -34,6 +40,8 @@ export const useUsersStore = defineStore('users', () => {
|
|||||||
return users.value.find(el =>el.id === id)
|
return users.value.find(el =>el.id === id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getUsers = computed(() => users.value)
|
||||||
|
|
||||||
function userNameById (id: number) {
|
function userNameById (id: number) {
|
||||||
const user = userById(id)
|
const user = userById(id)
|
||||||
return user?.fullname
|
return user?.fullname
|
||||||
@@ -42,13 +50,22 @@ export const useUsersStore = defineStore('users', () => {
|
|||||||
|| '---'
|
|| '---'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setMyId (telegram_id: number | undefined) {
|
||||||
|
if (!telegram_id) return
|
||||||
|
const me = users.value.find(el => el.telegram_id === telegram_id)
|
||||||
|
if (me) myId.value = { telegram_id: telegram_id, id: me.id }
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
users,
|
users,
|
||||||
isInit,
|
isInit,
|
||||||
|
myId,
|
||||||
init,
|
init,
|
||||||
reset,
|
reset,
|
||||||
reload,
|
reload,
|
||||||
userById,
|
userById,
|
||||||
userNameById
|
getUsers,
|
||||||
|
userNameById,
|
||||||
|
setMyId
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
interface File {
|
interface FileLink {
|
||||||
id: number
|
id: number
|
||||||
project_id: number
|
project_id: number
|
||||||
origin_chat_id: number
|
origin_chat_id: number
|
||||||
origin_message_id: number
|
origin_message_id: number
|
||||||
chat_id: number
|
chat_id: number
|
||||||
message_id: number
|
message_id: number
|
||||||
|
telegram_chat_id: number
|
||||||
file_id: number
|
file_id: number
|
||||||
filename: string
|
filename: string
|
||||||
mime: string
|
mime: string
|
||||||
@@ -19,5 +20,5 @@ interface File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
File
|
FileLink
|
||||||
}
|
}
|
||||||
@@ -3,16 +3,18 @@ interface MeetingParams {
|
|||||||
description: string
|
description: string
|
||||||
place: string
|
place: string
|
||||||
meet_date: number
|
meet_date: number
|
||||||
chat_attach: number | null
|
chat_id: number | null
|
||||||
participants: number[]
|
participants: number[]
|
||||||
files: number[]
|
files: number[]
|
||||||
is_cancel: boolean
|
is_cancel: boolean
|
||||||
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Meeting extends MeetingParams {
|
interface Meeting extends MeetingParams {
|
||||||
id: number
|
id: number
|
||||||
project_id: number
|
project_id: number
|
||||||
created_by: number
|
created_by: number
|
||||||
|
is_editable: boolean
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
interface TaskParams {
|
interface TaskParams {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
assigned_to: number
|
assigned_to: number | null
|
||||||
priority: 0 | 1 | 2 | 3
|
priority: 0 | 1 | 2 | 3
|
||||||
status: 1 | 5
|
status: 1 | 5 | 6
|
||||||
time_spent?: number
|
time_spent?: number
|
||||||
create_date: number
|
|
||||||
plan_date: number
|
plan_date: number
|
||||||
|
observers: number[]
|
||||||
|
files: number[]
|
||||||
|
chat_id: number | null
|
||||||
|
close_files: number[]
|
||||||
|
close_comment: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Task extends TaskParams {
|
interface Task extends TaskParams {
|
||||||
@@ -14,8 +18,7 @@ interface Task extends TaskParams {
|
|||||||
project_id: number
|
project_id: number
|
||||||
created_by: number
|
created_by: number
|
||||||
closed_by: number | null
|
closed_by: number | null
|
||||||
observers: number[]
|
create_date: number
|
||||||
files: number[]
|
|
||||||
close_date: number
|
close_date: number
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user