Compare commits

..

2 Commits

Author SHA1 Message Date
83d2666f22 udpate
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-27 09:50:32 +03:00
3b628cbfb3 before update auth 2025-08-18 13:21:54 +03:00
62 changed files with 598 additions and 769 deletions

View File

@@ -12,5 +12,8 @@
"typescript", "typescript",
"vue" "vue"
], ],
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib",
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
}
} }

View File

@@ -1,234 +0,0 @@
<template>
<pn-page-card>
<template #title>
{{ $t('software__title') }}
</template>
<pn-scroll-list>
<template #card-body-header>
<div class="q-pa-md">
{{ $t('software__description') }}
</div>
</template>
<q-list separator>
<q-expansion-item
v-for="item in software"
group="somegroup"
>
<template #header>
<q-item-section avatar>
<q-avatar square size="sm" v-if="item.logo">
<img :src="'3software/logo/' + item.logo">
</q-avatar>
</q-item-section>
<q-item-section>
<div class="flex items-baseline">
<span class="text-h6">
{{ item.name }}
</span>
<span v-if = "item.ver" class="text-caption q-pl-xs">
{{ 'v.' + item.ver }}
</span>
</div>
</q-item-section>
</template>
<div class="w100 flex column q-px-md q-gutter-y-md q-py-sm">
<div class="flex row no-wrap items-center">
<q-icon name="mdi-scale-balance" size="sm" class="q-pr-lg" color="grey"/>
<div
@click="downloadFile('3software/license/' + item.license_file)"
class="flex w100 column q-pl-sm cursor-pointer"
>
<span> {{ item.license }} </span>
<span
class="text-caption"
style="white-space: pre-line;"
>
{{ item.license_copyright }}
</span>
</div>
</div>
<div class="flex row no-wrap items-center" v-if="item.web">
<q-icon name="mdi-web" size="sm" class="q-pr-lg" color="grey"/>
<span
class="q-pl-sm cursor-pointer"
@click="tg.openLink(item.web_url)"
>
{{ item.web }}
</span>
</div>
<div class="flex row no-wrap items-center" v-if="item.git">
<q-icon name="mdi-github" size="sm" class="q-pr-lg" color="grey"/>
<span
class="q-pl-sm cursor-pointer"
@click="tg.openLink(item.git_url)"
>
{{ item.git }}
</span>
</div>
</div>
</q-expansion-item>
</q-list>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { inject } from 'vue'
import type { WebApp } from '@twa-dev/types'
const tg = inject('tg') as WebApp
const software = [
{
id: 1,
name: 'Vue',
ver: '3.4',
logo: 'vue.webp',
web: 'vuejs.org',
web_url: 'https://vuejs.org/',
git: 'vuejs/core',
git_url: 'https://github.com/vuejs/core',
license: 'MIT License',
license_copyright: 'Copyright (c) 2018-present, Yuxi (Evan) You and Vue contributors',
license_file: 'vue/LICENSE.txt'
},
{
id: 2,
name: 'Quasar',
ver: '2.x',
logo: 'quasar.png',
web: 'quasar.dev',
web_url: 'https://quasar.dev/',
git: 'quasarframework/quasar',
git_url: 'https://github.com/quasarframework/quasar',
license: 'MIT License',
license_copyright: 'Copyright (c) 2015-present Razvan Stoenescu',
license_file: 'quasar/LICENSE.txt'
},
{
id: 3,
name: 'Pinia',
ver: '2.x',
logo: 'pinia.svg',
web: 'pinia.vuejs.org',
web_url: 'https://pinia.vuejs.org/',
git: 'vuejs/pinia',
git_url: 'https://github.com/vuejs/pinia',
license: 'MIT License',
license_copyright: 'Copyright (c) 2019-present Eduardo San Martin Morote',
license_file: 'pinia/LICENSE.txt'
},
{
id: 4,
name: 'Vue Router',
ver: '4.x',
logo: 'vue.webp',
web: 'router.vuejs.org',
web_url: 'https://router.vuejs.org/',
git: 'vuejs/router',
git_url: 'https://github.com/vuejs/router',
license: 'MIT License',
license_copyright: 'Copyright (c) 2019-present Eduardo San Martin Morote',
license_file: 'vue-router/LICENSE.txt'
},
{
id: 5,
name: 'Vue i18n',
ver: '9.x',
logo: 'vue-i18n.svg',
web: 'vue-i18n.intlify.dev',
web_url: 'https://vue-i18n.intlify.dev/',
git: 'intlify/vue-i18n',
git_url: 'https://github.com/intlify/vue-i18n',
license: 'MIT License',
license_copyright: 'Copyright (c) 2016-present kazuya kawaguchi and contributors',
license_file: 'vue-i18n/LICENSE.txt'
},
{
id: 6,
name: 'Axios',
ver: '1.x',
logo: 'axios.svg',
web: 'axios-http.com',
web_url: 'https://axios-http.com/',
git: 'axios/axios',
git_url: 'https://github.com/axios/axios',
license: 'MIT License',
license_copyright: 'Copyright (c) 2014-present Matt Zabriskie & Collaborators',
license_file: 'axios/LICENSE.txt'
},
{
id: 7,
name: 'better-sqlite3',
ver: '11.x',
logo: '',
web: '',
web_url: '',
git: 'WiseLibs/better-sqlite3',
git_url: 'https://github.com/WiseLibs/better-sqlite3',
license: 'MIT License',
license_copyright: 'Copyright (c) 2017 Joshua Wise',
license_file: 'better-sqlite3/LICENSE.txt'
},
{
id: 8,
name: 'Express',
ver: '4.x',
logo: 'express.svg',
web: 'expressjs.com',
web_url: 'https://expressjs.com/',
git: 'expressjs/express',
git_url: 'https://github.com/expressjs/express',
license: 'MIT License',
license_copyright: `Copyright (c) 2009-2014 TJ Holowaychuk <tj@vision-media.ca>
Copyright (c) 2013-2014 Roman Shtylman <shtylman+expressjs@gmail.com>
Copyright (c) 2014-2015 Douglas Christopher Wilson <doug@somethingdoug.com>`,
license_file: 'express/LICENSE.txt'
}
]
const downloadFile = async (url: string) => {
try {
const fullUrl = '/admin/' + url;
const fileUrl = new URL(fullUrl, window.location.origin).href;
const response = await fetch(fileUrl)
if (!response.ok) throw new Error(`HTTP error: ${response.status}`)
const blob = await response.blob()
const downloadUrl = URL.createObjectURL(blob)
const link = document.createElement('a')
const extractFileName = (url: string): string => {
return url.substring(url.lastIndexOf('/') + 1)
}
link.href = downloadUrl
link.download = extractFileName(url)
link.style.display = 'none'
document.body.appendChild(link)
link.click()
setTimeout(() => {
document.body.removeChild(link)
URL.revokeObjectURL(downloadUrl)
}, 100)
} catch (error) {
console.error('Download error:', error)
}
}
</script>
<style scope>
</style>

Binary file not shown.

View File

@@ -17,6 +17,7 @@
<!-- <!--
<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<% } %>">
--> -->
<link rel="manifest" href="/site.webmanifest">
<link rel="icon" href="icons/favicon.svg" type="image/svg+xml"> <link rel="icon" href="icons/favicon.svg" type="image/svg+xml">
<link rel="icon" type="image/ico" href="icons/favicon.ico"> <link rel="icon" type="image/ico" href="icons/favicon.ico">
<link rel="apple-touch-icon" href="icons/apple-touch-icon.png"> <link rel="apple-touch-icon" href="icons/apple-touch-icon.png">

16
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@quasar/extras": "^1.17.0", "@quasar/extras": "^1.17.0",
"@quasar/vite-plugin": "^1.10.0", "@quasar/vite-plugin": "^1.10.0",
"axios": "^1.2.1", "axios": "^1.2.1",
"browser-image-compression": "^2.0.2",
"pinia": "^2.0.11", "pinia": "^2.0.11",
"quasar": "^2.18.2", "quasar": "^2.18.2",
"vue": "^3.4.18", "vue": "^3.4.18",
@@ -3478,6 +3479,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/browser-image-compression": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz",
"integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==",
"license": "MIT",
"dependencies": {
"uzip": "0.20201231.0"
}
},
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.4", "version": "4.24.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
@@ -11023,6 +11033,12 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/uzip": {
"version": "0.20201231.0",
"resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz",
"integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==",
"license": "MIT"
},
"node_modules/varint": { "node_modules/varint": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",

View File

@@ -18,6 +18,7 @@
"@quasar/extras": "^1.17.0", "@quasar/extras": "^1.17.0",
"@quasar/vite-plugin": "^1.10.0", "@quasar/vite-plugin": "^1.10.0",
"axios": "^1.2.1", "axios": "^1.2.1",
"browser-image-compression": "^2.0.2",
"pinia": "^2.0.11", "pinia": "^2.0.11",
"quasar": "^2.18.2", "quasar": "^2.18.2",
"vue": "^3.4.18", "vue": "^3.4.18",

View File

@@ -1,7 +0,0 @@
# Copyright (c) 2014-present Matt Zabriskie & Collaborators
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2017 Joshua Wise
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,24 +0,0 @@
(The MIT License)
Copyright (c) 2009-2014 TJ Holowaychuk <tj@vision-media.ca>
Copyright (c) 2013-2014 Roman Shtylman <shtylman+expressjs@gmail.com>
Copyright (c) 2014-2015 Douglas Christopher Wilson <doug@somethingdoug.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2019-present Eduardo San Martin Morote
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015-present Razvan Stoenescu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016-present kazuya kawaguchi and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2019-present Eduardo San Martin Morote
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2018-present, Yuxi (Evan) You and Vue contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,8 +0,0 @@
<svg width="188" height="28" viewBox="0 0 188 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M93.295 3.65206L86.356 9.30495H90.8876V27.68L93.295 25.7411V3.65206Z" fill="#5A29E4"/>
<path d="M95.295 24.0997L102.356 18.305H97.6975V0.350052L95.295 2.02275V24.0997Z" fill="#5A29E4"/>
<path d="M182.695 6.95295C183.495 7.36895 184.071 7.72095 184.423 8.00895L186.919 3.25695C185.671 2.48895 184.167 1.80095 182.407 1.19295C180.679 0.584955 178.807 0.280952 176.791 0.280952C174.871 0.280952 173.095 0.600952 171.463 1.24095C169.863 1.88095 168.583 2.82495 167.623 4.07295C166.695 5.32095 166.231 6.87295 166.231 8.72895C166.231 10.809 166.887 12.409 168.199 13.529C169.543 14.617 171.591 15.513 174.343 16.217C176.551 16.793 178.327 17.321 179.671 17.801C181.047 18.249 181.735 19.001 181.735 20.057C181.735 21.625 180.263 22.409 177.319 22.409C175.847 22.409 174.455 22.233 173.143 21.881C171.831 21.529 170.679 21.097 169.687 20.585C168.727 20.073 168.039 19.609 167.623 19.193L165.031 24.233C166.695 25.289 168.599 26.121 170.743 26.729C172.887 27.337 175.047 27.641 177.223 27.641C179.111 27.641 180.871 27.385 182.503 26.873C184.135 26.329 185.447 25.465 186.439 24.281C187.463 23.065 187.975 21.4649 187.975 19.4809C187.975 17.8489 187.591 16.537 186.823 15.545C186.087 14.521 185.015 13.705 183.607 13.097C182.231 12.489 180.599 11.945 178.711 11.465C176.567 10.953 174.935 10.489 173.815 10.073C172.727 9.65695 172.183 8.95295 172.183 7.96095C172.183 6.26495 173.687 5.41695 176.695 5.41695C177.815 5.41695 178.903 5.57695 179.959 5.89695C181.015 6.18495 181.927 6.53695 182.695 6.95295Z" fill="#5A29E4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M132.182 27.497C130.07 27.497 128.15 27.129 126.422 26.393C124.694 25.625 123.205 24.601 121.957 23.321C120.709 22.009 119.749 20.537 119.077 18.905C118.406 17.273 118.069 15.593 118.069 13.865C118.069 12.105 118.421 10.409 119.125 8.77695C119.829 7.14495 120.822 5.70496 122.102 4.45695C123.382 3.17695 124.885 2.16895 126.613 1.43295C128.341 0.696953 130.229 0.328949 132.277 0.328949C134.389 0.328949 136.31 0.728952 138.038 1.52895C139.766 2.29695 141.238 3.33695 142.454 4.64895C143.702 5.92895 144.661 7.38495 145.333 9.01695C146.005 10.649 146.342 12.3129 146.342 14.0089C146.342 15.7689 145.99 17.465 145.286 19.097C144.582 20.697 143.589 22.137 142.309 23.417C141.061 24.665 139.574 25.657 137.846 26.393C136.118 27.129 134.23 27.497 132.182 27.497ZM123.925 13.913C123.925 15.353 124.262 16.729 124.934 18.041C125.605 19.321 126.549 20.361 127.765 21.161C129.013 21.961 130.501 22.361 132.229 22.361C133.989 22.361 135.477 21.945 136.693 21.113C137.91 20.249 138.837 19.177 139.477 17.8969C140.117 16.5849 140.438 15.241 140.438 13.865C140.438 12.425 140.102 11.0649 139.43 9.78495C138.758 8.50495 137.798 7.48095 136.549 6.71295C135.333 5.91295 133.878 5.51295 132.182 5.51295C130.422 5.51295 128.917 5.92895 127.669 6.76095C126.453 7.59295 125.525 8.64896 124.885 9.92896C124.245 11.209 123.925 12.537 123.925 13.913Z" fill="#5A29E4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 27.305L11.712 0.472954H16.464L28.128 27.305H21.984L19.296 21.017H8.88L6.192 27.305H0ZM14.112 7.52895L10.176 15.977H17.904L14.112 7.52895Z" fill="#5A29E4"/>
<path d="M50.8211 0.472954L58.2131 9.97695L65.6051 0.472954H71.8931L61.2851 14.057L71.5571 27.305H65.2691L58.2131 18.185L51.2051 27.305H44.8211L55.1411 14.057L44.4851 0.472954H50.8211Z" fill="#5A29E4"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" class="express-logo" viewBox="0 0 1120.322 250" width="90" height="30">
<path d="M347.47162 250V4.890464h13.29516v38.55596a50.415241 50.415241 0 0 0 4.33954-5.43506c11.10412-23.03785 34.52487-37.57744 60.09412-37.29026 30.31296-.90408 54.94623 10.31704 69.422 37.29026a119.86915 119.86915 0 0 1 2.89302 109.881816c-13.4866 30.22788-46.79895 45.25672-82.46189 39.73657a66.688515 66.688515 0 0 1-53.22317-35.12049v97.4801zm13.29516-158.403836 2.97812 28.781356c5.25425 32.75927 24.62263 52.11702 55.03132 55.75458a62.540425 62.540425 0 0 0 68.69874-39.73657c11.82737-28.18574 10.95521-60.094116-2.33995-87.620406a61.26409 61.26409 0 0 0-64.54001-35.66294 59.668671 59.668671 0 0 0-53.30827 44.07611 304.40595 304.40595 0 0 0-6.51995 34.39724zm420.10574 33.312346a71.687494 71.687494 0 0 1-70.06017 63.35941c-55.75458 2.80794-81.91945-34.21642-86.07817-76.94174a123.27271 123.27271 0 0 1 10.85948-67.890396 75.729222 75.729222 0 0 1 78.83497-42.26797 72.432023 72.432023 0 0 1 64.26348 55.12705 371.94535 371.94535 0 0 1 6.24341 40.73636H638.50796c-2.71221 38.736776 18.10269 69.879356 47.6073 77.388456 36.74782 9.04071 68.1563-6.88157 79.55823-41.82125 2.5314-8.96625 7.14748-10.23195 15.29475-7.68992zM638.4016 84.629504h130.97326c-.81898-41.26817-26.51586-71.26205-61.37045-71.60241-39.35367-.63816-67.89039 28.15383-69.60281 71.60241zm169.53986 41.183076h12.83781a51.478853 51.478853 0 0 0 30.22787 44.35265 79.026422 79.026422 0 0 0 68.61365-1.80814 30.844768 30.844768 0 0 0 18.10269-30.3236 27.973013 27.973013 0 0 0-18.82595-27.97301c-14.12477-5.25425-29.14298-8.14727-43.53366-12.763346a319.0838 319.0838 0 0 1-43.81021-16.01801c-23.18675-11.31684-24.62263-55.39295 1.62733-69.34755a92.427941 92.427941 0 0 1 88.34367-1.36142c16.95398 9.35979 26.32441 28.26019 23.53775 47.43712h-11.00839c0-.5318-.9998-.99979-.9998-1.54223-1.36142-35.09922-30.86604-46.07571-62.54043-42.99123-9.57251 1.06361-18.64513 3.95664-27.15403 8.23236a27.122123 27.122123 0 0 0-15.74147 27.15404 27.122123 27.122123 0 0 0 18.10269 25.5267c13.82697 5.07343 28.50482 8.32809 42.81041 12.306l34.56741 9.04071a40.842727 40.842727 0 0 1 28.05811 36.843536c2.76539 18.56004-6.70076 36.801-23.44203 45.25672-30.22787 17.10289-80.01558 12.58254-102.1919-9.04071-11.34875-11.41256-17.67724-26.9094-17.54961-42.99123zm306.10774-67.794666h-12.0401c0-1.62733-.6382-3.19084-.819-4.43526a39.353669 39.353669 0 0 0-32.0466-37.83271 79.026422 79.026422 0 0 0-50.7769 2.44631 30.844768 30.844768 0 0 0-22.35715 29.41953 28.398458 28.398458 0 0 0 21.71895 28.60054l55.0313 14.12478a153.05386 153.05386 0 0 1 17.5497 5.33934c17.5496 6.381666 29.462 22.676216 29.9938 41.300076a45.203539 45.203539 0 0 1-27.6539 42.96995 100.72412 100.72412 0 0 1-81.4621.81898 56.477833 56.477833 0 0 1-34.0356-54.85051h11.76356c4.42463 21.32544 19.07054 39.08777 39.16224 47.49031 20.0916 8.40254 43.0337 6.33913 61.3066-5.48824a32.333825 32.333825 0 0 0 17.3794-30.22787 27.973013 27.973013 0 0 0-19.1024-27.7922c-14.1248-5.25425-29.143-8.05155-43.5337-12.763356a320.67922 320.67922 0 0 1-44.0761-15.83719c-22.6337-11.13602-24.44184-54.8505 1.3614-68.79447a91.151606 91.151606 0 0 1 89.7902-.99979 47.330764 47.330764 0 0 1 22.7188 46.43733zM325.59311 183.84329a20.740447 20.740447 0 0 1-25.70752-9.7746l-46.79895-64.72083-6.78585-9.04071-54.30807 73.85727a19.889557 19.889557 0 0 1-24.44182 9.59378l69.96445-93.863816-65.0931-84.81247c9.6576-3.48865 20.42136.29781 25.79261 9.04071l48.50074 65.51854 48.78791-65.26328a19.464112 19.464112 0 0 1 24.261-9.05134l-25.2608 33.51443-34.21642 44.53347a9.040708 9.040708 0 0 0 0 13.48661l65.16755 86.982236zM622.66013 4.177844v12.76335a65.624902 65.624902 0 0 0-69.87935 67.79467v99.564786h-12.94417V4.975554h12.76336v36.74781c15.65637-26.80304 39.82165-36.74781 70.14525-37.47107ZM.021272 88.724414l5.700964-28.15383c15.656379-55.66949 79.473139-78.83497 123.379074-44.35265 25.70751 20.18737 32.1211 48.78792 30.86604 81.01538H15.135208C12.79526 154.79603 54.329335 189.55489 107.45679 171.81383c17.50706-6.38167 30.68522-20.93189 35.02476-39.01331 2.80794-9.04071 7.44529-10.59359 15.93292-7.9771a73.495636 73.495636 0 0 1-35.12049 53.68054 85.089014 85.089014 0 0 1-99.118064-12.66763c-12.93353-14.53959-20.740447-32.91881-22.261413-52.32975 0-3.19084-1.063613-6.16895-1.808142-9.0407Q0 96.414334 0 88.724414Zm15.294751-3.89282H146.28929c-.81898-41.72553-27.15403-71.32587-62.274525-71.60241-39.098402-.53181-67.071415 28.41973-68.794468 71.42159Z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,123 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="256"
height="224"
viewBox="0 0 67.733332 59.266668"
version="1.1"
id="svg8"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
sodipodi:docname="vue-i18n.svg"
inkscape:export-filename="/Users/kazupon/Desktop/vue-i18n.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2"
inkscape:cx="65.171196"
inkscape:cy="106.17152"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
showguides="false"
inkscape:lockguides="false"
inkscape:window-width="1280"
inkscape:window-height="751"
inkscape:window-x="0"
inkscape:window-y="1"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid3715" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-237.73331)">
<rect
style="fill:#42b983;fill-opacity:1;stroke-width:0.44801387"
id="rect4524"
width="67.73333"
height="59.266666"
x="0"
y="237.73331"
ry="6.8791666" />
<rect
style="fill:#34495e;fill-opacity:1;stroke-width:1.29214942"
id="rect4528"
width="55.033333"
height="45.508335"
x="6.3499999"
y="244.61247"
ry="6.8791666" />
<path
style="fill:#34495e;fill-opacity:1;stroke:none;stroke-width:5.39703941;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 33.866667,270.54165 v 26.45833 L 6.35,270.54165 Z"
id="path4538"
inkscape:connector-curvature="0" />
<circle
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2.11716342;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="path4567"
cx="34.008522"
cy="-267.371"
transform="scale(1,-1)"
r="15.610168" />
<ellipse
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2.067662;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="path4567-7"
cx="33.929485"
cy="-267.40591"
transform="scale(1,-1)"
rx="7.4328356"
ry="15.634919" />
<path
style="fill:none;stroke:#ffffff;stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 33.866667,252.02081 v 30.42708 z"
id="path4596"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#ffffff;stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 48.947917,267.89581 H 18.520837 Z"
id="path4596-2"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#ffffff;stroke-width:2.02254486;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 47.625,259.95831 H 19.843753 Z"
id="path4596-2-8"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#ffffff;stroke-width:2.02254486;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 47.625,275.30414 H 19.843753 Z"
id="path4596-2-8-4"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -58,7 +58,7 @@ 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') 'utils': path.resolve(__dirname, './src/utils')
}, },
vueRouterMode: 'history', // available values: 'hash', 'history' vueRouterMode: 'history', // available values: 'hash', 'history'

View File

@@ -0,0 +1,44 @@
*This document is an English adaptation of the Subscription Plan Terms originally drafted in Russian. In the event of any disputes subject to resolution in courts of the Russian Federation, the Russian-language version shall prevail.*
# Subscription Plan Terms
###### Version 1.02 dated 02.09.2025
The text of this document is an integral part of the Terms of Use.
## 1. General Principles
1.1. All Plans provide identical functionality, except for the limitations on the number of supported (active) Connected Chats.
1.2. The maximum subscription term is 730 days (2 years).
1.3. Upon termination of the subscription, access to the Application's functionality may be suspended. Data created during the period without an active subscription may not be displayed in the Application.
1.4. A notification of the need to renew the subscription is sent to the Administrator via the bot or by email (depending on the authentication method) no later than 14 calendar days before its expiration.
1.5. Renewal and changes of subscription plans are performed exclusively through the Admin Panel in the "Settings > Subscribe" section.
1.6. The Developer has the right to unilaterally extend the subscription term without additional charge (within the framework of marketing promotions or for other reasons).
## 2. Changing Subscription Plans
2.1. The Application Administrator has the right to change the subscription plan after 14 (fourteen) calendar days from the moment of the previous Plan change (unless the TEST Plan is used).
2.2. The change of the subscription plan is performed automatically (except for the case specified in clause 2.5.5), after confirmation of the remuneration transfer is received.
2.3. When changing the Plan, the cost of the unused period of the current subscription is credited towards the payment for the new plan in the form of additional days.
2.4. Upgrade to a Plan with a Higher Number of Chats
2.4.1. When switching from the TEST Plan, no additional days are accrued.
2.4.2. The base daily cost is calculated using the formula:
Base Daily Cost = (Base Plan Cost / 30) * (100% - Period Discount)
2.4.3. The number of additional days is calculated using the formula, with the result rounded up to a whole number:
Number of Additional Days = (Base Daily Cost on the current Plan * Number of unused days) / Base Daily Cost on the new Plan
2.5. Downgrade to a Plan with a Lower Number of Chats
2.5.1. A Plan change is only possible if the number of current (active) Connected Chats in the Application does not exceed the number supported by the new Plan.
2.5.2. When calculating the daily cost of the current Plan, the period discount is not taken into account if more than 14 days remain until the subscription expiration.
2.5.3. The base daily cost is determined according to the formula, taking into account clause 2.5.2:
Base Daily Cost = (Base Plan Cost / 30) * (100% - Period Discount)
2.5.4. The number of additional days is calculated using the formula, with the result rounded up to a whole number or zero:
Number of Additional Days = (Paid cost of the current Plan (Base Daily Cost on the current Plan * Number of days used)) / Base Daily Cost on the new Plan
2.5.5. Transition to the TEST Plan is performed only by contacting support, followed by a manual recalculation and refund.
2.6. All calculations are performed using rounding. Claims for the under-accrual of up to 3 (Three) additional days are not accepted by the Developer.
2.7. The Developer reserves the right to suspend the Administrator's account or cancel the results of the recalculation if signs of systematic abuse of the plan change mechanism are detected (e.g., frequent cyclic switching between Plans). In such cases, the Administrator's account may be blocked without a refund.

View File

@@ -0,0 +1,40 @@
# Положение о Тарифных планах подписки
###### Версия 1.02 от 02.09.2025
Текст настоящего документа является неотъемлемой частью Пользовательского соглашения.
## 1. Общие принципы
1.1. Все Тарифы обеспечивают идентичный функционал, за исключением ограничений на количество поддерживаемых (активных) Подключенных чатов.
1.2. Максимальный срок действия подписки составляет 730 дней (2 года).
1.3. При прекращении действия подписки доступ к функционалу Приложения может быть приостановлен. Данные, созданные в период отсутствия активной подписки, могут не отображаться в Приложении.
1.4. Уведомление о необходимости продления подписки направляется Администратору через бота или по электронной почте (в зависимости от способа аутентификации) не позднее чем за 14 календарных дней до истечения её срока действия.
1.5. Продление и смена тарифных планов осуществляются исключительно через Панель администратора в разделе «Настройки > Подписка».
1.6. Разработчик вправе в одностороннем порядке продлить действие подписки без взимания дополнительной платы (в рамках маркетинговых акций или по иным причинам).
## 2. Смена тарифных планов
2.1. Администратор Приложения вправе сменить тарифный план по истечении 14 (четырнадцати) календарных дней с момента предыдущего изменения Тарифа (если не используется Тариф TEST).
2.2. Смена тарифного плана производится в автоматическом режиме (за исключением случая, указанного в п. 2.5.5), после получения подтверждения перечисления вознаграждения.
2.3. При смене Тарифа стоимость неиспользованного периода текущей подписки засчитывается в счёт оплаты нового тарифного плана в виде дополнительных дней.
2.4. Переход на Тариф с большим количеством чатов
2.4.1. При переходе с Тарифа TEST дополнительные дни не начисляются.
2.4.2. Базовая стоимость дня рассчитывается по формуле:
**Базовая стоимость дня = (Базовая стоимость Тарифа / 30) * (100% - Скидка за период)**
2.4.3. Количество дополнительных дней рассчитывается по формуле, с округлением полученного результата вверх до целого значения:
**Количество дополнительных дней = (Базовая стоимость дня на текущем Тарифе * Количество неиспользованных дней) / Базовая стоимость дня на новом Тарифе**
2.5. Переход на Тариф с меньшим количеством чатов
2.5.1. Смена Тарифа возможна, только если количество текущих (активных) Подключенных чатов в Приложении не превышает количества, которое поддерживает новый Тариф.
2.5.2. При расчете стоимости дня текущего Тарифа скидка за период не учитывается, если до окончания срока действия подписки осталось более 14 дней.
2.5.3. Базовая стоимость дня принимается согласно формуле, с учетом п. 2.5.2:
**Базовая стоимость дня = (Базовая стоимость Тарифа / 30) * (100% - Скидка за период)**
2.5.4. Количество дополнительных дней рассчитывается по формуле, с округлением полученного результата вверх до целого значения или нуля:
**Количество дополнительных дней = (Оплаченная стоимость текущего Тарифа (Базовая стоимость дня на текущем Тарифе * Количество использованных дней)) / Базовая стоимость дня на новом Тарифе**
2.5.5. Переход на Тариф TEST осуществляется только через обращение в поддержку с последующим ручным перерасчетом и возвратом средств.
2.6. Все расчеты производятся с применением округлений. Претензии по недоначислению до 3 (Трех) дополнительных дней не принимаются Разработчиком.
2.7. Разработчик оставляет за собой право приостановить действие аккаунта Администратора или отменить результаты перерасчета при выявлении признаков систематического злоупотребления механизмом смены тарифных планов (например, частых циклических переключений между Тарифами). В таких случаях аккаунт Администратора может быть заблокирован без возврата средств.

View File

@@ -13,6 +13,7 @@ export default defineBoot(({ app }) => {
if (window.Telegram?.WebApp) { if (window.Telegram?.WebApp) {
const webApp = window.Telegram.WebApp const webApp = window.Telegram.WebApp
webApp.ready() webApp.ready()
sessionStorage.setItem('isTelegram', webApp.initData ? 'true' : 'false')
webApp.SettingsButton.isVisible = true webApp.SettingsButton.isVisible = true
app.config.globalProperties.$tg = webApp app.config.globalProperties.$tg = webApp
app.provide('tg', webApp) app.provide('tg', webApp)

View File

@@ -0,0 +1,25 @@
<template>
<svg
class="q-ma-none q-pa-none"
:style="{
fill: color ?? 'red',
height: size ?? '42px',
width: 'auto'
}"
width="14" height="15" viewBox="0 0 14 15" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M6.63869 12.1902L3.50621 14.1092C3.18049 14.3087 2.75468 14.2064 2.55515 13.8807C2.45769 13.7216 2.42864 13.5299 2.47457 13.3491L2.95948 11.4405C3.13452 10.7515 3.60599 10.1756 4.24682 9.86791L7.6642 8.22716C7.82352 8.15067 7.89067 7.95951 7.81418 7.80019C7.75223 7.67116 7.61214 7.59896 7.47111 7.62338L3.66713 8.28194C2.89387 8.41581 2.1009 8.20228 1.49941 7.69823L0.297703 6.69116C0.00493565 6.44581 -0.0335059 6.00958 0.211842 5.71682C0.33117 5.57442 0.502766 5.48602 0.687982 5.47153L4.35956 5.18419C4.61895 5.16389 4.845 4.99974 4.94458 4.75937L6.36101 1.3402C6.5072 0.987302 6.91179 0.819734 7.26469 0.965925C7.43413 1.03612 7.56876 1.17075 7.63896 1.3402L9.05539 4.75937C9.15496 4.99974 9.38101 5.16389 9.6404 5.18419L13.3322 5.47311C13.713 5.50291 13.9975 5.83578 13.9677 6.2166C13.9534 6.39979 13.8667 6.56975 13.7269 6.68896L10.9114 9.08928C10.7131 9.25826 10.6267 9.52425 10.6876 9.77748L11.5532 13.3733C11.6426 13.7447 11.414 14.1182 11.0427 14.2076C10.8642 14.2506 10.676 14.2208 10.5195 14.1249L7.36128 12.1902C7.13956 12.0544 6.8604 12.0544 6.63869 12.1902Z"></path></svg>
</template>
<script setup lang="ts">
defineProps({
color: String,
size: String,
})
</script>
<style scoped>
.telegram-star-wrapper :deep(.telegram-star svg) {
fill: red;
}
</style>

View File

@@ -60,7 +60,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { CompanyParams } from 'types/Company' import type { CompanyParams } from 'types/Company'
import { convertEmptyStringsToNull } from 'helpers/helpers' import { convertEmptyStringsToNull } from 'src/utils/helpers'
const { t }= useI18n() const { t }= useI18n()

View File

@@ -1,13 +1,13 @@
<template> <template>
<pn-page-card> <pn-page-card>
<template #title> <template #title>
{{ $t(type + '__title') }} {{ $t(title) }}
</template> </template>
<pn-scroll-list> <pn-scroll-list>
<markdown-viewver <markdown-viewver
class="q-pa-md" class="q-pa-md"
:locale :locale
:documentName = "getDocumentName()" :documentName
/> />
</pn-scroll-list> </pn-scroll-list>
</pn-page-card> </pn-page-card>
@@ -18,22 +18,15 @@
import { useSettingsStore } from 'stores/settings' import { useSettingsStore } from 'stores/settings'
import MarkdownViewver from 'components/MarkdownViewver.vue' import MarkdownViewver from 'components/MarkdownViewver.vue'
const props = defineProps<{ defineProps<{
type: 'terms_of_use' | 'privacy' | 'consent' title: string
documentName: string
}>() }>()
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const DEFAULT_LOCALE = 'ru' const DEFAULT_LOCALE = 'ru'
const locale = ref('ru') const locale = ref('ru')
const getDocumentName = () =>{
switch(props.type) {
case 'terms_of_use': return 'Terms_of_use'
case 'privacy': return 'Privacy-Policy'
case 'consent': return 'Consent_to_Personal_Data_Processing'
}
}
const parseLocale = (locale: string) => locale.split(/[-_]/)[0] || DEFAULT_LOCALE const parseLocale = (locale: string) => locale.split(/[-_]/)[0] || DEFAULT_LOCALE
onMounted(() => { onMounted(() => {

View File

@@ -61,6 +61,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, Ref, computed } from 'vue' // eslint-disable-line import { ref, Ref, computed } from 'vue' // eslint-disable-line
import { QFile } from 'quasar' import { QFile } from 'quasar'
import imageCompression from 'browser-image-compression'
const modelValue = defineModel<string>() const modelValue = defineModel<string>()
@@ -70,6 +71,13 @@
avatar?: boolean avatar?: boolean
}>() }>()
const compressionOptions = {
maxSizeMB: 0.5, // Максимальный размер ~500 КБ
maxWidthOrHeight: 1200, // Максимальное разрешение
useWebWorker: true, // Для производительности
fileType: 'image/jpeg' // Формат вывода
}
const imageFile = ref(null) // file-from selector const imageFile = ref(null) // file-from selector
const imgFileSelector= ref() as Ref<QFile> // input file DOM const imgFileSelector= ref() as Ref<QFile> // input file DOM
const size = ref<number>(props.size ? props.size : 100) const size = ref<number>(props.size ? props.size : 100)
@@ -85,10 +93,21 @@
return String(size.value) + 'px' return String(size.value) + 'px'
}) })
async function handleUpload () { async function handleUpload() {
if (imageFile.value) { if (imageFile.value) {
const img = await imgToBase64(imageFile.value) try {
modelValue.value = typeof img === 'string' ? img : '' const compressedFile = await imageCompression(
imageFile.value,
compressionOptions
);
const img = await imgToBase64(compressedFile);
modelValue.value = typeof img === 'string' ? img : '';
} catch (error) {
console.error('Image error compression:', error);
const img = await imgToBase64(imageFile.value);
modelValue.value = typeof img === 'string' ? img : '';
}
} }
} }
@@ -119,7 +138,14 @@
} }
function checkImgType(files: File[]): File[] { function checkImgType(files: File[]): File[] {
return files.filter((file: File) => file.type === 'image/x-png' || file.type === 'image/jpeg' || file.type === 'image/webp' ) const allowedTypes = [
'image/jpeg',
'image/png',
'image/webp',
'image/gif',
'image/bmp'
]
return files.filter((file: File) => allowedTypes.includes(file.type))
} }
</script> </script>

View File

@@ -8,7 +8,7 @@
rounded color="primary" rounded color="primary"
class="w100 q-mt-md q-mb-xs fix-disabled-btn" class="w100 q-mt-md q-mb-xs fix-disabled-btn"
:disable="!isFormValid" :disable="!isFormValid"
@click = "emit('update')" @click = "onSubmit"
> >
{{ $t(btnText) }} {{ $t(btnText) }}
</q-btn> </q-btn>
@@ -45,15 +45,6 @@
class="w100 q-pt-sm" class="w100 q-pt-sm"
:label="$t('project_block__project_description')" :label="$t('project_block__project_description')"
/> />
<!-- <q-checkbox
v-if="modelValue.logo"
v-model="modelValue.is_logo_bg"
class="w100"
dense
>
{{ $t('project_block__image_use_as_background_chats') }}
</q-checkbox> -->
</div> </div>
</div> </div>
</pn-scroll-list> </pn-scroll-list>
@@ -62,9 +53,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, computed, ref } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { ProjectParams } from 'types/Project' import type { ProjectParams } from 'types/Project'
import { convertEmptyStringsToNull } from 'src/utils/helpers'
const { t } = useI18n() const { t } = useI18n()
@@ -95,11 +87,10 @@
return Object.values(validations).every(Boolean) return Object.values(validations).every(Boolean)
}) })
const initialProject = ref({} as ProjectParams) function onSubmit() {
const cleanedData = convertEmptyStringsToNull(modelValue.value)
onMounted(() => { emit('update', cleanedData)
initialProject.value = { ...modelValue.value } }
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -25,7 +25,7 @@
/> />
<div <div
v-if="userStatus" v-if="userStatus"
class="absolute-center text-h4 text-bold q-pa-sm" class="absolute-center text-h4 text-bold q-pa-sm text-center"
:class ="'status-' + userStatus.status" :class ="'status-' + userStatus.status"
> >
{{ $t(userStatus.text) }} {{ $t(userStatus.text) }}
@@ -132,8 +132,8 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useCompaniesStore } from 'stores/companies' import { useCompaniesStore } from 'stores/companies'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { convertEmptyStringsToNull } from 'src/helpers/helpers' import { convertEmptyStringsToNull } from 'src/utils/helpers'
import type { User } from 'types/Users' import type { User } from 'types/User'
const { t } = useI18n() const { t } = useI18n()
@@ -177,8 +177,9 @@
) )
const userStatus = computed(() => { const userStatus = computed(() => {
if (modelValue.value.is_blocked) return { status: 'blocked', text: 'user_block__user_blocked'} if (modelValue.value.is_blocked) return { status: 'blocked', text: 'user_block__user_blocked' }
if (modelValue.value.is_leave) return { status: 'leave', text: 'user_block__user_leave'} if (modelValue.value.is_leave) return { status: 'leave', text: 'user_block__user_leave' }
if (!modelValue.value.is_terms_accepted) return { status: 'pending', text: 'user_block__user_pending' }
return null return null
}) })
@@ -190,12 +191,17 @@
} }
.status-blocked { .status-blocked {
border: 2px solid red; border: 2px solid var(--q-negative);
color: red; color: var(--q-negative);
} }
.status-leave { .status-leave {
border: 2px solid var(--q-primary); border: 2px solid var(--q-primary);
color: var(--q-primary); color: var(--q-primary);
} }
.status-pending{
border: 2px solid var(--q-secondary);
color: var(--q-secondary);
}
</style> </style>

View File

@@ -1,5 +1,5 @@
import { useCompaniesStore } from 'stores/companies' import { useCompaniesStore } from 'stores/companies'
import type { User } from 'types/Users' import type { User } from 'types/User'
export function useUserSection() { export function useUserSection() {
const companiesStore = useCompaniesStore() const companiesStore = useCompaniesStore()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -234,7 +234,6 @@
}, },
2: async () => { 2: async () => {
await authStore.confirmCurrentEmailCode(code.value) await authStore.confirmCurrentEmailCode(code.value)
console.log(code.value)
}, },
3: async () => { 3: async () => {
await authStore.getCodeNewEmail(code.value, newLogin.value) await authStore.getCodeNewEmail(code.value, newLogin.value)

View File

@@ -105,7 +105,8 @@
await router.push({ name: 'login' }) await router.push({ name: 'login' })
} }
function onConfirmStopUsing () { async function onConfirmStopUsing () {
await authStore.termsRevoked()
tg.close() tg.close()
} }

View File

@@ -6,37 +6,35 @@
<pn-scroll-list> <pn-scroll-list>
<div class="flex column w100 q-pa-md q-gutter-y-md"> <div class="flex column w100 q-pa-md q-gutter-y-md">
<div> <div>
{{ $t('agreements__description') }} <div v-if="route.query.type === 'update'">{{ $t('agreements__description_update') }}</div>
<div>{{ $t('agreements__description') }}</div>
</div> </div>
<div class="flex items-center no-wrap"> <div class="flex items-center no-wrap text-caption">
<q-checkbox v-model="agreement" val="1" class="q-pr-sm"/> <q-checkbox v-model="agreement" val="1" class="q-pr-sm"/>
<span> <span>
{{ $t('agreements__checkbox_agreement_terms') + ' ' }} {{ $t('agreements__checkbox_agreement_terms') + ' ' }}
<span <span
@click="router.push({ name: 'terms' })" @click="router.push({ name: 'terms' })"
class="cursor-pointer" class="cursor-pointer text-primary"
style="text-decoration: underline;"
> >
{{ $t('agreements__checkbox_agreement_terms_doc') }} {{ $t('agreements__checkbox_agreement_terms_doc') }}
</span> </span>
</span> </span>
</div> </div>
<div class="flex items-center no-wrap"> <div class="flex items-center no-wrap text-caption">
<q-checkbox v-model="agreement" val="2" class="q-pr-sm"/> <q-checkbox v-model="agreement" val="2" class="q-pr-sm"/>
<span> <span>
{{ $t('agreements__checkbox_agreement_consent') + ' ' }} {{ $t('agreements__checkbox_agreement_consent') + ' ' }}
<span <span
@click="router.push({ name: 'consent' })" @click="router.push({ name: 'consent' })"
class="cursor-pointer" class="cursor-pointer text-primary"
style="text-decoration: underline;"
> >
{{ $t('agreements__checkbox_agreement_consent_doc') }} {{ $t('agreements__checkbox_agreement_consent_doc') }}
</span> </span>
{{ ' ' + $t('agreements__checkbox_agreement_privacy') + ' ' }} {{ ' ' + $t('agreements__checkbox_agreement_privacy') + ' ' }}
<span <span
@click="router.push({ name: 'consent' })" @click="router.push({ name: 'privacy' })"
class="cursor-pointer" class="cursor-pointer text-primary"
style="text-decoration: underline;"
> >
{{ $t('agreements__checkbox_agreement_privacy_doc') }} {{ $t('agreements__checkbox_agreement_privacy_doc') }}
</span> </span>
@@ -78,7 +76,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, inject } from 'vue' import { ref, inject } from 'vue'
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from 'stores/auth' import { useAuthStore } from 'stores/auth'
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
@@ -87,13 +85,33 @@
tg.close() tg.close()
} }
const route = useRoute()
const authStore = useAuthStore() const authStore = useAuthStore()
const router = useRouter() const router = useRouter()
async function onSubmit () { async function onSubmit () {
await authStore.termsAccepted()
await router.push({ name: 'projects' }) if (!route.query.method || !route.query.type) {
await authStore.logout()
await router.push({ name: 'login' })
return
}
if (route.query.method === 'telegram' && route.query.type === 'register') {
await authStore.registerWithTelegram(tg.initData)
await authStore.loginWithTelegram(tg.initData)
await router.push({ name: 'projects' })
}
if (route.query.method === 'email' && route.query.type === 'register') {
await router.push({ name: 'create_account' })
}
if (route.query.type === 'update') {
await authStore.termsAccepted()
await router.push({ name: 'projects' })
}
} }
const agreement = ref([]) const agreement = ref([])

View File

@@ -26,15 +26,13 @@
rounded rounded
/> />
<div <div
class="flex row items-start justify-center q-pt-md text-bold" class="flex row items-start justify-center q-pt-md text-bold text-center"
align="center"
> >
{{ chat.name }} {{ chat.name }}
</div> </div>
<div <div
v-if="chat.description" v-if="chat.description"
class="flex row items-start justify-center text-caption" class="flex row items-start justify-center text-caption text-center"
align="center"
> >
{{ chat.description }} {{ chat.description }}
</div> </div>
@@ -58,13 +56,12 @@
/> />
</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" v-if="getUserStatus(item).status">
<q-badge <q-badge :color="getUserStatus(item).color">
v-if="item.is_blocked" {{ $t(getUserStatus(item).text) }}
color="negative"
>
{{ $t('chat_page__user_blocked') }}
</q-badge> </q-badge>
</q-item-label>
<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="2" caption v-if="item.section3"> <q-item-label lines="2" caption v-if="item.section3">
@@ -91,8 +88,9 @@
import { useChatsStore } from 'stores/chats' import { useChatsStore } from 'stores/chats'
import { useUsersStore } from 'stores/users' import { useUsersStore } from 'stores/users'
import { useCompaniesStore } from 'stores/companies' import { useCompaniesStore } from 'stores/companies'
import { parseIntString } from 'helpers/helpers' import { parseIntString } from 'src/utils/helpers'
import type { Chat } from 'types/Chat' import type { Chat } from 'types/Chat'
import type { User } from 'types/User'
import type { WebApp } from '@twa-dev/types' import type { WebApp } from '@twa-dev/types'
import { useUserSection } from 'composables/useUserSection' import { useUserSection } from 'composables/useUserSection'
@@ -135,8 +133,9 @@
return arr.map(el => ({ return arr.map(el => ({
...el, ...el,
...userSection(el), ...userSection(el),
companyName: el.company_id && companiesStore.companyById(el.company_id) companyName:
? companiesStore.companyById(el.company_id)?.name (el.company_id && companiesStore.companyById(el.company_id))
? companiesStore.companyById(el.company_id)?.name ?? null
: null : null
})) }))
}) })
@@ -145,4 +144,19 @@
await router.push({ name: 'user_info', params: { id: route.params.id, userId: id }}) await router.push({ name: 'user_info', params: { id: route.params.id, userId: id }})
} }
interface chatUser extends User {
section1: string
section2_1: string
section2_2: string
section3: string
companyName: string | null
}
function getUserStatus (item: chatUser) {
if (item.is_blocked) return { status: 'blocked', text: 'user_block__user_blocked', color: 'negative' }
if (item.is_leave) return { status: 'leave', text: 'user_block__user_leave', color: 'primary' }
if (!item.is_terms_accepted) return { status: 'pending', text: 'user_block__user_pending', color: 'secondary' }
return { status: null, text: '', color: '' }
}
</script> </script>

View File

@@ -3,7 +3,7 @@
v-model="newCompany" v-model="newCompany"
title="company_add__title_card" title="company_add__title_card"
btnText="company_add__btn" btnText="company_add__btn"
@update = "addCompany" @update="addCompany"
/> />
</template> </template>

View File

@@ -30,7 +30,7 @@
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import companyBlock from 'components/companyBlock.vue' import companyBlock from 'components/companyBlock.vue'
import { useCompaniesStore } from 'stores/companies' import { useCompaniesStore } from 'stores/companies'
import { parseIntString } from 'helpers/helpers' import { parseIntString } from 'src/utils/helpers'
import type { CompanyParams } from 'types/Company' import type { CompanyParams } from 'types/Company'
const router = useRouter() const router = useRouter()
@@ -59,7 +59,7 @@
await router.push({ name: 'your_company' }) await router.push({ name: 'your_company' })
} }
async function updateCompany(companyData: CompanyParams) { async function updateCompany (companyData: CompanyParams) {
if (companyId.value) { if (companyId.value) {
await companiesStore.update(companyId.value, companyData) await companiesStore.update(companyId.value, companyData)
router.go(-1) router.go(-1)

View File

@@ -212,7 +212,7 @@
async function createAccount() { async function createAccount() {
sessionStorage.setItem('pendingLogin', login.value) sessionStorage.setItem('pendingLogin', login.value)
await router.push({ name: 'create_account' }) await router.push({ name: 'agreements', query: { method: 'email', type: 'register' }})
} }
const isTelegramApp = computed(() => { const isTelegramApp = computed(() => {
@@ -221,10 +221,12 @@
}) })
async function handleTelegramLogin () { async function handleTelegramLogin () {
// @ts-expect-ignore try {
const initData = window.Telegram.WebApp.initData await authStore.loginWithTelegram(tg.initData)
await authStore.loginWithTelegram(initData) await router.push({ name: 'projects' })
await router.push({ name: 'projects' }) } catch {
await router.push({ name: 'agreements', query: { method: 'telegram', type: 'register' }})
}
} }
onUnmounted(() => { onUnmounted(() => {

View File

@@ -17,15 +17,10 @@
const router = useRouter() const router = useRouter()
const projectsStore = useProjectsStore() const projectsStore = useProjectsStore()
const newProject = ref(<ProjectParams>{ const newProject = ref<ProjectParams>({} as ProjectParams)
name: '',
logo: '',
description: '',
is_logo_bg: false
})
async function addProject () { async function addProject (projectData: ProjectParams) {
const newDataProject = await projectsStore.add(newProject.value) const newDataProject = await projectsStore.add(projectData)
await router.replace({ name: 'chats', params: { id: newDataProject.id }}) await router.replace({ name: 'chats', params: { id: newDataProject.id }})
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<project-block <project-block
v-if="projectMod" v-if="project"
v-model="projectMod" v-model="project"
title="project_edit__title_card" title="project_edit__title_card"
btnText="project_edit__btn" btnText="project_edit__btn"
@update="updateProject" @update="updateProject"
@@ -18,25 +18,27 @@
const router = useRouter() const router = useRouter()
const projectsStore = useProjectsStore() const projectsStore = useProjectsStore()
const project = ref<ProjectParams | null>(null)
const projectId = computed(() => projectsStore.currentProjectId) const projectId = computed(() => projectsStore.currentProjectId)
const projectMod = ref<ProjectParams | null>(null)
if (projectsStore.isInit) { if (projectsStore.isInit) {
projectMod.value = projectId.value project.value = projectId.value
? { ...projectsStore.projectById(projectId.value) } as ProjectParams ? { ...projectsStore.projectById(projectId.value) } as ProjectParams
: null : null
} }
watch(() => projectsStore.isInit, (isInit) => { watch(() => projectsStore.isInit, (isInit) => {
if (isInit && projectId.value && !projectMod.value) { if (isInit && projectId.value && !project.value) {
projectMod.value = { ...projectsStore.projectById(projectId.value) as ProjectParams } project.value = { ...projectsStore.projectById(projectId.value) as ProjectParams }
} }
}) })
const updateProject = async () => { async function updateProject (projectData: ProjectParams) {
if (!projectId.value || !projectMod.value) return if (projectId.value) {
await projectsStore.update(projectId.value, projectMod.value) await projectsStore.update(projectId.value, projectData)
router.go(-1) router.go(-1)
}
} }
</script> </script>

View File

@@ -1,9 +1,9 @@
<template> <template>
<pn-page-card> <pn-page-card>
<template #title> <template #title>
{{ $t('projects__projects') }} {{ $t('projects__title') }}
<q-btn <q-btn
@click="goAccount()" @click="goAccount"
flat rounded flat rounded
no-caps no-caps
icon-right="mdi-chevron-right" icon-right="mdi-chevron-right"
@@ -18,10 +18,10 @@
<pn-scroll-list> <pn-scroll-list>
<template <template
#card-body-header #card-body-header
v-if="projects.length !== 0 || archiveProjects.length !== 0" v-if="projects.length!==0 || archiveProjects.length!==0"
> >
<q-input <q-input
v-if="projects.length !== 0" v-if="projects.length!== 0"
v-model="searchProject" v-model="searchProject"
clearable clearable
clear-icon="close" clear-icon="close"
@@ -33,9 +33,9 @@
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
</q-input> </q-input>
</template> </template>
<q-list separator v-if="projects.length !== 0"> <q-list separator v-if="projects.length!==0">
<template <template
v-for = "item in activeProjects" v-for = "item in activeProjects"
:key="item.id" :key="item.id"
@@ -66,7 +66,7 @@
caption lines="2" caption lines="2"
style="max-width: -webkit-fill-available; white-space: pre-line" style="max-width: -webkit-fill-available; white-space: pre-line"
> >
{{item.description}} {{ item.description }}
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
<q-item-section side top> <q-item-section side top>
@@ -149,7 +149,7 @@
caption lines="2" caption lines="2"
style="max-width: -webkit-fill-available; white-space: pre-line" style="max-width: -webkit-fill-available; white-space: pre-line"
> >
{{item.description}} {{ item.description }}
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
@@ -158,7 +158,7 @@
</div> </div>
</div> </div>
<pn-onboard-btn <pn-onboard-btn
v-if="projects.length === 0 && projectsInit" v-if="projects.length===0 && projectsInit"
icon="mdi-briefcase-plus-outline" icon="mdi-briefcase-plus-outline"
:message1="$t('projects__lets_start')" :message1="$t('projects__lets_start')"
:message2="$t('projects__lets_start_description')" :message2="$t('projects__lets_start_description')"

View File

@@ -0,0 +1,38 @@
<template>
<div
class="q-pa-md flex items-center justify-center"
style="height: 100vh"
>
<mesh-background/>
<q-card
class="q-py-md q-px-xl flex column q-gutter-y-lg justify-center items-center"
style="opacity: 0.8"
>
<base-logo class="text-h6"/>
<span class="text-h6 text-center">
{{$t('only_telegram__continue')}}
</span>
<q-btn
@click="openWebsite"
color="primary"
rounded
class="q-mb-lg"
>
<div class="flex items-center">
<q-icon name="telegram"/>
Telegram
</div>
</q-btn>
</q-card>
</div>
</template>
<script setup lang="ts">
import baseLogo from 'components/BaseLogo.vue'
import meshBackground from 'components/meshBackground.vue'
import { BOT_NAME } from 'src/utils/constants'
function openWebsite () {
window.open('https://t.me/'+ BOT_NAME)
}
</script>

View File

@@ -13,8 +13,8 @@
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import userBlock from 'components/userBlock.vue' import userBlock from 'components/userBlock.vue'
import { useUsersStore } from 'stores/users' import { useUsersStore } from 'stores/users'
import { parseIntString } from 'helpers/helpers' import { parseIntString } from 'src/utils/helpers'
import type { User } from 'types/Users' import type { User } from 'types/User'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()

View File

@@ -0,0 +1,10 @@
<template>
<doc-block title="subscription_guide__title" document-name="Subscription_guide"/>
</template>
<script setup lang="ts">
import docBlock from 'components/docBlock.vue'
</script>
<style>
</style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<doc-block type="consent"/> <doc-block title="consent__title" document-name="Consent_to_Personal_Data_Processing"/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -1,5 +1,5 @@
<template> <template>
<doc-block type="privacy"/> <doc-block title="privacy__title" document-name="Privacy-Policy"/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -1,72 +1,205 @@
<!-- eslint-disable @typescript-eslint/ban-ts-comment -->
<!-- eslint-disable @typescript-eslint/ban-ts-comment -->
<!-- eslint-disable @typescript-eslint/ban-ts-comment -->
<template> <template>
<pn-page-card> <pn-page-card>
<template #title> <template #title>
{{$t('subscribe__title')}} {{$t('subscribe__title')}}
</template> </template>
<pn-scroll-list class="q-px-md"> <pn-scroll-list >
<div id="subscribe-current-balance" class="flex w100 justify-between items-center no-wrap text-h6"> <div class="q-px-md">
<span> <div
{{ $t('subscribe__current_balance') }} id="subscribe-current-balance"
</span> class="flex w100 q-px-md q-py-sm row"
style="border-radius: var(--top-raduis); border: 1px solid var(--q-primary);"
>
<div class="flex items-center"> <div class="flex w100 justify-between items-center no-wrap">
<q-icon name = "mdi-crown-circle-outline" color="orange" size="sm"/> <div class="flex no-wrap items-center text-h6 col-9">
<div class="text-bold q-pa-xs "> {{ $t('subscribe__current_plan') }}
50 </div>
</div>
</div>
</div>
<div id="subscribe-need-tocken" :style = "{ borderLeft: 'solid 5px var(--q-info)' }" class="q-pl-sm">
<q-icon name = "mdi-crown-circle-outline" color="orange" size="xs"/>{{ $t('subscribe__token_formula') }}
<div class="text-caption">{{ $t('subscribe__token_formula_description') }}</div>
</div>
<div id="qty_chats" class="flex column q-pt-lg w100">
<div class="text-h6 flex items-center">
<span>{{ $t('account__chats') }}</span>
</div>
<div class="flex row justify-between">
<qty-chat-card
v-for = "chat in chats"
:key = chat.title
:qty = chat.qty
:bgColor = chat.color
:title = chat.title
/>
</div>
</div>
<div class="flex items-center column col-3">
<span class="text-bold">
{{ currentPlanData?.name }}
</span>
<span class="text-caption" style="line-height: 0.5em;">
{{ $t('subscribe__plan_exp') }}
{{ date.formatDate(currentPlanData.exp * 1000, 'DD.MM.YYYY') }}
</span>
</div>
</div>
<div class="flex row w100 ow-wrap items-center text-caption q-pt-sm">
<div class="col-9">{{ $t('subscribe__plan_active_chats') }}</div>
<div class="col-3" align="center">
<span class="text-brand2 text-bold q-pr-xs">{{ currentPlanData?.active_chats }}</span>
<span class="text-grey">/{{ currentPlanData?.chatsQty }}</span>
</div>
</div>
</div>
<div class="flex w100 justify-center text-grey q-pt-md q-pb-none">{{ $t('subscribe__plans')}}</div>
<q-list separator>
<q-item
v-for="item in plans"
:key="item.id"
>
<q-item-section avatar>
<q-radio v-model="newPlan" :val="item" v-if="item.name!=='TEST'"/>
</q-item-section>
<q-item-section>
<q-item-label class="text-bold">{{ item.name }}</q-item-label>
<div class="flex no-wrap items-center text-caption">
<span v-if="item.chatsQty" class="q-pr-xs">
{{ $t('subscribe__chats_max') }}
</span>
<span v-if="item.chatsQty">
{{ item.chatsQty }}
</span>
<q-icon v-else name="mdi-all-inclusive" size="sm"/>
<span class="q-pl-xs">
{{ $t('subscribe__chats')}}
</span>
</div>
</q-item-section>
<q-item-section side>
<div v-if="item.price" class="flex column items-center">
<div class="flex no-wrap items-center">
<telegram-star color="gold" size="18px" class="q-mr-xs"/>
<span class="text-h6">{{ formatNumber(item.price) }}</span>
<span class="text-caption q-pl-xs">{{ $t('subscribe__per_month') }}</span>
</div>
</div>
<div v-else class="text-bold">
{{ $t('subscribe__free_tax') }}
</div>
</q-item-section>
</q-item>
</q-list>
<div class="flex w100 justify-center text-caption text-grey q-pt-sm text-center">
{{ $t('subscribe__plans_description') }}
</div>
<q-btn-group spread flat class="w100 q-py-sm">
<q-btn
v-for="period in periods"
:key="period.id"
:color="selectPeriod === period.value ? 'primary' : 'white'"
:text-color="selectPeriod === period.value ? 'white' : 'primary'"
@click="selectPeriod = period.value"
no-caps
>
<div class="column items-center w100 self-end">
<q-badge v-show="period.sale" color="red">{{ period.sale }}% off</q-badge>
<span>{{ $t(period.name) }}</span>
<span class="text-caption">({{ $t(period.name_in_days) }})</span>
</div>
</q-btn>
</q-btn-group>
<div class="text-caption column text-grey">
<span>{{ $t('subscribe__plans_period_notes') + ' ' }}</span>
<span
@click="router.push({ name: 'change_plan_rules' })"
class="text-info cursor-pointer"
>
{{ $t('subscribe__plans_period_notes_more_info') }}
</span>
</div>
</div>
</pn-scroll-list> </pn-scroll-list>
</pn-page-card> </pn-page-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, computed, inject, onUnmounted, watch } from 'vue'
// import { useRouter } from 'vue-router' import telegramStar from 'components/TelegramStar.vue'
// import { useAuthStore } from 'stores/auth' import { useRouter } from 'vue-router'
// import type { WebApp } from '@twa-dev/types' import { date } from 'quasar'
import qtyChatCard from 'components/account-page/qtyChatCard.vue' import { useI18n } from 'vue-i18n'
// import optionPayment from 'components/admin/account-page/optionPayment.vue' import type { WebApp } from '@twa-dev/types'
import { colors } from 'quasar';
// const router = useRouter() const router = useRouter()
// const authStore = useAuthStore()
// const tg = inject('tg') as WebApp const tg = inject('tg') as WebApp
// const tgUser = tg.initDataUnsafe.user tg.MainButton.show()
const chats = ref([ // @ts-expect-error: get hex text
{ title: 'account__chats_active', qty: 8, color: 'var(--q-primary)' }, tg.MainButton.color = colors.getPaletteColor('primary')
{ title: 'account__chats_unbound', qty: 2, color: 'grey' },
{ title: 'account__chats_free', qty: 5, color: 'green' },
{ title: 'account__chats_total', qty: 15, color: 'var(--q-info)' },
])
/* const payment=ref([ const plans = [
{ id: 1, qty: 50, stars: 200, discount: 0 }, { id: 1, name: 'TEST', val: 'test', price: null, chatsQty: 5 },
{ id: 2, qty: 120, stars: 400, discount: 20 }, { id: 2, name: 'START', val: 'start', price: 1000, chatsQty: 15 },
{ id: 3, qty: 220, stars: 500, discount: 30 } { id: 3, name: 'PRO', val: 'pro', price: 5000, chatsQty: 40 },
]) */ { id: 4, name: 'VIP', val: 'vip', price: 12000, chatsQty: null }
] as const
const periods = [
{ id: 1, name: 'subscribe__1month', name_in_days: 'subscribe__30days', value: 30, sale: 0 },
{ id: 2, name: 'subscribe__3months', name_in_days: 'subscribe__91days', value: 91, sale: 5 },
{ id: 3, name: 'subscribe__1year', name_in_days: 'subscribe__365days', value: 365, sale: 15 }
]
const selectPeriod = ref(periods[1]?.value)
const newPlan = ref(plans[1])
interface CurrentPlan {
plan: string
exp: number | null
active_chats: number | null
}
const currentPlan = ref<CurrentPlan>({ // temp, this get from api
plan: plans[0].val,
active_chats: 20,
exp: Date.now() / 1000 + 500000
})
interface CurrentPlanData extends CurrentPlan {
price: number | null
}
const currentPlanData = computed((): CurrentPlanData => ({
active_chats: currentPlan.value.active_chats ?? null,
exp: currentPlan.value.exp ?? null,
...plans.find(el=> el?.val === currentPlan.value.plan)
}))
const { t } = useI18n()
const textBtn = computed(() => {
const prorata = currentPlanData.value.price
? Math.ceil(
currentPlanData.value?.price / 30 *
date.getDateDiff(new Date(currentPlan.value.exp * 1000), Date.now())
)
: 0
const k = 1 - Number(periods.find(el => el.value === selectPeriod.value)?.sale) / 100
const stars = formatNumber(
Number(selectPeriod.value) *
Number(newPlan.value?.price) *
k - prorata
)
const newDateExp = date.addToDate(Date.now(), { months: Number(selectPeriod.value) })
return t('subscribe__pay') + ' ⭐' + stars +
' (' + t('subscribe__plan_exp') + ' ' +
date.formatDate(newDateExp, 'DD.MM.YYYY') + ')'
})
function formatNumber (number: string | number) {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ")
}
onUnmounted(() => tg.MainButton.hide())
watch(textBtn, () => tg.MainButton.setText(textBtn.value),
{ immediate: true })
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<doc-block type="terms_of_use"/> <doc-block title="terms_of_use__title" document-name="Terms_of_use"/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -56,9 +56,10 @@
flat flat
no-caps no-caps
dense dense
class="q-ml-xl" rounded
class="q-ml-lg"
> >
<span class="flex items-center no-wrap text-caption"> <span class="flex items-center no-wrap q-pl-sm">
{{ $t('header__to_projects') }} {{ $t('header__to_projects') }}
<q-icon name="mdi-chevron-right"/> <q-icon name="mdi-chevron-right"/>
</span> </span>

View File

@@ -42,6 +42,11 @@
/> />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label lines="1" v-if="!item.is_terms_accepted">
<q-badge color="secondary">
{{ $t('user_block__user_pending') }}
</q-badge>
</q-item-label>
<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>
@@ -239,6 +244,7 @@
import { useUsersStore } from 'stores/users' import { useUsersStore } from 'stores/users'
import { useCompaniesStore } from 'stores/companies' import { useCompaniesStore } from 'stores/companies'
import { useUserSection } from 'composables/useUserSection' import { useUserSection } from 'composables/useUserSection'
defineOptions({ inheritAttrs: false }) defineOptions({ inheritAttrs: false })
const router = useRouter() const router = useRouter()
@@ -261,7 +267,7 @@
...el, ...el,
...userSection(el), ...userSection(el),
companyName: el.company_id && companiesStore.companyById(el.company_id) companyName: el.company_id && companiesStore.companyById(el.company_id)
? companiesStore.companyById(el.company_id)?.name ? companiesStore.companyById(el.company_id)?.name ?? null
: null : null
}))) })))

View File

@@ -33,29 +33,29 @@ export default defineRouter(function (/* { store, ssrContext } */) {
history: createHistory(process.env.VUE_ROUTER_BASE) history: createHistory(process.env.VUE_ROUTER_BASE)
}) })
const publicPath = ['404', 'terms', 'privacy', 'consent']
Router.beforeEach(async (to) => { Router.beforeEach(async (to) => {
if (to.name !== 'telegram_only' && sessionStorage.getItem('isTelegram') === 'false') {
return { name: 'telegram_only', replace: true }
}
if (to.name === 'telegram_only') return true
const authStore = useAuthStore() const authStore = useAuthStore()
const projectsStore = useProjectsStore() const projectsStore = useProjectsStore()
if (to.name === '404' || to.name === 'terms' || to.name === 'privacy' || to.name === 'consent') return true if (to.name === 'login')
return authStore.isAuth ? { name: 'projects', replace: true } : true
if (to.name === 'login') return true if (typeof to.name === 'string' && publicPath.includes(to.name)) return true
if (to.name === 'agreements' && authStore.isAuth) return true if (to.name === 'agreements') return true
// if (!authStore.isTermsAccepted && authStore.isAuth) return { name: 'agreements' } if (to.name === 'create_account' && !authStore.isAuth) return true
if (to.meta.guestOnly && authStore.isAuth) {
return { name: 'projects' }
}
if (to.meta.requiresAuth && !authStore.isAuth) {
return {
name: 'login',
replace: true
}
}
if (!authStore.isAuth) return { name: 'login', replace: true }
if (to.params.id) { if (to.params.id) {
if (!projectsStore.isInit) await projectsStore.init() if (!projectsStore.isInit) await projectsStore.init()

View File

@@ -14,32 +14,27 @@ const routes: RouteRecordRaw[] = [
path: '/projects', path: '/projects',
component: () => import('pages/ProjectsPage.vue'), component: () => import('pages/ProjectsPage.vue'),
meta: { meta: {
hideBackButton: true, hideBackButton: true
requiresAuth: true
} }
}, },
{ {
name: 'project_add', name: 'project_add',
path: '/project/add', path: '/project/add',
component: () => import('pages/ProjectAddPage.vue'), component: () => import('pages/ProjectAddPage.vue')
meta: { requiresAuth: true }
}, },
{ {
name: 'project_info', name: 'project_info',
path: '/project/:id(\\d+)/info', path: '/project/:id(\\d+)/info',
component: () => import('pages/ProjectEditPage.vue'), component: () => import('pages/ProjectEditPage.vue')
meta: { requiresAuth: true }
}, },
{ {
name: 'company_mask', name: 'company_mask',
path: '/project/:id(\\d+)/company-mask', path: '/project/:id(\\d+)/company-mask',
component: () => import('pages/CompanyMaskPage.vue'), component: () => import('pages/CompanyMaskPage.vue')
meta: { requiresAuth: true }
}, },
{ {
path: '/project/:id(\\d+)', path: '/project/:id(\\d+)',
component: () => import('pages/ProjectPage.vue'), component: () => import('pages/ProjectPage.vue'),
meta: { requiresAuth: true },
children: [ children: [
{ {
name: 'project', name: 'project',
@@ -51,8 +46,7 @@ const routes: RouteRecordRaw[] = [
path: 'chats', path: 'chats',
component: () => import('pages/project-page/ProjectPageChats.vue'), component: () => import('pages/project-page/ProjectPageChats.vue'),
meta: { meta: {
backRoute: 'projects', backRoute: 'projects'
requiresAuth: true
} }
}, },
{ {
@@ -60,8 +54,7 @@ const routes: RouteRecordRaw[] = [
path: 'users', path: 'users',
component: () => import('pages/project-page/ProjectPageUsers.vue'), component: () => import('pages/project-page/ProjectPageUsers.vue'),
meta: { meta: {
backRoute: 'projects', backRoute: 'projects'
requiresAuth: true
} }
}, },
{ {
@@ -69,8 +62,7 @@ const routes: RouteRecordRaw[] = [
path: 'companies', path: 'companies',
component: () => import('pages/project-page/ProjectPageCompanies.vue'), component: () => import('pages/project-page/ProjectPageCompanies.vue'),
meta: { meta: {
backRoute: 'projects', backRoute: 'projects'
requiresAuth: true
} }
} }
] ]
@@ -78,67 +70,57 @@ const routes: RouteRecordRaw[] = [
{ {
name: 'chat_info', name: 'chat_info',
path: '/project/:id(\\d+)/chat/:chatId', path: '/project/:id(\\d+)/chat/:chatId',
component: () => import('pages/ChatPage.vue'), component: () => import('pages/ChatPage.vue')
meta: { requiresAuth: true }
}, },
{ {
name: 'add_company', name: 'add_company',
path: '/project/:id(\\d+)/add-company', path: '/project/:id(\\d+)/add-company',
component: () => import('pages/CompanyAddPage.vue'), component: () => import('pages/CompanyAddPage.vue')
meta: { requiresAuth: true }
}, },
{ {
name: 'company_info', name: 'company_info',
path: '/project/:id(\\d+)/company/:companyId', path: '/project/:id(\\d+)/company/:companyId',
component: () => import('pages/CompanyEditPage.vue'), component: () => import('pages/CompanyEditPage.vue')
meta: { requiresAuth: true }
}, },
{ {
name: 'user_info', name: 'user_info',
path: '/project/:id(\\d+)/user/:userId', path: '/project/:id(\\d+)/user/:userId',
component: () => import('pages/UserEditPage.vue'), component: () => import('pages/UserEditPage.vue')
meta: { requiresAuth: true }
}, },
{ {
name: 'account', name: 'account',
path: '/account', path: '/account',
component: () => import('pages/AccountPage.vue'), component: () => import('pages/AccountPage.vue')
meta: { requiresAuth: true }
}, },
{ {
name: 'create_account', name: 'create_account',
path: '/create-account', path: '/create-account',
component: () => import('pages/AccountCreatePage.vue'), component: () => import('pages/AccountCreatePage.vue')
meta: { guestOnly: true }
}, },
{ {
name: 'change_account_password', name: 'change_account_password',
path: '/change-password', path: '/change-password',
component: () => import('pages/AccountChangePasswordPage.vue'), component: () => import('pages/AccountChangePasswordPage.vue')
meta: { requiresAuth: true }
}, },
{ {
name: 'change_account_email', name: 'change_account_email',
path: '/change-email', path: '/change-email',
component: () => import('pages/AccountChangeEmailPage.vue'), component: () => import('pages/AccountChangeEmailPage.vue')
meta: { requiresAuth: true }
}, },
{ {
name: 'change_account_auth_method', name: 'change_account_auth_method',
path: '/change-auth-method', path: '/change-auth-method',
component: () => import('pages/AccountChangeAuthMethodPage.vue'), component: () => import('pages/AccountChangeAuthMethodPage.vue')
meta: { requiresAuth: true }
}, },
{ {
name: 'subscribe', name: 'subscribe',
path: '/subscribe', path: '/subscribe',
component: () => import('pages/account/SubscribePage.vue'), component: () => import('pages/account/SubscribePage.vue')
meta: { requiresAuth: true }
}, },
{ {
name: 'agreements', name: 'agreements',
path: '/agreements', path: '/agreements',
component: () => import('pages/account/AgreementsPage.vue') component: () => import('src/pages/AgreementsPage.vue')
}, },
{ {
name: 'terms', name: 'terms',
@@ -155,6 +137,11 @@ const routes: RouteRecordRaw[] = [
path: '/consent-pd', path: '/consent-pd',
component: () => import('pages/account/ConsentPage.vue') component: () => import('pages/account/ConsentPage.vue')
}, },
{
name: 'change_plan_rules',
path: '/change-plan-rules',
component: () => import('pages/account/ChangePlanRules.vue')
},
{ {
name: 'support', name: 'support',
path: '/support', path: '/support',
@@ -163,35 +150,36 @@ const routes: RouteRecordRaw[] = [
{ {
name: 'your_company', name: 'your_company',
path: '/your-company', path: '/your-company',
component: () => import('pages/CompanyYourPage.vue'), component: () => import('pages/CompanyYourPage.vue')
meta: { requiresAuth: true }
}, },
{ {
name: 'login', name: 'login',
path: '/login', path: '/login',
component: () => import('pages/LoginPage.vue'), component: () => import('pages/LoginPage.vue'),
meta: { meta: {
hideBackButton: true, hideBackButton: true
guestOnly: true
} }
}, },
{ {
name: 'recovery_password', name: 'recovery_password',
path: '/recovery-password', path: '/recovery-password',
component: () => import('pages/AccountForgotPasswordPage.vue'), component: () => import('pages/AccountForgotPasswordPage.vue')
meta: { guestOnly: true }
}, },
{ {
name: 'settings', name: 'settings',
path: '/settings', path: '/settings',
component: () => import('pages/account/SettingsPage.vue'), component: () => import('pages/account/SettingsPage.vue')
meta: { requiresAuth: true }
} }
] ]
}, },
{
name: 'telegram_only',
path: '/telegram-only',
component: () => import('pages/TelegramOnlyPage.vue')
},
{ {
path: '/:catchAll(.*)*', path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue'), component: () => import('pages/ErrorNotFound.vue')
} }
] ]

View File

@@ -33,7 +33,6 @@ export type AuthFlowType = keyof typeof ENDPOINT_MAP
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const customer = ref<Customer | null>(null) const customer = ref<Customer | null>(null)
const isTermsAccepted = ref(false)
const wsEvent = ref<WSMessage | null>(null) const wsEvent = ref<WSMessage | null>(null)
const projectsStore = useProjectsStore() const projectsStore = useProjectsStore()
@@ -74,10 +73,14 @@ export const useAuthStore = defineStore('auth', () => {
} }
const loginWithTelegram = async (initData: string) => { const loginWithTelegram = async (initData: string) => {
await api.post('/auth/telegram?'+ initData, {}, { withCredentials: true }) await api.post('/auth/telegram?'+ initData, {}, { withCredentials: true, suppressNotify: true })
await initialize() await initialize()
} }
const registerWithTelegram = async (initData: string) => {
await api.post('/auth/telegram/register?'+ initData, {}, { withCredentials: true })
}
const logout = async () => { const logout = async () => {
await api.get('/auth/logout', {}) await api.get('/auth/logout', {})
customer.value = null customer.value = null
@@ -129,23 +132,21 @@ export const useAuthStore = defineStore('auth', () => {
// agreement // agreement
async function termsAccepted () { async function termsAccepted () {
const { data } = await api.post('/terms/accept') await api.post('/terms/accept')
if (data.success) isTermsAccepted.value = true
} }
async function termsRevoked () { async function termsRevoked () {
const { data } = await api.post('/terms/revoke') await api.post('/terms/revoke')
if (data.success) isTermsAccepted.value = false
} }
return { return {
customer, customer,
isTermsAccepted,
isAuth, isAuth,
wsEvent, wsEvent,
initialize, initialize,
loginWithCredentials, loginWithCredentials,
loginWithTelegram, loginWithTelegram,
registerWithTelegram,
logout, logout,
initRegistration, initRegistration,
confirmCode, confirmCode,

View File

@@ -39,8 +39,7 @@ export const useChatsStore = defineStore('chats', () => {
} }
async function getChatUsers (chatId: number) { async function getChatUsers (chatId: number) {
const { data } = await api.get('/project/' + currentProjectId.value + '/chat/' + chatId) await api.get('/project/' + currentProjectId.value + '/chat/' + chatId)
console.log(222, data)
} }
const getChats = computed(() => chats.value) const getChats = computed(() => chats.value)

View File

@@ -58,7 +58,6 @@ export const useCompaniesStore = defineStore('companies', () => {
const { data } = await api.get('/project/' + currentProjectId.value + '/company/mapping') const { data } = await api.get('/project/' + currentProjectId.value + '/company/mapping')
const companiesMaskAPI = data.data // arr [company_id, company_list[]] const companiesMaskAPI = data.data // arr [company_id, company_list[]]
companiesMask.value = companiesMaskAPI companiesMask.value = companiesMaskAPI
console.log(11, companiesMaskAPI)
} }
function checkCompanyMasked (id: number) { function checkCompanyMasked (id: number) {

View File

@@ -1,7 +1,7 @@
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { api } from 'boot/axios' import { api } from 'boot/axios'
import type { User, UserParams } from 'types/Users' import type { User, UserParams } from 'types/User'
import { useProjectsStore } from 'stores/projects' import { useProjectsStore } from 'stores/projects'
import { useAuthStore } from 'stores/auth' import { useAuthStore } from 'stores/auth'
@@ -27,7 +27,6 @@ export const useUsersStore = defineStore('users', () => {
async function update (userId: number, userData: UserParams) { async function update (userId: number, userData: UserParams) {
const { data } = await api.put('/project/' + currentProjectId.value + '/user/' + userId, userData) const { data } = await api.put('/project/' + currentProjectId.value + '/user/' + userId, userData)
console.log('update', data.data)
const userAPI = data.data const userAPI = data.data
const idx = users.value.findIndex(item => item.id === userAPI.id) const idx = users.value.findIndex(item => item.id === userAPI.id)
if (users.value[idx]) Object.assign(users.value[idx], userAPI) if (users.value[idx]) Object.assign(users.value[idx], userAPI)

View File

@@ -1,8 +1,8 @@
interface ProjectParams { interface ProjectParams {
name: string name: string
description: string description: string | null
logo: string logo: string | null
is_logo_bg: boolean is_logo_bg: boolean | null
[key: string]: unknown [key: string]: unknown
} }

View File

@@ -18,6 +18,7 @@ interface User extends UserParams {
username: string | null username: string | null
photo: string | null photo: string | null
is_leave: boolean is_leave: boolean
is_terms_accepted: boolean
} }
export type { export type {

1
src/utils/constants.ts Normal file
View File

@@ -0,0 +1 @@
export const BOT_NAME = 'ready_or_not_2025_bot'