Compare commits

...

10 Commits

Author SHA1 Message Date
04696450bc before update add chat 2 2026-06-11 13:16:11 +03:00
1f7f25841f before update add chat 2026-06-10 22:29:34 +03:00
94851c8b82 before remake add chat window 2026-04-23 08:51:14 +03:00
09d7812a7a before_redesign 2026-02-13 15:06:52 +03:00
53ca63ec91 v11 2026-01-28 22:37:27 +03:00
cbaa1cda9d before liquid glass 2026-01-03 18:14:30 +03:00
4cad91440c change 2025-12-03 17:40:50 +03:00
d3d8b7522a update 2025-11-02 10:08:57 +03:00
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
156 changed files with 15354 additions and 6739 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

@@ -13,10 +13,12 @@
<meta name="HandheldFriendly" content="True"/> <meta name="HandheldFriendly" content="True"/>
<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="./telegram-web-app.js"></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<% } %>">
--> -->
<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">

6430
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
{ {
"name": "projectsnode", "name": "tgcrew",
"version": "0.0.1", "version": "0.1.01",
"description": "telegram miniapp", "description": "telegram admin miniapp",
"productName": "projectsNode", "productName": "tgCrew (admin)",
"author": "Alex Mart", "author": "Alex Mart",
"type": "module", "type": "module",
"private": true, "private": true,
@@ -15,19 +15,21 @@
}, },
"dependencies": { "dependencies": {
"@quasar/cli": "^2.5.0", "@quasar/cli": "^2.5.0",
"@quasar/extras": "^1.17.0", "@quasar/extras": "^1.18.0",
"@quasar/vite-plugin": "^1.10.0", "@quasar/vite-plugin": "^1.11.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.19.3",
"vue": "^3.4.18", "sass-embedded": "^1.97.2",
"vue": "^3.5.27",
"vue-i18n": "^9.2.2", "vue-i18n": "^9.2.2",
"vue-router": "^4.0.12" "vue-router": "^4.0.12"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.14.0", "@eslint/js": "^9.14.0",
"@intlify/unplugin-vue-i18n": "^2.0.0", "@intlify/unplugin-vue-i18n": "^2.0.0",
"@quasar/app-vite": "^2.3.0", "@quasar/app-vite": "^2.6.0",
"@quasar/quasar-app-extension-qmarkdown": "^2.0.5", "@quasar/quasar-app-extension-qmarkdown": "^2.0.5",
"@twa-dev/types": "^8.0.2", "@twa-dev/types": "^8.0.2",
"@types/node": "^20.17.30", "@types/node": "^20.17.30",

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

@@ -1,30 +0,0 @@
Программное обеспечение SURVy (далее "Инструмент") разработано, чтобы помочь в выборе оборудования систем видеонаблюдения согласно вашим потребностям.
Перед началом использования Инструмента просьба внимательно ознакомиться с нижеуказанными условиями использования (далее Условия использования) и убедиться, что вы понимаете их до начала использования Инструмента. Выполняя загрузку, установку, активацию Инструмента, осуществляя доступ к нему или иное его использование, вы соглашаетесь принять и соблюдать положения настоящих Условий использования. Если вы выполняете Условия использования от имени юридического лица, вы заявляете, что имеете юридические основания связывать обязательствами такое юридическое лицо. Если у вас нет таких полномочий или вы не согласны с положениями настоящих Условий использования, то ни вам, ни такому юридическому лицу не разрешается и не следует загружать, устанавливать Инструмент, осуществлять доступ к нему или использовать его.
Разработчиком Инструмента (далее "Разработчик") является индивидуальный предприниматель Мартышкин Алексей Александрович (Россия, г. Москва, ОГРНИП 318774600262084, ИНН 366316608346). Все права на программу SURVy принадлежат Разработчику.
Под "Представителями разработчика" в данном документе подразумевается круг лиц, привлекаемых Разработчиком в рамках разработки и поддержки Инструмента.
УСЛОВИЯ ИСПОЛЬЗОВАНИЯ
Инструмент предоставляется исключительно для оказания помощи в выборе. Оценки, рекомендации, результаты расчетов (совместно именуемые «Результаты работы»), полученные путем использования Инструмента являются ориентировочными.
РАЗРАБОТЧИК И/ИЛИ ЕГО ПРЕДСТАВИТЕЛИ В ЛЮБОМ СЛУЧАЕ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ЗА УБЫТКИ ЛЮБОГО РОДА, ПОНЕСЕННЫЕ В РЕЗУЛЬТАТЕ ИСПОЛЬЗОВАНИЯ ИНСТРУМЕНТА И РЕЗУЛЬТАТОВ РАБОТЫ ИЛИ ПРИНЯТИЯ РЕШЕНИЙ НА ИХ ОСНОВЕ.
Инструмент для авторизации использует только HttpOnly cookie-файлы. Также в локальной памяти вашего веб-браузера (Локальное хранилище) хранятся языковые настройки.
Согласие на использование данных
Ваши проекты будут храниться на серверах Разработчика. Принимая данные условия использования, вы соглашаетесь с тем, что данные ваших проектов будут доступны Разработчику и/или его представителям и могут использоваться для внутренних целей (например, для последующего улучшения Инструмента).
Вы можете быть уверены в том, что Разработчик и его представители не будут преднамеренно передавать кому-либо данные о ваших проектах.
Разработчик может использовать адрес электронной почты, указанный в учетной записи для отправки уведомлений об изменении данных Условий, запроса получения обратной связи по пользованию Инструментом и оказания технической поддержки.
Ограничения
Вы не вправе (а также не вправе разрешать кому-либо):
(i) осуществлять обратную разработку, декомпиляцию, дизассемблировать Инструмент или иным образом получать доступ к его исходному коду или любой его части;
(ii) неправильно использовать Инструмент, нарушая его обычный режим работы или пытаясь использовать его не с помощью тех интерфейсов и инструкций, которые не предоставляются;
(iii) отправлять или загружать в Инструмент какие-либо данные или содержание, являющиеся незаконными или нарушающие настоящие Условия использования;
(iv) использовать Инструмент на территории Российской Федерации для объектов государственных предприятий/учреждений (в том числе компаний с государственным участием), а также тех объектов, данные по которым могут классифицироваться как секретная информация (любого класса).
Общий размер данных, отправляемых вами при использовании Инструмента, не может превышать 1 ГБ.
Разработчик оставляет за собой право удалить ваши данные (учетную запись и проекты) по своему усмотрению.
Вы обязуетесь защитить и оградить Разработчика и его представителей от каких-либо убытков или потерь, понесенных ими в результате нарушения обязательства в настоящем разделе.
ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ
ДАННЫЙ ИНСТРУМЕНТ И РЕЗУЛЬТАТЫ РАБОТЫ ПОСТАВЛЯЮТСЯ БЕЗВОЗМЕЗДНО «КАК ЕСТЬ» БЕЗ ПРЕДОСТАВЛЕНИЯ КАКИХ-ЛИБО ГАРАНТИЙ. ВСЕ РИСКИ В ОТНОШЕНИИ РЕЗУЛЬТАТОВ РАБОТЫ И РАБОТЫ ИНСТРУМЕНТА ПОЛНОСТЬЮ ЛОЖАТСЯ НА ВАС/ПОЛЬЗОВАТЕЛЯ. В РАМКАХ ПРИМЕНИМОГО ЗАКОНОДАТЕЛЬСТВА РАЗРАБОТЧИК ОТКАЗЫВАЕТСЯ ОТ ВСЕХ ГАРАНТИЙНЫХ ОБЯЗАТЕЛЬСТВ, КАК ЯВНЫХ, ТАК И СКРЫТЫХ, ВКЛЮЧАЯ, В ЧИСЛЕ ПРОЧЕГО, СКРЫТЫЕ ГАРАНТИИ ТОВАРНОЙ ПРИГОДНОСТИ, ПРИГОДНОСТЬ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ, СОБЛЮДЕНИЕ ПРАВ ИЛИ ЛЮБЫЕ ГАРАНТИИ, ВЫТЕКАЮЩИЕ ИЗ ЛЮБОГО ПРЕДЛОЖЕНИЯ, СПЕЦИФИКАЦИИ ИЛИ ОБРАЗЦА В ОТНОШЕНИИ РЕЗУЛЬТАТОВ РАБОТЫ И ИНСТРУМЕНТА. РАЗРАБОЧИК И ЕГО ПРЕДСТАВИТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ЗА ПОТЕРЮ ДАННЫХ, ПРОИЗВОДСТВЕННЫЕ ПОТЕРИ, ПОТЕРЮ ПРИБЫЛИ, НЕВОЗМОЖНОСТЬ ИСПОЛЬЗОВАНИЯ, НЕЗАКЛЮЧЕНИЕ ДОГОВОРА ИЛИ ЗА КАКИЕ-ЛИБО ДРУГИЕ КОСВЕННЫЕ, ЭКОНОМИЧЕСКИЕ ИЛИ НЕПРЯМЫЕ ПОТЕРИ В РЕЗУЛЬТАТЕ ПОЛУЧЕНИЯ, ИСПОЛЬЗОВАНИЯ ИНСТРУМЕНТА ИЛИ РЕЗУЛЬТАТОВ РАБОТЫ ИЛИ РАСПОРЯЖЕНИЯ ИМИ. ОБЩАЯ ОТВЕТСТВЕННОСТЬ РАЗРАБОТЧИКА И ЕГО ПРЕДСТАВИТЕЛЕЙ ПО ЛЮБЫМ ТРЕБОВАНИЯМ, УБЫТКАМИ ИЛИ ВИДАМ ОТВЕТСТВЕННОСТИ ВСЛЕДСТВИЕ ИСПОЛЬЗОВАНИЯ И ПОЛУЧЕНИЯ ИНСТРУМЕНТА И РЕЗУЛЬТАТОВ РАБОТЫ НЕ ПРЕВЫШАЕТ ЦЕНЫ, УПЛАЧЕННОЙ ЗА ИНСТРУМЕНТ.
Регулирующее право и разрешение споров
Настоящие Условия использования должны считаться выполненными, подлежат толкованию и регулируются согласно законодательству Российской Федерации.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
public/logos/Telegram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

BIN
public/logos/YandexDisk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

1
public/resources Symbolic link
View File

@@ -0,0 +1 @@
F:/project/tgCrew/backend/data/resources

3352
public/telegram-web-app.js Normal file

File diff suppressed because it is too large Load Diff

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'
@@ -117,6 +117,16 @@ export default defineConfig((ctx) => {
changeOrigin: true, changeOrigin: true,
ws: true ws: true
}, },
// необходимо создать simlink
// mklink /D "F:\project\tgCrew\admin\public\resources" "F:\project\tgCrew\backend\public\resources"
'/resources': {
target: 'http://localhost:3000',
changeOrigin: true,
secure: false,
pathRewrite: {
'^/resources': '/resources'
}
}
/* '/ws': { /* '/ws': {
target: 'ws://localhost:3000', // или wss:// для HTTPS target: 'ws://localhost:3000', // или wss:// для HTTPS
changeOrigin: true, changeOrigin: true,
@@ -126,7 +136,7 @@ export default defineConfig((ctx) => {
}, },
// https: true, // https: true,
// open: true // opens browser window automatically open: false // opens browser window automatically
}, },
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework // https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework
@@ -244,7 +254,7 @@ export default defineConfig((ctx) => {
builder: { builder: {
// https://www.electron.build/configuration/configuration // https://www.electron.build/configuration/configuration
appId: 'projectsnode' appId: 'tgcrew'
} }
}, },

View File

@@ -1,24 +1,121 @@
<template> <template>
<router-view/> <router-view/>
<!-- fix auth error -->
<q-dialog
v-model="needReloadPage"
persistent
transition-show="scale"
>
<q-card
flat class="q-py-lg q-px-md text-center w80 rounded-card no-scroll"
style="width: calc(min(100vw, var(--body-width)) - 32px);"
>
<q-card-section class="q-pa-none">
<div class="text-h6 text-weight-bold q-mb-sm">
{{ $t('telegram_auth_error__title')}}
</div>
<div class="text-body1 text-grey-8">
{{ $t('telegram_auth_error__description')}}
</div>
</q-card-section>
<q-card-actions align="center" class="q-mt-lg q-pa-none">
<q-btn
unelevated
rounded
:label="$t('telegram_auth_error__btn')"
color="primary"
class="full-width q-py-sm text-weight-bold"
@click="closePage"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, inject } from 'vue' import { ref, computed, watch, inject, onMounted, onUnmounted, getCurrentInstance } from 'vue'
import { useSettingsStore } from 'stores/settings' import { useSettingsStore } from 'stores/settings'
import { useAuthStore } from 'stores/auth'
import { useSSEStore } from 'stores/SSE'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import type { WebApp } from '@twa-dev/types' import type { WebApp } from '@twa-dev/types'
import { storeToRefs } from 'pinia'
import { useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
import type { CompanyParams } from 'types/Company'
const authStore = useAuthStore()
const settingsStore = useSettingsStore()
const sseStore = useSSEStore()
const { isAuth } = storeToRefs(authStore)
const { t } = useI18n()
const app = getCurrentInstance()
const isMobile = app?.appContext.config.globalProperties.$isMobile
const router = useRouter() const router = useRouter()
const tg = inject('tg') as WebApp const tg = inject('tg') as WebApp
const needReloadPage = ref(false)
tg.onEvent('settingsButtonClicked', async () => { tg.onEvent('settingsButtonClicked', async () => {
await router.push({ name: 'account' }) await router.push(authStore.isAuth ? { name: 'account' } : { name: 'settings' } )
}) })
const settingsStore = useSettingsStore() watch(isAuth, async (newVal: boolean) => {
if (newVal) sseStore.connect()
else sseStore.disconnect()
if (authStore.customer?.company ||
(authStore.customer?.company &&
Object.keys(authStore.customer?.company).length === 0
)) {
await authStore.updateMyCompany({ name: t('company_edit__my_company')} as CompanyParams)
}
if (newVal) await settingsStore.init()
})
// displayMode
const displayMode = computed(() => settingsStore.settings.displayMode)
const bgColor = getComputedStyle(document.documentElement)
.getPropertyValue('--bg-base').trim() as `#${string}`
const headerColor = bgColor || tg.themeParams.header_bg_color || tg.themeParams.bg_color
watch(displayMode, (val) => {
if (val === 'full' && tg.isVersionAtLeast('8.0') && isMobile) {
tg.requestFullscreen()
tg.setHeaderColor('bg_color')
} else {
tg.exitFullscreen()
tg.expand()
tg.setHeaderColor(headerColor)
}
}, { immediate: true })
onMounted(async () => { onMounted(async () => {
await settingsStore.init() await settingsStore.init()
if (isAuth.value) sseStore.connect()
needReloadPage.value = (!(
tg.initDataUnsafe.user &&
Object.keys(tg.initDataUnsafe.user).length !== 0
) && isAuth && !authStore.customer?.email) ?? false
}) })
onUnmounted(() => sseStore.disconnect())
function closePage () {
tg.close()
}
const $q = useQuasar()
$q.iconMapFn = (iconName) => {
if (iconName.startsWith('pn-') === true) {
const name = iconName.substring(3)
return { cls: 'pn-icon ' + name }
}
}
</script> </script>

13
src/Global-properties.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
import type { WebApp } from "@twa-dev/types"
declare module 'vue' {
export interface ComponentCustomProperties {
$tg: WebApp;
$isDesktop: boolean;
$isMobile: boolean;
$isIOS: boolean;
$isAndroid: boolean;
}
}
export {}

View File

@@ -1,73 +1,70 @@
*This document is an English translation of the original Russian-language "Consent to Personal Data Processing" (Version 1.01 dated 15.08.2025). In the event of any disputes, the original Russian version shall take precedence, particularly for matters resolved within the jurisdiction of the Russian Federation.* ###### Version 1.05 dated April 15, 2026
# Consent to Personal Data Processing In accordance with the requirements of the Federal Law dated July 27, 2006 No. 152-FZ "On Personal Data", acting freely, of my own will and in my own interest, as well as confirming my legal capacity, I (hereinafter — the **User**) hereby give my consent to individual entrepreneur Martynshkin Alexey Alexandrovich (OGRNIP 318774600262084, INN 366316608346, PData operator reg. number 77-25-471585; hereinafter — the **Operator**) for the processing of my personal data under the conditions set forth below.
###### Version 1.01 dated 15.08.2025
Pursuant to Federal Law No. 152-FZ dated 27.07.2006 "On Personal Data", acting freely, by my own will and in my own interest, and hereby confirming my legal capacity, I (hereinafter the **User**) grant my consent to Alexey Alexandrovich Martyshkin, Individual Entrepreneur (OGRNIP 318774600262084, INN 366316608346) (hereinafter the **Operator**) for the processing of my personal data under the following terms. The following terms and definitions are used in this Consent:
The following terms and definitions apply in this Consent: **Bot** — an account named @tgCrewBot and located at [https://t.me/tgCrewBot](https://t.me/tgCrewBot) in the Telegram messenger, managed programmatically by the Operator via API (Application Programming Interface). The Bot provides access to the Application and can be used for basic interaction from the text string (inline-mode).
**Bot** An account named @tgCrewBot with URL [https://t.me/tgCrewBot](https://t.me/tgCrewBot) in the Telegram messenger, programmatically managed by the Operator via API (Application Programming Interface). The Bot provides access to the Application and supports basic interaction via inline mode. **Application** — the tgCrew mini-application operating within the Telegram messenger environment as part of the Telegram Mini Apps (TMA) ecosystem and accessible through interaction with the Bot. Although the Application uses the Telegram platform for its functioning and distribution, it is developed and provided exclusively by the Operator. The Application is not a product of the Telegram messenger development company, is not supported by it, is not approved by it, and is not associated with it legally or organizationally in any way.
**Application** The tgCrew mini-app operating within the Telegram messenger environment as part of the Telegram Mini Apps (TMA) ecosystem, accessible through interaction with the Bot. While the Application utilizes Telegram's platform for functionality and distribution, it is exclusively developed and provided by the Operator. The Application is not a product of Telegram Messenger LLP, nor is it endorsed, supported, or legally affiliated with Telegram Messenger LLP. **Personal Data (or PData)** — any information relating to a directly or indirectly identified or identifiable individual (subject of personal data) (according to No. 152-FZ, Chapter 1, Article 3, Clause 1).
**Personal Data (PD)** Any information relating to an identified or identifiable natural person (data subject) (per Federal Law No. 152-FZ, Chapter 1, Article 3, Clause 1). **Administrator** — a User of the Application who manages the Application using the Admin Panel.
**Administrator** A User of the Application who manages it through the Admin Panel. **Administrator Account** — the account through which the Administrator authenticates to access the Admin Panel.
**Administrator Account** An account used by the Administrator to authenticate access to the Admin Panel. **Admin Panel** — the Application interface for managing the Application, including the following functions:
- connecting chats to the Application (within the meaning of the term Connected Chat);
- entering additional information data for Users;
- controlling the payment of the License Fee to the Developer for using the Application.
**Admin Panel** The Application interface for management functions, including: **Connected Chat** (to the Application) — a chat in Telegram to which the Bot has been added with the necessary access rights. Information is available in the Application only from Connected Chats.
- Connecting chats to the Application (see §1.12 Connected Chat)
- Adding supplementary User information
- Monitoring and processing Developer remuneration payments for Application usage.
**Connected Chat** A Telegram chat where the Bot has been added with necessary access rights. The Application only processes data from Connected Chats. 1. This consent is specific, informed, conscious, and unambiguous.
1. This Consent is specific, informed, conscious, and unambiguous. 2. Consent is given to the Operator for the processing of the User's personal data (using automation tools), including the following actions:
2. The User consents to the Operator's processing of their Personal Data (using automated means), including: a) Collection, recording, systematization, accumulation.
b) Storage for the period necessary to achieve the purposes of processing, but no less than the periods established by the Operator's Privacy Policy and the legislation of the Russian Federation.
c) Clarification (updating, modification).
d) Extraction, use.
e) Transfer (provision, access) to state bodies, courts, or law enforcement agencies exclusively upon official request within the framework of the legislation of the Russian Federation.
f) Transfer (provision, access) to other Users of the Application who have a common Connected Chat with the User, in the scope provided by the Application functionality (displaying the first name, last name, avatar, position or role in participant lists, in the description of meetings and tasks, etc.).
g) Blocking, deletion, destruction.
h) Implementation of other actions provided for by the legislation of the Russian Federation.
a) Collection, recording, systematization, accumulation 3. Consent is given for the processing of the following personal data of the User, which may be provided by the User themselves and (or) entered by the Application Administrator (if the User is a participant of a Connected Chat) and (or) automatically collected by the Application:
b) Storage for the duration of the Administrator Account's existence, but no less than required by Russian law. Data is deleted upon Administrator Account closure, except where legally mandated (e.g., fraud prevention or court order execution)
c) Updating (modification, correction)
d) Retrieval, usage
e) Transfer (provision, access) to state authorities, courts, or law enforcement agencies exclusively upon official request under Russian law
f) Transfer (provision, access) to other Application Users within the same Connected Chat, limited to Application functionality requirements
g) Blocking, deletion, destruction
h) Other lawful processing activities under Russian legislation
3. Consent covers processing of the following User Personal Data, which may be: a) Surname, first name, patronymic.
- Provided directly by the User b) Email address.
- Entered by the Application Administrator (if User participates in a Connected Chat) c) Contact phone number.
- Automatically collected by the Application: d) Information about the place of work (name of the organization, structural unit, position and/or functional role).
e) Data of the account in the Telegram messenger: user name (Name, including first_name and last_name), pseudonym (Username), user identifier (ID), profile image identifier (used to display an avatar without saving copies of files on the Operator's servers), language settings of the Telegram client, as well as, if the User is an Administrator, Telegram Stars transaction data (user identifier, number of Telegram Stars, transaction identifier, operation timestamp), which are automatically transmitted via the Telegram API. Processing of biometric personal data is not performed by the Operator.
f) Data on interaction with the Application servers (technical data): IP address from which the request is made, date and time the request was received by the Application server, requested URL, HTTP response code of the Application server, response size in bytes, User-Agent (information about the browser and operating system of the client), Referrer (URL of the page from which the request was made, if available).
a) Full name (surname, given name, patronymic) 4. Personal data processing is carried out for the following purposes:
b) Email address
c) Contact phone number
d) Employment details (organization, department, position/functional role)
e) Telegram account data: Display name (First/Last name), username, user ID, profile picture, Telegram client language settings; and for Administrators: Telegram Stars transaction data (user ID, amount, transaction ID, timestamp) transmitted automatically via Telegram API
f) Server interaction data (technical): Request IP address, request timestamp, requested URL, HTTP response code, response size (bytes), User-Agent, Referrer URL (if available)
4. Processing purposes: a) Identification and authentication in the Application (sub-clauses 3.b and 3.e).
b) Provision of technical support and processing of inquiries (sub-clauses 3.a-3.d).
c) Sending system notifications if the User is an Application Administrator and uses email for authentication (sub-clauses 3.b and 3.e).
d) Accounting and confirmation of payment transactions via Telegram Stars, in case the User is an Application Administrator (sub-clauses 3.b and 3.e).
e) Formation of an address book of contacts within Connected Chats (sub-clauses 3.a-3.d).
f) Ensuring the correct operation of the Application functionality (sub-clauses 3.a-3.d).
g) Implementation of technical measures aimed at preventing technical failures, identifying errors, and ensuring the information security of the Application (clause 3.f).
a) Application authentication and identification (§§3b, 3e) 5. I express my consent that checking the corresponding interactive element (checkbox) of the Application interface next to the text: "I give Consent for the processing of my personal data and accept the terms of the Privacy Policy" is recognized by the Operator and the User as an analogue of the User's handwritten signature (simple electronic signature), certifying the fact of familiarization and agreement with the terms of this Consent and the Operator's Privacy Policy, posted in the Application and in public access on the Internet at: [https://tgcrew.ru/privacy-policy](https://tgcrew.ru/privacy-policy).
b) Technical support and request handling (§§3a-3e)
c) System notifications (for Administrators using email authentication) (§§3b, 3e)
d) Processing Telegram Stars transactions (for Administrators) (§§3b, 3e)
e) Generating contact directories within Connected Chats (§§3a-3e)
f) Ensuring proper Application functionality (§§3a-3e)
g) Technical maintenance and security (§§3f)
5. The User acknowledges that ticking the checkbox adjacent to the text: "✔ I consent to the processing of my personal data and accept the Privacy Policy" in the Application interface constitutes a simple electronic signature under Russian law, confirming: 6. This consent enters into force from the moment it is provided by performing the actions specified in clause 5 of this Consent and is valid until the moment of withdrawal.
- Review of this Consent
- Acceptance of the Operator's Privacy Policy available at: [https://tgcrew.ru/privacy-policy](https://tgcrew.ru/privacy-policy)
6. This Consent becomes effective upon completion of the action specified in §5 and remains valid until withdrawal. 7. Withdrawal of this Consent is carried out by clicking on the "Refusal to use the application" button and subsequently clicking the "Confirm" button in the dialog that opens. The "Refusal to use the application" button is located in the Settings section (both for the User part and for the Admin Panel). Also, the withdrawal of Consent can be sent to the Operator in writing to the email address [support@tgcrew.ru](mailto:support@tgcrew.ru). Withdrawal of Consent entails the termination of personal data processing and its destruction, except for cases provided for in clause 8 of this Consent.
7. Withdrawal is performed by: 8. In case of withdrawal of this consent, the Operator has the right to continue processing the User's personal data without their consent if there are grounds provided for by clauses 2-11 of part 1 of article 6, part 2 of article 10 and part 2 of article 11 of the Federal Law dated July 27, 2006 No. 152-FZ "On Personal Data".
- Selecting "Opt-Out from Application Usage" in Settings (User/Admin Panel)
- Confirming via "Confirm" in the dialog
Withdrawal results in cessation of processing and data destruction, except as stipulated in §8.
8. Post-withdrawal, the Operator retains the right to continue processing Personal Data without consent where grounds exist under Clauses 2-11 (Part 1, Article 6), Part 2 (Article 10), and Part 2 (Article 11) of Federal Law No. 152-FZ dated 27.07.2006 "On Personal Data". 9. Contact Information and Details of the Operator
Individual Entrepreneur Martynshkin Alexey Alexandrovich
Legal address: 111394, Russian Federation, Moscow, Perovskaya street, house 66, building 3, apartment 187
OGRNIP 318774600262084
INN 366316608346
Phone: +7 (916) 439-04-25
Email: [info@tgcrew.ru](mailto:info@tgcrew.ru)

View File

@@ -1,7 +1,6 @@
# Согласие на обработку персональных данных ###### Версия 1.05 от 15.04.2026
###### Версия 1.01 от 15.08.2025
В соответствии с требованиями Федерального закона от 27.07.2006 № 152-ФЗ «О персональных данных», действуя свободно, своей волей и в своем интересе, а также подтверждая свою дееспособность, я (далее -- **Пользователь**) даю свое согласие индивидуальному предпринимателю Мартышкину Алексею Александровичу (ОГРНИП 318774600262084, ИНН 366316608346) (далее -- **Оператор**) на обработку своих персональных данных на изложенных ниже условиях. В соответствии с требованиями Федерального закона от 27.07.2006 № 152-ФЗ «О персональных данных», действуя свободно, своей волей и в своем интересе, а также подтверждая свою дееспособность, я (далее -- **Пользователь**) даю свое согласие индивидуальному предпринимателю Мартышкину Алексею Александровичу (ОГРНИП 318774600262084, ИНН 366316608346, оператор ПДн регистр. номер 77-25-471585; далее -- **Оператор**) на обработку своих персональных данных на изложенных ниже условиях.
В настоящем Согласии используются следующие термины и определения: В настоящем Согласии используются следующие термины и определения:
@@ -9,16 +8,16 @@
**Приложение** -- мини-приложение tgCrew, работающее в среде мессенджера Telegram как часть экосистемы Telegram Mini Apps (TMA) и доступное через взаимодействие с Ботом. Хотя Приложение использует платформу Telegram для своего функционирования и распространения, оно разработано и предоставляется исключительно Оператором. Приложение не является продуктом компании-разработчика мессенджера Telegram, не поддерживается ею, не одобрено ею и никак не связано с ней юридически или организационно. **Приложение** -- мини-приложение tgCrew, работающее в среде мессенджера Telegram как часть экосистемы Telegram Mini Apps (TMA) и доступное через взаимодействие с Ботом. Хотя Приложение использует платформу Telegram для своего функционирования и распространения, оно разработано и предоставляется исключительно Оператором. Приложение не является продуктом компании-разработчика мессенджера Telegram, не поддерживается ею, не одобрено ею и никак не связано с ней юридически или организационно.
**Персональные данные (или ПДн)** -- любая информация, относящаяся к прямо или косвенно определенному или определяемому физическому лицу (субъекту персональных данных) (согласно N152-ФЗ, Глава 1, Статья 3, п. 1). **Персональные данные (или ПДн)** -- любая информация, относящаяся к прямо или косвенно определенному или определяемому физическому лицу (субъекту персональных данных) (согласно 152-ФЗ, Глава 1, Статья 3, п. 1).
**Администратор** -- Пользователь Приложения, который осуществляет его управление Приложением с помощью Панели администратора. **Администратор** -- Пользователь Приложения, который осуществляет управление Приложением с помощью Панели администратора.
**Учетная запись Администратора** - учетная запись, с помощью которой Администратор осуществляет аутентификацию для доступа к Панели администратора. **Учетная запись Администратора** -- учетная запись, с помощью которой Администратор осуществляет аутентификацию для доступа к Панели администратора.
**Панель администратора** -- интерфейс Приложения для управления Приложением, в том числе осуществления следующих функций: **Панель администратора** -- интерфейс Приложения для управления Приложением, в том числе осуществления следующих функций:
- подключение чатов к Приложению (см. п. 1.12 Подключенный чат); - подключение чатов к Приложению (в значении термина Подключенный чат);
- внесение дополнительных информационных данных для Пользователей; - внесение дополнительных информационных данных для Пользователей;
- контроль и осуществление выплат вознаграждения Разработчику за использование Приложения. - контроль оплаты Лицензионного вознаграждения Разработчику за использование Приложения.
**Подключенный чат** (к Приложению) -- чат в Telegram, в который добавлен Бот с необходимыми правами доступа. В Приложении доступна информация только из Подключенных чатов. **Подключенный чат** (к Приложению) -- чат в Telegram, в который добавлен Бот с необходимыми правами доступа. В Приложении доступна информация только из Подключенных чатов.
@@ -27,11 +26,11 @@
2. Согласие дано Оператору на обработку персональных данных Пользователя (с использованием средств автоматизации), включающую следующие действия: 2. Согласие дано Оператору на обработку персональных данных Пользователя (с использованием средств автоматизации), включающую следующие действия:
а) Сбор, запись, систематизация, накопление. а) Сбор, запись, систематизация, накопление.
б) Хранение в течение всего срока существования Учетной записи Администратора, но не менее срока, требуемого законодательством РФ. Данные удаляются при закрытии Учетной записи Администратора, за исключением случаев, предусмотренных законом (например, для предотвращения мошенничества или исполнения судебных решений). б) Хранение в течение срока, необходимого для достижения целей обработки, но не менее сроков, установленных Политикой конфиденциальности Оператора и законодательством Российской Федерации.
в) Уточнение (обновление, изменение). в) Уточнение (обновление, изменение).
г) Извлечение, использование. г) Извлечение, использование.
д) Передача (предоставление, доступ) государственным органам, судам или правоохранительным структурам исключительно по официальному запросу в рамках законодательства РФ. д) Передача (предоставление, доступ) государственным органам, судам или правоохранительным структурам исключительно по официальному запросу в рамках законодательства РФ.
е) Передачу (предоставление, доступ) другим Пользователям Приложения, имеющим общий Подключенный чат с Пользователем, в объёме, необходимом для функциональности Приложения. е) Передачу (предоставление, доступ) другим Пользователям Приложения, имеющим общий Подключенный чат с Пользователем, в объёме, предусмотренном функционалом Приложения (отображение имени, фамилии, аватара, должности или роли в списках участников, в описании совещаний и задач и т.п.).
ж) Блокирование, удаление, уничтожение. ж) Блокирование, удаление, уничтожение.
з) Осуществление иных действий, предусмотренных законодательством РФ. з) Осуществление иных действий, предусмотренных законодательством РФ.
@@ -41,23 +40,31 @@
б) Адрес электронной почты. б) Адрес электронной почты.
в) Контактный телефон. в) Контактный телефон.
г) Сведения о месте работы (наименование организации, структурное подразделение, должность и/или функциональная роль). г) Сведения о месте работы (наименование организации, структурное подразделение, должность и/или функциональная роль).
д) Данные учетной записи в мессенджере Telegram: имя пользователя (Name, вкл. в себя - First name и Last name), псевдоним (Username), идентификатор пользователя (ID), изображение профиля (аватар), языковые настройки клиента Telegram, а также, если Пользователь является Администратором, данные транзакций Telegram Stars (идентификатор пользователя, количество Telegram Stars, идентификатор транзакции, временная метка операции), которые автоматически передаются через API Telegram. д) Данные учетной записи в мессенджере Telegram: имя пользователя (Name, включающее в себя first_name и last_name), псевдоним (Username), идентификатор пользователя (ID), идентификатор изображения профиля (используется для отображения аватара без сохранения копий файлов на серверах Оператора), языковые настройки клиента Telegram, а также, если Пользователь является Администратором, данные транзакций Telegram Stars (идентификатор пользователя, количество Telegram Stars, идентификатор транзакции, временная метка операции), которые автоматически передаются через API Telegram. Обработка биометрических персональных данных Оператором не осуществляется.
е) Данные о взаимодействии с серверами Приложения (технические данные): IP-адрес с которого осуществляется запрос, дата и время получения запроса сервером Приложения, запрошенный URL, HTTP-код ответа сервера Приложения, размер ответа в байтах, User-Agent (информация о браузере и операционной системе клиента), Referrer (URL страницы, с которой был сделан запрос, если доступно). е) Данные о взаимодействии с серверами Приложения (технические данные): IP-адрес, с которого осуществляется запрос, дата и время получения запроса сервером Приложения, запрошенный URL, HTTP-код ответа сервера Приложения, размер ответа в байтах, User-Agent (информация о браузере и операционной системе клиента), Referrer (URL страницы, с которой был сделан запрос, если доступно).
4. Обработка персональных данных осуществляется в следующих целях: 4. Обработка персональных данных осуществляется в следующих целях:
а) Идентификация и аутентификация в Приложении (пп. 3б и 3д). а) Идентификация и аутентификация в Приложении (пп. 3.б и 3.д).
б) Предоставление технической поддержки и обработка обращений (пп. 3а-3д). б) Предоставление технической поддержки и обработка обращений (пп. 3.а-3.д).
в) Отправка системных уведомлений если Пользователь является Администратором Приложения и использует электронную почту для аутентификации (пп. 3б и 3д). в) Отправка системных уведомлений, если Пользователь является Администратором Приложения и использует электронную почту для аутентификации (пп. 3.б и 3.д).
г) Обработка платежных транзакций через Telegram Stars, в случае если Пользователь является Администратором Приложения (пп. 3б и 3д). г) Учет и подтверждение платежных транзакций через Telegram Stars, в случае если Пользователь является Администратором Приложения (пп. 3.б и 3.д).
д) Формирование адресной книги контактов в рамках Подключенных чатов (пп. 3а-3д). д) Формирование адресной книги контактов в рамках Подключенных чатов (пп. 3.а-3.д).
е) Обеспечение корректной работы функционала Приложения (пп. 3а-3д). е) Обеспечение корректной работы функционала Приложения (пп. 3.а-3.д).
ж) Технические: профилактика технических сбоев и обеспечение безопасности (пп. 3е). ж) Осуществление технических мер, направленных на профилактику технических сбоев, выявление ошибок и обеспечение информационной безопасности Приложения (п. 3.е).
5. Я выражаю согласие на квалификацию проставления символа «V» (галочки) в чек-боксе интерфейса Приложения рядом с текстом: «Я даю Согласие на обработку своих персональных данных и принимаю условия Политики конфиденциальности» в качестве простой электронной подписи, удостоверяющей факт ознакомления и согласия с условиями настоящего Согласия и Политики конфиденциальности Оператора, размещенной в Приложении и в открытом доступе в сети Интернет по адресу: [https://tgcrew.ru/privacy-policy]( https://tgcrew.ru/privacy-policy). 5. Я выражаю согласие, что проставление отметки в соответствующем интерактивном элементе (поле для отметки) интерфейса Приложения рядом с текстом: «Я даю Согласие на обработку своих персональных данных и принимаю условия Политики конфиденциальности» признается Оператором и Пользователем аналогом собственноручной подписи Пользователя (простой электронной подписью), удостоверяющей факт ознакомления и согласия с условиями настоящего Согласия и Политики конфиденциальности Оператора, размещенной в Приложении и в открытом доступе в сети Интернет по адресу: [https://tgcrew.ru/privacy-policy](https://tgcrew.ru/privacy-policy).
6. Настоящее согласие вступает в силу с момента его предоставления путем совершения действий, указанных в пункте 5 настоящего Согласия, и действует до момента отзыва. 6. Настоящее согласие вступает в силу с момента его предоставления путем совершения действий, указанных в пункте 5 настоящего Согласия, и действует до момента отзыва.
7. Отзыв настоящего Согласия осуществляется путем нажатия на кнопку «Отказ от использования приложения» и последующим нажатием в открывшемся диалоге кнопки «Подтвердить». Кнопка «Отказ от использования приложения» размещена в разделе Настройки (как для Пользовательской части, так и для Панели Администратора). Отзыв Согласия влечет прекращение обработки персональных данных и их уничтожение, за исключением случаев, предусмотренных пунктом 8 настоящего Согласия. 7. Отзыв настоящего Согласия осуществляется путем нажатия на кнопку «Отказ от использования приложения» и последующим нажатием в открывшемся диалоге кнопки «Подтвердить». Кнопка «Отказ от использования приложения» размещена в разделе Настройки (как для Пользовательской части, так и для Панели Администратора). Также отзыв Согласия может быть направлен Оператору в письменной форме по адресу электронной почты [support@tgcrew.ru](mailto:support@tgcrew.ru). Отзыв Согласия влечет прекращение обработки персональных данных и их уничтожение, за исключением случаев, предусмотренных пунктом 8 настоящего Согласия.
8. В случае отзыва настоящего согласия Оператор вправе продолжить обработку персональных данных Пользователя без его согласия при наличии оснований, предусмотренных пунктами 2-11 части 1 статьи 6, частью 2 статьи 10 и частью 2 статьи 11 Федерального закона от 27.07.2006 № 152-ФЗ «О персональных данных». 8. В случае отзыва настоящего согласия Оператор вправе продолжить обработку персональных данных Пользователя без его согласия при наличии оснований, предусмотренных пунктами 2-11 части 1 статьи 6, частью 2 статьи 10 и частью 2 статьи 11 Федерального закона от 27.07.2006 № 152-ФЗ «О персональных данных».
9. Контактная информация и реквизиты Оператора
Индивидуальный предприниматель Мартышкин Алексей Александрович
Юридический адрес: 111394, Российская Федерация, город Москва, улица Перовская, дом 66, корпус 3, квартира 187
ОГРНИП 318774600262084
ИНН 366316608346
Телефон: +7 (916) 439-04-25
Электронная почта: [info@tgcrew.ru](mailto:info@tgcrew.ru)

View File

@@ -1,162 +1,190 @@
*This document is an English adaptation of the Privacy Policy 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.* ###### Version 1.05 dated April 15, 2026
# Privacy Policy This document is an English adaptation of «Политика конфиденциальности» 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.
###### Version 1.01 dated 15.08.2025
This Privacy Policy describes the scope of data (including personal data) collection from Users of the Application by the Developer, explains the purposes of such collection, and outlines processing methods. In this Privacy Policy, the Developer provides information on the scope of collection of data (including personal data) of Users in the Application, explains the reasons for its collection and the ways of its use.
Current versions of the Terms of Use and the Privacy Policy are available within the Application interface. Information posted on the Website is for reference purposes only. In case of discrepancies between the text on the Website and in the Application, the version placed in the Application shall have legal force.
## 1. Terms and Definitions ## 1. Terms and Definitions
1.1. **Bot** The Telegram account "@tgCrewBot" ([https://t.me/tgCrewBot](https://t.me/tgCrewBot)), programmatically managed by the Developer via Telegrams API. The Bot provides access to the Application and supports basic inline-mode interaction. 1.1. **Bot** — an account named @tgCrewBot and located at [https://t.me/tgCrewBot](https://t.me/tgCrewBot) in the Telegram messenger, managed programmatically by the Developer via API (Application Programming Interface). The Bot provides access to the Application and can be used for basic interaction from the text string (inline-mode).
1.2. **Application** The Telegram Mini App (TMA) "tgCrew", operating within Telegrams ecosystem and accessible via the Bot. Though hosted on Telegrams platform, the Application is developed and provided solely by the Developer. It is not a product of, endorsed by, or legally affiliated with Telegram FZ-LLC. Access requires acceptance of the Terms of Use and this Privacy Policy. 1.2. **Application** — the tgCrew mini-application operating within the Telegram messenger environment as part of the Telegram Mini Apps (TMA) ecosystem and accessible through interaction with the Bot. Although the Application uses the Telegram platform for its functioning and distribution, it is developed and provided exclusively by the Developer. The Application is not a product of the Telegram messenger development company, is not supported by it, is not approved by it, and is not associated with it legally or organizationally in any way. The functionality of the Application is available only after acceptance of the Terms of Use and the Privacy Policy.
1.3. **Website** The official site [https://tgcrew.ru](https://tgcrew.ru), hosting Application documentation and information. 1.3. **Website** — the website located at [https://tgcrew.ru](https://tgcrew.ru), where information about the Application and its documentation is posted.
1.4. **Terms of Use** The current version available at [https://tgcrew.ru/terms-of-use](https://tgcrew.ru/terms-of-use) and within the Application. In case of discrepancies, the in-Application version prevails. Contractual relations commence upon in-Application acceptance. 1.4. **Terms of Use** — this document, the current version of which is posted in the Application and on the Website at [https://tgcrew.ru/terms-of-use](https://tgcrew.ru/terms-of-use). Contractual relations between the Developer and the User arise only after acceptance of the Terms of Use within the Application.
1.5. **Privacy Policy** This document governing the collection, storage, and processing of User data (including personal data). An integral annex to the Terms of Use. The current version is at [https://tgcrew.ru/privacy-policy](https://tgcrew.ru/privacy-policy) and in the Application. In-Application information prevails in conflicts. Consent is obtained via in-Application acceptance. 1.5. **Privacy Policy** — a document regulating the procedure for collecting, storing, and processing data (including personal data) of the User entered during the registration procedure, use of the Application, as well as other data. It is an integral annex to the Terms of Use. The current version of the Privacy Policy is posted in the Application and on the Website at [https://tgcrew.ru/privacy-policy](https://tgcrew.ru/privacy-policy). Consent for data processing within the framework of the Privacy Policy is provided by the User within the Application.
1.6. **Developer** Individual Entrepreneur Martyshkin Alexey Alexandrovich (OGRNIP 318774600262084, INN 366316608346, Russian Federation). 1.6. **Developer** — individual entrepreneur Martynshkin Alexey Alexandrovich (IP Martynshkin A.A.), OGRNIP 318774600262084, INN 366316608346 (Russian Federation).
1.7. **User** An individual, legal entity, or authorized representative accepting the Terms of Use and Privacy Policy within the Application. 1.7. **User** — an individual, individual entrepreneur, legal entity, or their authorized representative who has accepted the terms of the Terms of Use and the Privacy Policy by their explicit confirmation within the Application interface.
1.8. **Administrator** A User managing the Application via the Admin Panel. 1.8. **Administrator** — a User of the Application who manages the Application using the Admin Panel.
1.9. **Administrator Account** Authentication credentials for Admin Panel access. 1.9. **Administrator Account** — the account through which the Administrator authenticates to access the Admin Panel.
1.10. **Chat Participant** A member of a Connected Chat who may not be an Application User. 1.10. **Chat Participant** — a participant of a Connected Chat who may not be a User of the Application.
1.11. **Admin Panel** The Application interface for: 1.11. **Admin Panel** — the Application interface for managing the Application, including the following functions:
- Connecting chats to the Application (see §1.12); - connecting chats to the Application (see clause 1.12 Connected Chat);
- Adding supplemental User data; - entering additional information data for Users;
- Managing Developer remuneration payments. - controlling the payment of the License Fee to the Developer for using the Application.
1.12. **Connected Chat** A Telegram chat where the Bot is added with necessary permissions. Only data from Connected Chats is processed. 1.12. **Connected Chat** (to the Application) — a chat in Telegram to which the Bot has been added with the necessary access rights. Information is available in the Application only from Connected Chats.
1.13. **Application Use** User actions to view/utilize Application functionality via online interfaces. 1.13. **Use of the Application** — the User performing actions to view and (or) use the available functionality of the Application through reproducible online interfaces on the screen of the User's device.
1.14. **Developers Representatives** Persons engaged by the Developer for Application development/support. 1.14. **Developer's Representatives** — the circle of persons engaged by the Developer within the framework of development and support of the Application, acting on behalf of and (or) on the instructions of the Developer.
1.15. **Personal Data (PD)** Any information relating to an identified/identifiable natural person (Federal Law No. 152-FZ, Art. 3.1). 1.15. **Personal Data (or PData)** — any information relating to a directly or indirectly identified or identifiable individual (subject of personal data) (according to Federal Law No. 152-FZ, Chapter 1, Article 3, Clause 1).
1.16. **Personal Data Processing** Any operation(s) performed with PD, including collection, recording, storage, alteration, retrieval, use, transfer, anonymization, blocking, or destruction (Federal Law No. 152-FZ, Art. 3.3). 1.16. **Processing of Personal Data** — any action (operation) or set of actions (operations) performed with personal data using automation tools or without using such tools, including collection, recording, systematization, accumulation, storage, clarification (updating, modification), extraction, use, transfer (distribution, provision, access), depersonalization, blocking, deletion, destruction of personal data (according to Federal Law No. 152-FZ, Chapter 1, Article 3, Clause 3).
1.17. **Operator** A legal/natural person organizing and/or processing PD, defining processing purposes and scope (Federal Law No. 152-FZ, Art. 3.2). 1.17. **Operator** — a state body, municipal body, legal entity or individual, independently or jointly with other persons organizing and (or) performing the processing of personal data, as well as determining the purposes of processing personal data, the composition of personal data subject to processing, and actions (operations) performed with personal data (according to Federal Law No. 152-FZ, Chapter 1, Article 3, Clause 2).
1.18. **Automated PD Processing** PD processing via computer technology (Federal Law No. 152-FZ, Art. 3.4). 1.18. **Automated Processing of Personal Data** — processing of personal data using computer technology (according to Federal Law No. 152-FZ, Chapter 1, Article 3, Clause 4).
1.19. **PD Provision** Disclosure of PD to specific person(s) (Federal Law No. 152-FZ, Art. 3.6). 1.19. **Provision of Personal Data** — actions aimed at disclosing personal data to a specific person or a specific circle of persons (according to Federal Law No. 152-FZ, Chapter 1, Article 3, Clause 6).
1.20. **Cross-Border PD Transfer** Transfer of PD to foreign authorities/entities (Federal Law No. 152-FZ, Art. 3.11). 1.20. **Cross-border Transfer of Personal Data** — the transfer of personal data to the territory of a foreign state to an authority of a foreign state, a foreign individual, or a foreign legal entity (according to Federal Law No. 152-FZ, Chapter 1, Article 3, Clause 11).
1.21. **PD Destruction** Actions rendering PD irrecoverable (Federal Law No. 152-FZ, Art. 3.8). 1.21. **Destruction of Personal Data** — actions as a result of which it becomes impossible to restore the content of personal data in the personal data information system and (or) as a result of which physical carriers of personal data are destroyed (according to Federal Law No. 152-FZ, Chapter 1, Article 3, Clause 8).
## 2. General Provisions ## 2. General Provisions
2.1. This Policy governs solely the Developer-User relationship. It does not replace Telegrams Privacy Policy. 2.1. This Privacy Policy exclusively regulates the relationship between the Developer and the User. It cannot regulate the relationship between the Telegram messenger (hereinafter - Telegram) and its users and does not replace the Telegram Privacy Policy.
2.2. This Policy complies with: 2.2. This Privacy Policy is drawn up in accordance with the requirements of:
- Federal Law No. 152-FZ "On Personal Data" (27.07.2006); - Federal Law dated July 27, 2006 No. 152-FZ "On Personal Data".
- Section "4. Privacy" of Telegrams "Bot Platform Developer Terms of Service". - Requirements of section "4. Privacy" of the document "Telegram Bot Platform Developer Terms of Service" by Telegram FZ-LLC (the developer of the Telegram messenger).
2.3. The Developer acts as the **Operator** for PD processing. 2.3. The Developer acts as the Operator when Processing personal data of Application Users.
## 3. Acceptance of this Policy ## 3. Acceptance of the Terms of this Privacy Policy
3.1. Acceptance occurs by ticking "✔ I consent to the processing of my personal data and accept the Privacy Policy" within the Application. 3.1. Evidence of full and unconditional acceptance of the terms of this Privacy Policy (acceptance) is the performance of the procedure for accepting the Privacy Policy by activating (checking) the corresponding interactive element (checkbox) of the Application interface next to the text: "I give Consent for the processing of my personal data and accept the terms of the Privacy Policy" on the relevant screens in the Application.
3.2. Application Use implies consent to data collection from Connected Chats (§§4.2-4.4). 3.2. Use of the Application signifies the User's consent to the collection and processing of information from Connected Chats in accordance with the Privacy Policy (see clauses 4.2-4.4).
3.3. Non-acceptance requires immediate cessation of Application Use. 3.3. In the event that the User does not agree with the terms of the Privacy Policy, they are obliged to immediately leave the Application and not start/cease the Use of the Application.
## 4. Data Collection and Processing ## 4. Data Collection and Processing
4.1. Essential User data is required for Application access. 4.1. To provide the User with access to the Application, the User must provide the Developer with access to important information about themselves.
4.2. **Collected Personal Data**: 4.2. Personal Data Collected by the Application
4.2.1. Automatically collected via Telegram API:
- Username, Telegram ID, profile picture, language settings, Telegram Stars transaction data (Administrators only).
- Technical data: IP address, request timestamp, requested URL, HTTP status, response size, User-Agent, Referrer.
4.2.2. **Administrator-provided**: 4.2.1. The Application automatically collects the following data of Users, which may be defined as Personal Data:
- Email (if used for authentication).
4.2.3. **Administrator-supplied Chat Participant data** (via Admin Panel): a) Data provided by Telegram via API: user name (Name, including first_name and last_name), pseudonym (Username), user identifier (ID), profile image identifier (used to display an avatar without saving copies of files on the Developer's servers), Telegram client language settings, as well as Telegram Stars transaction data (for the Administrator only).
- Full name, phone number, email, employment details. b) Data on the User's interaction with the Application servers (technical data): IP address from which the request is made, date and time the request was received by the Application server, requested URL, HTTP response code of the Application server, response size in bytes, User-Agent (information about the browser and operating system of the client), Referrer (URL of the page from which the request was made, if available).
*The Developer ensures lawful processing but assumes no liability for data accuracy. Administrators must secure legal grounds for PD submission. Data is visible only to Users sharing a Connected Chat. Users may request data correction/deletion from Administrators.*
4.2.4. **User-provided data** (e.g., support requests): 4.2.2. The Administrator provides:
- Full name, phone number, email, employment details. - email address, if authentication in the Application is performed using it.
4.3. **Cookies**: Only HttpOnly cookies (mitigating XSS risks) are used. 4.2.3. The Administrator in the Admin Panel can specify data for each participant of the Connected Chat, which may be defined as Personal Data:
- surname, first name, patronymic;
- contact phone number;
- email address;
- information about the place of work (name of the organization, structural unit, position and (or) functional role).
4.4. **Chat Monitoring**: The Bot tracks events in Connected Chats (message/file changes). Relevant data is stored. The Developer is not responsible for the correctness and accuracy of the specified data, but as the Operator ensures the legality of its processing. The Administrator is obliged to ensure the existence of legal grounds for the provision of personal data of Users in accordance with applicable legislation (in the case of specifying real data).
The provision of specified data about a User in the Application, which may be defined as Personal Data, to other Users is carried out only for Users who have a common Connected Chat. By accepting this Privacy Policy, the User consents to such provision of their data. If necessary, the user may contact the Administrator to delete or change the above data.
4.5. **User-Input Data Storage**: Project/company/task/meeting details. 4.2.4. Users, when contacting the Developer (for example, when requesting technical support for the Application), are entitled to provide any following data, which may be defined as Personal Data:
- surname, first name, patronymic;
- contact phone number;
- email address;
- information about the place of work (name of the organization, structural unit, position and (or) functional role).
4.6. **Non-Storage**: Chat/files are stored exclusively within Telegram. 4.3. When creating and maintaining an http connection, the Application uses HttpOnly cookie files, which are inaccessible from JavaScript via Document.cookie API properties and are used to reduce the risk of XSS (Cross-Site Scripting). Other cookie files are not set or used.
4.7. **No Additional Collection**: Only data specified in §4.2 is collected. 4.4. The Application, with the help of the Bot, monitors events in Connected Chats (such as adding/changing/deleting messages, adding/deleting files, etc.), but only on condition that the Bot is granted Administrator rights to access messages in Connected Chats.
## 5. Data Collection Purposes 4.5. The Application stores data that Users enter in the Application, including:
5.1. Purposes include: - project data (name, description, logo, etc.),
- User identification/authentication (§4.2.1a, 4.2.2); - company data (name, address, contact phone, website, etc.),
- Technical support (§4.2.1, 4.2.2, 4.2.4); - data on tasks and meetings (description, date and time, etc.).
- Administrator notifications (§4.2.1a, 4.2.2);
- Telegram Stars transactions (§4.2.1a);
- Connected Chat contact management (§4.2.1a, 4.2.3);
- Application functionality (§4.2, 4.4);
- Security/technical maintenance (§4.2.1b, 4.3).
5.2. Data is used solely for these purposes, unless required by Russian law or compatible purposes. New purposes trigger User notification. 4.6. The Developer does not store or analyze correspondence from Connected Chats on its own computing facilities (servers).
5.3. PD processing is **automated**. 4.7. The Developer does not store files from chats, tasks, and meetings on its own computing facilities (servers). Storage is carried out within Telegram or on third-party cloud storage services connected by the Administrator (Yandex Disk or Google Drive).
The responsibility for choosing a third-party storage service, complying with its terms of use and privacy policy, as well as for the existence of legal grounds for transferring data to such services, lies entirely with the Administrator. The Developer is not a storage operator in relation to these third-party services and is not responsible for their availability, data safety, or information security incidents on the part of cloud service providers.
When the Administrator connects third-party storage services (Yandex Disk or Google Drive), the Application's interaction with these services is carried out exclusively through the API of the respective services. The Developer does not request, collect, or store authorization data of these services. Authorization in such services occurs on the side of the respective providers.
## 6. Data Security 4.8. The Application does not collect any additional information on the User's side that can be defined as Personal Data, except for that specified in clause 4.2, and does not perform an analysis of information that is available to it in chats.
6.1. Technical/organizational measures ensure PD confidentiality, integrity, and protection against loss/theft/unauthorized access.
6.2. Measures include SSL encryption for data transmission. ## 5. Purposes and Terms of Data Processing
5.1. Data collection is carried out for the following purposes:
- Identification and authentication of Users in the Application (clauses 4.2.1.a and 4.2.2).
- Provision of technical support and processing of inquiries (clauses 4.2.1, 4.2.2 and 4.2.4).
- Sending system notifications to the Administrator (clauses 4.2.1.a and 4.2.2).
- Accounting and confirmation of payment transactions via Telegram Stars (clause 4.2.1.a).
- Formation of an address book of contacts within Connected Chats (clauses 4.2.1.a and 4.2.3).
- Ensuring the correct operation of the Application's functionality (clauses 4.2 and 4.4).
- Implementation of technical measures aimed at preventing technical failures, identifying errors, and ensuring the information security of the Application (clauses 4.2.1.b and 4.3).
6.3. Only authorized Developer personnel access PD. Representatives have no PD access. Security protocols are periodically reviewed. 5.2. The Developer undertakes to collect and use User data only for the above purposes, except for cases when the collection and use of data is necessary for other purposes compatible with the original purpose of data collection, or when provided for by the legislation of the Russian Federation. If the Developer needs to use User data for other purposes not mentioned above, the Developer has the right to notify the User by updating this Privacy Policy.
5.3. The processing of personal data of Users is carried out by the Developer using automation tools.
5.4. Terms of data processing and storage. The Developer processes Personal Data during the term of the Terms of Use.
5.5. The Developer has the right to continue storing data after the termination of the Terms of Use or withdrawal of consent in the following cases:
- For 5 (five) years in relation to transaction information (according to the tax legislation of the Russian Federation).
- For 3 (three) years (general statute of limitations) to ensure the possibility of protecting the Developer's interests in court.
5.6. Due to technical characteristics, data may be stored in encrypted backup copies of systems for up to 6 (six) months after its actual deletion from active databases. Such copies are used only for disaster recovery.
## 6. Data Protection
6.1. The Developer has implemented a series of technical, organizational, and administrative measures to ensure the confidentiality, integrity, availability, and inviolability and protection of your data (including PData) from loss, theft, unauthorized access, improper use, modification, or destruction.
6.2. These measures include, among other things, the implementation of modern security technologies: Secure Sockets Layered (SSL) technology to ensure full encryption of User data and its secure transmission over the Internet.
6.3. Only authorized personnel of the Developer have access to Users' Personal Data, and this personnel is obliged to treat Personal Data as confidential. Developer's Representatives do not gain access to User data (including personal data). Security measures may be periodically reviewed in accordance with legal and technical changes.
## 7. Data Transfer ## 7. Data Transfer
7.1. No third-party transfers except to courts/state bodies when legally mandated. 7.1. The Developer does not transfer data received from Users (including technical metrics, project data, and other information) to third parties for marketing, advertising, or other commercial purposes. Data transfer is carried out exclusively to the extent necessary to ensure the functionality of the Application: to the Telegram messenger and cloud storage services (according to clause 4.7) initiated by the Administrator.
7.2. The Developer will not sell/exchange PD without explicit consent. 7.2. The Developer does not transfer personal data received from Users to third parties, except in cases of providing data to courts and (or) state bodies and (or) law enforcement agencies in cases when required by the laws and regulations of the Russian Federation.
7.3. **Data Localization**: All servers reside in Russia. **No cross-border transfers**. 7.3. The Developer guarantees to the User that it will not sell, exchange, or transfer your data (including PData) to third parties without your explicit consent.
7.4. All Application servers are located on the territory of the Russian Federation. Cross-border transfer of personal data is not carried out.
## 8. User Rights ## 8. User Rights
8.1. PD subject rights include: 8.1. The main rights of the User as a subject of Personal Data include:
- **Access**: PD copies provided within 30 calendar days. - **Right to access Personal Data.** The User can request the Developer to provide them with a copy of their Personal Data to which the Developer has access. The Developer provides information to the User within 10 (ten) working days from the moment the request is received. This period may be extended by no more than 5 (five) working days in case the Developer sends a motivated notification to the User about the reasons for the extension.
- **Rectification**: Correction via Application functionality, Administrator request, or direct contact. - **Right to correct Personal Data.** The User can demand the Developer to correct or update any of their Personal Data. The User can do this using the corresponding functionality of the Application, by requesting the Administrator, or by contacting the Developer directly. The Developer reserves the right to refuse personal assistance in cases where the correction of Personal Data is available through the functionality of the Application or with the help of the Administrator.
- **Erasure**: Deletion requests honored where legally permissible. Account closure triggers automatic deletion, except where retention is legally required (e.g., fraud prevention, legal obligations). - **Right to delete Personal Data.** The User can demand the Developer to delete their Personal Data, subject to applicable legislation. In some cases, the Application will automatically delete Personal Data upon closing the Account in the Application. If the User closes their Account, the Developer will not use their Personal Data for any further purposes, nor will it transfer them to third parties, except as provided by law. The Developer may be unable to fulfill the User's request for deletion due to legal obligations, which will be communicated to the User if there are appropriate grounds (for example, if processing is necessary to achieve purposes provided for by law, for the execution of a judicial act, for the exercise of rights and legitimate interests of the Developer or third parties).
- **Consent Withdrawal**: Withdrawal does not affect pre-withdrawal processing lawfulness. - **Right to withdraw consent.** To the extent that the processing of the User's Personal Data is based solely on their consent, the User can withdraw their consent at any time. This will not affect the lawfulness of any processing that was carried out before the withdrawal. Any processing actions not based on the User's consent will remain unaffected.
8.2. Rights are not absolute and may be balanced against the Developers legal obligations/legitimate interests. Refusals include justification. 8.2. None of the rights are absolute, which means that they generally must be weighed against the Developer's own legal obligations, as well as its legitimate interests and the interests of third parties. If a decision is made to reject the User's request, the Developer will inform them of this along with the reasons for such a decision.
## 9. Data Retention ## 9. Data Storage
9.1. PD is retained only as necessary for collection purposes. 9.1. The Developer stores User data (including PData) for no longer than necessary for the purpose of its collection or processing (with subsequent deletion).
9.2. Standard retention period: Duration of the Administrator Account. 9.2. As a rule, the Developer stores data (including PData) throughout the lifetime of the Administrator Account.
9.3. Post-account closure retention occurs only if mandated by Russian law. 9.3. In cases provided for by applicable law, Personal Data may be stored after the closure of the Administrator Account for the period established by such laws.
9.4. Extended retention applies for legal compliance, fraud prevention, or dispute resolution. 9.4. The Developer may store User data (including PData) for a longer period if required by relevant laws and regulations. The Developer may retain some User data (including PData) after the closure of the Administrator Account to prevent fraud, ensuring that persons attempting to commit fraud cannot avoid detection simply by closing an account and opening a new one, as well as to fulfill the Developer's legal obligations.
## 10. Policy Amendments ## 10. Changes to the Privacy Policy
10.1. The Developer may revise this Policy without prior notice. The revision date will be updated. 10.1. This Privacy Policy may be revised, updated, and (or) changed at any time without prior notice at the Developer's discretion. If such changes are made, the update date of this Privacy Policy will be displayed on the first page of the Privacy Policy.
10.2. Users should periodically review the Policy on the Website or in the Application. 10.2. The User is advised to independently and regularly monitor changes to the Privacy Policy by familiarizing themselves with the current version of the Privacy Policy on the Website or in the Application.
10.3. Continued Application Use after revisions implies acceptance. 10.3. After the publication of the updated version of the Privacy Policy, further use of the Application is considered acceptance of the terms of the updated Privacy Policy.
## 11. Miscellaneous ## 11. Other Provisions
11.1. Queries regarding Policy interpretation: Contact the Developer at [a-mart@ya.ru](mailto:a-mart@ya.ru). 11.1. If any provisions of this Privacy Policy remain unclear, the Developer is ready to clarify its provisions. To do this, contact the Developer at the email address [support@tgcrew.ru](mailto:support@tgcrew.ru).
11.2. Contact details (§12) may be used for Policy-related matters. 11.2. The User can use the contact details provided in section 12 for any reason provided for by this Policy.
## 12. Developer Contact Information ## 12. Contact Information and Details of the Developer
**Individual Entrepreneur Martyshkin Alexey Alexandrovich** Individual Entrepreneur Martynshkin Alexey Alexandrovich
Legal address: 111394, Russia, Moscow, Perovskaya St., 66, Bldg. 3, Apt. 187 Legal address: 111394, Russian Federation, Moscow, Perovskaya street, house 66, building 3, apartment 187
OGRNIP: 318774600262084 OGRNIP 318774600262084
INN: 366316608346 INN 366316608346
Phone: +7 (926) 339-04-25 Phone: +7 (916) 439-04-25
Email: [a-mart@ya.ru](mailto:a-mart@ya.ru) Email: [info@tgcrew.ru](mailto:info@tgcrew.ru)

View File

@@ -1,7 +1,8 @@
# Политика конфиденциальности ###### Версия 1.05 от 15.04.2026
###### Версия 1.01 от 15.08.2025
В настоящей Политике конфиденциальности Разработчик предоставляет информацию о пределах сбора данных (в том числе персональных) Пользователей в Приложении, объясняет причины их сбора и способы их использования. В настоящей Политике конфиденциальности Разработчик предоставляет информацию о пределах сбора данных (в том числе персональных) Пользователей в Приложении, объясняет причины их сбора и способы их использования.
Актуальные версии Пользовательского соглашения и Политики конфиденциальности доступны внутри интерфейса Приложения. Информация, размещенная на Сайте, носит справочный характер. В случае расхождений между текстом на Сайте и в Приложении, юридическую силу имеет версия, размещенная в Приложении.
## 1. Термины и определения ## 1. Термины и определения
1.1. **Бот** -- аккаунт с именем @tgCrewBot и адресом [https://t.me/tgCrewBot](https://t.me/tgCrewBot) в мессенджере Telegram, управляемый программно Разработчиком через API (Application Programming Interface, программный интерфейс приложений). Бот предоставляет доступ к Приложению и может использоваться для базового взаимодействия из текстовой строки (inline-mode). 1.1. **Бот** -- аккаунт с именем @tgCrewBot и адресом [https://t.me/tgCrewBot](https://t.me/tgCrewBot) в мессенджере Telegram, управляемый программно Разработчиком через API (Application Programming Interface, программный интерфейс приложений). Бот предоставляет доступ к Приложению и может использоваться для базового взаимодействия из текстовой строки (inline-mode).
@@ -9,24 +10,24 @@
1.3. **Сайт** -- веб-сайт, расположенный по адресу: [https://tgcrew.ru](https://tgcrew.ru), на котором размещается информация о Приложении и его документация. 1.3. **Сайт** -- веб-сайт, расположенный по адресу: [https://tgcrew.ru](https://tgcrew.ru), на котором размещается информация о Приложении и его документация.
1.4. **Пользовательское соглашение** -- документ, актуальная (текущая) версия которого размещается на Сайте по ссылке [https://tgcrew.ru/terms-of-use](https://tgcrew.ru/terms-of-use), а также в Приложении. Если между текстом Пользовательского соглашения, размещенным на Сайте, и текстом, доступным в Приложении, возникают расхождения, то приоритет имеет текст, доступный в Приложении. Договорные отношения между Разработчиком и Пользователем возникают только после принятия Пользовательского соглашения внутри Приложения. 1.4. **Пользовательское соглашение** -- настоящий документ, актуальная (текущая) версия которого размещается в Приложении и на Сайте по ссылке [https://tgcrew.ru/terms-of-use](https://tgcrew.ru/terms-of-use). Договорные отношения между Разработчиком и Пользователем возникают только после принятия Пользовательского соглашения внутри Приложения.
1.5. **Политика конфиденциальности** -- настоящий документ, регулирующий порядок сбора, хранения и обработки данных (в том числе персональных) Пользователя, введенных им в ходе процедуры регистрации, использования Приложения, а также иных данных. Является неотъемлемым приложением к Пользовательскому соглашению. Актуальная (текущая) версия Политики конфиденциальности размещается на Сайте по ссылке [https://tgcrew.ru/privacy-policy](https://tgcrew.ru/privacy-policy), а также в Приложении. Если между информацией о Политике конфиденциальности, размещенной на Сайте, и информацией, доступной в Приложении, возникают расхождения, то приоритет имеет информация, доступная в Приложении. Согласие на обработку данных в рамках Политики конфиденциальности предоставляется Пользователем внутри Приложения. 1.5. **Политика конфиденциальности** -- документ, регулирующий порядок сбора, хранения и обработки данных (в том числе персональных) Пользователя, введенных им в ходе процедуры регистрации, использования Приложения, а также иных данных. Является неотъемлемым приложением к Пользовательскому соглашению. Актуальная (текущая) версия Политики конфиденциальности размещается в Приложении и на Сайте по ссылке [https://tgcrew.ru/privacy-policy](https://tgcrew.ru/privacy-policy). Согласие на обработку данных в рамках Политики конфиденциальности предоставляется Пользователем внутри Приложения.
1.6. **Разработчик** -- индивидуальный предприниматель Мартышкин Алексей Александрович (ИП Мартышкин А.А.), ОГРНИП 318774600262084, ИНН 366316608346 (Российская Федерация). 1.6. **Разработчик** -- индивидуальный предприниматель Мартышкин Алексей Александрович (ИП Мартышкин А.А.), ОГРНИП 318774600262084, ИНН 366316608346 (Российская Федерация).
1.7. **Пользователь** -- физическое лицо, индивидуальный предприниматель, юридическое лицо или их уполномоченный представитель, принявшее условия Пользовательского соглашения и Политики конфиденциальности путем их явного подтверждения внутри интерфейса Приложения. 1.7. **Пользователь** -- физическое лицо, индивидуальный предприниматель, юридическое лицо или их уполномоченный представитель, принявшее условия Пользовательского соглашения и Политики конфиденциальности путем их явного подтверждения внутри интерфейса Приложения.
1.8. **Администратор** -- Пользователь Приложения, который осуществляет его управление Приложением с помощью Панели администратора. 1.8. **Администратор** -- Пользователь Приложения, который осуществляет управление Приложением с помощью Панели администратора.
1.9. **Учетная запись Администратора** - учетная запись, с помощью которой Администратор осуществляет аутентификацию для доступа к Панели администратора. 1.9. **Учетная запись Администратора** -- учетная запись, с помощью которой Администратор осуществляет аутентификацию для доступа к Панели администратора.
1.10. **Участник чата** -- участник Подключенного чата, который может не являться Пользователем Приложения. 1.10. **Участник чата** -- участник Подключенного чата, который может не являться Пользователем Приложения.
1.11. **Панель администратора** -- интерфейс Приложения для управления Приложением, в том числе осуществления следующих функций: 1.11. **Панель администратора** -- интерфейс Приложения для управления Приложением, в том числе осуществления следующих функций:
- подключение чатов к Приложению (см. п. 1.12 Подключенный чат); - подключение чатов к Приложению (см. п. 1.12 Подключенный чат);
- внесение дополнительных информационных данных для Пользователей; - внесение дополнительных информационных данных для Пользователей;
- контроль и осуществление выплат вознаграждения Разработчику за использование Приложения. - контроль оплаты Лицензионного вознаграждения Разработчику за использование Приложения.
1.12. **Подключенный чат** (к Приложению) -- чат в Telegram, в который добавлен Бот с необходимыми правами доступа. В Приложении доступна информация только из Подключенных чатов. 1.12. **Подключенный чат** (к Приложению) -- чат в Telegram, в который добавлен Бот с необходимыми правами доступа. В Приложении доступна информация только из Подключенных чатов.
@@ -34,31 +35,31 @@
1.14. **Представители Разработчика** -- круг лиц, привлекаемых Разработчиком в рамках разработки и поддержки Приложения, действующие от имени и (или) по поручению Разработчика. 1.14. **Представители Разработчика** -- круг лиц, привлекаемых Разработчиком в рамках разработки и поддержки Приложения, действующие от имени и (или) по поручению Разработчика.
1.15. **Персональные данные (или ПДн)** -- любая информация, относящаяся к прямо или косвенно определенному или определяемому физическому лицу (субъекту персональных данных) (согласно N152-ФЗ, Глава 1, Статья 3, п. 1). 1.15. **Персональные данные (или ПДн)** -- любая информация, относящаяся к прямо или косвенно определенному или определяемому физическому лицу (субъекту персональных данных) (согласно 152-ФЗ, Глава 1, Статья 3, п. 1).
1.16. **Обработка персональных данных** -- любое действие (операция) или совокупность действий (операций), совершаемых с использованием средств автоматизации или без использования таких средств с персональными данными, включая сбор, запись, систематизацию, накопление, хранение, уточнение (обновление, изменение), извлечение, использование, передачу (распространение, предоставление, доступ), обезличивание, блокирование, удаление, уничтожение персональных данных (согласно N152-ФЗ, Глава 1, Статья 3, п. 3). 1.16. **Обработка персональных данных** -- любое действие (операция) или совокупность действий (операций), совершаемых с использованием средств автоматизации или без использования таких средств с персональными данными, включая сбор, запись, систематизацию, накопление, хранение, уточнение (обновление, изменение), извлечение, использование, передачу (распространение, предоставление, доступ), обезличивание, блокирование, удаление, уничтожение персональных данных (согласно 152-ФЗ, Глава 1, Статья 3, п. 3).
1.17. **Оператор** -- государственный орган, муниципальный орган, юридическое или физическое лицо, самостоятельно или совместно с другими лицами организующие и (или) осуществляющие обработку персональных данных, а также определяющие цели обработки персональных данных, состав персональных данных, подлежащих обработке, действия (операции), совершаемые с персональными данными (согласно N152-ФЗ, Глава 1, Статья 3, п. 2). 1.17. **Оператор** -- государственный орган, муниципальный орган, юридическое или физическое лицо, самостоятельно или совместно с другими лицами организующие и (или) осуществляющие обработку персональных данных, а также определяющие цели обработки персональных данных, состав персональных данных, подлежащих обработке, действия (операции), совершаемые с персональными данными (согласно 152-ФЗ, Глава 1, Статья 3, п. 2).
1.18. **Автоматизированная обработка персональных данных** -- обработка персональных данных с помощью средств вычислительной техники (согласно N152-ФЗ, Глава 1, Статья 3, п. 4). 1.18. **Автоматизированная обработка персональных данных** -- обработка персональных данных с помощью средств вычислительной техники (согласно 152-ФЗ, Глава 1, Статья 3, п. 4).
1.19. **Предоставление персональных данных** -- действия, направленные на раскрытие персональных данных определенному лицу или определенному кругу лиц (согласно N152-ФЗ, Глава 1, Статья 3, п. 6). 1.19. **Предоставление персональных данных** -- действия, направленные на раскрытие персональных данных определенному лицу или определенному кругу лиц (согласно 152-ФЗ, Глава 1, Статья 3, п. 6).
1.20. **Трансграничная передача персональных данных** -- передача персональных данных на территорию иностранного государства органу власти иностранного государства, иностранному физическому лицу или иностранному юридическому лицу (согласно N152-ФЗ, Глава 1, Статья 3, п. 11). 1.20. **Трансграничная передача персональных данных** -- передача персональных данных на территорию иностранного государства органу власти иностранного государства, иностранному физическому лицу или иностранному юридическому лицу (согласно 152-ФЗ, Глава 1, Статья 3, п. 11).
1.21. **Уничтожение персональных данных** -- действия, в результате которых становится невозможным восстановить содержание персональных данных в информационной системе персональных данных и (или) в результате которых уничтожаются материальные носители персональных данных (согласно N152-ФЗ, Глава 1, Статья 3, п. 8). 1.21. **Уничтожение персональных данных** -- действия, в результате которых становится невозможным восстановить содержание персональных данных в информационной системе персональных данных и (или) в результате которых уничтожаются материальные носители персональных данных (согласно 152-ФЗ, Глава 1, Статья 3, п. 8).
## 2. Общие положения ## 2. Общие положения
2.1. Настоящая Политика конфиденциальности регулирует исключительно отношения между Разработчиком и Пользователем. Она не может регулировать отношения между мессенджером Telegram (далее - Telegram) и его пользователями и не заменяет Политику конфиденциальности Telegram. 2.1. Настоящая Политика конфиденциальности регулирует исключительно отношения между Разработчиком и Пользователем. Она не может регулировать отношения между мессенджером Telegram (далее - Telegram) и его пользователями и не заменяет Политику конфиденциальности Telegram.
2.2. Настоящая Политика конфиденциальности составлена согласно требованиям: 2.2. Настоящая Политика конфиденциальности составлена согласно требованиям:
- Федерального закона от 27.07.2006. № 152-ФЗ «О персональных данных». - Федерального закона от 27.07.2006 № 152-ФЗ «О персональных данных».
- Требованиям раздела «4. Privacy» документа «Telegram Bot Platform Developer Terms of Service» от компании Telegram FZ-LLC (разработчик мессенджера Telegram). - Требованиям раздела «4. Privacy» документа «Telegram Bot Platform Developer Terms of Service» от компании Telegram FZ-LLC (разработчик мессенджера Telegram).
2.3. Разработчик выступает в роли Оператора при Обработке персональных данных Пользователей Приложения. 2.3. Разработчик выступает в роли Оператора при Обработке персональных данных Пользователей Приложения.
## 3. Прием условий настоящей Политики конфиденциальности ## 3. Прием условий настоящей Политики конфиденциальности
3.1. Свидетельством полного и безоговорочного принятия условий настоящей Политики конфиденциальности (акцептом), является осуществление процедуры принятия Политики конфиденциальности путем проставления символа «V» (галочки) в чек-боксе интерфейса Приложения рядом с текстом: «Я даю Согласие на обработку своих персональных данных и принимаю условия Политики конфиденциальности» на соответствующих экранах в Приложении. 3.1. Свидетельством полного и безоговорочного принятия условий настоящей Политики конфиденциальности (акцептом) является осуществление процедуры принятия Политики конфиденциальности путем активации (проставления отметки) в соответствующем интерактивном элементе (поле для отметки) интерфейса Приложения рядом с текстом: «Я даю Согласие на обработку своих персональных данных и принимаю условия Политики конфиденциальности» на соответствующих экранах в Приложении.
3.2. Использование Приложения означает согласие Пользователя на сбор и обработку информации из Подключенных чатов в соответствии с Политикой конфиденциальности (см. пп. 4.2-4.4). 3.2. Использование Приложения означает согласие Пользователя на сбор и обработку информации из Подключенных чатов в соответствии с Политикой конфиденциальности (см. пп. 4.2-4.4).
@@ -71,7 +72,7 @@
4.2.1. Приложение в автоматическом режиме собирает следующие данные Пользователей, которые могут быть определены как Персональные данные: 4.2.1. Приложение в автоматическом режиме собирает следующие данные Пользователей, которые могут быть определены как Персональные данные:
а) Данные предоставляемые Telegram через API: имя пользователя (Name, вкл. в себя First name и Last name), псевдоним (Username), идентификатор пользователя (ID), изображение профиля (аватар), языковые настройки клиента Telegram, данные транзакций Telegram Stars (только для Администратора). а) Данные предоставляемые Telegram через API: имя пользователя (Name, включающее в себя first_name и last_name), псевдоним (Username), идентификатор пользователя (ID), идентификатор изображения профиля (используется для отображения аватара без сохранения копий файлов на серверах Разработчика), языковые настройки клиента Telegram, а также данные транзакций Telegram Stars (только для Администратора).
б) Данные о взаимодействии Пользователя с серверами Приложения (технические данные): IP-адрес с которого осуществляется запрос, дата и время получения запроса сервером Приложения, запрошенный URL, HTTP-код ответа сервера Приложения, размер ответа в байтах, User-Agent (информация о браузере и операционной системе клиента), Referrer (URL страницы, с которой был сделан запрос, если доступно). б) Данные о взаимодействии Пользователя с серверами Приложения (технические данные): IP-адрес с которого осуществляется запрос, дата и время получения запроса сервером Приложения, запрошенный URL, HTTP-код ответа сервера Приложения, размер ответа в байтах, User-Agent (информация о браузере и операционной системе клиента), Referrer (URL страницы, с которой был сделан запрос, если доступно).
4.2.2. Администратор предоставляет: 4.2.2. Администратор предоставляет:
@@ -92,33 +93,45 @@
- адрес электронной почты; - адрес электронной почты;
- сведения о месте работы (наименование организации, структурное подразделение, должность и (или) функциональная роль). - сведения о месте работы (наименование организации, структурное подразделение, должность и (или) функциональная роль).
4.3. Приложение при создании и поддержания http-соединения использует HttpOnly cookie-файлы, которые не доступны из JavaScript через свойства Document.cookie API и используются для снижения риска XSS (Cross-Site Scripting - «межсайтовый скриптинг»). Другие cookie-файлы не устанавливаются и не используются. 4.3. Приложение при создании и поддержании http-соединения использует HttpOnly cookie-файлы, которые недоступны из JavaScript через свойства Document.cookie API и используются для снижения риска XSS (Cross-Site Scripting - «межсайтовый скриптинг»). Другие cookie-файлы не устанавливаются и не используются.
4.4. Приложение с помощью Бота отслеживает события в Подключенных чатах (такие как добавление/изменение/удаление сообщений, добавление/удаление файлов и т.п.). При необходимости для реализации функционала Приложения эти изменения фиксируются в базе данных Приложения. 4.4. Приложение с помощью Бота отслеживает события в Подключенных чатах (такие как добавление/изменение/удаление сообщений, добавление/удаление файлов и т.п.), но только при условии - Боту предоставлены права Администратора на доступ к сообщениям в Подключенных чатах.
4.5. Приложение хранит данные, которые Пользователи вводят в Приложении, в том числе: 4.5. Приложение хранит данные, которые Пользователи вводят в Приложении, в том числе:
- данные проектов (имя, описание, логотип), - данные проектов (имя, описание, логотип и т.п.),
- данные компаний (название, адрес, контактный телефон, веб-сайт и т.п.), - данные компаний (название, адрес, контактный телефон, веб-сайт и т.п.),
- данные задач и совещаний (описание, дата и время и т.п.). - данные задач и совещаний (описание, дата и время и т.п.).
4.6. Разработчик не хранит на своих серверах файлы из чатов, задач и совещаний (хранение осуществляется внутри Telegram). 4.6. Разработчик не хранит и не анализирует на собственных вычислительных мощностях (серверах) переписку из Подключенных чатов.
4.7. Приложение не собирает на стороне Пользователя никакую дополнительную информацию, которая может быть определена как Персональные данные, кроме той, что указана в п. 4.2, а также не осуществляет анализ информации, которая доступна ему в чатах. 4.7. Разработчик не хранит на собственных вычислительных мощностях (серверах) файлы из чатов, задач и совещаний. Хранение осуществляется внутри Telegram либо на подключаемых Администратором сторонних сервисах облачного хранения (Яндекс Диск или Google Drive).
Ответственность за выбор стороннего сервиса хранения, соблюдение его условий использования и политики конфиденциальности, а также за наличие правовых оснований для передачи данных в такие сервисы, целиком лежит на Администраторе. Разработчик не является оператором хранения в отношении указанных сторонних сервисов и не несет ответственности за их доступность, сохранность данных или инциденты информационной безопасности на стороне провайдеров облачных услуг.
При подключении Администратором сторонних сервисов хранения (Яндекс Диск или Google Drive), взаимодействие Приложения с указанными сервисами осуществляется исключительно через API соответствующих сервисов. Разработчик не запрашивает, не собирает и не хранит авторизационные данные этих сервисов. Авторизация в таких сервисах происходит на стороне соответствующих провайдеров.
## 5. Цели сбора данных 4.8. Приложение не собирает на стороне Пользователя никакую дополнительную информацию, которая может быть определена как Персональные данные, кроме той, что указана в п. 4.2, а также не осуществляет анализ информации, которая доступна ему в чатах.
## 5. Цели и сроки обработки данных
5.1. Сбор данных осуществляется со следующими целями: 5.1. Сбор данных осуществляется со следующими целями:
- Идентификация и аутентификация Пользователей в Приложении (пп. 4.2.1а и 4.2.2). - Идентификация и аутентификация Пользователей в Приложении (пп. 4.2.1.а и 4.2.2).
- Предоставление технической поддержки и обработка обращений (пп. 4.2.1, 4.2.2 и 4.2.4). - Предоставление технической поддержки и обработка обращений (пп. 4.2.1, 4.2.2 и 4.2.4).
- Отправка системных уведомлений Администратору (пп. 4.2.1а и 4.2.2). - Отправка системных уведомлений Администратору (пп. 4.2.1.а и 4.2.2).
- Обработка платежных транзакций через Telegram Stars (пп. 4.2.1а). - Учет и подтверждение платежных транзакций через Telegram Stars (пп. 4.2.1.а).
- Формирование адресной книги контактов в рамках Подключенных чатов (пп. 4.2.1а и 4.2.3). - Формирование адресной книги контактов в рамках Подключенных чатов (пп. 4.2.1.а и 4.2.3).
- Обеспечение корректной работы функционала Приложения (пп. 4.2 и 4.4). - Обеспечение корректной работы функционала Приложения (пп. 4.2 и 4.4).
- Технические: направленные на профилактику технических сбоев и обеспечение безопасности (пп. 4.2.1б и 4.3). - Осуществление технических мер, направленных на профилактику технических сбоев, выявление ошибок и обеспечение информационной безопасности Приложения (пп. 4.2.1.б и 4.3).
5.2. Разработчик обязуется собирать и использовать данные Пользователя только для вышеуказанных целей, за исключением случаев, когда сбор и использование данных необходим для других целей, совместимых с первоначальной целью сбора данных, или когда это предусмотрено законодательством Российской Федерации. Если Разработчику потребуется использовать данные Пользователя для других целей, не упомянутых выше, Разработчик обязан уведомить Пользователя и разъяснить правовые основания для такого сбора. 5.2. Разработчик обязуется собирать и использовать данные Пользователя только для вышеуказанных целей, за исключением случаев, когда сбор и использование данных необходим для других целей, совместимых с первоначальной целью сбора данных, или когда это предусмотрено законодательством Российской Федерации. Если Разработчику потребуется использовать данные Пользователя для других целей, не упомянутых выше, Разработчик вправе уведомить Пользователя путем обновления настоящей Политики конфиденциальности.
5.3. Обработка персональных данных Пользователей осуществляется Разработчиком с использованием средств автоматизации. 5.3. Обработка персональных данных Пользователей осуществляется Разработчиком с использованием средств автоматизации.
5.4. Сроки обработки и хранения данных. Разработчик осуществляет обработку Персональных данных в течение срока действия Пользовательского соглашения.
5.5. Разработчик вправе продолжить хранение данных после прекращения действия Пользовательского соглашения или отзыва согласия в следующих случаях:
- В течение 5 (пяти) лет в отношении сведений о транзакциях (согласно налоговому законодательству РФ).
- В течение 3 (трех) лет (общий срок исковой давности) для обеспечения возможности защиты интересов Разработчика в суде.
5.6. В силу технических особенностей, данные могут сохраняться в зашифрованных резервных копиях систем до 6 (шести) месяцев после их фактического удаления из активных баз данных. Такие копии используются только для аварийного восстановления.
## 6. Защита данных ## 6. Защита данных
6.1. Разработчик внедрил ряд технических, организационных и административных мер для обеспечения конфиденциальности, целостности, доступности и неприкосновенности и защиты Ваших данных (в том числе ПДн) от потери, кражи, несанкционированного доступа, неправомерного использования, изменения или уничтожения. 6.1. Разработчик внедрил ряд технических, организационных и административных мер для обеспечения конфиденциальности, целостности, доступности и неприкосновенности и защиты Ваших данных (в том числе ПДн) от потери, кражи, несанкционированного доступа, неправомерного использования, изменения или уничтожения.
@@ -127,17 +140,19 @@
6.3. Только уполномоченный персонал Разработчика имеет доступ к Персональным данным Пользователей, и этот персонал обязан относиться к Персональным данным как к конфиденциальным. Представители Разработчика не получают доступа к данным Пользователей (в том числе персональным). Меры безопасности могут периодически пересматриваться в соответствии с юридическими и техническими изменениями. 6.3. Только уполномоченный персонал Разработчика имеет доступ к Персональным данным Пользователей, и этот персонал обязан относиться к Персональным данным как к конфиденциальным. Представители Разработчика не получают доступа к данным Пользователей (в том числе персональным). Меры безопасности могут периодически пересматриваться в соответствии с юридическими и техническими изменениями.
## 7. Передача данных ## 7. Передача данных
7.1. Разработчик не осуществляет передачу данных (в том числе ПДн), полученных от Пользователей, третьим лицам, кроме случаев предоставления данных судам и(или) государственным органам и(или) правоохранительным органам в случаях, когда это требуется законами и нормативными актами. 7.1. Разработчик не осуществляет передачу данных, полученных от Пользователей (включая технические метрики, данные проектов и иную информацию), третьим сторонам для маркетинговых, рекламных или иных коммерческих целей. Передача данных осуществляется исключительно в объеме, необходимом для обеспечения функциональности Приложения: мессенджеру Telegram и сервисам облачного хранения (согласно п.4.7), инициируемым Администратором.
7.2. Разработчик гарантирует Пользователю, что не будет продавать, обменивать или передавать ваши данные (в том числе ПДн) третьим лицам без вашего явного согласия. 7.2. Разработчик не осуществляет передачу персональных данных, полученных от Пользователей, третьим лицам, кроме случаев предоставления данных судам и(или) государственным органам и(или) правоохранительным органам в случаях, когда это требуется законами и нормативными актами Российской Федерации.
7.3. Все сервера Приложения находятся на территории Российской Федерации. Трансграничная передача персональных данных не осуществляется. 7.3. Разработчик гарантирует Пользователю, что не будет продавать, обменивать или передавать ваши данные (в том числе ПДн) третьим лицам без вашего явного согласия.
7.4. Все сервера Приложения находятся на территории Российской Федерации. Трансграничная передача персональных данных не осуществляется.
## 8. Права пользователей ## 8. Права пользователей
8.1. Основные права Пользователя как субъекта Персональных данных включают: 8.1. Основные права Пользователя как субъекта Персональных данных включают:
- **Право на доступ к Персональным данным.** Пользователь может запросить у Разработчика предоставление ему копии своих Персональных данных, к которым у Разработчика есть доступ. Разработчик предоставляет такую информацию Пользователю в разумные сроки (не более 30 календарных дней). - **Право на доступ к Персональным данным.** Пользователь может запросить у Разработчика предоставление ему копии своих Персональных данных, к которым у Разработчика есть доступ. Разработчик предоставляет информацию Пользователю в течение 10 (десяти) рабочих дней с момента получения запроса. Этот срок может быть продлен не более чем на 5 (пять) рабочих дней в случае направления Разработчиком в адрес Пользователя мотивированного уведомления о причинах продления.
- **Право на исправление Персональных данных.** Пользователь может потребовать от Разработчика исправить или обновить любые свои Персональные данные. Пользователь может сделать это с помощью соответствующей функциональности Приложения, запросив Администратора или связавшись с нами напрямую. Разработчик оставляет за собой право отказывать в персональной помощи в случаях, когда исправление Персональных данных доступно через функциональность Приложения или с помощью Администратора. - **Право на исправление Персональных данных.** Пользователь может потребовать от Разработчика исправить или обновить любые свои Персональные данные. Пользователь может сделать это с помощью соответствующей функциональности Приложения, запросив Администратора или связавшись с Разработчиком напрямую. Разработчик оставляет за собой право отказывать в персональной помощи в случаях, когда исправление Персональных данных доступно через функциональность Приложения или с помощью Администратора.
- **Право на удаление Персональных данных.** Пользователь может потребовать от Разработчика удалить свои Персональные данные, с учетом применимого законодательства. В некоторых случаях Приложение автоматически удалит Персональные данные при закрытии Учетной записи в Приложении. Если Пользователь закрывает свою Учетную запись, Разработчик не будет использовать его Персональные данные для каких-либо дальнейших целей, а также передавать их третьим лицам, за исключением случаев, предусмотренных законом. Разработчик не всегда сможет выполнить запрос Пользователя на удаление по конкретным юридическим причинам, о которых будет сообщено Пользователю при наличии соответствующих оснований (например, если обработка необходима для достижения целей, предусмотренных законом, для исполнения судебного акта, для осуществления прав и законных интересов Разработчика или третьих лиц). - **Право на удаление Персональных данных.** Пользователь может потребовать от Разработчика удалить свои Персональные данные, с учетом применимого законодательства. В некоторых случаях Приложение автоматически удалит Персональные данные при закрытии Учетной записи в Приложении. Если Пользователь закрывает свою Учетную запись, Разработчик не будет использовать его Персональные данные для каких-либо дальнейших целей, а также передавать их третьим лицам, за исключением случаев, предусмотренных законом. Разработчик может быть лишен возможности выполнить запрос Пользователя на удаление в силу юридических обязательств, о которых будет сообщено Пользователю при наличии соответствующих оснований (например, если обработка необходима для достижения целей, предусмотренных законом, для исполнения судебного акта, для осуществления прав и законных интересов Разработчика или третьих лиц).
- **Право на отзыв согласия.** В той мере, в какой обработка Персональных данных Пользователя основана исключительно на его согласии, Пользователь может отозвать свое согласие в любое время. Это не повлияет на правомерность любой обработки, которая была осуществлена до отзыва. Любые действия по обработке, не основанные на согласии Пользователя, останутся незатронутыми. - **Право на отзыв согласия.** В той мере, в какой обработка Персональных данных Пользователя основана исключительно на его согласии, Пользователь может отозвать свое согласие в любое время. Это не повлияет на правомерность любой обработки, которая была осуществлена до отзыва. Любые действия по обработке, не основанные на согласии Пользователя, останутся незатронутыми.
8.2. Ни одно из прав не является абсолютным, что означает, что они, как правило, должны быть взвешены против собственных юридических обязательств Разработчика, а также его законных интересов и интересов третьих лиц. Если будет принято решение отклонить запрос Пользователя, то Разработчик проинформирует его об этом вместе с причинами такого решения. 8.2. Ни одно из прав не является абсолютным, что означает, что они, как правило, должны быть взвешены против собственных юридических обязательств Разработчика, а также его законных интересов и интересов третьих лиц. Если будет принято решение отклонить запрос Пользователя, то Разработчик проинформирует его об этом вместе с причинами такого решения.
@@ -159,14 +174,14 @@
10.3. После публикации обновленной версии Политики конфиденциальности дальнейшее использование Приложения считается принятием условий обновленной Политики конфиденциальности. 10.3. После публикации обновленной версии Политики конфиденциальности дальнейшее использование Приложения считается принятием условий обновленной Политики конфиденциальности.
## 11. Прочие положения ## 11. Прочие положения
11.1. Если какие-либо положения настоящей Политики конфиденциальности остались неясными, Разработчик готов разъяснить ее положения. Для этого свяжитесь с Разработчиком по адресу электронной почты [a-mart@ya.ru](mailto:a-mart@ya.ru). 11.1. Если какие-либо положения настоящей Политики конфиденциальности остались неясными, Разработчик готов разъяснить ее положения. Для этого свяжитесь с Разработчиком по адресу электронной почты [support@tgcrew.ru](mailto:support@tgcrew.ru).
11.2. Пользователь может использовать контактные данные, приведенные в разделе 12, по любой причине, предусмотренной настоящей Политикой. 11.2. Пользователь может использовать контактные данные, приведенные в разделе 12, по любой причине, предусмотренной настоящей Политикой.
## 12. Контактная информация и реквизиты Разработчика ## 12. Контактная информация и реквизиты Разработчика
Индивидуальный предприниматель Мартышкин Алексей Александрович Индивидуальный предприниматель Мартышкин Алексей Александрович
Юридический адрес: 111394, Российская Федерация, г. Москва, ул. Перовская, д. 66, к.3, кв. 187 Юридический адрес: 111394, Российская Федерация, город Москва, улица Перовская, дом 66, корпус 3, квартира 187
ОГРНИП 318774600262084 ОГРНИП 318774600262084
ИНН 366316608346 ИНН 366316608346
Телефон: +7 (926)339-04-25 Телефон: +7 (916) 439-04-25
Электронная почта: [a-mart@ya.ru](mailto:a-mart@ya.ru) Электронная почта: [info@tgcrew.ru](mailto:info@tgcrew.ru)

View File

@@ -0,0 +1,75 @@
###### Version 1.05 dated April 15, 2026
This document is an English adaptation of «Положение о тарифных планах подписки» 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.
The text of this document is an integral part of the Terms of Use. All terms and definitions used in this Provision on subscription tariffs (hereinafter the "Provision") are adopted according to Section 1 of the Terms of Use.
## 1. General Principles
1.1. The License is provided under the terms of the Tariff selected by the Administrator.
1.2. All Tariffs provide identical functionality, except for restrictions on the number of supported (active) Connected Chats. Tariffs can be paid and/or free (not requiring payment).
1.3. Tariffs may be changed by the Developer no more than once every 30 (Thirty) calendar days. Changes in Tariffs do not apply to the already paid period of use of the Application. New Tariffs apply only to subsequent payment periods (access renewals).
1.4. The License is considered active until the expiration date specified in the Application interface. If no expiration date is specified, the License is considered to have the maximum duration (according to clause 1.6).
1.5. The term "day" in the Application refers to an interval of 24 hours.
1.6. The maximum allowable period from the current moment to the expiration date is 730 days (2 years).
1.7. Under "Subscription" in the Application is meant a combination of a selected Tariff and the number of days of the License.
## 2. Order of Access Provision
2.1. Upon first launch of the Application, the Administrator is automatically assigned a free Tariff.
2.2. To increase the number of supported Connected Chats, the Administrator must change the Tariff in the Admin Panel and pay the License Fee (for paid Tariffs).
2.3. The right to use the Application is considered granted to the Administrator for the period paid by them (or provided free of charge under the terms of the Tariff) from the moment the corresponding information about the License activation appears in the Application interface.
## 3. Financial Terms
3.1. The amount of the License Fee is determined in accordance with the Tariffs active at the time of payment, which are available in the Application interface.
3.2. All payments are made in the internal currency of the Telegram messenger — Telegram Stars.
3.3. Payment for the License (Subscription renewal) is carried out through the tools and interface provided by the Telegram messenger. The Developer does not collect or store the User's bank card data or other payment information, as the payment process is entirely on the side of Telegram.
3.4. Changing the Tariff and Subscription
3.4.1. The Administrator can change the current Tariff to another one at any time.
3.4.2. Changing the Tariff with an increase in the number of Connected Chats (Upgrade) requires payment for at least 1 (One) day of the License under the new Tariff.
3.4.3. When performing an Upgrade, the remaining unused days of the previous Tariff are converted into additional days of the new Tariff according to the cost ratio of the Tariffs.
3.4.4. Changing the Tariff with a decrease in the number of Connected Chats (Downgrade) is possible only if the number of currently active Connected Chats does not exceed the limit of the new Tariff.
3.4.5. When performing a Downgrade, the current number of days of the License is recalculated towards an increase according to the cost ratio of the Tariffs.
3.4.6. Conversion and recalculation of the number of days occur automatically in the Application at the time of changing the Tariff.
3.4.7. The cost of one day under the Tariff is calculated by dividing the cost of the Tariff for 30 days by 30.
3.4.8. When calculating the cost of one day, rounding is performed to the second decimal place (0.01 Telegram Stars) according to mathematical rules.
3.4.9. The final number of License days after recalculation is always rounded down to the nearest whole integer.
3.4.10. Fractional residues of Telegram Stars arising during calculations and rounding (clause 3.4.9) are not returned to the Administrator and are recognized as the Developer's expenses for processing the operation.
3.4.11. Transition to a free Tariff is possible at any time, provided that the number of active Connected Chats corresponds to the limits of the free Tariff. In this case, the remaining paid days of the License are canceled without automatic refund (see section 3.5).
3.5. Refund Policy
3.5.1. The License Fee for the granted right to use the Application is not subject to refund after the start of the period of use of the Application.
3.5.2. Refund of the License Fee is made for full unused days of the License in the following cases:
- upon unilateral refusal of the Licensee to use the Application;
- upon transition to a free Tariff (clause 3.4.11);
- upon the Licensor's decision to terminate support for the Application or its liquidation.
3.5.3. In the event of termination of the Application's operation at the initiative of the Licensor, the Licensor shall notify the Licensee at least 30 (Thirty) calendar days in advance via the Application interface or by email.
3.5.4. The refund amount is calculated based on the monetary equivalent of the unused balance on the date of processing the request by the support service according to the rules set forth in clauses 3.4.7-3.4.9 of this Provision.
3.5.5. To carry out a refund, the Licensee sends the Licensor an official application (a scanned copy on the organization's letterhead with signature and seal) to the email address [support@tgcrew.ru](mailto:support@tgcrew.ru) with the mandatory indication of the technical identifier of the Administrator's account in the Application.
3.5.6. The refund of funds is carried out by the Licensor within 10 (Ten) working days from the moment of confirmation of receipt of the application using the Licensee's details from which the payment was made.

View File

@@ -0,0 +1,177 @@
###### Версия 1.05 от 15.04.2026
Текст настоящего документа является неотъемлемой частью Пользовательского соглашения. Все термины и определения, используемые в данном Положении о Тарифных планах (далее «Положение»), принимаются согласно разделу 1 Пользовательского соглашения.
## 1. Общие принципы
1.1. Лицензия предоставляется на условиях выбранного Администратором Тарифа.
1.2. Все Тарифы обеспечивают идентичный функционал, за исключением ограничений на количество поддерживаемых (активных) Подключенных чатов. Тарифы могут быть платными и/или бесплатными (не требующими оплаты).
1.3. Тарифы могут изменяться Разработчиком не чаще одного раза в 30 (Тридцать) календарных дней. Изменение Тарифов не распространяется на уже оплаченный период использования Приложения. Новые Тарифы применяются только к последующим периодам оплаты (продлениям доступа).
1.4. Лицензия считается активной до наступления даты окончания, указанной в интерфейсе Приложения. Если дата окончания не указана, то Лицензия считается максимального срока действия (согласно п. 1.6).
1.5. Под понятием «день» в Приложении принимается интервал длительностью 24 часа.
1.6. Максимально допустимый срок от текущего момента до даты окончания составляет 730 дней (2 года).
1.7. Под Подпиской в Приложении подразумевается комбинация выбранного Тарифа и срока действия Лицензии.
1.8. Разработчик вправе в одностороннем порядке продлить действие Подписки без взимания дополнительной платы (в рамках маркетинговых акций или по иным причинам).
1.9. Лицензия считается предоставленной и активной в течение всего оплаченного периода, вне зависимости от того, используют ли Участники чатов и Администратор Приложение или нет.
1.10. При использовании Приложения идентификация Администратора осуществляется по его техническому идентификатору, доступному в Приложении (см. раздел Настройки > Подписка). Технический идентификатор Администратора Приложения автоматически прикрепляется в Приложении к Telegram ID или адресу электронной почты (только один вариант).
1.11. По вопросам предположительно некорректных или ошибочных списаний Администратор должен незамедлительно сообщить Разработчику по электронной почте [support@tgcrew.ru](mailto:support@tgcrew.ru). Разработчик обязуется рассмотреть обращение в разумный срок, обычно не превышающий 10 (Десять) рабочих дней.
1.12. При прекращении действия подписки Разработчик вправе приостановить доступ к функционалу Приложения. Данные, созданные в период отсутствия активной подписки, могут не отображаться в Приложении до момента продления подписки.
1.13. Уведомление о необходимости продления Подписки (только для платных Тарифов) направляется Администратору через бота или по электронной почте (в зависимости от способа аутентификации) не позднее чем за 14 календарных дней до истечения её срока действия.
1.14. Все расчеты производятся с применением округлений. Погрешность до 3 (Трех) дополнительных дней признается Администратором и Разработчиком допустимым техническим допуском, претензии по которому не предъявляются.
## 2. Расчеты с физическими лицами
2.1. Способы и условия оплаты
2.1.1. Стоимость Лицензии определяется Тарифами, размещенными в интерфейсе Приложения. Разработчик вправе изменять стоимость Тарифов в одностороннем порядке, что не влияет на уже оплаченные и активные Подписки.
2.1.2. Физические лица осуществляют оплату Лицензии исключительно через интерфейс Приложения с использованием внутренней валюты (цифровые единицы) мессенджера Telegram (Telegram Stars).
2.1.3. Порядок приобретения и использования Telegram Stars регулируется правилами платформы Telegram. Разработчик не контролирует и не несет ответственности за работу платежной системы Telegram Stars. Разработчик не предоставляет услуги хранения средств. Telegram Stars, переведенные Разработчику, не являются банковским вкладом или иным финансовым инструментом и предназначены исключительно для оплаты вознаграждения Разработчику в соответствии с Тарифом.
2.2. Порядок активации
2.2.1. Лицензия активируется автоматически в момент выбора Подписки (Тарифа и срока действия) Администратором. По умолчанию, Приложение применяет Тариф без оплаты.
2.2.2. Техническим подтверждением активации является указание выбранного Тарифа и соответствующей даты окончания в качестве текущих в интерфейсе Приложения.
2.2.3. Активация Подписки на условиях платного Тарифа производится в момент подтверждения успешной транзакции от Telegram в сторону Разработчика с необходимым количеством Telegram Stars.
2.2.4. С момента активации (установки) Подписки обязательства Разработчика по предоставлению Лицензии считаются исполненными в полном объеме.
2.3. Продление срока действия
2.3.1. Если при совершении транзакции в Telegram Stars выбранный Тариф совпадает с текущим, то такое действие считается продлением Подписки. В противном случае действие расценивается как смена Подписки (согласно п. 2.4).
2.3.2. Продление осуществляется путем сдвига даты окончания текущего периода на количество дней, соответствующих транзакции. Неиспользованные дни текущего периода сохраняются и суммируются с новым периодом. Максимальный срок, на который может быть продлена Подписка, ограничен максимально допустимым сроком, установленным в п. 1.6.
2.3.3. При продлении Подписки применяется скидка, соответствующая выбранному периоду продления.
2.4. Смена Тарифа
2.4.1. Администратор Приложения может изменять Тариф не ранее чем через 1 (Один) день с момента предыдущего изменения Тарифа.
2.4.2. Переход на новый Тариф производится в автоматическом режиме (за исключением случая, указанного в п. 2.4.10). При переходе с бесплатного Тарифа активация платного Тарифа происходит в момент подтверждения транзакции от Telegram в сторону Разработчика с необходимым количеством Telegram Stars. Смена одного платного Тарифа на другой и пересчет срока действия происходят мгновенно в момент выбора в интерфейсе Приложения.
2.4.3. При смене Тарифа ограничения нового Тарифа (максимальное количество чатов) применяются немедленно.
2.4.4. Переход на Тариф с меньшим количеством чатов возможен, только если количество текущих (активных) Подключенных чатов в Приложении не превышает ограничения нового Тарифа.
2.4.5. При смене Тарифа эквивалент неиспользованного периода текущей Подписки засчитывается в счёт нового периода в виде дополнительных дней согласно правилам расчета, изложенным в пп.2.4.62.4.8 настоящего Положения.
2.4.6. Для платных Тарифов базовая стоимость дня для целей перерасчета принимается согласно формуле:
**Базовая стоимость дня = (Базовая стоимость Тарифа / 30) * (100% - Скидка за период)**
2.4.7. Расчет количества дополнительных дней производится по формуле:
**Количество дополнительных дней = (Базовая стоимость дня на текущем Тарифе * Количество неиспользованных дней) / Базовая стоимость дня на новом Тарифе**
2.4.8. Размер скидки при расчете Базовой стоимости дня принимается:
- на текущем Тарифе: скидка за последний оплаченный период,
- на новом Тарифе: скидка за выбранный период.
2.4.9. При выявлении признаков злоупотребления механизмом смены Тарифов (например, частых переключений между Тарифами) Разработчик оставляет за собой право на приостановку действия аккаунта Администратора, отмены результатов перерасчета, а в особых случаях и полной блокировки без возврата Telegram Stars (согласно пп. 8.12-8.13 Пользовательского соглашения).
2.4.10. Переход с платного на бесплатный Тариф осуществляется только через обращение в службу поддержки Приложения с последующим ручным перерасчетом и возвратом средств (см. п. 2.6).
2.5. Использование промокодов
2.5.1. Промокоды предоставляются на количество Telegram Stars, которые конвертируются в дополнительные дни согласно действующему Тарифу. Базовая стоимость дня учитывает скидку за период в оплаченной подписке (при ее наличии).
2.5.2. На бесплатных Тарифах промокоды недоступны.
2.6. Возврат средств
2.6.1. Возврат вознаграждения, уплаченного в виде Telegram Stars, после активации Лицензии не производится, за исключением случаев:
- расторжения Пользовательского соглашения по инициативе Администратора (полный отказ от использования Приложения);
- принятия Разработчиком решения о прекращении поддержки Приложения или его ликвидации.
2.6.2. При принятии решения о ликвидации или прекращении поддержки Приложения, Разработчик уведомляет об этом Администратора не менее чем за 30 (Тридцать) календарных дней через интерфейс Приложения, через бота или по электронной почте.
2.6.3. При необходимости возврата средств Администратор должен уведомить Разработчика по электронной почте [support@tgcrew.ru](mailto:support@tgcrew.ru) с указанием технического идентификатора учетной записи Администратора в Приложении.
2.6.4. Возврат средств осуществляется Разработчиком в течение 30 (Тридцати) календарных дней с момента подтверждения Разработчиком размера возврата и его возможности, учитывая технический функционал Telegram.
2.6.5. Возврат средств осуществляется за вычетом фактически понесенных Разработчиком расходов, связанных с исполнением настоящего Пользовательского соглашения и осуществлением возврата (включая комиссии платежных систем, транзакционные издержки и банковские комиссии). Стороны признают, что при сумме остатка менее 300 (Трехсот) Telegram Stars размер фактически понесенных расходов на обработку возврата превышает сумму самого возврата, в связи с чем возврат сумм менее указанного порога признается Сторонами экономически нецелесообразным и не производится.
## 3. Расчеты с юридическими лицами и индивидуальными предпринимателями
3.1. Способы и условия оплаты
3.1.1. Положения раздела 3 применимы только для резидентов РФ.
3.1.2. Оплата Лицензионного вознаграждения юридическими лицами и индивидуальными предпринимателями (далее — «Лицензиат») осуществляется в безналичном порядке в рублях РФ на основании выставляемого Разработчиком (далее «Лицензиар») счета. Данный счет является офертой, а его оплата Лицензиатом признается полным и безоговорочным акцептом (принятием) условий такой оферты и заключением Лицензионного договора на условиях настоящего Положения и Пользовательского соглашения (далее — «Счет-договор»). Цена в Счете-договоре имеет приоритет над Тарифами в Приложении или на Сайте. Лицензиар вправе указать Тариф в Счете-договоре, который не приведен в Тарифах в Приложении или на Сайте.
3.1.3. Выставление Счета-договора производится Лицензиаром по запросу Лицензиата по электронной почте [support@tgcrew.ru](mailto:support@tgcrew.ru).
3.1.4. Оплата Счета-договора подтверждает, что лицо, принявшее условия Пользовательского соглашения в интерфейсе Приложения и получившее технический идентификатор Администратора, обладает всеми необходимыми полномочиями для совершения данных действий и заключения договора от имени Лицензиата.
3.1.5. Обязательства Лицензиата по оплате считаются исполненными в момент зачисления денежных средств на расчетный счет Разработчика в полном объеме.
3.1.6. Владельцем прав на использование Приложения признается Лицензиат, совершивший оплату на счет с указанием технического идентификатора Администратора.
3.2. Порядок активации
3.2.1. Лицензия активируется Лицензиаром в течение 1 (Одного) рабочего дня (по московскому времени) с момента фактического зачисления денежных средств на расчетный счет Лицензиара.
3.2.2. Техническим подтверждением активации является указание Тарифа (согласно Счету-договору) и соответствующей даты окончания в качестве текущих в интерфейсе Приложения.
3.2.3. Лицензиар предоставляет Лицензиату документы, подтверждающие передачу прав (акты приема-передачи прав или универсальные передаточные документы), в электронном виде (через системы электронного документооборота или путем направления скан-копии на электронную почту Лицензиата). По запросу Лицензиата Лицензиар направляет оригиналы документов на бумажном носителе посредством почтового отправления по адресу, указанному Лицензиатом.
3.2.4. Акт считается подписанным Лицензиатом (права считаются переданными в полном объеме и надлежащего качества), если в течение 5 (Пяти) рабочих дней с момента направления Лицензиаром электронной версии документа Лицензиату не поступило мотивированных письменных возражений.
3.2.5. С момента активации Лицензии обязательства Лицензиара по её предоставлению на оплаченный период считаются исполненными в полном объеме.
3.3. Продление срока действия
3.3.1. Если в Счете-договоре выбранный Тариф совпадает с текущим активным Тарифом Лицензиата, то такая оплата считается продлением Лицензии. В противном случае действие расценивается как смена Тарифа (согласно п. 3.4).
3.3.2. Продление осуществляется путем сдвига даты окончания текущего периода на количество дней, оплаченных по Счету-договору. Неиспользованные дни текущего периода сохраняются и суммируются с новым периодом. Максимальный срок, на который может быть продлена Лицензия, ограничен сроком, установленным в п. 1.6.
3.3.3. При продлении Лицензии применяется скидка (при её наличии), соответствующая выбранному периоду продления и зафиксированная в Счете-договоре.
3.4. Смена Тарифа
3.4.1. Лицензиат (Администратор Приложения) вправе изменять Тариф самостоятельно в интерфейсе Приложения не ранее чем через 1 (Один) день с момента предыдущего изменения Тарифа. Также Лицензиат может обратиться по электронной почте [support@tgcrew.ru](mailto:support@tgcrew.ru) для смены Тарифа.
3.4.2. Стороны признают, что самостоятельная смена Тарифа Лицензиатом является изменением количественных характеристик Лицензии за счет пропорционального изменения срока её действия. Такая смена не влечет за собой изменения общей стоимости Лицензионного вознаграждения, зафиксированной в Счете-договоре, и не требует оформления дополнительных документов.
3.4.3. Переход на новый Тариф производится в автоматическом режиме (за исключением случая, указанного в п.3.4.11). При переходе с бесплатного Тарифа на платный активация Тарифа производится Лицензиаром в соответствии с п. 3.2.1 настоящего Положения. Смена одного платного Тарифа на другой и пересчет срока действия происходят мгновенно в момент выбора Лицензиатом нового Тарифа в интерфейсе Приложения.
3.4.4. При смене Тарифа ограничения нового Тарифа (максимальное количество чатов) применяются немедленно.
3.4.5. Переход на Тариф с меньшим количеством чатов возможен, только если количество текущих (активных) Подключенных чатов в Приложении не превышает ограничения нового Тарифа.
3.4.6. При смене Тарифа эквивалент неиспользованного периода текущей Подписки засчитывается в счёт нового периода в виде дополнительных дней согласно правилам расчета, изложенным в пп. 3.4.7-3.4.9 настоящего Положения.
3.4.7. Для платных Тарифов базовая стоимость дня для целей перерасчета принимается согласно формуле:
**Базовая стоимость дня = (Базовая стоимость Тарифа / 30) * (100% - Скидка за период)**
3.4.8. Расчет количества дополнительных дней производится по формуле:
**Количество дополнительных дней = (Базовая стоимость дня на текущем Тарифе * Количество неиспользованных дней) / Базовая стоимость дня на новом Тарифе**
3.4.9. Размер скидки при расчете Базовой стоимости дня принимается:
- на текущем Тарифе: скидка за последний оплаченный период,
- на новом Тарифе: скидка за выбранный период.
3.4.10. При выявлении признаков злоупотребления механизмом смены Тарифов (например, частых переключений между Тарифами) Лицензиар оставляет за собой право на приостановку действия аккаунта Администратора, отмены результатов перерасчета, а в особых случаях и полной блокировки без возврата средств (согласно пп. 8.12-8.13 Пользовательского соглашения).
3.4.11. Переход с платного на бесплатный Тариф осуществляется только через обращение в службу поддержки Приложения с последующим ручным перерасчетом и возвратом средств (см. п. 3.5).
3.5. Возврат средств
3.5.1. Лицензиат вправе в любое время в одностороннем порядке отказаться от использования Приложения.
3.5.2. Возврат Лицензионного вознаграждения производится за полные неиспользованные дни Лицензии в следующих случаях:
- при одностороннем отказе Лицензиата от использования Приложения;
- при переходе на бесплатный Тариф (п. 3.4.11);
- при принятии Лицензиаром решения о прекращении поддержки Приложения или его ликвидации.
3.5.3. В случае прекращения работы Приложения по инициативе Лицензиара, Лицензиар уведомляет об этом Лицензиата не менее чем за 30 (Тридцать) календарных дней через интерфейс Приложения или по электронной почте.
3.5.4. Сумма возврата рассчитывается исходя из денежного эквивалента неиспользованного остатка на дату обработки обращения службой поддержки по правилам, изложенным в пп. 3.4.7-3.4.9 настоящего Положения.
3.5.5. Для осуществления возврата Лицензиат направляет Лицензиару официальное заявление (скан-копию на бланке организации с подписью и печатью) на адрес электронной почты [support@tgcrew.ru](mailto:support@tgcrew.ru) с обязательным указанием технического идентификатора учетной записи Администратора в Приложении.
3.5.6. Возврат денежных средств осуществляется Лицензиаром в течение 10 (Десяти) рабочих дней с момента подтверждения получения заявления по реквизитам Лицензиата, с которых была произведена оплата.
3.5.7. В случае блокировки доступа к Приложению за нарушение условий Пользовательского соглашения или выявления признаков злоупотребления (согласно п. 3.4.10) возврат денежных средств не производится.

View File

@@ -1,153 +1,198 @@
*This document is an English adaptation of the Terms of Use 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.* ###### Version 1.05 dated April 15, 2026
# Terms of Use This document is an English adaptation of «Пользовательское соглашение» 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.
###### Version 1.01 dated 15.08.2025
This Terms of Use constitutes a Public Offer by Individual Entrepreneur Martyshkin A.A. (IE Martyshkin A.A.) addressed to any individual, individual entrepreneur, and/or legal entity under the terms specified herein. The text of this Terms of Use constitutes a public offer of IP Martynshkin A.A. and is addressed to any individual, individual entrepreneur, and/or legal entity under the conditions set forth herein.
Current versions of the Terms of Use and Privacy Policy are available within the Application interface. Information posted on the Website is for reference purposes only. In case of discrepancies between the text on the Website and in the Application, the version placed in the Application shall have legal force.
## 1. Terms and Definitions ## 1. Terms and Definitions
1.1. **Bot**An account named @tgCrewBot located at [https://t.me/tgCrewBot](https://t.me/tgCrewBot) within the Telegram messenger, programmatically managed by the Developer via API (Application Programming Interface). The Bot provides access to the Application and supports basic interaction via inline mode. 1.1. **Bot**an account named @tgCrewBot and located at [https://t.me/tgCrewBot](https://t.me/tgCrewBot) in the Telegram messenger, managed programmatically by the Developer via API (Application Programming Interface). The Bot provides access to the Application and can be used for basic interaction from the text string (inline-mode).
1.2. **Application**The tgCrew mini-application operating within the Telegram messenger as part of the Telegram Mini Apps (TMA) ecosystem, accessible via interaction with the Bot. While the Application utilizes Telegrams platform for operation and distribution, it is developed and provided solely by the Developer. The Application is not a product of Telegram Messenger LLP, is not endorsed, supported, or affiliated with it legally or organizationally. Functionality requires acceptance of this Terms of Use and the Privacy Policy. 1.2. **Application**the tgCrew mini-application operating within the Telegram messenger environment as part of the Telegram Mini Apps (TMA) ecosystem and accessible through interaction with the Bot. Although the Application uses the Telegram platform for its functioning and distribution, it is developed and provided exclusively by the Developer. The Application is not a product of the Telegram messenger development company, is not supported by it, is not approved by it, and is not associated with it legally or organizationally in any way. The functionality of the Application is available only after acceptance of the Terms of Use and the Privacy Policy.
1.3. **Website**The web platform located at: [https://tgcrew.ru](https://tgcrew.ru), hosting information and documentation about the Application. 1.3. **Website**the website located at [https://tgcrew.ru](https://tgcrew.ru), where information about the Application and its documentation is posted.
1.4. **Terms of Use**This document, the current version of which is published on the Website at [https://tgcrew.ru/terms-of-use](https://tgcrew.ru/terms-of-use) and within the Application. In case of discrepancies between the Website and Application versions, the Application version prevails. Contractual relations commence upon acceptance within the Application. 1.4. **Terms of Use**this document, the current version of which is posted in the Application and on the Website at [https://tgcrew.ru/terms-of-use](https://tgcrew.ru/terms-of-use). Contractual relations between the Developer and the User arise only after acceptance of the Terms of Use within the Application.
1.5. **Privacy Policy**The document governing the collection, storage, and processing of User data (including personal data) entered during registration, application use, or otherwise. It is an integral annex to this Terms of Use. The current version resides at [https://tgcrew.ru/privacy-policy](https://tgcrew.ru/privacy-policy) and within the Application. The Application version prevails in case of discrepancies. Consent under the Privacy Policy is granted within the Application. 1.5. **Privacy Policy**a document regulating the procedure for collecting, storing, and processing data (including personal data) of the User entered during the registration procedure, use of the Application, as well as other data. It is an integral annex to the Terms of Use. The current version of the Privacy Policy is posted in the Application and on the Website at [https://tgcrew.ru/privacy-policy](https://tgcrew.ru/privacy-policy). Consent for data processing within the framework of the Privacy Policy is provided by the User within the Application.
1.6. **Developer**Individual Entrepreneur Martyshkin Alexey Alexandrovich (IE Martyshkin A.A.), OGRNIP (Primary State Registration Number of Individual Entrepreneur) 318774600262084, TIN (Taxpayer Identification Number) 366316608346 (Russian Federation). 1.6. **Developer**individual entrepreneur Martynshkin Alexey Alexandrovich (IP Martynshkin A.A.), OGRNIP 318774600262084, INN 366316608346 (Russian Federation).
1.7. **User**An individual, individual entrepreneur, legal entity, or authorized representative accepting this Terms of Use and Privacy Policy via explicit confirmation within the Application interface. 1.7. **User**an individual, individual entrepreneur, legal entity, or their authorized representative who has accepted the terms of the Terms of Use and the Privacy Policy by their explicit confirmation within the Application interface.
1.8. **Administrator**A User managing the Application via the Admin Panel. 1.8. **Administrator**a User of the Application who manages the Application using the Admin Panel.
1.9. **Administrator Account**The account used by the Administrator to authenticate and access the Admin Panel. 1.9. **Administrator Account**the account through which the Administrator authenticates to access the Admin Panel.
1.10. **Chat Participant**A participant in a Connected Chat who may not be an Application User. 1.10. **Chat Participant**a participant of a Connected Chat who may not be a User of the Application.
1.11. **Admin Panel**The Application interface for management functions, including: 1.11. **Admin Panel**the Application interface for managing the Application, including the following functions:
- Connecting chats to the Application (see 1.12 Connected Chat); - connecting chats to the Application (see clause 1.12 Connected Chat);
- Adding supplementary User information; - entering additional information data for Users;
- Managing payments to the Developer. - controlling the payment of the License Fee to the Developer for using the Application.
1.12. **Connected Chat** — A Telegram chat where the Bot has been added with necessary permissions. The Application only processes data from Connected Chats. 1.12. **Connected Chat** (to the Application) — a chat in Telegram to which the Bot has been added with the necessary access rights. Information is available in the Application only from Connected Chats.
1.13. **Application Use**Actions performed by the User to view and/or utilize Application functionality via online interfaces rendered on the Users device. 1.13. **Use of the Application**the User performing actions to view and (or) use the available functionality of the Application through reproducible online interfaces on the screen of the User's device.
1.14. **Developers Representatives**Parties engaged by the Developer for development and support, acting on behalf of the Developer. 1.14. **Developer's Representatives**the circle of persons engaged by the Developer within the framework of development and support of the Application, acting on behalf of and (or) on the instructions of the Developer.
1.15. **Tariff Plan** — The Developers offering to the Administrator for Application use, including functionality and remuneration. Tariffs are described on the Website and in the Application. Tariffs may be modified by the Developer no more than once per 3 (three) months. The Application version prevails in case of discrepancies. 1.15. **Tariff** (or Tariff Plans) — an offer formed by the Developer for the Administrator regarding the use of the Application, which includes, among other things: functionality and the Developer's remuneration for using the Application. The description of the Tariff/Tariffs is available on the Website and in the Application. If discrepancies arise between the information about the Tariff/Tariffs posted on the Website and the information available in the Application, the information available in the Application shall prevail.
## 2. Subject Matter of the Terms of Use 1.16. **License** — a simple non-exclusive license for the right to use the Application under the terms of this Terms of Use.
2.1. This Terms of Use grants the User the right to use the Application under the terms herein.
2.2. Use within Telegram may require User account registration. 1.17. **License Fee** — the fee for granting the right to use the Application in accordance with the selected Tariff.
## 3. User Warranties and Representations ## 2. Subject of the Terms of Use
3.1. By accepting these Terms, the User warrants: 2.1. This Terms of Use is a license agreement. The Developer provides the User with the right to use the Application (a simple non-exclusive license) under the terms of the selected Tariff, and the User undertakes to comply with the terms of this Terms of Use.
3.1.1. They are at least 18 years old (or the age of majority in their jurisdiction) and capable of fulfilling obligations under these Terms.
3.1.2. Application use is permitted under their jurisdictions laws. If prohibited, use must cease immediately.
3.1.3. They have read and fully understand these Terms and the Privacy Policy.
3.1.4. Use will comply with applicable laws.
3.1.5. They will take reasonable steps to secure their Telegram account. Loss of Administrator Account access requires immediate notification to the Developer at [a-mart@ya.ru](mailto:a-mart@ya.ru) and cooperation to resolve the issue.
3.1.6. If warranties (3.1.1-3.1.5) are breached, the User must cease use immediately and notify the Developer. The Developer may suspend or terminate access.
3.2. Use is prohibited if warranties are not met. 2.2. To use the Application in the Telegram messenger (hereinafter - Telegram), the User may be required to register an account.
## 4. Acceptance of Offer ## 3. Warranties and Representations of the User
4.1. Full acceptance is evidenced by checking "✔ I accept the Terms of Use" within the Application interface. 3.1. By accepting this Terms of Use, the User warrants:
3.1.1. The User has reached the age of 18 (as well as the age of majority in their country of residence) and that there are no circumstances preventing the User from fully performing their obligations under this Terms of Use.
4.2. Use implies consent to data processing from Connected Chats per the Privacy Policy. 3.1.2. The legislation of the User's country of residence allows the use of the Application. In case of prohibition, the User is obliged to immediately stop using it.
4.3. Actions within the Application constitute legally binding conclusive acts. Logs from the Developers database serve as primary evidence of such actions in disputes. 3.1.3. The User has fully read the text of the Terms of Use and the Privacy Policy, understands their terms, and accepts them in full.
4.4. Non-acceptance requires immediate cessation of use. 3.1.4. The use of the Application will be carried out within the framework of applicable legislation.
## 5. Term 3.1.5. The User takes reasonable measures to maintain control over their Telegram account. In case of loss of access to the account used to manage the Application in the role of Administrator, the User is obliged to immediately notify the Developer by email at [support@tgcrew.ru](mailto:support@tgcrew.ru) and cooperate to resolve the situation.
5.1. Effective upon acceptance until obligations are fulfilled or termination occurs.
5.2. Access termination ends these Terms.
## 6. Application Description 3.1.6. Upon termination of compliance with the warranties (clauses 3.1.1-3.1.5), the User is obliged to immediately stop using the Application and notify the Developer. In this case, the Developer has the right to suspend or terminate the User's access to the Application (by blocking the Administrator Account).
6.1. Provided "AS IS" and "AS AVAILABLE". No warranties (fitness, availability, defect-free operation) are provided.
6.2. Allows Administrators via the Admin Panel to consolidate data from Telegram chats by adding the Bot.
6.3. Participants can track chat data (files, tasks, meetings) via the Application.
6.4. Users access data only from their participating Connected Chats.
6.5. Bot access requires explicit addition by a chat admin with appropriate rights.
## 7. Remuneration Procedure 3.2. The User is not entitled to use the Application if they do not comply with the warranties in clause 3.1.
7.1. Payments are made solely via the Admin Panel per the Tariff Plan.
7.2. Telegram Stars are the exclusive payment method.
7.3. Administrators transfer Stars to the Applications internal balance. Daily deductions occur per Tariff (~00:00 UTC+3).
7.4. Unused Stars refund:
- Only upon termination of use.
- Requires email notification to [a-mart@ya.ru](mailto:a-mart@ya.ru) with Administrator ID.
- Processed within 30 calendar days post-Developer confirmation.
- Not applicable if balance <100 Stars or technically infeasible.
7.5. Chat Participants use the Application free of charge.
7.6. Suspected incorrect deductions must be reported immediately to [a-mart@ya.ru](mailto:a-mart@ya.ru). Developer will investigate within 10 business days.
7.7. Developer bears no responsibility for Telegram Stars payment system operations.
7.8. Telegram Stars are not stored funds; solely for Developer remuneration.
7.9. Insufficient balance may suspend functionality until replenishment (notification via Bot/email).
## 8. Liability ## 4. Acceptance of the Offer
8.1. Developer may conduct maintenance, potentially causing downtime. Advance notice (24h+) for extended planned outages. 4.1. Evidence of full and unconditional acceptance of the terms of this Terms of Use (acceptance) is the performance of the procedure for accepting the Terms of Use by activating (checking) the corresponding interactive element (checkbox) of the Application interface next to the text: "I accept the Terms of Use" on the relevant screens in the Application.
8.2. Developer may modify/restrict functionality at any time (except Tariffs (1.15) and material Terms changes (13.1)).
8.3. Developer and Representatives bear no liability for losses (direct, indirect, consequential) arising from use. 4.2. Use of the Application signifies the User's consent to the collection and processing of information from Connected Chats in accordance with the Privacy Policy.
8.4. User is solely responsible for Telegram account security. Actions under their account are deemed theirs.
8.5. User Prohibitions: 4.3. All actions performed within the functionality of the Application by the User are recognized by the Developer and such User as conclusive actions giving rise to corresponding legal consequences. At the same time, the User agrees that the main evidence of the performance or non-performance of a certain action using the Application interfaces for the Developer can be the provision of an extract from its database about the presence of information fixed by software recording (logging) of the history of actions of such person or other similar method. In case of a dispute, such information is provided in the form chosen by the Developer and is recognized as exhaustive evidence of the actions specified in this clause.
- Reverse engineer, decompile, disassemble the Application.
- Create derivative works. 4.4. In the event that the User does not agree with the terms of the Terms of Use, they are obliged to immediately leave the Application and not start/cease the Use of the Application.
- Remove/hide Developers intellectual property notices.
8.6. No malware introduction, scraping, or unauthorized automated access. 4.5. The Parties recognize the use of the Application interface and the Telegram account (Telegram ID) as the use of a simple electronic signature. Actions performed through the Application are recognized as having legal force similar to documents on paper.
8.7. No unauthorized access attempts or DDoS attacks. Violations result in immediate termination and legal reporting.
8.8. No illegal, obscene, offensive, or rights-infringing use. ## 5. Term of the Terms of Use
8.9. User responsible for own IT setup. 5.1. The Terms of Use is valid from the moment of acceptance by the User until the full fulfillment of obligations by the Parties or the termination of this agreement.
8.10. Developer may suspend/terminate access for suspected Terms breaches or reputational harm.
8.11. Access may be terminated for Privacy Policy or legal violations. 5.2. Early termination of access to the Application entails the termination of the Terms of Use.
8.12. For terminations under 8.10-8.11, Developer may retain unused Stars as penalty/compensation if permitted by Russian law.
8.13. Developer not liable for content processed/displayed via the Application or User actions in chats. Users bear content legality responsibility. ## 6. Description of the Application
8.14. Developer may terminate the Application entirely, notifying Administrators 30+ days in advance. Unused Stars will be refunded per 7.4. 6.1. The User acknowledges and agrees that the functionality of the Application is provided "AS IS" and "AS AVAILABLE", without any warranties, including warranties regarding commercial value, fitness for specific purposes, constant availability, etc. The Developer does not guarantee error-free operation of the Application, compliance of its functionality with the User's expectations, or the absence of defects, viruses, or other harmful components.
6.2. The Application allows the Administrator, using the Admin Panel, to combine information from different Telegram chats by adding the Bot to these chats. After adding the Bot (with the necessary rights pre-configured in the Application) to the chat, the Bot gains access to all messages in that chat and consolidates the received information into the Application's database.
6.3. Participants of Connected Chats can, using the graphical interface of the Application, track information from these chats, such as: uploaded files, assigned tasks, and scheduled meetings, as well as see the list of connected chats.
6.4. Only information from those chats of which the User is a participant and which were connected to the Application by the Administrator is available to the User in the Application.
6.5. Access of the Bot to messages in the chat is carried out exclusively after its explicit addition to the chat by a participant possessing the necessary chat administrator rights in Telegram (the chat owner or an administrator with corresponding rights) and granting the Bot these chat administrator rights.
## 7. Payment Procedure
7.1. For the provision of the License, the Administrator pays the License Fee to the Developer. For Chat Participants, the use of the Application is free of charge.
7.2. Taxes and fees applicable to the remuneration are determined in accordance with the current legislation of the Russian Federation on the date of payment.
7.3. Remuneration is paid in the manner and on the terms according to the document "Provision on subscription tariffs", which is an integral part of this Terms of Use. The text of the document can be found on the website [https://tgcrew.ru](https://tgcrew.ru) or in the Application in the Admin Panel.
## 8. Liability of the Parties
8.1. The Developer has the right to carry out technical and other works aimed at improving and (or) changing the functionality of the Application. During the period of such works, the Application may be completely or partially unavailable. The Developer will, if possible, notify Users of lengthy scheduled works through the Application and (or) the Website at least 1 (One) business day (Moscow time) in advance.
8.2. The Developer reserves the right to change, modify, or introduce additional restrictions on the functionality of the Application at its discretion at any time without prior notice, except for changes to Tariffs (see the document "Provision on subscription tariffs") and material changes to the terms of this Terms of Use (clause 13.1).
8.3. The Developer and its Representatives are not liable for any losses (direct, indirect, lost profit) arising in connection with the use of the Application.
8.4. The User is independently responsible for the safety and confidentiality of their Telegram account data. All actions carried out using the User's account are considered to have been carried out by the User. The User is independently liable to third parties for all actions performed using the User's account. The Developer is not liable for unauthorized use of the User's account by third parties.
8.5. The User is prohibited from:
- decompiling, disassembling, performing reverse engineering, reconstructing the source code of the Application or attempting to obtain it by other means;
- creating derivative works based on the Application;
- removing, hiding, or changing notifications about the Developer's intellectual property in the Application.
8.6. The User must not abuse the Application by intentionally introducing viruses, trojans, worms, logic bombs, or other malicious or technologically dangerous material. The User must not use any kind of robots, spiders, automated information collection (page scraping), or other automatic devices, algorithms, methodologies, or similar manual processes to copy, monitor, obtain, or access any part of the Application, to attempt to obtain any information or materials by any means that are not specifically available through the Application.
8.7. The User must not attempt to gain unauthorized access to any part or function of the Application, computing devices (servers) where the Application is stored. The User must not attack the Application via a denial-of-service attack or a distributed denial-of-service attack. The Developer has the right to report any such violation to the relevant law enforcement authorities and will cooperate with these authorities by disclosing the User's identity to them. In the event of such a violation, the User's right to use the Application ceases immediately.
8.8. The User may not use the Application for any purposes prohibited by this Terms of Use, or which are illegal, indecent, or offensive, or to induce the commission of any activity that violates the rights of any third parties, or any illegal activity.
8.9. The User is responsible for configuring their information technologies, computer programs, and platform for access to the Application.
8.10. The User undertakes not to use the Application to perform actions that violate the laws of the Russian Federation, including but not limited to: distribution of materials containing public calls for terrorist activities or justifying terrorism, extremist materials, propaganda of narcotic drugs, psychotropic substances, as well as any other information for the distribution of which criminal or administrative liability is provided.
8.11. The Developer has the right to suspend or prematurely terminate the User's access to the Use of the Application if the Developer has reason to believe that the use of the Application is or will be carried out by the User in violation of this Terms of Use, or the User, at the Developer's discretion, performs actions that cause or may cause damage to the Developer's reputation or the security of the Application.
8.12. The Developer has the right to terminate access to the Application in case of violation of the Privacy Policy or legislation.
8.13. In case of violation by the User of the terms of this Terms of Use, the Developer has the right to unilaterally and extrajudicially suspend access to the Application or terminate this Terms of Use. In this case, the remuneration paid for the current period of the License (subscription) is not refundable. The Parties recognize that the specified amount is proportionate compensation for the expenses and losses actually incurred by the Developer related to processing the violation and early termination of service due to the User's fault.
8.14. The Developer is not responsible for the content (messages, files, data) processed, transmitted, or displayed through the Application, as well as for the actions of Users in connected chats. The Developer does not perform pre-moderation, content analysis, storage, or modification of information provided by the User, including the Administrator. Responsibility for the content of placed files, compliance with copyrights and legislation on information protection during their upload to the chat lies entirely with the User who uploaded the file and the Chat Administrator.
8.15. In the event of the Developer's decision to terminate support for the Application or its liquidation, the refund of the unused portion of the License Fee is carried out in the manner, terms, and subject to the restrictions (including the minimum refund amount threshold) established in the document "Provision on subscription tariffs".
8.16. When using the functionality of connecting third-party cloud storages (Yandex Disk or Google Drive), the Administrator understands and agrees that:
- The Developer does not control the performance, limits, and security policy of these services.
- Loss of access to the cloud storage, deletion of files by the service provider, or failures in the API of the third-party service are not within the Developer's area of responsibility.
- Any disputes related to access to files in third-party storages are resolved by the Administrator directly with the corresponding storage service provider.
- The Administrator independently bears the risks associated with cross-border data transfer or violation of the terms of use of third-party services.
- The Administrator is independently responsible for the safety of authorization data used for integration with third-party cloud storages.
## 9. Intellectual Property ## 9. Intellectual Property
9.1. Developer grants a limited, non-exclusive, non-transferable, revocable license to use the Application. All rights not expressly granted are reserved. License terminates automatically upon Terms termination. 9.1. Subject to the User's compliance with the Terms of Use, the Developer grants the User a limited, non-exclusive, non-sublicensable, revocable, non-transferable license to access the Application. Any rights not expressly granted in this section are reserved by the Developer. The license automatically terminates without notice upon termination of this Terms of Use.
9.2. Global intellectual property rights belong solely to the Developer. Rights are licensed, not sold.
9.2. All intellectual property rights in relation to the Application worldwide belong to the Developer, and rights to the Application are licensed (not sold) to the User. The User has no intellectual property rights in relation to the Application, except for the right to use them in accordance with this Terms of Use.
## 10. Force Majeure ## 10. Force Majeure
10.1. Parties exempt from liability for force majeure (natural disasters, war, government actions, telecom failures, etc.). 10.1. The Parties are released from liability upon the occurrence of force majeure circumstances (natural disasters, wars, decisions of state bodies, telecommunication failures, etc.).
10.2. Performance deadlines extended for the force majeure duration (max 30 calendar days).
10.3. Force majeure exceeding 30 days triggers negotiations for alternatives or termination without damages. 10.2. The period for fulfilling obligations is postponed for the duration of the force majeure, but not more than 30 (Thirty) calendar days.
10.3. In case of force majeure lasting more than 30 calendar days, the Parties shall conduct negotiations on alternative solutions or termination of the Terms of Use without compensation for losses.
## 11. Severability ## 11. Severability
11.1. Invalidity of any provision does not affect the validity of others. Invalid provisions shall be replaced by valid ones reflecting the original intent as closely as possible. 11.1. The User and the Developer agree that if any part of this Terms of Use or the Privacy Policy is found by a competent court to be invalid or unenforceable, in whole or in part, then only such part of this Terms of Use or the Privacy Policy that is declared invalid or unenforceable will be considered invalid in that jurisdiction, and only to the extent that it was found to be such, and this will not affect the validity or enforceability in any aspect and in any jurisdiction of other parts of this Terms of Use or the Privacy Policy, which remain in full force and effect. In this case, such invalid provisions are subject to replacement by provisions as close as possible in meaning to the original ones, which will be recognized as valid in the relevant jurisdiction, and are subject to application in modified form (including to already established legal relations).
11.2. Invalidity does not affect Sections 7 (Remuneration), 9 (IP), 12 (Governing Law), 15.4 (Indemnification), and 15.6 (No Waiver).
## 12. Governing Law and Dispute Resolution 11.2. The recognition of any provision as invalid or unenforceable does not affect the validity or enforceability of the provisions of sections 7 (Payment Procedure), 9 (Intellectual Property), 12 (Applicable Law and Dispute Resolution), 15.4 (Indemnification), and 15.6 (Inaction).
12.1. Governed by and construed under the laws of the Russian Federation. Unforeseen matters resolved under Russian law.
12.2. Disputes resolved via negotiation/email (mandatory pre-trial claim procedure). Unresolved disputes after 30 calendar days subject to litigation in Moscow, Russia.
## 13. Amendments ## 12. Applicable Law and Dispute Resolution
13.1. Developer may amend these Terms by publishing the revised version on the Website/Application. Changes effective the day after publication. User must review amendments regularly. 12.1. This Terms of Use is governed by and construed in accordance with the laws of the Russian Federation. Issues not regulated by the Terms of Use are subject to resolution in accordance with the laws of the Russian Federation.
13.2. User may reject amendments, implying cessation of use.
## 14. Assignment 12.2. All disputes of the Parties under this Terms of Use are subject to resolution through correspondence and negotiations using the mandatory pre-trial (claim) procedure. In case of impossibility to reach an agreement between the Parties through negotiations within 30 (Thirty) calendar days from the date of receipt by the other Party of a written claim sent by mail in accordance with clause 15.2 of this Terms of Use, the consideration of the dispute must be transferred by any interested Party to the court at the location of the Developer (Russian Federation, Moscow) in accordance with the rules of jurisdiction established by the GPC RF or APC RF.
14.1. Developer may assign rights/obligations without User consent. User may not assign without Developers prior written consent.
14.2. Developers assignment does not release it from pre-assignment liabilities unless otherwise agreed or required by law. ## 13. Procedure for Changing the Terms of Use
13.1. This Terms of Use (including additions) may be changed by the Developer with notification to the User by posting a new edition of the changed Terms of Use on the Website or in the Application. The changes made by the Developer to the Terms of Use enter into force on the day following the day of publication of such changes on the Website or in the Application (whichever comes first). The User undertakes to independently check this Terms of Use for any changes. Failure of the User to familiarize themselves cannot serve as a basis for non-fulfillment of their obligations and non-compliance with the restrictions established by this Terms of Use. The Developer recommends that Users regularly familiarize themselves with the current edition of the Terms of Use.
13.2. The User has the right to refuse to accept changes and additions to this Terms of Use, which means the User's refusal to use all previously granted rights.
## 14. Assignment of Rights (Claims)
14.1. The Developer may assign or transfer rights under this Terms of Use and (or) the Privacy Policy in whole or in part to any individual or legal entity at any time with or without the User's consent. The User is not entitled to assign or transfer any rights and obligations arising from the Terms of Use or the Privacy Policy without the prior written consent of the Developer, and any assignment or transfer of rights made by the User without such consent will be invalid.
14.2. Assignment by the Developer of rights under this Terms of Use to a third party does not release the Developer from liability to the User for the performance of obligations under this Terms of Use that arose before the moment of assignment, unless otherwise agreed with the User or provided by law.
## 15. Final Provisions ## 15. Final Provisions
15.1. Russian language version prevails over translations. 15.1. This Terms of Use is drawn up in the Russian language. In case of contradictions between the Russian version and any translations, the Russian version shall prevail.
15.2. Notices to Developer: [a-mart@ya.ru](mailto:a-mart@ya.ru) or methods specified in App/Website. Developer notices deemed received 1 day after publishing/sending.
15.3. Personal data processed per the Privacy Policy (integral annex).
15.4. User indemnifies Developer for losses (including legal costs) arising from Users breach of Terms, law, or third-party rights.
15.5. Entire Terms supersedes prior arrangements.
15.6. Developers inaction on breach does not waive future rights.
15.7. No agency, partnership, joint venture, or franchise relationship created.
15.8. Compatibility with all devices/OS/Telegram versions not guaranteed.
## 16. Developer Contact & Details 15.2. All notifications, requests, and claims in connection with this Terms of Use or the use of the Application are sent by the User to the Developer by email at [support@tgcrew.ru](mailto:support@tgcrew.ru) or by other means explicitly indicated by the Developer in the Application or on the Website. The Developer's notifications to the User are considered sent properly if they are posted in the Application and (or) on the Website and (or) sent to the contact email address (if any) associated with the Administrator's Telegram account and are considered received by the User on the next day after their placement/sending. It is also allowed to send notifications, requests, and claims on paper by mail to the Developer's address specified in section 16 of this Terms of Use.
**Individual Entrepreneur Martyshkin Alexey Alexandrovich**
Legal Address: 111394, Russian Federation, Moscow, Perovskaya St., 66, Bldg. 3, Apt. 187 15.3. The Developer processes personal data of Users exclusively in the manner and for the purposes defined by the Privacy Policy, which is an integral part of this Terms of Use.
15.4. The User undertakes to indemnify the Developer for any losses (including legal costs and reasonable expenses for legal services) incurred by the Developer in connection with the User's violation of this Terms of Use, legislation, or the rights of third parties when using the Application, including losses resulting from the User's violation of third-party rights (including intellectual property rights and privacy rights) when using the Application.
15.5. This Terms of Use constitutes the entire agreement between the User and the Developer regarding the use of the Application and supersedes all previous oral or written agreements and understandings between the Parties on this subject (if any).
15.6. Inaction on the part of the Developer in case of violation by the User of the provisions of this Terms of Use does not deprive the Developer of the right to take appropriate actions to protect its interests in the future, nor does it mean a waiver of its rights in case of subsequent similar or related violations.
15.7. This Terms of Use does not create agency, partnership, joint venture, or franchise relationships between the User and the Developer.
15.8. The Developer does not guarantee the compatibility of the Application with all devices, operating systems, and versions of the Telegram messenger.
## 16. Contact Information and Details of the Developer
Individual Entrepreneur Martynshkin Alexey Alexandrovich
Legal address: 111394, Russian Federation, Moscow, Perovskaya street, house 66, building 3, apartment 187
OGRNIP 318774600262084 OGRNIP 318774600262084
TIN 366316608346 INN 366316608346
Phone: +7 (926) 339-04-25 Phone: +7 (916) 439-04-25
Email: [a-mart@ya.ru](mailto:a-mart@ya.ru) Email: [info@tgcrew.ru](mailto:info@tgcrew.ru)

View File

@@ -1,8 +1,8 @@
# Пользовательское соглашение ###### Версия 1.05 от 15.04.2026
###### Версия 1.01 от 15.08.2025
Текст настоящего Пользовательского соглашения является публичной офертой ИП Мартышкин А.А. и адресован любому физическому лицу, индивидуальному предпринимателю и (или) юридическому лицу на изложенных в нём условиях. Текст настоящего Пользовательского соглашения является публичной офертой ИП Мартышкин А.А. и адресован любому физическому лицу, индивидуальному предпринимателю и (или) юридическому лицу на изложенных в нём условиях.
Актуальные версии Пользовательского соглашения и Политики конфиденциальности доступны внутри интерфейса Приложения. Информация, размещенная на Сайте, носит справочный характер. В случае расхождений между текстом на Сайте и в Приложении, юридическую силу имеет версия, размещенная в Приложении.
## 1. Термины и определения ## 1. Термины и определения
1.1. **Бот** -- аккаунт с именем @tgCrewBot и адресом [https://t.me/tgCrewBot](https://t.me/tgCrewBot) в мессенджере Telegram, управляемый программно Разработчиком через API (Application Programming Interface, программный интерфейс приложений). Бот предоставляет доступ к Приложению и может использоваться для базового взаимодействия из текстовой строки (inline-mode). 1.1. **Бот** -- аккаунт с именем @tgCrewBot и адресом [https://t.me/tgCrewBot](https://t.me/tgCrewBot) в мессенджере Telegram, управляемый программно Разработчиком через API (Application Programming Interface, программный интерфейс приложений). Бот предоставляет доступ к Приложению и может использоваться для базового взаимодействия из текстовой строки (inline-mode).
@@ -10,42 +10,44 @@
1.3. **Сайт** -- веб-сайт, расположенный по адресу: [https://tgcrew.ru](https://tgcrew.ru), на котором размещается информация о Приложении и его документация. 1.3. **Сайт** -- веб-сайт, расположенный по адресу: [https://tgcrew.ru](https://tgcrew.ru), на котором размещается информация о Приложении и его документация.
1.4. **Пользовательское соглашение** -- настоящий документ, актуальная (текущая) версия которого размещается на Сайте по ссылке [https://tgcrew.ru/terms-of-use](https://tgcrew.ru/terms-of-use), а также в Приложении. Если между текстом Пользовательского соглашения, размещенным на Сайте, и текстом, доступным в Приложении, возникают расхождения, то приоритет имеет текст, доступный в Приложении. Договорные отношения между Разработчиком и Пользователем возникают только после принятия Пользовательского соглашения внутри Приложения. 1.4. **Пользовательское соглашение** -- настоящий документ, актуальная (текущая) версия которого размещается в Приложении и на Сайте по ссылке [https://tgcrew.ru/terms-of-use](https://tgcrew.ru/terms-of-use). Договорные отношения между Разработчиком и Пользователем возникают только после принятия Пользовательского соглашения внутри Приложения.
1.5. **Политика конфиденциальности** -- документ, регулирующий порядок сбора, хранения и обработки данных (в том числе персональных) Пользователя, введенных им в ходе процедуры регистрации, использования Приложения, а также иных данных. Является неотъемлемым приложением к Пользовательскому соглашению. Актуальная (текущая) версия Политики конфиденциальности размещается на Сайте по ссылке [https://tgcrew.ru/privacy-policy](https://tgcrew.ru/privacy-policy), а также в Приложении. Если между информацией о Политике конфиденциальности, размещенной на Сайте, и информацией, доступной в Приложении, возникают расхождения, то приоритет имеет информация, доступная в Приложении. Согласие на обработку данных в рамках Политики конфиденциальности предоставляется Пользователем внутри Приложения. 1.5. **Политика конфиденциальности** -- документ, регулирующий порядок сбора, хранения и обработки данных (в том числе персональных) Пользователя, введенных им в ходе процедуры регистрации, использования Приложения, а также иных данных. Является неотъемлемым приложением к Пользовательскому соглашению. Актуальная (текущая) версия Политики конфиденциальности размещается в Приложении и на Сайте по ссылке [https://tgcrew.ru/privacy-policy](https://tgcrew.ru/privacy-policy). Согласие на обработку данных в рамках Политики конфиденциальности предоставляется Пользователем внутри Приложения.
1.6. **Разработчик** -- индивидуальный предприниматель Мартышкин Алексей Александрович (ИП Мартышкин А.А.), ОГРНИП 318774600262084, ИНН 366316608346 (Российская Федерация). 1.6. **Разработчик** -- индивидуальный предприниматель Мартышкин Алексей Александрович (ИП Мартышкин А.А.), ОГРНИП 318774600262084, ИНН 366316608346 (Российская Федерация).
1.7. **Пользователь** -- физическое лицо, индивидуальный предприниматель, юридическое лицо или их уполномоченный представитель, принявшее условия Пользовательского соглашения и Политики конфиденциальности путем их явного подтверждения внутри интерфейса Приложения. 1.7. **Пользователь** -- физическое лицо, индивидуальный предприниматель, юридическое лицо или их уполномоченный представитель, принявшее условия Пользовательского соглашения и Политики конфиденциальности путем их явного подтверждения внутри интерфейса Приложения.
1.8. **Администратор** -- Пользователь Приложения, который осуществляет его управление Приложением с помощью Панели администратора. 1.8. **Администратор** -- Пользователь Приложения, который осуществляет управление Приложением с помощью Панели администратора.
1.9. **Учетная запись Администратора** - учетная запись, с помощью которой Администратор осуществляет аутентификацию для доступа к Панели администратора. 1.9. **Учетная запись Администратора** -- учетная запись, с помощью которой Администратор осуществляет аутентификацию для доступа к Панели администратора.
1.10. **Участник чата** -- участник Подключенного чата, который может не являться Пользователем Приложения. 1.10. **Участник чата** -- участник Подключенного чата, который может не являться Пользователем Приложения.
1.11. **Панель администратора** -- интерфейс Приложения для управления Приложением, в том числе осуществления следующих функций: 1.11. **Панель администратора** -- интерфейс Приложения для управления Приложением, в том числе осуществления следующих функций:
- подключение чатов к Приложению (см. п. 1.12 Подключенный чат); - подключение чатов к Приложению (см. п. 1.12 Подключенный чат);
- внесение дополнительных информационных данных для Пользователей; - внесение дополнительных информационных данных для Пользователей;
- контроль и осуществление выплат вознаграждения Разработчику за использование Приложения. - контроль оплаты Лицензионного вознаграждения Разработчику за использование Приложения.
1.12. **Подключенный чат** (к Приложению) -- чат в Telegram, в который добавлен Бот с необходимыми правами доступа. В Приложении доступна информация только из Подключенных чатов. 1.12. **Подключенный чат** (к Приложению) -- чат в Telegram, в который добавлен Бот с необходимыми правами доступа. В Приложении доступна информация только из Подключенных чатов.
1.13. **Использование Приложения** -- совершение Пользователем действий по просмотру и (или) использованию доступного функционала Приложения посредством воспроизводимых онлайн-интерфейсов на экране устройства Пользователя. 1.13. **Использование Приложения** -- совершение Пользователем действий по просмотру и (или) использованию доступного функционала Приложения посредством воспроизводимых онлайн-интерфейсов на экране устройства Пользователя.
1.14. **Представители Разработчика** -- круг лиц, привлекаемых Разработчиком в рамках разработки и поддержки Приложения, действующие от имени и (или) по поручению Разработчика. 1.14. **Представители Разработчика** -- круг лиц, привлекаемых Разработчиком в рамках разработки и поддержки Приложения, действующие от имени и (или) по поручению Разработчика.
1.15. **Тариф** -- предложение, сформированное Разработчиком для Администратора по использованию Приложения, которое включает, в том числе: функциональность и вознаграждение Разработчика за использование Приложения. Описание Тарифа/Тарифов доступны на Сайте и в Приложении. Тарифы могут изменяться Разработчиком не чаще одного раза в 3 (Три) месяца. Если между информацией о Тарифе/Тарифах, размещенной на Сайте, и информацией, доступной в Приложении, возникают расхождения, то приоритет имеет информация, доступная в Приложении. 1.15. **Тариф** (или Тарифные планы) -- предложение, сформированное Разработчиком для Администратора по использованию Приложения, которое включает, в том числе: функциональность и вознаграждение Разработчика за использование Приложения. Описание Тарифа/Тарифов доступно на Сайте и в Приложении. Если между информацией о Тарифе/Тарифах, размещенной на Сайте, и информацией, доступной в Приложении, возникают расхождения, то приоритет имеет информация, доступная в Приложении.
1.16. **Лицензия** -- простая неисключительная лицензия на право использования Приложения на условиях настоящего Пользовательского соглашения.
1.17. **Лицензионное вознаграждение** -- плата за предоставление права использования Приложения в соответствии с выбранным Тарифом.
## 2. Предмет Пользовательского соглашения ## 2. Предмет Пользовательского соглашения
2.1. Настоящее Пользовательское соглашение предоставляет Пользователю право использовать Приложение на условиях, определенных в данном документе. 2.1. Настоящее Пользовательское соглашение является лицензионным договором. Разработчик предоставляет Пользователю право использования Приложения (простую неисключительную лицензию) на условиях выбранного Тарифа, а Пользователь обязуется соблюдать условия данного Пользовательского соглашения.
2.2. Для использования Приложения в мессенджере Telegram (далее - Telegram) от Пользователя может потребоваться регистрация учетной записи. 2.2. Для использования Приложения в мессенджере Telegram (далее - Telegram) от Пользователя может потребоваться регистрация учетной записи.
## 3. Гарантии и заверения Пользователя ## 3. Гарантии и заверения Пользователя
3.1. Принимая данное Пользовательское соглашение, Пользователь гарантирует: 3.1. Принимая данное Пользовательское соглашение, Пользователь гарантирует:
3.1.1. Пользователь достиг возраста 18 лет (а также возраста совершеннолетия в своей стране проживания) и что не существует обстоятельств, препятствующих Пользователю в полном объеме исполнять свои обязательства по данному Пользовательскому соглашению.
3.1.1. Достиг возраста 18 лет (а также возраста совершеннолетия в своей стране проживания) и что не существует обстоятельств, препятствующих Пользователю в полном объеме исполнять свои обязательства по данному Пользовательскому соглашению.
3.1.2. Законодательство страны проживания Пользователя разрешает использование Приложения. В случае запрета Пользователь обязан немедленно прекратить использование. 3.1.2. Законодательство страны проживания Пользователя разрешает использование Приложения. В случае запрета Пользователь обязан немедленно прекратить использование.
@@ -53,21 +55,23 @@
3.1.4. Использование Приложения будет осуществляться в рамках применимого законодательства. 3.1.4. Использование Приложения будет осуществляться в рамках применимого законодательства.
3.1.5. Пользователь принимает разумные меры для сохранения контроля над своей учетной записью Telegram. В случае утраты доступа к учетной записи, используемой для управления Приложением в роли Администратора, Пользователь обязан незамедлительно уведомить Разработчика по электронной почте [a-mart@ya.ru](mailto:a-mart@ya.ru) и сотрудничать для разрешения ситуации. 3.1.5. Пользователь принимает разумные меры для сохранения контроля над своей учетной записью Telegram. В случае утраты доступа к учетной записи, используемой для управления Приложением в роли Администратора, Пользователь обязан незамедлительно уведомить Разработчика по электронной почте [support@tgcrew.ru](mailto:support@tgcrew.ru) и сотрудничать для разрешения ситуации.
3.1.6. При прекращении соответствия гарантиям (пп. 3.1.1-3.1.5) Пользователь обязан немедленно прекратить использование Приложения и уведомить Разработчика. В этом случае Разработчик вправе приостановить или прекратить доступ Пользователя к Приложению (путем блокировки Учетной записи Администратора). 3.1.6. При прекращении соответствия гарантиям (пп. 3.1.1-3.1.5) Пользователь обязан немедленно прекратить использование Приложения и уведомить Разработчика. В этом случае Разработчик вправе приостановить или прекратить доступ Пользователя к Приложению (путем блокировки Учетной записи Администратора).
3.2. Пользователь не вправе использовать Приложение, если не соответствует гарантиям в п. 3.1. 3.2. Пользователь не вправе использовать Приложение, если не соответствует гарантиям в п. 3.1.
## 4. Прием оферты ## 4. Прием оферты
4.1. Свидетельством полного и безоговорочного принятия условий настоящего Пользовательского соглашения (акцептом), является осуществление процедуры принятия Пользовательского соглашения путем проставления символа «V» (галочки) в чек-боксе интерфейса Приложения рядом с текстом: «Я принимаю Пользовательское соглашение» на соответствующих экранах в Приложении. 4.1. Свидетельством полного и безоговорочного принятия условий настоящего Пользовательского соглашения (акцептом), является осуществление процедуры принятия Пользовательского соглашения путем активации (проставления отметки) в соответствующем интерактивном элементе (поле для отметки) интерфейса Приложения рядом с текстом: «Я принимаю Пользовательское соглашение» на соответствующих экранах в Приложении.
4.2. Использование Приложения означает согласие Пользователя на сбор и обработку информации из Подключенных чатов в соответствии с Политикой конфиденциальности. 4.2. Использование Приложения означает согласие Пользователя на сбор и обработку информации из Подключенных чатов в соответствии с Политикой конфиденциальности.
4.3. Все действия, совершенные в рамках функциональности Приложения Пользователем, признаются Разработчиком и таким Пользователем как конклюдентные действия, порождающие соответствующие юридические последствия. При этом Пользователь соглашается с тем, что основным доказательством совершения или несовершения определенного действия с использованием интерфейсов Приложения может служить для Разработчика предоставление выписки из своей базы данных о наличии информации, зафиксированной путем логирования (сохранения истории) действий такого лица (или иным аналогичным способом). В случае спора такая информация предоставляется в выбранной Разработчиком форме и признается исчерпывающим доказательством действий, указанных в настоящем пункте. 4.3. Все действия, совершенные в рамках функциональности Приложения Пользователем, признаются Разработчиком и таким Пользователем как конклюдентные действия, порождающие соответствующие юридические последствия. При этом Пользователь соглашается с тем, что основным доказательством совершения или несовершения определенного действия с использованием интерфейсов Приложения может служить для Разработчика предоставление выписки из своей базы данных о наличии информации, зафиксированной путем программной фиксации (записи) истории действий такого лица или иным аналогичным способом. В случае спора такая информация предоставляется в выбранной Разработчиком форме и признается исчерпывающим доказательством действий, указанных в настоящем пункте.
4.4. В случае, если Пользователь не согласен с условиями Пользовательского соглашения, он обязан немедленно покинуть Приложение и не начинать/прекратить Использование Приложения. 4.4. В случае, если Пользователь не согласен с условиями Пользовательского соглашения, он обязан немедленно покинуть Приложение и не начинать/прекратить Использование Приложения.
4.5. Стороны признают использование интерфейса Приложения и учетной записи Telegram (Telegram ID) использованием простой электронной подписи. Действия, совершенные через Приложение, признаются имеющими юридическую силу, аналогичную документам на бумажном носителе
## 5. Срок действия Пользовательского соглашения ## 5. Срок действия Пользовательского соглашения
5.1. Пользовательское соглашение действует с момента акцепта Пользователем до полного исполнения обязательств Сторонами или прекращения этого соглашения. 5.1. Пользовательское соглашение действует с момента акцепта Пользователем до полного исполнения обязательств Сторонами или прекращения этого соглашения.
@@ -84,44 +88,28 @@
6.5. Доступ Бота к сообщениям в чате осуществляется исключительно после его явного добавления в чат участником, обладающим необходимыми правами администратора чата в Telegram (владелец чата или администратор с соответствующими правами), и предоставления Боту этих прав администратора чата. 6.5. Доступ Бота к сообщениям в чате осуществляется исключительно после его явного добавления в чат участником, обладающим необходимыми правами администратора чата в Telegram (владелец чата или администратор с соответствующими правами), и предоставления Боту этих прав администратора чата.
## 7. Порядок и способы выплаты вознаграждения ## 7. Порядок расчетов
7.1. Выплата вознаграждения Разработчику производится только в Панели администратора Приложения, согласно Тарифу. 7.1. За предоставление Лицензии Администратор уплачивает Разработчику Лицензионное вознаграждение. Для Участников чатов использование Приложения является бесплатным.
7.2. Единственным способом выплаты вознаграждения Разработчику является использование внутренней валюты Telegram (Telegram Stars). 7.2. Налоги и сборы, применимые к вознаграждению, определяются в соответствии с действующим законодательством РФ на дату оплаты.
7.3. Администратор перечисляет (переводит) Telegram Stars на внутренний баланс в Приложении, откуда происходит выплата (списание) вознаграждения Разработчику на ежедневной основе, согласно Тарифу (ориентировочное время списания 00:00 UTC +3:00, Европа/Москва). 7.3. Вознаграждение выплачивается в порядке и на условиях, согласно документу «Положение о Тарифных планах подписки», который является неотъемлемой частью настоящего Пользовательского соглашения. С текстом документа можно ознакомиться на сайте [https://tgcrew.ru](https://tgcrew.ru) или в Приложении в Панели администратора.
7.4. Возврат неизрасходованных Telegram Stars с баланса Приложения:
- Возможен только при прекращении использования Приложения (отказ от Пользовательского соглашения).
- Требует уведомления Разработчика по электронной почте [a-mart@ya.ru](mailto:a-mart@ya.ru) с указанием идентификатора учетной записи Telegram Администратора.
- Осуществляется Разработчиком в течение 30 календарных дней с момента подтверждения Разработчиком возможности и размера возврата.
- Не производится, если остаток менее 100 Telegram Stars или возврат технически невозможен со стороны платежной системы Telegram.
7.5. Участники чата осуществляют Использование Приложения бесплатно.
7.6. По вопросам предположительно некорректных или ошибочных списаний Администратор должен незамедлительно сообщить Разработчику по электронной почте [a-mart@ya.ru](mailto:a-mart@ya.ru). Разработчик обязуется рассмотреть обращение в разумный срок, обычно не превышающий 10 (Десять) рабочих дней.
7.7. Разработчик не контролирует и не несет ответственности за работу платежной системы Telegram Stars.
7.8. Разработчик не предоставляет услуги хранения средств. Telegram Stars, переведенные на баланс Приложения, не являются банковским вкладом или иным финансовым инструментом и предназначены исключительно для оплаты вознаграждения Разработчику в соответствии с Тарифом.
7.9. В случае недостаточного баланса Telegram Stars на счету Администратора в Приложении для ежедневного списания, функционал Приложения может быть частично приостановлен до пополнения баланса. Администратор будет уведомлен о необходимости пополнения баланса через сообщение от Бота или по электронной почте (в зависимости от способа аутентификации).
## 8. Ответственность сторон ## 8. Ответственность сторон
8.1. Разработчик вправе проводить технические и иные работы, направленные на улучшение и (или) изменение функциональности Приложения. В период проведения таких работ Приложение может быть недоступно полностью или частично. Разработчик по возможности будет уведомлять Пользователей о длительных плановых работах через Приложение и (или) Сайт не менее чем за 24 часа. 8.1. Разработчик вправе проводить технические и иные работы, направленные на улучшение и (или) изменение функциональности Приложения. В период проведения таких работ Приложение может быть недоступно полностью или частично. Разработчик по возможности будет уведомлять Пользователей о длительных плановых работах через Приложение и (или) Сайт не менее чем на 1 (Один) рабочий день (по московскому времени).
8.2. Разработчик оставляет за собой право изменять, модифицировать или вводить дополнительные ограничения по своему усмотрению в любое время без предварительного уведомления, за исключением изменений Тарифов (п. 1.1) и существенных изменений условий настоящего Пользовательского соглашения (п. 13.1). 8.2. Разработчик оставляет за собой право изменять, модифицировать или вводить дополнительные ограничения на функционал Приложения по своему усмотрению в любое время без предварительного уведомления, за исключением изменений Тарифов (см. документ «Положение о Тарифных планах подписки») и существенных изменений условий настоящего Пользовательского соглашения (п. 13.1).
8.3. Разработчик и его Представители не несут ответственности за любые убытки (прямые, косвенные, упущенная выгода), возникшие в связи с использованием Приложения. 8.3. Разработчик и его Представители не несут ответственности за любые убытки (прямые, косвенные, упущенная выгода), возникшие в связи с использованием Приложения.
8.4. Пользователь самостоятельно несет ответственность за сохранность и конфиденциальность данных своей учетной записи в Telegram. Все действия, осуществленные с использованием учетной записи Пользователя, считаются осуществленными Пользователем. Пользователь самостоятельно несет ответственность перед третьими лицами за все действия, совершенные с использованием учетной записи Пользователя. Разработчик не несет ответственности за несанкционированное использование учетной записи Пользователя третьими лицами. 8.4. Пользователь самостоятельно несет ответственность за сохранность и конфиденциальность данных своей учетной записи в Telegram. Все действия, осуществленные с использованием учетной записи Пользователя, считаются осуществленными Пользователем. Пользователь самостоятельно несет ответственность перед третьими лицами за все действия, совершенные с использованием учетной записи Пользователя. Разработчик не несет ответственности за несанкционированное использование учетной записи Пользователя третьими лицами.
8.5. Пользователю запрещается: 8.5. Пользователю запрещается:
- декомпилировать, дизассемблировать, осуществлять реверс-инжиниринг, реконструировать исходный код Приложения или пытаться получить его иными способами; - декомпилировать, дизассемблировать, осуществлять обратное проектирование (реверс-инжиниринг), реконструировать исходный код Приложения или пытаться получить его иными способами;
- создавать производные работы на основе Приложения; - создавать производные работы на основе Приложения;
- удалять, скрывать или изменять уведомления об интеллектуальной собственности Разработчика в Приложении. - удалять, скрывать или изменять уведомления об интеллектуальной собственности Разработчика в Приложении.
8.6. Пользователь не должен злоупотреблять Приложением путем умышленного внедрения вирусов, троянов, червей, логических бомб или иного вредоносного или технологически опасного материала. Пользователь не должен использовать любого рода роботов, пауков (spider), скрапинг страниц или иные автоматические устройства, алгоритмы, методологии или аналогичные ручные процессы для копирования, мониторинга, получения или доступа к любой части Приложения, для попытки получения любой информации или материалов любыми средствами, которые специально не доступны через Приложение. 8.6. Пользователь не должен злоупотреблять Приложением путем умышленного внедрения вирусов, троянов, червей, логических бомб или иного вредоносного или технологически опасного материала. Пользователь не должен использовать любого рода роботов, пауков (spider), автоматизированный сбор информации (скрапинг страниц) или иные автоматические устройства, алгоритмы, методологии или аналогичные ручные процессы для копирования, мониторинга, получения или доступа к любой части Приложения, для попытки получения любой информации или материалов любыми средствами, которые специально не доступны через Приложение.
8.7. Пользователь не должен пытаться получить несанкционированный доступ к любой части или функции Приложения, вычислительным устройствам (серверам), на которых хранится Приложение. Пользователь не должен атаковать Приложение посредством атаки типа "отказ в обслуживании" (denial-of-service) или распределенной атаки типа "отказ в обслуживании" (distributed denial-of-service attack). Разработчик имеет право сообщить о любом таком нарушении в соответствующие правоохранительные органы и будет сотрудничать с этими органами, раскрывая им личность Пользователя. В случае такого нарушения право Пользователя на использование Приложения немедленно прекращается. 8.7. Пользователь не должен пытаться получить несанкционированный доступ к любой части или функции Приложения, вычислительным устройствам (серверам), на которых хранится Приложение. Пользователь не должен атаковать Приложение посредством атаки типа "отказ в обслуживании" (denial-of-service) или распределенной атаки типа "отказ в обслуживании" (distributed denial-of-service attack). Разработчик имеет право сообщить о любом таком нарушении в соответствующие правоохранительные органы и будет сотрудничать с этими органами, раскрывая им личность Пользователя. В случае такого нарушения право Пользователя на использование Приложения немедленно прекращается.
@@ -129,15 +117,24 @@
8.9. Пользователь несет ответственность за настройку своих информационных технологий, компьютерных программ и платформы для доступа к Приложению. 8.9. Пользователь несет ответственность за настройку своих информационных технологий, компьютерных программ и платформы для доступа к Приложению.
8.10. Разработчик вправе приостановить или досрочно прекратить доступ Пользователя к Использованию Приложения, если у Разработчика есть основания полагать, что использование Приложения осуществляется или будет осуществляться Пользователем с нарушением настоящего Пользовательского соглашения, или Пользователь, по усмотрению Разработчика, совершает действия, наносящие или могущие нанести ущерб репутации Разработчика или безопасности Приложения. 8.10. Пользователь обязуется не использовать Приложение для совершения действий, нарушающих законодательство РФ, включая, но не ограничиваясь: распространение материалов, содержащих публичные призывы к осуществлению террористической деятельности или оправдывающих терроризм, экстремистских материалов, пропаганду наркотических средств, психотропных веществ, а также любой иной информации, за распространение которой предусмотрена уголовная или административная ответственность.
8.11. Разработчик имеет право прекратить доступ к Приложению при нарушении Политики конфиденциальности или законодательства. 8.11. Разработчик вправе приостановить или досрочно прекратить доступ Пользователя к Использованию Приложения, если у Разработчика есть основания полагать, что использование Приложения осуществляется или будет осуществляться Пользователем с нарушением настоящего Пользовательского соглашения, или Пользователь, по усмотрению Разработчика, совершает действия, наносящие или могущие нанести ущерб репутации Разработчика или безопасности Приложения.
8.12. В случае прекращения доступа Пользователя к Приложению по основаниям, указанным в пп. 8.10 и 8.11, Разработчик вправе удержать неиспользованный остаток Telegram Stars на балансе в качестве штрафной санкции или компенсации ущерба, если нарушение Пользователя повлекло убытки для Разработчика и это предусмотрено применимым законодательством. 8.12. Разработчик имеет право прекратить доступ к Приложению при нарушении Политики конфиденциальности или законодательства.
8.13. Разработчик не несет ответственности за контент (сообщения, файлы, данные), обрабатываемый, передаваемый или отображаемый через Приложение, а также за действия Пользователей в подключенных чатах. Ответственность за законность контента и соответствие его правилам Telegram возлагается на Пользователей. 8.13. В случае нарушения Пользователем условий настоящего Пользовательского соглашения, Разработчик имеет право в одностороннем внесудебном порядке приостановить доступ к Приложению или расторгнуть настоящее Пользовательское соглашение. При этом вознаграждение, уплаченное за текущий период действия Лицензии (подписки), возврату не подлежит. Стороны признают, что указанная сумма является соразмерной компенсацией фактически понесенных Разработчиком расходов и убытков, связанных с обработкой нарушения и досрочным прекращением обслуживания по вине Пользователя.
8.14. Разработчик оставляет за собой право прекратить предоставление доступа к Приложению всем Пользователям, полностью прекратить работу Приложения или его существенных функций, уведомив об этом всех Администраторов не менее чем за 30 (Тридцать) календарных дней через Приложение, Бота и (или) по электронной почте на адрес, связанный с их Учетной записью Администратора. В этом случае Администраторам будет произведен возврат неизрасходованного остатка Telegram Stars в соответствии с п. 7.4. 8.14. Разработчик не несет ответственности за контент (сообщения, файлы, данные), обрабатываемый, передаваемый или отображаемый через Приложение, а также за действия Пользователей в подключенных чатах. Разработчик не осуществляет премодерацию, анализ содержимого, хранение или изменение информации, предоставляемой Пользователем, в том числе Администратором. Ответственность за содержание размещаемых файлов, соблюдение авторских прав и законодательства о защите информации при их загрузке в чат целиком лежит на Пользователе, загрузившем файл, и Администраторе чата.
8.15. В случае принятия Разработчиком решения о прекращении поддержки Приложения или его ликвидации, возврат неиспользованной части Лицензионного вознаграждения осуществляется в порядке, сроки и с учетом ограничений (включая минимальный порог суммы возврата), установленных в документе «Положение о Тарифных планах подписки».
8.16. При использовании функционала подключения сторонних облачных хранилищ (Яндекс Диск или Google Drive), Администратор понимает и соглашается, что:
- Разработчик не контролирует работоспособность, лимиты и политику безопасности данных сервисов.
- Утрата доступа к облачному хранилищу, удаление файлов провайдером сервиса или сбои в API стороннего сервиса не являются зоной ответственности Разработчика.
- Любые споры, связанные с доступом к файлам в сторонних хранилищах, решаются Администратором напрямую с соответствующим провайдером сервиса хранения.
- Администратор самостоятельно несет риски, связанные с трансграничной передачей данных или нарушением условий использования сторонних сервисов.
- Администратор самостоятельно несет ответственность за сохранность авторизационных данных, используемых для интеграции со сторонними облачными хранилищами.
## 9. Интеллектуальная собственность ## 9. Интеллектуальная собственность
9.1. При условии соблюдения Пользователем Пользовательского соглашения, Разработчик предоставляет Пользователю ограниченную, неисключительную, непередаваемую по сублицензии, отзывную, не подлежащую передаче лицензию на доступ к Приложению. Любые права, прямо не предоставленные в данном разделе, сохраняются за Разработчиком. Лицензия автоматически прекращается без уведомления при прекращении действия настоящего Пользовательского соглашения. 9.1. При условии соблюдения Пользователем Пользовательского соглашения, Разработчик предоставляет Пользователю ограниченную, неисключительную, непередаваемую по сублицензии, отзывную, не подлежащую передаче лицензию на доступ к Приложению. Любые права, прямо не предоставленные в данном разделе, сохраняются за Разработчиком. Лицензия автоматически прекращается без уведомления при прекращении действия настоящего Пользовательского соглашения.
@@ -149,32 +146,32 @@
10.2. Срок исполнения обязательств отодвигается на время действия форс-мажора, но не более чем на 30 (Тридцать) календарных дней. 10.2. Срок исполнения обязательств отодвигается на время действия форс-мажора, но не более чем на 30 (Тридцать) календарных дней.
10.3. При форс-мажоре длительностью более 30 дней Стороны проводят переговоры об альтернативных решениях или расторжении Пользовательского соглашения без возмещения убытков. 10.3. При форс-мажоре длительностью более 30 календарных дней Стороны проводят переговоры об альтернативных решениях или расторжении Пользовательского соглашения без возмещения убытков.
## 11. Разделимость положений ## 11. Разделимость положений
11.1. Пользователь и Разработчик соглашаются с тем, что если какая-либо часть настоящего Пользовательского соглашения или Политики конфиденциальности будет признана компетентным судом недействительной или не подлежащей защите, полностью или частично, то только такая часть настоящего Пользовательского соглашения или Политики конфиденциальности, которая объявлена недействительной или не подлежащей защите, будет считаться недействительной в данной юрисдикции, и только в той части, в которой она была признана таковой, и это не повлияет на действительность или возможность защиты в каком-либо аспекте и в любой юрисдикции других частей настоящего Пользовательского соглашения или Политики конфиденциальности, которые остаются в полной силе и действии. При этом такие недействительные положения подлежат замене положениями, максимально близкими по смыслу к исходным, которые будут признаны действительными в соответствующей юрисдикции, и подлежат применению в измененной форме (в том числе к уже установленным правоотношениям). 11.1. Пользователь и Разработчик соглашаются с тем, что если какая-либо часть настоящего Пользовательского соглашения или Политики конфиденциальности будет признана компетентным судом недействительной или не подлежащей защите, полностью или частично, то только такая часть настоящего Пользовательского соглашения или Политики конфиденциальности, которая объявлена недействительной или не подлежащей защите, будет считаться недействительной в данной юрисдикции, и только в той части, в которой она была признана таковой, и это не повлияет на действительность или возможность защиты в каком-либо аспекте и в любой юрисдикции других частей настоящего Пользовательского соглашения или Политики конфиденциальности, которые остаются в полной силе и действии. При этом такие недействительные положения подлежат замене положениями, максимально близкими по смыслу к исходным, которые будут признаны действительными в соответствующей юрисдикции, и подлежат применению в измененной форме (в том числе к уже установленным правоотношениям).
11.2. Признание какого-либо положения недействительным или неисполнимым не затрагивает действительность или исполнимость положений разделов 7 (Порядок и способы выплаты вознаграждения), 9 (Интеллектуальная собственность), 12 (Применимое право и разрешение споров), 15.4 (Возмещение убытков) и 15.6 (Бездействие). 11.2. Признание какого-либо положения недействительным или неисполнимым не затрагивает действительность или исполнимость положений разделов 7 (Порядок расчетов), 9 (Интеллектуальная собственность), 12 (Применимое право и разрешение споров), 15.4 (Возмещение убытков) и 15.6 (Бездействие).
## 12. Применимое право и разрешение споров ## 12. Применимое право и разрешение споров
12.1. Настоящее Пользовательское соглашение регулируется и толкуется в соответствии с законодательством Российской Федерации. Вопросы, не урегулированные Пользовательским соглашением, подлежат разрешению в соответствии с законодательством Российской Федерации. 12.1. Настоящее Пользовательское соглашение регулируется и толкуется в соответствии с законодательством Российской Федерации. Вопросы, не урегулированные Пользовательским соглашением, подлежат разрешению в соответствии с законодательством Российской Федерации.
12.2. Все споры Сторон по настоящему Пользовательскому соглашению подлежат разрешению путем переписки и переговоров с использованием обязательного досудебного (претензионного) порядка. В случае невозможности достичь согласия между Сторонами путем переговоров в течение 30 (Тридцати) календарных дней с момента получения другой Стороной письменной претензии, направленной в соответствии с п. 15.2 настоящего Пользовательского соглашения, рассмотрение спора должно быть передано любой заинтересованной Стороной в суд по месту государственной регистрации Разработчика (Российская Федерация, г. Москва). 12.2. Все споры Сторон по настоящему Пользовательскому соглашению подлежат разрешению путем переписки и переговоров с использованием обязательного досудебного (претензионного) порядка. В случае невозможности достичь согласия между Сторонами путем переговоров в течение 30 (Тридцати) календарных дней с момента получения другой Стороной письменной претензии, направленной почтовым отправлением в соответствии с п. 15.2 настоящего Пользовательского соглашения, рассмотрение спора должно быть передано любой заинтересованной Стороной в суде по месту нахождения Разработчика (Российская Федерация, г. Москва) в соответствии с правилами о подсудности, установленными ГПК РФ или АПК РФ.
## 13. Порядок изменения Пользовательского соглашения ## 13. Порядок изменения Пользовательского соглашения
13.1. Настоящее Пользовательское соглашение (включая дополнения) могут быть изменены Разработчиком с уведомлением Пользователя посредством размещения новой редакции изменяемого Пользовательского соглашения на Сайте или Приложении. Внесенные Разработчиком изменения в Пользовательском соглашении, вступают в силу в день, следующий за днем опубликования таких изменений на Сайте или в Приложении (смотря что наступит раньше). Пользователь обязуется самостоятельно проверять настоящее Пользовательское соглашение на предмет внесенных изменений. Неосуществление Пользователем действий по ознакомлению не может служить основанием для неисполнения Пользователем своих обязательств и несоблюдения Пользователем ограничений, установленных настоящим Пользовательским соглашением. Разработчик рекомендует Пользователям регулярно знакомиться с актуальной редакцией Пользовательского соглашения. 13.1. Настоящее Пользовательское соглашение (включая дополнения) могут быть изменены Разработчиком с уведомлением Пользователя посредством размещения новой редакции изменяемого Пользовательского соглашения на Сайте или в Приложении. Внесенные Разработчиком изменения в Пользовательском соглашении вступают в силу в день, следующий за днем опубликования таких изменений на Сайте или в Приложении (смотря что наступит раньше). Пользователь обязуется самостоятельно проверять настоящее Пользовательское соглашение на предмет внесенных изменений. Неосуществление Пользователем действий по ознакомлению не может служить основанием для неисполнения Пользователем своих обязательств и несоблюдения Пользователем ограничений, установленных настоящим Пользовательским соглашением. Разработчик рекомендует Пользователям регулярно знакомиться с актуальной редакцией Пользовательского соглашения.
13.2. Пользователь вправе отказаться от принятия изменений и дополнений в настоящее Пользовательское соглашение, что означает отказ Пользователя от использования всех предоставленных ему ранее прав. 13.2. Пользователь вправе отказаться от принятия изменений и дополнений в настоящее Пользовательское соглашение, что означает отказ Пользователя от использования всех предоставленных ему ранее прав.
## 14. Уступка прав (требований) ## 14. Уступка прав (требований)
14.1. Разработчик может уступить или передать права по настоящему Пользовательскому соглашению и (или) Политике конфиденциальности полностью или частично любому физическому или юридическому лицу в любое время с согласия Пользователя или без такового. Пользователь не вправе уступать или передавать какие-либо права и обязательства, вытекающие из Пользовательского соглашения или Политики конфиденциальности, без предварительного письменного согласия Разработчика, и любая уступка или передача прав, совершенная Пользователем без такого согласия, будет недействительной. 14.1. Разработчик может уступить или передать права по настоящему Пользовательскому соглашению и (или) Политике конфиденциальности полностью или частично любому физическому или юридическому лицу в любое время с согласия Пользователя или без такового. Пользователь не вправе уступать или передавать какие-либо права и обязательства, вытекающие из Пользовательского соглашения или Политики конфиденциальности, без предварительного письменного согласия Разработчика, и любая уступка или передача прав, совершенная Пользователем без такого согласия, будет недействительной.
14.2. Уступка Разработчиком прав по настоящему Соглашению третьему лицу не освобождает Разработчика от ответственности перед Пользователем за исполнение обязательств по настоящему Соглашению, возникших до момента уступки, если иное не согласовано с Пользователем или не предусмотрено законодательством. 14.2. Уступка Разработчиком прав по настоящему Пользовательскому соглашению третьему лицу не освобождает Разработчика от ответственности перед Пользователем за исполнение обязательств по настоящему Пользовательскому соглашению, возникших до момента уступки, если иное не согласовано с Пользователем или не предусмотрено законодательством.
## 15. Заключительные положения ## 15. Заключительные положения
15.1. Настоящее Пользовательское соглашение составлено на русском языке. В случае возникновения противоречий между русской версией и любыми переводами, приоритет имеет русская версия. 15.1. Настоящее Пользовательское соглашение составлено на русском языке. В случае возникновения противоречий между русской версией и любыми переводами, приоритет имеет русская версия.
15.2. Все уведомления, запросы и претензии в связи с настоящим Пользовательским соглашением или использованием Приложения направляются Пользователем Разработчику по электронной почте [a-mart@ya.ru](mailto:a-mart@ya.ru) или иными способами, явно указанными Разработчиком в Приложении или на Сайте. Уведомления Разработчика Пользователю считаются направленными надлежащим образом, если они размещены в Приложении и (или) на Сайте и (или) отправлены на контактный адрес электронной почты (при наличии), связанный с учетной записью Администратора в Telegram и считаются полученными Пользователем на следующий день после их размещения и (или) отправки. 15.2. Все уведомления, запросы и претензии в связи с настоящим Пользовательским соглашением или использованием Приложения направляются Пользователем Разработчику по электронной почте [support@tgcrew.ru](mailto:support@tgcrew.ru) или иными способами, явно указанными Разработчиком в Приложении или на Сайте. Уведомления Разработчика Пользователю считаются направленными надлежащим образом, если они размещены в Приложении и (или) на Сайте и (или) отправлены на контактный адрес электронной почты (при наличии), связанный с учетной записью Администратора в Telegram и считаются полученными Пользователем на следующий день после их размещения/отправки. Также допускается отправка уведомлений, запросов и претензий на бумажном носителе посредством почтового отправления по адресу Разработчика, указанному в разделе 16 настоящего Пользовательского соглашения.
15.3. Разработчик осуществляет обработку персональных данных Пользователей исключительно в порядке и целях, определенных Политикой конфиденциальности, являющейся неотъемлемой частью настоящего Пользовательского соглашения. 15.3. Разработчик осуществляет обработку персональных данных Пользователей исключительно в порядке и целях, определенных Политикой конфиденциальности, являющейся неотъемлемой частью настоящего Пользовательского соглашения.
@@ -190,8 +187,8 @@
## 16. Контактная информация и реквизиты Разработчика ## 16. Контактная информация и реквизиты Разработчика
Индивидуальный предприниматель Мартышкин Алексей Александрович Индивидуальный предприниматель Мартышкин Алексей Александрович
Юридический адрес: 111394, Российская Федерация, г. Москва, ул. Перовская, д. 66, к.3, кв. 187 Юридический адрес: 111394, Российская Федерация, город Москва, улица Перовская, дом 66, корпус 3, квартира 187
ОГРНИП 318774600262084 ОГРНИП 318774600262084
ИНН 366316608346 ИНН 366316608346
Телефон: +7 (926)339-04-25 Телефон: +7 (916) 439-04-25
Электронная почта: [a-mart@ya.ru](mailto:a-mart@ya.ru) Электронная почта: [info@tgcrew.ru](mailto:info@tgcrew.ru)

View File

@@ -25,7 +25,7 @@ const api = axios.create({
api.interceptors.response.use( api.interceptors.response.use(
response => response, response => response,
async (error: AxiosError<{ error?: { code: string; message: string } }>) => { (error: AxiosError<{ error?: { code: string; message: string } }>) => {
const errorData = error.response?.data?.error || { const errorData = error.response?.data?.error || {
code: 'ZERO', code: 'ZERO',
message: error.message || 'Unknown error' message: error.message || 'Unknown error'
@@ -38,7 +38,6 @@ api.interceptors.response.use(
type: 'negative', type: 'negative',
message: errorData.code + ': ' + errorData.message, message: errorData.code + ': ' + errorData.message,
icon: 'mdi-alert-outline', icon: 'mdi-alert-outline',
position: 'bottom'
}) })
} }

View File

@@ -1,26 +1,28 @@
import { boot } from 'quasar/wrappers' import { boot } from 'quasar/wrappers'
import pnPageWrapper from 'components/pnPageWrapper.vue'
import pnPageCard from 'components/pnPageCard.vue' import pnPageCard from 'components/pnPageCard.vue'
import pnScrollList from 'components/pnScrollList.vue' import pnPageTemplate from 'components/pnPageTemplate.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 pnSmallDialog from 'components/pnSmallDialog.vue' import pnSmallDialog from 'components/pnSmallDialog.vue'
import pnImageSelector from 'components/pnImageSelector.vue'
import pnShadowScroll from 'components/pnShadowScroll.vue' import pnShadowScroll from 'components/pnShadowScroll.vue'
import pnMagicOverlay from 'components/pnMagicOverlay.vue' import pnMagicOverlay from 'components/pnMagicOverlay.vue'
import pnAccountBlockName from 'components/pnAccountBlockName.vue' import pnAccountBlockName from 'components/pnAccountBlockName.vue'
import pnOnboardBtn from 'components/pnOnboardBtn.vue' import pnOnboardBtn from 'components/pnOnboardBtn.vue'
import pnBottomSheetDialog from 'components/pnBottomSheetDialog.vue'
const components = { const components = {
pnPageWrapper,
pnPageCard, pnPageCard,
pnScrollList, pnPageTemplate,
pnAutoAvatar, pnAutoAvatar,
pnOverlay, pnOverlay,
pnImageSelector,
pnSmallDialog, pnSmallDialog,
pnShadowScroll, pnShadowScroll,
pnMagicOverlay, pnMagicOverlay,
pnAccountBlockName, pnAccountBlockName,
pnOnboardBtn pnOnboardBtn,
pnBottomSheetDialog
} }
export default boot(({ app }) => { export default boot(({ app }) => {

View File

@@ -3,18 +3,47 @@ import type { WebApp } from "@twa-dev/types"
declare global { declare global {
interface Window { interface Window {
Telegram: { Telegram: { WebApp: WebApp }
WebApp: WebApp
}
} }
} }
export default defineBoot(({ app }) => { export default defineBoot(({ app }) => {
if (window.Telegram?.WebApp) { const tg = window.Telegram?.WebApp
const webApp = window.Telegram.WebApp
webApp.ready() tg.ready()
webApp.SettingsButton.isVisible = true tg.disableVerticalSwipes()
app.config.globalProperties.$tg = webApp sessionStorage.setItem('isTelegram', tg.initData ? 'true' : 'false')
app.provide('tg', webApp)
} const platform = tg.platform.toLowerCase()
const isDesktopOrWeb = [
'tdesktop',
'tdlib',
'macos',
'web',
'weba',
'webk'
].includes(platform)
const isTablet = window.innerWidth > 1024
if (!isDesktopOrWeb && !isTablet) tg.expand()
tg.SettingsButton.show()
app.config.globalProperties.$tg = tg
app.config.globalProperties.$isDesktop = isDesktopOrWeb || isTablet
app.config.globalProperties.$isMobile = !isDesktopOrWeb && !isTablet
app.config.globalProperties.$isIOS = platform === 'ios'
app.config.globalProperties.$isAndroid = platform === 'android'
app.provide('tg', tg)
}) })
/* flag use
<div v-if="$isDesktop || isToolbarExpanded">
<q-tabs ...> </q-tabs>
</div>
script setup
import { getCurrentInstance } from 'vue'
const app = getCurrentInstance()
const isMobile = app?.appContext.config.globalProperties.$isMobile
*/

View File

@@ -1,124 +1,56 @@
<template> <template>
<div class="flex row items-center no-wrap logo-component">
<LogoIcon
:class="{
'is-animated': animated,
'hide-bg': !withBackground
}"
:style="iconStyle"
/>
<div <div
class="flex row items-center no-wrap logo-component" :class="'text-' + textColor"
class="logo-text"
style="margin-left: 0.15em;"
> >
<svg <span>tg</span>
class="iconcolor" <span class="text-bold">Crew</span>
viewBox="0 0 8.4666662 8.4666662" </div>
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
>
<defs id="defs1" />
<g id="layer1">
<rect
class="fill-brand"
style="stroke-width:0.233149"
id="rect5"
width="6.9885192"
height="0.35581663"
x="3.114475"
y="0.86827624"
transform="matrix(0.77578367,0.63099897,-0.77578367,0.63099897,0,0)"
/>
<rect
class="fill-brand"
style="stroke-width:0.24961"
id="rect5-7"
width="7.4819207"
height="0.3809379"
x="-3.9267058"
y="5.7988153"
transform="matrix(-0.70756824,0.70664502,0.70756824,0.70664502,0,0)"
/>
<circle
class="fill-brand"
style="stroke-width:0.134869"
id="path5-8"
cx="1.5875"
cy="6.8791666"
r="1.0583333"
/>
<circle
class="fill-brand"
style="stroke-width:0.168586"
id="path5-8-5"
cx="7.1437502"
cy="7.1437502"
r="1.3229166"
/>
<circle
class="fill-brand"
style="stroke-width:0.118011"
id="path5-8-5-1"
cx="1.4552083"
cy="2.5135417"
r="0.92604166"
/>
<circle
class="fill-brand"
style="stroke-width:0.101152"
id="path5-8-5-1-7"
cx="7.1437502"
cy="1.3229166"
r="0.79374999"
/>
<circle
style="stroke-width:0.23602"
id="path5"
cx="3.96875"
cy="4.4979167"
r="1.8520833"
/>
</g>
</svg>
<span
class="text-brand"
style="margin-right: 0.075em;"
>
tg
</span>
<span class="text-brand2 text-bold">
Crew
</span>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { getCssVar } from 'quasar'
import LogoIcon from 'components/BaseLogoSvg.vue'
interface Props {
textColor?: string
iconColor?: string
withBackground?: boolean
animated?: boolean
}
const props = withDefaults(defineProps<Props>(), {
textColor: 'primary',
iconColor: 'primary',
withBackground: false,
animated: true
})
const iconStyle = computed(() => {
const color = getCssVar(props.iconColor) || props.iconColor
return {
color: color,
transform: 'scale(1.08)',
height: '1cap'
}
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.logo-component { .hide-bg :deep(.logo-bg) {
svg { display: none;
width: 1em;
height: 1em;
margin-right: 0.125em;
}
span {
line-height: 1;
}
}
.fill-brand {
fill: $brand;
}
@keyframes blink {
100%,
0% {
fill: $brand2;
}
60% {
fill: $brand2;
opacity: 0.8;
}
}
#path5 {
animation: blink 3s infinite;
} }
</style> </style>

View File

@@ -0,0 +1,69 @@
<template>
<svg
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
:class="{ 'is-animated': animated }"
class="base-logo-svg"
>
<defs>
<radialGradient id="logo-gradient" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
<stop offset="0%" stop-color="white" />
<stop offset="25%" stop-color="var(--q-primary, #1976D2)" stop-opacity="0.5" />
<stop offset="50%" stop-color="var(--q-primary, #1976D2)">
<animate
attributeName="offset"
values="1;0.6;1"
dur="6s"
repeatCount="indefinite"
/>
</stop>
</radialGradient>
</defs>
<rect class="logo-bg" width="32" height="32" rx="4" ry="4" fill="url(#logo-gradient)" />
<circle class="x" cx="16" cy="16" r="6" style="--t:scale(1.2)" fill="currentColor" />
<circle class="c" cx="7" cy="7" r="3" style="--t:translate(9px,9px)" fill="currentColor" />
<circle class="c" cx="28" cy="4" r="2" style="--t:translate(-12px,12px)" fill="currentColor" />
<circle class="c" cx="6" cy="26" r="4" style="--t:translate(10px,-10px)" fill="currentColor" />
<circle class="c" cx="26" cy="26" r="5" style="--t:translate(-10px,-10px)" fill="currentColor" />
<line class="l" x1="16" y1="16" x2="7" y2="7" pathLength="1" stroke="currentColor" />
<line class="l" x1="16" y1="16" x2="28" y2="4" pathLength="1" stroke="currentColor" />
<line class="l" x1="16" y1="16" x2="6" y2="26" pathLength="1" stroke="currentColor" />
<line class="l" x1="16" y1="16" x2="26" y2="26" pathLength="1" stroke="currentColor" />
</svg>
</template>
<script setup lang="ts">
defineProps({
animated: {
type: Boolean,
default: true
}
})
</script>
<style lang="scss" scoped>
.base-logo-svg {
.x, .c { transform-box: fill-box; }
.l { stroke-width: 1.5; }
&.is-animated {
.x { animation: expand-r 6s infinite; }
.c { animation: o 6s infinite; }
.l {
stroke-dasharray: 1;
stroke-dashoffset: 1;
animation: draw 6s infinite;
}
}
}
@keyframes expand-r { 50% { r: 8px; } }
@keyframes o { 50% { transform: var(--t); } }
@keyframes draw {
0%, 100% { stroke-dashoffset: 0; }
50% { stroke-dashoffset: 1; }
}
</style>

View File

@@ -19,8 +19,15 @@
} }
}) })
// Импорт всех Markdown-файлов как raw-строк interface ImportMetaWithGlob {
const mdFiles = import.meta.glob('assets/docs/*.md', { glob: (pattern: string, options?: {
eager?: boolean;
query?: string;
import?: string;
}) => Record<string, string>;
}
const mdFiles = (import.meta as unknown as ImportMetaWithGlob).glob('assets/docs/*.md', {
query: '?raw', query: '?raw',
import: 'default', import: 'default',
eager: true eager: true
@@ -29,16 +36,13 @@
const markdownContent = computed(() => { const markdownContent = computed(() => {
const baseLang = props.locale.split('-')[0]?.toLowerCase() const baseLang = props.locale.split('-')[0]?.toLowerCase()
// Формируем имена файлов
const localizedFileName = `${props.documentName}_${baseLang}.md` const localizedFileName = `${props.documentName}_${baseLang}.md`
const fallbackFileName = `${props.documentName}_en.md` const fallbackFileName = `${props.documentName}_en.md`
// Находим пути к файлам
const filePaths = Object.keys(mdFiles) const filePaths = Object.keys(mdFiles)
const localizedPath = filePaths.find(path => path.endsWith(localizedFileName)) const localizedPath = filePaths.find(path => path.endsWith(localizedFileName))
const fallbackPath = filePaths.find(path => path.endsWith(fallbackFileName)) const fallbackPath = filePaths.find(path => path.endsWith(fallbackFileName))
// Возвращаем контент в порядке приоритета
if (localizedPath) return mdFiles[localizedPath] if (localizedPath) return mdFiles[localizedPath]
if (fallbackPath) return mdFiles[fallbackPath] if (fallbackPath) return mdFiles[fallbackPath]

View File

@@ -0,0 +1,34 @@
<template>
<svg
class="q-ma-none q-pa-none"
:style="{
fill: color,
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">
withDefaults(defineProps<{
color: string
size: string // for example '24px'
}>(),
{
color: 'gold'
}
)
</script>
<style scoped>
</style>

View File

@@ -5,7 +5,7 @@
color="primary" color="primary"
animated animated
flat flat
class="bg-transparent" class="bg-transparent q-pt-none"
> >
<q-step <q-step
:name="1" :name="1"
@@ -24,6 +24,7 @@
@focus="($refs.emailInput as typeof QInput)?.resetValidation()" @focus="($refs.emailInput as typeof QInput)?.resetValidation()"
ref="emailInput" ref="emailInput"
:disable="type==='changePwd'" :disable="type==='changePwd'"
class="q-pb-none"
/> />
<q-stepper-navigation> <q-stepper-navigation>
<q-btn <q-btn
@@ -72,7 +73,33 @@
</q-step> </q-step>
<q-step <q-step
v-if="type === 'register'"
:name="3" :name="3"
:title="$t('account_helper__agreement')"
:done="step>3"
>
<agreement-block v-model="agreement"/>
<q-stepper-navigation>
<q-btn
@click="handleSubmit"
color="primary"
:label="$t('account_helper__agreement_btn')"
:disabled="agreement.length !== 2"
:unelevated="agreement.length !== 2"
class="fix-disabled-btn"
/>
<q-btn
flat
@click="step=2"
color="primary"
:label="$t('back')"
class="q-ml-sm"
/>
</q-stepper-navigation>
</q-step>
<q-step
:name="4"
:title="$t('account_helper__set_password')" :title="$t('account_helper__set_password')"
> >
<q-input <q-input
@@ -89,6 +116,7 @@
no-error-icon no-error-icon
@focus="($refs.passwordInput as typeof QInput)?.resetValidation()" @focus="($refs.passwordInput as typeof QInput)?.resetValidation()"
ref="passwordInput" ref="passwordInput"
class="q-pb-none"
> >
<template #append> <template #append>
<q-icon <q-icon
@@ -111,7 +139,7 @@
/> />
<q-btn <q-btn
flat flat
@click="step=2" @click="step= type === 'register' ? 3 : 2"
color="primary" color="primary"
:label="$t('back')" :label="$t('back')"
class="q-ml-sm" class="q-ml-sm"
@@ -123,18 +151,25 @@
<pn-magic-overlay <pn-magic-overlay
v-if="showSuccessOverlay" v-if="showSuccessOverlay"
icon="mdi-check-circle-outline" icon="mdi-check-circle-outline"
:message1="getHelperMessage1()" :message1="getHelperMessage.m1"
:message2="getHelperMessage2()" :message2="getHelperMessage.m2"
route-name="projects" :route-name="getRoute"
/> />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed, onMounted, onUnmounted, inject } from 'vue'
import type { AxiosError } from 'axios' import type { AxiosError } from 'axios'
import { useI18n } from "vue-i18n"
import { QInput } from 'quasar' import { QInput } from 'quasar'
import { useAuthStore, type AuthFlowType } from 'stores/auth' import { useAuthStore, type AuthFlowType } from 'stores/auth'
import { useSettingsStore } from 'stores/settings'
import agreementBlock from 'components/agreementBlock.vue'
import { useInputRules } from 'composables/useInputRules'
import type { WebApp } from '@twa-dev/types'
const tg = inject('tg') as WebApp
const { inputRules } = useInputRules()
const flowType = computed<AuthFlowType>(() => { const flowType = computed<AuthFlowType>(() => {
return props.type === 'register' return props.type === 'register'
@@ -146,16 +181,15 @@
: 'changeMethod' : 'changeMethod'
}) })
const { t } = useI18n()
const authStore = useAuthStore() const authStore = useAuthStore()
const settingsStore = useSettingsStore()
const props = defineProps<{ const props = defineProps<{
type: 'register' | 'forgotPwd' | 'changePwd' | 'changeMethod' type: 'register' | 'forgotPwd' | 'changePwd' | 'changeMethod'
email?: string email?: string
}>() }>()
type ValidationRule = (val: string) => boolean | string type Step = 1 | 2 | 3 | 4
type Step = 1 | 2 | 3
const step = ref<Step>(1) const step = ref<Step>(1)
const login = ref<string>(props.email || '') const login = ref<string>(props.email || '')
@@ -164,10 +198,16 @@
const showSuccessOverlay = ref(false) const showSuccessOverlay = ref(false)
const isPwd = ref<boolean>(true) const isPwd = ref<boolean>(true)
const validationRules = { const validationRules = {
email: [(val: string) => /.+@.+\..+/.test(val) || t('login__incorrect_email')] as [ValidationRule], email: [inputRules.email],
password: [(val: string) => val.length >= 8 || t('login__password_require')] as [ValidationRule] password: [inputRules.password]
} }
const getRoute = computed(() => {
return (props.type === 'changeMethod' || props.type === 'changePwd')
? 'account'
: 'projects'
})
const isEmailValid = computed(() => const isEmailValid = computed(() =>
validationRules.email.every(f => f(login.value) === true) validationRules.email.every(f => f(login.value) === true)
) )
@@ -177,21 +217,26 @@
) )
const passwordHint = computed(() => { const passwordHint = computed(() => {
const result = validationRules.password[0](password.value) const result = validationRules.password[0] ? validationRules.password[0](password.value) : null
return typeof result === 'string' ? result : '' return typeof result === 'string' ? result : ''
}) })
const stepActions: Record<Step, () => Promise<void>> = { const stepActions: Record<Step, () => Promise<void> | void> = {
1: async () => { 1: async () => {
await authStore.initRegistration(flowType.value, login.value) await authStore.initRegistration(flowType.value, login.value, settingsStore.settings.locale)
}, },
2: async () => { 2: async () => {
await authStore.confirmCode(flowType.value, login.value, code.value) await authStore.confirmCode(flowType.value, login.value, code.value)
}, },
3: async () => { 3: () => {},
4: async () => {
await authStore.setPassword(flowType.value, login.value, code.value, password.value) await authStore.setPassword(flowType.value, login.value, code.value, password.value)
if (flowType.value === 'register' || flowType.value === 'changeMethod') { if (flowType.value === 'register' || flowType.value === 'changeMethod') {
await authStore.loginWithCredentials(login.value, password.value) await authStore.loginWithCredentials(login.value, password.value, tg.initData, settingsStore.settings.locale)
}
if (flowType.value === 'register') {
acceptSubmit.value = true
sessionStorage.removeItem('account-helper')
} }
} }
} }
@@ -199,8 +244,9 @@
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
await stepActions[step.value]() await stepActions[step.value]()
if (step.value < 3) { if (step.value < 4) {
step.value++ step.value++
if (step.value === 3 && props.type !== 'register') step.value++
} else { } else {
showSuccessOverlay.value = true showSuccessOverlay.value = true
} }
@@ -209,26 +255,44 @@
} }
} }
const getHelperMessage1 = () => { const getHelperMessage = computed(() => {
switch (flowType.value) { switch (flowType.value) {
case 'register': return 'account_helper__register_message1' case 'register': return { m1: 'account_helper__register_message1', m2: 'slogan' }
case 'forgot': return 'account_helper__forgot_password_message1' case 'forgot': return { m1: 'account_helper__forgot_password_message1', m2: 'account_helper__go_projects' }
case 'changePwd': return 'account_helper__change_password_message1' case 'changePwd': return { m1: 'account_helper__change_password_message1', m2: '' }
case 'changeMethod': return 'account_helper__change_method_message1' case 'changeMethod': return { m1:'account_helper__change_method_message1', m2: '' }
default: return '' default: return { m1: '', m2: '' }
}
} }
})
const getHelperMessage2 = () => { const agreement = ref<('terms'| 'consent')[]>([])
switch (flowType.value) { const acceptSubmit = ref(false)
case 'register': return 'slogan' onMounted(() => {
case 'forgot': if (props.type !== 'register') return
case 'changePwd':
case 'changeMethod': const ah = sessionStorage.getItem('account-helper')
return 'account_helper__go_projects' if (!ah) return
default: return ''
} const ahVal = JSON.parse(ah)
login.value = ahVal.login
code.value = ahVal.code
agreement.value = ahVal.agreement
step.value = ahVal.step
})
onUnmounted(() => {
if (props.type === 'register' && step.value >= 3 && !acceptSubmit.value) {
sessionStorage.setItem(
'account-helper',
JSON.stringify({
login: login.value,
code: code.value,
agreement: agreement.value,
step: step.value
})
)
} }
})
</script> </script>

View File

@@ -0,0 +1,44 @@
<template>
<div class="column w100 text-caption">
<div class="flex items-center no-wrap">
<q-checkbox v-model="modelValue" val="terms" class="q-pr-sm"/>
<span>
{{ $t('agreements__checkbox_agreement_terms') + ' ' }}
<span
@click="router.push({ name: 'terms' })"
class="cursor-pointer text-primary"
>
{{ $t('agreements__checkbox_agreement_terms_doc') }}
</span>
</span>
</div>
<div class="flex items-center no-wrap">
<q-checkbox v-model="modelValue" val="consent" class="q-pr-sm"/>
<span>
{{ $t('agreements__checkbox_agreement_consent') + ' ' }}
<span
@click="router.push({ name: 'consent' })"
class="cursor-pointer text-primary"
>
{{ $t('agreements__checkbox_agreement_consent_doc') }}
</span>
{{ ' ' + $t('agreements__checkbox_agreement_privacy') + ' ' }}
<span
@click="router.push({ name: 'privacy' })"
class="cursor-pointer text-primary"
>
{{ $t('agreements__checkbox_agreement_privacy_doc') }}
</span>
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const modelValue = defineModel<('terms'| 'consent')[]>({ default: () => [] })
const router = useRouter()
</script>

View File

@@ -1,12 +1,12 @@
<template> <template>
<pn-page-card> <pn-page-template>
<template #title> <template #title>
{{ $t(title) }} {{ $t(title) }}
</template> </template>
<template #footer> <template #card-actions>
<q-btn <q-btn
rounded color="primary" rounded color="primary"
class="w100 q-mt-md q-mb-xs fix-disabled-btn" class="w100 fix-disabled-btn"
:disable="!isFormValid" :disable="!isFormValid"
@click = "onSubmit" @click = "onSubmit"
> >
@@ -14,33 +14,38 @@
</q-btn> </q-btn>
</template> </template>
<pn-scroll-list> <div class="flex column items-center q-px-md q-pb-sm w100">
<div class="flex column items-center q-pa-md q-pb-sm">
<slot name="myCompany"/> <slot name="myCompany"/>
<pn-image-selector <pn-image-selector
v-model="modelValue.logo" v-model="companyMod.logo"
:size="100" :size="100"
:iconsize="80" :iconsize="80"
class="q-pb-lg" class="q-pb-lg"
/> />
<div class="q-gutter-y-lg w100">
<q-input <q-form
ref="formRef"
@validation-success="isFormValid = true"
@validation-error="isFormValid = false"
class="q-gutter-y-lg w100"
>
<template
v-for="input in textInputs" v-for="input in textInputs"
:key="input.id" :key="input.id"
v-model.trim="modelValue[input.val]" >
<q-input
v-model="companyMod[input.val]"
no-error-icon
dense dense
filled filled
class="w100"
:autogrow="input.val === 'description' || input.val === 'address'"
:class="input.val === 'name'
? 'fix-bottom-padding'
: input.val === 'address'
? 'input-fix q-pt-sm'
: 'q-pt-sm'"
:label="input.label ? $t(input.label) : void 0" :label="input.label ? $t(input.label) : void 0"
:rules="input.val === 'name' ? [rules[input.val]] : []"
no-error-icon
:label-slot="Boolean(input.label)" :label-slot="Boolean(input.label)"
class="w100 fix-bottom-padding"
:class="input.val === 'name' ? '' : 'q-pt-sm'"
:rules="input.rules"
@update:model-value="formRef.validate()"
:autogrow="input.val === 'description' || input.val === 'address'"
debounce="300"
> >
<template #prepend> <template #prepend>
<q-icon v-if="input.icon" :name="input.icon"/> <q-icon v-if="input.icon" :name="input.icon"/>
@@ -50,31 +55,43 @@
<span v-if="input.val === 'name'" class="text-red">*</span> <span v-if="input.val === 'name'" class="text-red">*</span>
</template> </template>
</q-input> </q-input>
</template>
</q-form>
</div> </div>
</div> </pn-page-template>
</pn-scroll-list>
</pn-page-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { ref, reactive, onMounted, } from 'vue'
import { useI18n } from 'vue-i18n' import pnImageSelector from 'components/pnImageSelector.vue'
import { useInputRules } from 'composables/useInputRules'
import { removeNullKeys, getFieldValue } from 'utils/helpers'
import type { CompanyParams } from 'types/Company' import type { CompanyParams } from 'types/Company'
import { convertEmptyStringsToNull } from 'helpers/helpers'
const { t }= useI18n() const { inputRules } = useInputRules()
const modelValue = defineModel<CompanyParams>({ const props = defineProps<{
required: true
})
defineProps<{
title: string, title: string,
btnText: string btnText: string,
company?: CompanyParams
}>() }>()
const emit = defineEmits(['update']) const emit = defineEmits(['update'])
type CompType = {
[K in keyof CompanyParams]: CompanyParams[K] extends number ? never : CompanyParams[K]
}
const companyMod = reactive<CompType>({
name: props.company?.name ?? '',
description: props.company?.description ?? '',
site: props.company?.site ?? '',
address: props.company?.address ?? '',
phone: props.company?.phone ?? '',
email: props.company?.email ?? '',
logo: props.company?.logo ?? null
})
interface TextInput { interface TextInput {
id: number id: number
label?: string label?: string
@@ -84,35 +101,42 @@
} }
const textInputs: TextInput[] = [ const textInputs: TextInput[] = [
{ id: 1, val: 'name', label: 'company_block__name', rules: [] }, { id: 1, val: 'name', label: 'company_block__name', rules: [inputRules.require, inputRules.middleText] },
{ id: 2, val: 'description', label: 'company_block__description', rules: [] }, { id: 2, val: 'description', label: 'company_block__description', rules: [inputRules.longText] },
{ id: 3, val: 'site', icon: 'mdi-web', rules: [] }, { id: 3, val: 'site', icon: 'mdi-web', rules: [inputRules.middleText] },
{ id: 4, val: 'address', icon: 'mdi-map-marker-outline', rules: [] }, { id: 4, val: 'address', icon: 'mdi-map-marker-outline', rules: [inputRules.longText] },
{ id: 5, val: 'phone', icon: 'mdi-phone-outline', rules: [] }, { id: 5, val: 'phone', icon: 'mdi-phone-outline', rules: [inputRules.middleText] },
{ id: 6, val: 'email', icon: 'mdi-email-outline', rules: [] } { id: 6, val: 'email', icon: 'mdi-email-outline', rules: [inputRules.middleText] }
] ] as const
const rulesErrorMessage = { const formRef = ref()
name: t('company_block__error_name') const isFormValid = ref(false)
}
const rules = {
name: (val: CompanyParams['name']) => !!val?.trim() || rulesErrorMessage['name']
}
const isFormValid = computed(() => {
const validations = {
name: rules.name(modelValue.value.name) === true
}
return Object.values(validations).every(Boolean)
})
function onSubmit() { function onSubmit() {
const cleanedData = convertEmptyStringsToNull(modelValue.value) const { logo, name, description, site, address, phone, email } = companyMod
emit('update', cleanedData) let logoValue: string | null | undefined = undefined
if (typeof logo === 'string' && logo.startsWith('data:image')) logoValue = logo
if (logo === null) logoValue = null
const outData = {
name: name.trim(),
description: getFieldValue(description!.trim(), props.company?.description),
site: getFieldValue(site!.trim(), props.company?.site),
address: getFieldValue(address!.trim(), props.company?.address),
phone: getFieldValue(phone!.trim(), props.company?.phone),
email: getFieldValue(email!.trim(), props.company?.email),
...(logoValue !== undefined && { logo: logoValue })
} }
emit('update', removeNullKeys(outData, ['logo']))
}
onMounted(async () => {
await formRef.value.validate()
if (!isFormValid.value) formRef.value.resetValidation()
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,16 +1,15 @@
<template> <template>
<pn-page-card> <pn-page-template>
<template #title> <template #title>
{{ $t(type + '__title') }} {{ $t(title) }}
</template> </template>
<pn-scroll-list>
<markdown-viewver <markdown-viewver
class="q-pa-md" class="q-px-md"
:locale :locale
:documentName = "getDocumentName()" :documentName
/> />
</pn-scroll-list> </pn-page-template>
</pn-page-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -18,22 +17,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

@@ -0,0 +1,14 @@
<template>
<div class="column items-center text-grey w100">
<div class="text-h6 w60 text-center">
{{$t('empty_description')}}
</div>
<q-icon size="120px" name="mdi-clippy" class="q-ma-sm"/>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

View File

@@ -1,145 +0,0 @@
<template>
<div id="background-canvas-wrapper" class="flex fit column">
<canvas id="canvas"/>
</div>
</template>
<script setup> // eslint-disable-line
import { onMounted } from 'vue'
onMounted(() => {
const canvasBody = document.getElementById("canvas")
const drawArea = canvasBody.getContext("2d")
const opts = {
particleColor: "rgb(200,200,200)",
lineColor: "rgb(200,200,200)",
particleAmount: 30,
defaultSpeed: 0.1,
variantSpeed: 1,
defaultRadius: 2,
variantRadius: 2,
linkRadius: 200
}
const delay = 200
let tid
const rgb = opts.lineColor.match(/\d+/g)
let w
let h
const particles = []
function resizeReset () {
w = canvasBody.width = window.innerWidth
h = canvasBody.height = window.innerHeight
}
function deBouncer () {
clearTimeout(tid)
tid = setTimeout(function() {
resizeReset()
}, delay)
}
function checkDistance (x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
}
function setup () {
resizeReset()
for (let i = 0; i < opts.particleAmount; i++){
particles.push(new Particle())
}
window.requestAnimationFrame(loop)
}
function loop() {
window.requestAnimationFrame(loop)
drawArea.clearRect(0, 0, w, h)
for (let i = 0; i < particles.length; i++){
particles[i].update()
particles[i].draw()
}
for (let i = 0; i < particles.length; i++){
linkPoints(particles[i], particles)
}
}
function linkPoints (point1, hubs){
for (let i = 0; i < hubs.length; i++) {
const distance = checkDistance(point1.x, point1.y, hubs[i].x, hubs[i].y)
const opacity = 1 - distance / opts.linkRadius
if (opacity > 0) {
drawArea.lineWidth = 0.5
drawArea.strokeStyle = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${opacity})`
drawArea.beginPath()
drawArea.moveTo(point1.x, point1.y)
drawArea.lineTo(hubs[i].x, hubs[i].y)
drawArea.closePath()
drawArea.stroke()
}
}
}
function Particle () {
this.x = Math.random() * w
this.y = Math.random() * h
this.speed = opts.defaultSpeed + Math.random() * opts.variantSpeed
this.directionAngle = Math.floor(Math.random() * 360)
this.color = opts.particleColor
this.radius = opts.defaultRadius + Math.random() * opts. variantRadius
this.vector = {
x: Math.cos(this.directionAngle) * this.speed,
y: Math.sin(this.directionAngle) * this.speed
};
this.update = function(){
this.border();
this.x += this.vector.x
this.y += this.vector.y
};
this.border = function(){
if (this.x >= w || this.x <= 0) {
this.vector.x *= -1;
}
if (this.y >= h || this.y <= 0) {
this.vector.y *= -1;
}
if (this.x > w) this.x = w
if (this.y > h) this.y = h
if (this.x < 0) this.x = 0
if (this.y < 0) this.y = 0
}
this.draw = function(){
drawArea.beginPath()
drawArea.arc(this.x, this.y, this.radius, 0, Math.PI*2)
drawArea.closePath()
drawArea.fillStyle = this.color
drawArea.fill()
}
}
window.addEventListener("resize", function(){ deBouncer() })
resizeReset()
setup()
})
</script>
<style>
#background-canvas-wrapper {
position: absolute !important;
display: block;
top: 0;
left: 0;
z-index: -1;
background: var(--q-primary);
margin: 0;
min-height: 100%;
}
</style>

View File

@@ -2,34 +2,33 @@
<div class="flex items-center no-wrap overflow-hidden w100"> <div class="flex items-center no-wrap overflow-hidden w100">
<q-icon v-if="customer?.email" size="md" class="q-mr-sm" name="mdi-account-circle-outline"/> <q-icon v-if="customer?.email" size="md" class="q-mr-sm" name="mdi-account-circle-outline"/>
<q-avatar v-else size="32px" class="q-mr-sm"> <q-avatar v-else size="32px" class="q-mr-sm">
<q-img v-if="tgUser?.photo_url" :src="tgUser.photo_url"/> <q-img class="fix-loader" v-if="tgUser?.photo_url && !imgError" :src="tgUser.photo_url" @error="imgError = true"/>
<q-icon v-else size="md" class="q-mr-sm" name="mdi-account-circle-outline"/> <q-icon v-else size="md" class="q-mr-sm" name="mdi-account-circle-outline"/>
</q-avatar> </q-avatar>
<span v-if="customer?.email" class="ellipsis"> <span v-if="customer?.email" class="ellipsis">
{{ customer.email }} {{ customer.email }}
</span> </span>
<span v-else class="ellipsis"> <span v-else class="ellipsis">
{{ {{ nameTgUser(tgUser) }}
tgUser?.first_name +
(tgUser?.first_name && tgUser?.last_name ? ' ' : '') +
tgUser?.last_name +
(!(tgUser?.first_name || tgUser?.last_name) ? tgUser?.username : '')
}}
</span> </span>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { inject } from 'vue' import { ref, inject } from 'vue'
import { useAuthStore } from 'stores/auth' import { useAuthStore } from 'stores/auth'
import type { WebApp } from '@twa-dev/types' import { nameTgUser } from 'utils/helpers'
import type { WebApp, WebAppUser } from '@twa-dev/types'
const authStore = useAuthStore() const authStore = useAuthStore()
const customer = authStore.customer const customer = authStore.customer
const tg = inject('tg') as WebApp const tg = inject('tg') as WebApp
const tgUser = tg.initDataUnsafe.user const tgUser = tg.initDataUnsafe.user as WebAppUser | undefined
const imgError = ref(false)
</script> </script>
<style lang="scss"> <style scoped lang="scss">
.fix-loader :deep(.q-spinner-mat) {
height: 100% !important;
}
</style> </style>

View File

@@ -0,0 +1,99 @@
<template>
<div
class="row no-wrap q-px-md q-py-md justify-between items-center bg-trasparent w100"
>
<q-btn
v-if="showCalendarBtn"
round unelevated
:color="calendarActive ? 'primary' : 't-secondary'"
:class="(calendarActive ? 'text-white' : 'text-grey') + (shadows ? ' shadow-t' : '')"
class="q-mr-sm"
@click="emit('toggle-calendar')"
>
<div>
<q-icon name="mdi-calendar-month-outline" size="sm"/>
<q-badge
color="red"
rounded
floating
transparent
style="position: relative; top: -6px; margin-left: -12px"
:style="{ opacity: calendarBadge ? 0.8 : 0 }"
/>
</div>
</q-btn>
<div class="w100" style="padding-top: 1px; padding-bottom: 1px;">
<q-input
v-model="search"
clearable
filled
bg-color="t-secondary"
clear-icon="close"
:placeholder="$t(placeholder)"
dense
class="rounded-fix"
:class="shadows ? 'shadow-t' : ''"
>
<template #prepend>
<q-icon name="mdi-magnify" color="grey"/>
</template>
</q-input>
</div>
<q-btn
v-if="showFilterBtn"
@click="emit('open-filters')"
round unelevated
:color="filterActive ? 'primary' : 't-secondary'"
:class="(filterActive ? 'text-white' : 'text-grey') + (shadows ? ' shadow-t' : '')"
class="q-ml-sm"
>
<div>
<q-icon name="mdi-filter-outline" size="sm"/>
<q-badge
color="red"
rounded
floating
transparent
style="position: relative; top: -6px; margin-left: -12px;"
:style="{ opacity: filterBadge ? 0.8 : 0 }"
/>
</div>
</q-btn>
<slot/>
</div>
</template>
<script setup lang="ts">
const search = defineModel<string>({
required: true,
default: ''
})
withDefaults(defineProps<{
placeholder?: string
showCalendarBtn?: boolean
showFilterBtn?: boolean
calendarActive?: boolean,
filterActive?: boolean,
calendarBadge?: boolean,
filterBadge?: boolean,
shadows?: boolean
}>(), {
placeholder: 'Search...',
showCalendarBtn: true,
showFilterBtn: true,
calendarActive: false,
filterActive: false,
calendarBadge: false,
filterBadge: false,
shadows: false
})
const emit = defineEmits([
'toggle-calendar',
'open-filters'
])
</script>

View File

@@ -50,43 +50,3 @@
} }
</script> </script>
<style>
</style>
<!-- <template>
<div
:style="{ backgroundColor: stringToColour(props.name) } "
class="fit flex items-center justify-center text-white"
>
{{ props.name ? props.name.substring(0, 1) : '' }}
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
name: string
}>()
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> -->

View File

@@ -0,0 +1,146 @@
<template>
<q-dialog
v-model="modelValue"
transition-show="slide-up"
transition-hide="slide-down"
position="bottom"
:persistent="loading"
>
<q-card
ref="cardRef"
class="fix-card-width column no-scroll no-wrap q-px-none overflow-hidden relative-position"
style="
border-top-left-radius: var(--top-radius) !important;
border-top-right-radius: var(--top-radius) !important;
max-height: calc(100vh - 48px - var(--tg-content-safe-area-inset-top) - var(--tg-safe-area-inset-top));
"
>
<div
ref="cardHeaderRef"
class="flex items-center no-wrap justify-between w100 q-my-none q-pa-md"
>
<q-resize-observer @resize="onHeaderResize" />
<div class="column q-mx-xs">
<span class="text-bold">
{{ $t(title) }}
</span>
<span v-if="caption" class="text-grey text-caption">
{{ $t(caption) }}
</span>
</div>
<q-space/>
<div
v-if="!loading"
class="flex items-center justify-between no-wrap"
>
<slot name="btnSlot">
<q-btn
icon="mdi-close"
dense flat round
v-close-popup
/>
</slot>
</div>
</div>
<div class="q-px-none q-ma-none">
<pn-shadow-scroll
:height="bodyHeight"
:bottomOffset
:topOffset
>
<div class="q-pa-none q-ma-none">
<q-resize-observer @resize="onInnerBodyResize" />
<slot />
</div>
</pn-shadow-scroll>
</div>
<div v-if="hasFooter" class="absolute-bottom">
<q-resize-observer @resize="onFooterResize" />
<div class="q-pa-md">
<slot name="footer" />
</div>
<div :style="{ height: tgInsetBottom + 'px' }" />
</div>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, useSlots } from 'vue'
const modelValue = defineModel<boolean>({ required: true })
const props = withDefaults(defineProps<{
title: string
caption?: string
maximized?: boolean
loading?: boolean
}>(), {
maximized: true,
loading: false
})
const slots = useSlots()
const cardRef = ref()
const headerHeight = ref(0)
const footerHeight = ref(0)
const bodyInnerHeight = ref(0)
const tgInsetBottom = ref(0)
const topOffset = 16
const windowHeight = ref(window.innerHeight)
const hasFooter = computed(() => !!slots['footer'])
const bottomOffset = computed(() => hasFooter.value ? footerHeight.value : tgInsetBottom.value)
const bodyHeight = computed(() => {
// hack for update computed params on window resize
const _trigger = windowHeight.value //eslint-disable-line
const el = cardRef.value?.$el
if (!el) return 0
const maxH = parseFloat(window.getComputedStyle(el).maxHeight) || 0
return props.maximized
? maxH - headerHeight.value
: Math.min(
bodyInnerHeight.value + topOffset + (hasFooter.value ? footerHeight.value : 0),
maxH - headerHeight.value
)
})
interface SizeParams { height: number; width: number }
const onHeaderResize = (size: SizeParams) => headerHeight.value = size.height
const onFooterResize = (size: SizeParams) => footerHeight.value = hasFooter.value ? size.height : 0
const onInnerBodyResize = (size: SizeParams) => bodyInnerHeight.value = size.height + 32
const handleWindowResize = () => {
windowHeight.value = window.innerHeight
}
const getTgInsetBottom = () => {
const value = getComputedStyle(document.documentElement)
.getPropertyValue('--tg-safe-area-inset-bottom')
.trim()
tgInsetBottom.value = parseFloat(value) || 0
}
onMounted(() => getTgInsetBottom())
onMounted(() => {
window.addEventListener('resize', handleWindowResize)
getTgInsetBottom()
})
onUnmounted(() => window.removeEventListener('resize', handleWindowResize))
</script>
<style scoped>
.fix-card-width {
width: var(--body-width) !important;
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<q-item>
<q-item-section avatar>
<q-icon :name="chat.icon" :color="chat.color" size="md"/>
</q-item-section>
<q-item-section>
<q-item-label>
<div class="row no-wrap items-center justify-between">
{{ $t(chat.label) }}
<span class="text-caption text-grey text-no-wrap" v-if="time">
{{ getDate(time) }}
</span>
</div>
</q-item-label>
<q-item-label class="text-grey text-caption">
{{ $t(chat.description) }}
</q-item-label>
</q-item-section>
</q-item>
</template>
<script setup lang="ts">
import { chatAccess, chatState } from 'utils/chats'
import { date } from 'quasar'
const props = defineProps<{
access?: 0 | 1 | 2
state?: 1 | 2 | 3 | 4
conference?: boolean
time?: number
}>()
const chat = props.access
? chatAccess(props.access)
: props.state
? chatState(props.state)
: props.conference
? { info: '', icon: 'mdi-video-box', color: 'grey', label: 'chat_type__label_conference', description: 'chat_type__description_conference' }
: { info: '', icon: '', color: '', label: '', description: '' }
function getDate (d: number) {
return new Date(d * 1000).getFullYear() === new Date(Date.now()).getFullYear()
? date.formatDate(d * 1000, 'D MMM')
: date.formatDate(d * 1000, 'D MMM YY')
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,32 @@
<template>
<div v-if="text.length <= tailLength" class="w100">
{{ text }}
</div>
<div v-else class="flex no-wrap w100">
<div class="col ellipsis col-shrink">
{{ textStart }}
</div>
<div class="col col-grow">
{{ textEnd }}
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = withDefaults(defineProps<{
text: string
tailLength?: number
}>(), {
tailLength: 15
})
const textStart = computed(() => {
return props.text.slice(0, props.text.length - props.tailLength)
})
const textEnd = computed(() => {
return props.text.slice(-props.tailLength)
})
</script>

View File

@@ -0,0 +1,64 @@
<template>
<!-- <div :style="{ visibility: !modelValue ? 'inherit' : 'hidden' }">
<slot name="btn"/>
</div> -->
<!-- <div
:class="modelValue ? 'absolute' : ''"
:style="{ left: fabPosLeft + 'px', top: fabPosTop + 'px' }"
>
<slot name="btn"/>
</div> -->
<q-dialog v-model="modelValue" :transition-duration="0">
<div class="position-relative q-px-md">
<q-card
class="q-pa-none q-mx-md no-scroll rounded-card absolute"
:style="{ left: 0, top: fabPosTop - cardHeight - 16 + 'px' }"
align="center"
>
<q-resize-observer @resize="onCardResize"/>
<slot name="card"/>
</q-card>
<div
class="absolute bg-red"
:style="{ left: fabPosLeft + 100+ 'px', top: fabPosTop +100+ 'px', height: '100px', width: '100px' }"
id="fabDialog"
>
{{ fabPosLeft }} {{ fabPosTop }}
</div>
</div>
</q-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const modelValue = defineModel<boolean>({
required: true
})
withDefaults(defineProps<{
fabPosLeft?: number
fabPosTop?: number
}>(),
{
fabPosLeft: 0,
fabPosTop: 0
}
)
interface sizeParams {
height: number,
width: number
}
const cardHeight = ref(0)
function onCardResize (size: sizeParams) {
cardHeight.value = size.height
}
</script>
<style scoped>
</style>

121
src/components/pnGlass.vue Normal file
View File

@@ -0,0 +1,121 @@
<template>
<div class="glass-wrapper">
<div class="glass-effect" :style="glassStyle"></div>
<div class="glass-tint"></div>
<div class="glass-shine"></div>
<div class="glass-slot">
<slot/>
</div>
<svg style="display: none">
<filter
id="glass-distortion"
x="0%"
y="0%"
width="100%"
height="100%"
filterUnits="objectBoundingBox"
>
<feTurbulence
type="fractalNoise"
baseFrequency="0.01 0.01"
numOctaves="1"
seed="3"
result="turbulence"
/>
<!-- Seeds: 14, 17, -->
<feComponentTransfer in="turbulence" result="mapped">
<feFuncR type="gamma" amplitude="1" exponent="10" offset="0.5" />
<feFuncG type="gamma" amplitude="0" exponent="1" offset="0" />
<feFuncB type="gamma" amplitude="0" exponent="1" offset="0.5" />
</feComponentTransfer>
<feGaussianBlur in="turbulence" stdDeviation="3" result="softMap" />
<feSpecularLighting
in="softMap"
surfaceScale="5"
specularConstant="1"
specularExponent="100"
lighting-color="white"
result="specLight"
>
<fePointLight x="-200" y="-200" z="300" />
</feSpecularLighting>
<feComposite
in="specLight"
operator="arithmetic"
k1="0"
k2="1"
k3="1"
k4="0"
result="litImage"
/>
<feDisplacementMap
in="SourceGraphic"
in2="softMap"
scale="150"
xChannelSelector="R"
yChannelSelector="G"
/>
</filter>
</svg>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
color?: string
}>()
const glassStyle = computed(() => ({
'--glass-color': props.color ?? 'white'
}))
</script>
<style scoped lang="scss">
.glass-wrapper {
display: flex;
position: relative;
overflow: hidden;
inset: 0;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 2.2);
}
.glass-effect {
position: absolute;
z-index: 0;
inset: 0;
backdrop-filter: blur(3px);
/* filter: url(#glass-distortion); */
overflow: hidden;
}
.glass-tint {
z-index: 1;
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.8);
}
.glass-shine {
position: absolute;
inset: 0;
z-index: 2;
overflow: hidden;
box-shadow: inset 2px 2px 1px 0 rgba(255, 255, 255, 0.5),
inset 0px 0px 1px 1px rgba(255, 255, 255, 0.5);
}
.glass-slot {
z-index: 3;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div>
<div
class="q-pa-none"
:class="typeClass"
:style="{ backgroundColor: bgColor ? getColorWithOpacity(color, bgColor) : getColorWithOpacity(color) }"
>
<slot/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { getColorWithOpacity } from 'utils/colors'
const props = defineProps<{
type: string
color: string
bgColor?: string
}>()
const typeClass = computed(() => {
switch (props.type) {
case 'fab': return 'q-btn--fab q-btn--rounded'
case 'round': return 'q-btn--rounded'
case 'rounded': return 'q-btn--rounded'
default: return ''
}
})
</script>
<style scoped>
</style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div> <div class="q-mb-sm">
<div <div
class="relative-position" class="relative-position"
:style="{ :style="{
@@ -14,7 +14,7 @@
:style="{ display: 'none' }" :style="{ display: 'none' }"
@update:model-value="handleUpload()" @update:model-value="handleUpload()"
:filter="checkImgType" :filter="checkImgType"
accept="image/*" :accept="allowedMimeTypes.join(', ')"
/> />
<q-icon <q-icon
v-if="modelValue === '' || modelValue === undefined || modelValue === null" v-if="modelValue === '' || modelValue === undefined || modelValue === null"
@@ -30,39 +30,31 @@
:style="{ :style="{
height: sizePx, height: sizePx,
maxWidth: sizePx, maxWidth: sizePx,
borderRadius: avatar ? String(size/2) + 'px' : 'var(--top-raduis)', borderRadius: avatar ? String(size/2) + 'px' : 'var(--top-radius)',
}" }"
@click="showDialog = true" @click="imgFileSelectorClick"
/> />
</div>
<q-dialog v-model="showDialog">
<q-card class="w100 relative-position" style="height: auto;">
<q-img :src="modelValue"/>
<div
class="flex row items-center jutsify-center q-pb-sm"
style="bottom: 0; position: absolute; left: 50%; transform: translate(-50%, 0%);">
<q-btn <q-btn
v-for="btn in menuBtns" v-if="modelValue"
:key="btn.name" icon="mdi-delete-outline"
:icon="btn.icon" round
@click="btn.f"
class="q-mx-xs bg-white"
round flat
style="opacity: 0.8"
color="primary" color="primary"
size="md"
style="border: solid 2px white; bottom: -16px; right: -16px;"
class="absolute-bottom-right"
@click="deleteImage"
/> />
</div> </div>
</q-card>
</q-dialog>
</div> </div>
</template> </template>
<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 | null>()
const props = defineProps<{ const props = defineProps<{
size?: number size?: number
@@ -70,16 +62,17 @@
avatar?: boolean avatar?: boolean
}>() }>()
const compressionOptions = {
maxSizeMB: 0.5, // Max size ~500 KB
maxWidthOrHeight: 1200, // Max resolution
useWebWorker: true, // Performance
fileType: 'image/jpeg' // Output format
}
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)
const iconsize = ref<number>(props.iconsize ? props.iconsize : 75) const iconsize = ref<number>(props.iconsize ? props.iconsize : 75)
const showDialog = ref<boolean>(false)
const menuBtns = [
{ name: 'change', icon: 'mdi-swap-horizontal', f: imgFileSelectorClick },
{ name: 'delete', icon: 'mdi-delete-outline', f: deleteImage },
{ name: 'close', icon: 'mdi-close', f: () => showDialog.value = false }
]
const sizePx = computed(() => { const sizePx = computed(() => {
return String(size.value) + 'px' return String(size.value) + 'px'
@@ -87,19 +80,29 @@
async function handleUpload() { async function handleUpload() {
if (imageFile.value) { if (imageFile.value) {
try {
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) const img = await imgToBase64(imageFile.value)
modelValue.value = typeof img === 'string' ? img : '' modelValue.value = typeof img === 'string' ? img : ''
} }
} }
}
function imgFileSelectorClick () { function imgFileSelectorClick () {
imgFileSelector.value.pickFiles() imgFileSelector.value.pickFiles()
} }
function deleteImage () { function deleteImage () {
showDialog.value = false
imageFile.value = null imageFile.value = null
modelValue.value = '' modelValue.value = null
} }
function imgToBase64 (file: File): Promise<string | ArrayBuffer | null> { function imgToBase64 (file: File): Promise<string | ArrayBuffer | null> {
@@ -112,14 +115,20 @@
reject(new Error('Something went wrong')) reject(new Error('Something went wrong'))
} }
reader.onload = () => { reader.onload = () => resolve(reader.result)
resolve(reader.result)
}
}) })
} }
const allowedMimeTypes = [
'image/jpeg',
'image/png',
'image/bmp',
'image/webp',
'image/gif'
]
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' ) return files.filter((file: File) => allowedMimeTypes.includes(file.type))
} }
</script> </script>

View File

@@ -0,0 +1,64 @@
<template>
<div class="row no-wrap items-center w100">
<div
v-if="item1"
class="second-line-item flex no-wrap items-center q-mr-sm"
>
<q-icon
v-if="item1Icon"
:name="item1Icon"
:color="item1IconColor"
class="q-mr-none"
/>
<div
class="ellipsis"
:class="item1Class ?? '' "
>
{{ item1 }}
</div>
</div>
<div
v-if="item2"
class="second-line-item flex no-wrap items-center"
>
<q-icon
v-if="item2Icon"
:name="item2Icon"
:color="item2IconColor"
class="q-mr-none"
/>
<div
class="ellipsis"
:class="item2Class ?? '' "
>
{{ item2 }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
item1: string | null
item1Class?: string
item1Icon?: string
item1IconColor?: string
item2: string | null
item2Class?: string
item2Icon?: string
item2IconColor?: string
}>()
</script>
<style scoped>
.second-line-item {
display: flex;
align-items: center;
flex: 0 1 auto;
min-width: 0;
max-width: 60%;
}
</style>

View File

@@ -14,7 +14,7 @@
/> />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label> <q-item-label class="text-bold">
{{ $t(title) }} {{ $t(title) }}
</q-item-label> </q-item-label>
<q-item-label caption> <q-item-label caption>
@@ -26,66 +26,19 @@
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-dialog <pn-bottom-sheet-dialog
:title
:caption
v-model="showDialog" v-model="showDialog"
maximized
transition-show="slide-up"
transition-hide="slide-down"
position="bottom"
> >
<q-card <div class="q-px-md">
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>
<div class="flex column q-mx-xs">
<span class="text-h6 ellipsis">{{ $t(title) }}</span>
<span v-if="caption" class="text-grey text-caption">{{ $t(caption) }}</span>
</div>
</div>
<div class="flex items-center justify-between no-wrap">
<q-btn
icon="mdi-close"
@click="showDialog=false"
flat round
/>
</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/> <slot/>
</div> </div>
</pn-shadow-scroll> </pn-bottom-sheet-dialog>
</div>
<div
ref="cardFooterRef"
class="q-pa-md"
>
<slot name="footer"/>
</div>
</q-card>
</q-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue' import { ref } from 'vue'
import { useSlots } from 'vue'
defineProps<{ defineProps<{
title: string title: string
@@ -95,31 +48,7 @@
}>() }>()
const showDialog=ref<boolean>(false) const showDialog=ref<boolean>(false)
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
bodyHeight.value = window.innerHeight - headerHeight.value - footerHeight.value - 48
}
watch(() => slots.body?.(), updateDimensions, { flush: 'post' })
</script> </script>
<style scoped> <style scoped>
.fix-card-width {
width: var(--body-width) !important;
}
</style> </style>

View File

@@ -4,20 +4,23 @@
v-for="(option, index) in options" v-for="(option, index) in options"
:key="index" :key="index"
flat flat
no-caps dense no-caps
class="w100" dense
class="w100 q-pt-none"
align="left" align="left"
@click="selectItem(option)" @click="model = option.value"
:class="isSelected(option.value) ? 'text-primary' : ''" :class="isSelected(option.value) ? 'text-primary' : ''"
> >
<q-icon <q-radio
:name="isSelected(option.value) ? 'mdi-check' : ''" v-model="model"
:val="option.value"
size="md"
color="primary" color="primary"
class="q-pr-sm" class="q-mr-xs"
@click.stop
/> />
<span
:class="!isSelected(option.value) ? 'text-weight-regular' : ''" <span :class="!isSelected(option.value) ? 'text-weight-regular' : ''">
>
{{ $te(option.label) ? $t(option.label) : option.label }} {{ $te(option.label) ? $t(option.label) : option.label }}
</span> </span>
</q-btn> </q-btn>
@@ -43,10 +46,6 @@
const isSelected = computed(() => (value: string | number) => { const isSelected = computed(() => (value: string | number) => {
return model.value === value return model.value === value
}) })
const selectItem = (option: ListOption) => {
model.value = option.value
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -21,7 +21,7 @@
<q-icon <q-icon
v-if="showIcon" v-if="showIcon"
:name = "icon" :name = "icon"
color="brand" color="primary"
size="160px" size="160px"
class="icon-position" class="icon-position"
/> />

View File

@@ -2,36 +2,35 @@
<div class="flex w100 column q-pt-xl q-pa-md"> <div class="flex w100 column q-pt-xl q-pa-md">
<div class="flex column justify-center col-grow items-center text-grey"> <div class="flex column justify-center col-grow items-center text-grey">
<q-btn <q-btn
flat flat no-caps
no-caps
@click="handleClick" @click="handleClick"
v-if="!noBtn" v-if="!noBtn"
> >
<div class="flex column justify-center col-grow items-center"> <div class="flex column justify-center col-grow items-center">
<q-icon :name="icon" size="160px" class="q-pb-md"/> <q-icon :name="icon" size="160px" class="q-pb-md"/>
<div class="text-h6 text-brand"> <div class="text-h6 text-bold text-primary text-center">
{{message1}} {{ $t(message1) }}
</div> </div>
</div> </div>
</q-btn> </q-btn>
<div v-else> <div v-else>
<div class="flex column justify-center col-grow items-center"> <div class="flex column justify-center col-grow items-center">
<q-icon :name="icon" size="160px" class="q-pb-md"/> <q-icon :name="icon" size="160px" class="q-pb-md"/>
<div class="text-h6 text-brand"> <div class="text-h6 text-bold text-primary text-center">
{{message1}} {{ $t(message1) }}
</div> </div>
</div> </div>
</div> </div>
<div v-if="message2" class="text-caption" align="center"> <div v-if="message2" class="text-caption text-center" style="max-width: min(75vw, calc(var(--body-width) * 0.75));">
{{message2}} {{ $t(message2) }}
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ defineProps<{
icon: string icon: string
message1: string message1: string
message2?: string message2?: string
@@ -39,11 +38,5 @@
}>() }>()
const emit = defineEmits(['btn-click']) const emit = defineEmits(['btn-click'])
const handleClick = () => emit('btn-click')
const handleClick = () => {
emit('btn-click')
}
</script> </script>
<style>
</style>

View File

@@ -1,8 +1,8 @@
<template> <template>
<div <div
id="overlay" id="overlay"
class="fixed-full q-dialog__backdrop" class="q-dialog__backdrop absolute-top"
style="z-index: 5000; --q-transition-duration: 300ms;" style="z-index: 5000; --q-transition-duration: 300ms; height: 100vh; width: 10vw;"
@click.prevent.stop @click.prevent.stop
@touchstart.prevent.stop @touchstart.prevent.stop
@touchmove.prevent.stop @touchmove.prevent.stop
@@ -13,6 +13,5 @@
<script setup lang="ts"> <script setup lang="ts">
</script> </script>
<style> <style scoped>
</style> </style>

View File

@@ -1,18 +1,201 @@
<template> <template>
<div <div
class="flex items-center justify-between q-ma-none q-py-none q-px-md text-white text-h6 no-scroll no-wrap w100" class="w100 col-grow relative-position bg-white overflow-hidden"
style="min-height: 48px"
> >
<slot name="title"/> <q-resize-observer @resize="onCardResize"/>
<div
id="card-header"
class="absolute-top overflow-hidden"
v-if="hasHeader"
style="z-index: 100;"
>
<q-resize-observer @resize="onHeaderResize"/>
<slot name="card-header"/>
</div> </div>
<div
id="card-under-header"
class="absolute-top"
v-if="hasUnderHeader"
style="z-index: 100;"
:style="{ top: headerHeight + 'px' }"
>
<slot name="card-under-header"/>
</div>
<div
id="card-inner-header"
v-if="hasInnerHeader"
style="z-index: 100;"
>
<q-resize-observer @resize="onInnerHeaderResize"/>
<slot name="card-inner-header"/>
</div>
<pn-shadow-scroll
ref="shadowScrollRef"
:height="cardHeight - innerHeaderHeight"
:topOffset="headerHeight"
:bottomOffset
v-bind="$attrs"
class="overflow-hidden top-border"
@focusin="onFocusIn"
@editor-cursor-update="onEditorCursorUpdate"
@updateScrollPosition="onScroll"
:class="{ 'has-top-border': isScrolled }"
>
<slot/> <slot/>
<div class="bg-white w100 q-ma-none q-px-md flex items-center"> </pn-shadow-scroll>
<slot name="footer"/>
<div
class="absolute-bottom q-px-md q-py-md"
id="card-actions"
v-if="hasActions"
style="z-index: 100;"
>
<q-resize-observer @resize="onActionsResize"/>
<slot name="card-actions"/>
<div style="height: var(--tg-safe-area-inset-bottom)"/>
</div>
<div
class="absolute column q-px-md q-gutter-y-md"
:class="hasActions || tabsSlotHeight ? 'q-pb-none' : 'q-pb-md'"
:style="{ bottom: bottomOffset + 'px', right: '0' }"
v-if="hasFAB"
style="z-index: 100;"
>
<slot name="card-fab"/>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useSlots, computed, ref, onMounted, getCurrentInstance } from 'vue'
const app = getCurrentInstance()
const isMobile = app?.appContext.config.globalProperties.$isMobile
const props = withDefaults(defineProps<{
tabsSlotHeight?: number
}>(),
{
tabsSlotHeight: 0
}
)
const slots = useSlots()
const hasHeader = computed(() => !!slots['card-header'])
const hasUnderHeader = computed(() => !!slots['card-under-header'])
const hasInnerHeader = computed(() => !!slots['card-inner-header'])
const hasActions = computed(() => !!slots['card-actions'])
const hasFAB = computed(() => !!slots['card-fab'])
const cardHeight = ref(0)
const headerHeight = ref(16)
const innerHeaderHeight = ref(0)
const actionsHeight = ref(0)
interface sizeParams {
height: number,
width: number
}
function onCardResize (size: sizeParams) {
cardHeight.value = size.height
}
function onHeaderResize (size: sizeParams) {
headerHeight.value = size.height
}
function onInnerHeaderResize (size: sizeParams) {
innerHeaderHeight.value = size.height
}
function onActionsResize (size: sizeParams) {
actionsHeight.value = size.height
}
const tgInsetBottom = ref(0)
const getTgInsetBottom = () => {
const value = getComputedStyle(document.documentElement)
.getPropertyValue('--tg-safe-area-inset-bottom')
.trim()
tgInsetBottom.value = parseFloat(value) || 0
}
onMounted(() => getTgInsetBottom())
const bottomOffset = computed(() => {
return Math.max(props.tabsSlotHeight, actionsHeight.value, tgInsetBottom.value)
})
// smart scroll only for page with editable content
const shadowScrollRef = ref()
function onFocusIn(e: FocusEvent) {
if (!isMobile) return
const target = e.target as HTMLElement
if (!['INPUT', 'TEXTAREA'].includes(target.tagName)) return
const viewport = shadowScrollRef.value?.$el?.querySelector('.q-scrollarea__container') as HTMLElement
if (!viewport) return
setTimeout(() => {
const field = target.closest('.q-field') as HTMLElement || target
const fieldRect = field.getBoundingClientRect()
const viewportRect = viewport.getBoundingClientRect()
const fieldBottomRelative = fieldRect.bottom - viewportRect.top
const marginBottom = 0
const availableHeight = viewport.offsetHeight - actionsHeight.value - marginBottom
if (fieldBottomRelative > availableHeight) {
const scrollDiff = fieldBottomRelative - availableHeight
viewport.scrollTo({
top: viewport.scrollTop + scrollDiff,
behavior: 'smooth'
})
}
}, 1000) // fix apple slow keyboard
}
function onEditorCursorUpdate(e: Event) {
if (!isMobile) return
const coords = (e as CustomEvent).detail as { top: number, bottom: number } | null
if (!coords) return
const scrollEl = shadowScrollRef.value?.$el
const viewport = scrollEl?.querySelector('.q-scrollarea__container') as HTMLElement
if (!viewport) return
const viewportRect = viewport.getBoundingClientRect()
const cursorBottomRelativeToViewport = coords.bottom - viewportRect.top
const visibleThreshold = viewport.offsetHeight - actionsHeight.value
if (cursorBottomRelativeToViewport > visibleThreshold) {
const scrollDiff = cursorBottomRelativeToViewport - visibleThreshold
viewport.scrollBy({ top: scrollDiff, behavior: 'smooth' })
}
}
const isScrolled = ref(false)
const onScroll = (pos: number) => {
isScrolled.value = pos > 1
}
</script> </script>
<style> <style scoped lang="scss">
.top-border {
border-top: 1px solid transparent;
transition: border-color 0.3s ease;
}
.has-top-border {
border-color: $t-section-separator-color;
}
</style> </style>

View File

@@ -0,0 +1,19 @@
<template>
<pn-page-wrapper v-bind="$attrs">
<template #title>
<slot name="title"/>
</template>
<pn-page-card v-bind="$attrs">
<template v-for="(_, name) in $slots" v-slot:[name]>
<slot :name="name"/>
</template>
</pn-page-card>
</pn-page-wrapper>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,53 @@
<template>
<div
id="page-wrapper"
class="w100 relative-position column no-wrap no-scroll overflow-hidden"
style="height: 100vh;"
>
<div
id="tg-area-inset-top"
style="height: var(--tg-safe-area-inset-top) !important; flex-shrink: 0;"
/>
<div
id="tg-safe-area"
class="flex justify-center items-center overflow-hidden"
style="height: var(--tg-content-safe-area-inset-top) !important; flex-shrink: 0;"
>
<base-logo text-color="primary" :withBackground="false" animated/>
</div>
<div
class="text-primary text-bold flex items-center justify-between q-ma-none q-py-none q-px-md text-h6 no-scroll no-wrap w100"
style="min-height: 48px;"
id="page-title"
>
<slot name="title"/>
</div>
<div
class="column w100"
style="flex: 1 1 auto; min-height: 0;"
>
<slot/>
</div>
<div
id="tabs"
v-if="hasTabs"
class="absolute-bottom w100"
>
<slot name="tabs"/>
</div>
</div>
</template>
<script setup lang="ts">
import { useSlots, computed } from 'vue'
import BaseLogo from 'components/BaseLogo.vue'
const slots = useSlots()
const hasTabs = computed(() => !!slots.tabs)
</script>
<style scoped>
</style>

View File

@@ -1,94 +0,0 @@
<template>
<div
id="page-card"
class="w100 flex column glass-card top-rounded-card no-scroll no-wrap"
>
<div
id="card-body-header"
style="flex-shrink: 0; min-height: var(--top-raduis);"
>
<q-resize-observer @resize="onHeaderResize"/>
<slot name="card-body-header"/>
</div>
<div id="card-body" >
<q-resize-observer @resize="onBodyResize"/>
<pn-shadow-scroll :hideShadows="isResizing" :height="scrollAreaHeight">
<slot/>
</pn-shadow-scroll>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
const heightCard = ref(100)
const scrollAreaHeight = ref(100)
const headerHeight = ref(0)
interface sizeParams {
height: number,
width: number
}
async function onHeaderResize(size: sizeParams) {
headerHeight.value = size.height
await updateScrollAreaHeight()
}
async function onBodyResize(size: sizeParams) {
heightCard.value = size.height
await updateScrollAreaHeight()
}
async function updateScrollAreaHeight() {
await nextTick(() => {
scrollAreaHeight.value = Math.max(0, heightCard.value)
})
}
watch(heightCard, updateScrollAreaHeight)
watch(headerHeight, updateScrollAreaHeight)
const isResizing = ref(false)
let resizeTimer: ReturnType<typeof setTimeout> | null = null
watch(heightCard, () => {
isResizing.value = true
if (resizeTimer) {
clearTimeout(resizeTimer)
resizeTimer = null
}
resizeTimer = setTimeout(() => {
isResizing.value = false
resizeTimer = null
}, 150)
})
</script>
<style scoped>
.glass-card {
opacity: 1 !important;
background-color: white;
}
#page-card {
flex: 1 0 auto;
min-height: 0;
overflow: hidden;
}
#card-body {
overflow: hidden;
flex: 1 1 0;
min-height: 0;
position: relative;
}
#card-body :deep(.q-scrollarea__content) {
width: 100% !important;
}
</style>

View File

@@ -1,107 +1,152 @@
<template> <template>
<div :class="{'fix-scroll-area-content': hideShadows }">
<q-scroll-area <q-scroll-area
:style="{ height: height + 'px' }" ref="scrollAreaRef"
class="w100 q-pa-none q-ma-none" :style="scrollStyle"
:vertical-offset="verticalOffset"
:thumb-style="thumbStyle"
@scroll="onScroll" @scroll="onScroll"
:class=" { class="shadow-scroll position-relative w100"
'shadow-top': hasScrolled,
'shadow-bottom': hasScrolledBottom
}"
> >
<q-resize-observer @resize="onInnerResize" />
<div :style="topSpacerStyle" />
<slot /> <slot />
<div class="q-pa-sm"/> <div :style="{ height: bottom + 'px' }" />
</q-scroll-area> </q-scroll-area>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, computed, watch } from 'vue'
import { QScrollArea } from 'quasar'
defineProps<{ interface Props {
height: number height: number
hideShadows: boolean topOffset?: number
}>() bottomOffset?: number
scrollPosition?: number
const hasScrolled = ref(false) offset?: number
const hasScrolledBottom = ref(false) scrollHeaderSizeFull?: number
scrollHeaderSizeSticky?: number
interface ScrollInfo {
verticalPosition: number;
verticalPercentage: number;
verticalSize: number;
verticalContainerSize: number;
} }
function onScroll(info: ScrollInfo) { const props = withDefaults(defineProps<Props>(), {
hasScrolled.value = info.verticalPosition > 0 scrollPosition: 0,
const scrollEnd = info.verticalPosition + info.verticalContainerSize >= info.verticalSize - 1 offset: 16,
hasScrolledBottom.value = !scrollEnd scrollHeaderSizeFull: 0,
scrollHeaderSizeSticky: 0
})
const emit = defineEmits([
'updateScrollPosition',
'resizeScrollArea'
])
const scrollAreaRef = ref<QScrollArea | null>(null)
const currentScroll = ref(0)
const innerHeight = ref(0)
const top = computed(() => props.topOffset || props.offset)
const bottom = computed(() => props.bottomOffset || props.offset)
const offsetBefore = computed(() => {
if (props.scrollHeaderSizeFull && props.scrollHeaderSizeSticky) {
return Math.max(
props.scrollHeaderSizeFull - currentScroll.value,
props.scrollHeaderSizeSticky
)
}
return 0
})
const verticalOffset = computed<[number, number]>(() => [
top.value + offsetBefore.value,
bottom.value
])
const thumbStyle = {
borderRadius: '3px',
background: '#999',
width: '6px',
opacity: '0.4'
} }
const scrollStyle = computed(() => {
const isHeaderSticky = props.scrollHeaderSizeFull && props.scrollHeaderSizeSticky
? (props.scrollHeaderSizeFull - currentScroll.value <= props.scrollHeaderSizeSticky)
: true
return {
height: props.height + 'px',
'--top-height': top.value + 'px',
'--bottom-height': bottom.value + 'px',
'--offset-before': offsetBefore.value + 'px',
'--top-shadow-visibility': isHeaderSticky ? 'visible' : 'hidden'
}
})
const topSpacerStyle = computed(() => ({
height: top.value + 'px',
visibility: offsetBefore.value === 0 ? 'visible' as const : 'hidden' as const,
marginTop: offsetBefore.value === 0 ? '0px' : `-${top.value}px`
}))
function onScroll ({ verticalPosition }: { verticalPosition: number }) {
currentScroll.value = verticalPosition
emit('updateScrollPosition', verticalPosition)
}
function onInnerResize (size: { height: number }) {
innerHeight.value = size.height
emit('resizeScrollArea', {
height: props.height,
innerHeight: size.height,
topOffset: top.value,
bottomOffset: bottom.value
})
}
watch(() => props.scrollPosition, (newPos) => {
if (newPos !== currentScroll.value) {
scrollAreaRef.value?.setScrollPosition('vertical', newPos)
}
})
watch([top, bottom, () => props.height], () => {
emit('resizeScrollArea', {
height: props.height,
innerHeight: innerHeight.value,
topOffset: top.value,
bottomOffset: bottom.value
})
})
</script> </script>
<style scoped> <style scoped>
.q-scrollarea { .shadow-scroll:before,
position: relative; .shadow-scroll:after {
transform: translateY(0); content: "";
}
.q-scrollarea::before {
content: '';
position: absolute; position: absolute;
top: 0;
left: 0; left: 0;
right: 0; width: 100%;
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; pointer-events: none;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); z-index: 95;
opacity: 0; transition: opacity 0.2s ease;
transform: translateY(-8px);
will-change: opacity, transform;
z-index: 1;
} }
.q-scrollarea::after { .shadow-scroll:before {
content: ''; top: var(--offset-before);
position: absolute; height: var(--top-height);
background: linear-gradient(to bottom, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
visibility: var(--top-shadow-visibility);
}
.shadow-scroll:after {
bottom: 0; bottom: 0;
left: 0; height: var(--bottom-height);
right: 0; background: linear-gradient(to top, rgba(255, 255, 255, 1), rgba(255, 255, 255, 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) { .shadow-scroll :deep(.q-scrollarea__content) {
content: none; max-width: 100%;
}
.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> </style>

View File

@@ -0,0 +1,103 @@
<template>
<q-dialog
v-model="modelValue"
class="q-ma-none"
>
<q-card
class="q-pa-none q-ma-none w100 q-pb-md no-scroll rounded-card no-wrap overflow-hidden"
ref="cardRef"
style="
max-height: calc(100vh - 48px - 2 * max(var(--tg-content-safe-area-inset-top) + var(--tg-safe-area-inset-top), var(--tg-safe-area-inset-bottom)));
width: calc(100vw - 32px);
"
>
<q-resize-observer @resize="onResizeCard" />
<q-card-section class="row items-center q-pb-none" ref="cardHeaderRef">
<div class="flex no-wrap items-center w100">
<q-resize-observer @resize="onHeaderResize"/>
<span class="text-bold">
{{ $t(title)}}
</span>
<q-space/>
<q-btn
icon="mdi-close"
dense flat round
v-close-popup
/>
</div>
</q-card-section>
<div
ref="cardBodyRef"
class="q-px-none q-ma-none"
>
<pn-shadow-scroll
:height="bodyHeight"
>
<div ref="cardBodyInnerRef" class="q-pa-none q-ma-none">
<q-resize-observer @resize="updateDimensions" />
<slot/>
</div>
</pn-shadow-scroll>
</div>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import { useSlots } from 'vue'
const modelValue = defineModel<boolean>({
required: true
})
defineProps<{
title: string
}>()
const slots = useSlots()
const cardBodyInnerRef = ref<HTMLElement | null>(null)
const headerHeight = ref(0)
const bodyInnerHeight = ref(0)
const bodyHeight = ref(0)
const cardHeight = ref(0)
interface sizeParams {
height: number,
width: number
}
function onResizeCard (size :sizeParams) {
cardHeight.value = size.width
}
function onHeaderResize (size: sizeParams) {
headerHeight.value = size.height
}
const updateDimensions = async () => {
await nextTick(() => {
bodyInnerHeight.value = cardBodyInnerRef.value?.offsetHeight || 0
const maxH = cardMaxHeight()
const availableForBody = maxH - headerHeight.value - 32 // 16 - md-padding + 16px - shadow-block
bodyHeight.value = Math.min(bodyInnerHeight.value + 32, availableForBody)
})
}
watch(() => slots.body?.(), updateDimensions, { flush: 'post' })
watch(modelValue, updateDimensions)
const cardRef = ref()
function cardMaxHeight () {
const el = cardRef.value?.$el
if (el) {
const computedStyle = window.getComputedStyle(el)
return parseFloat(computedStyle.maxHeight)
}
return 0
}
</script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<q-dialog v-model="modelValue"> <q-dialog v-model="modelValue">
<q-card <q-card
class="q-pa-none q-ma-none w100 no-scroll" class="q-pa-none q-ma-none w100 no-scroll rounded-card"
align="center" align="center"
> >
<q-card-section> <q-card-section>
@@ -37,7 +37,7 @@
@click="emit('clickAuxBtn')" @click="emit('clickAuxBtn')"
/> />
</div> </div>
<div class="col-grow"> <div class="col-grow" v-if="!hideMainBtn">
<q-btn <q-btn
:label="$t(mainBtnLabel)" :label="$t(mainBtnLabel)"
:color="color" :color="color"
@@ -67,7 +67,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ withDefaults(defineProps<{
icon?: string icon?: string
color?: string color?: string
title: string title: string
@@ -75,7 +75,10 @@
message2?: string message2?: string
mainBtnLabel: string mainBtnLabel: string
auxBtnLabel?: string auxBtnLabel?: string
}>() hideMainBtn?: boolean
}>(), {
hideMainBtn: false
})
const modelValue = defineModel<boolean>({ const modelValue = defineModel<boolean>({
required: true required: true
@@ -86,8 +89,4 @@
'clickAuxBtn', 'clickAuxBtn',
'close' 'close'
]) ])
</script> </script>
<style scoped>
</style>

235
src/components/pnTabs.vue Normal file
View File

@@ -0,0 +1,235 @@
<template>
<pn-page-wrapper>
<template #title>
<slot name="title" />
</template>
<q-tab-panels
id="tab-panels"
v-model="tabSelect"
keep-alive
class="bg-transparent w100"
style="flex-grow: 1;"
>
<q-tab-panel
v-for="tab in tabs"
:key="tab.name"
:name="tab.name"
class="q-pa-none column w100"
>
<router-view :tabsSlotHeight/>
</q-tab-panel>
</q-tab-panels>
<template #tabs>
<div class="q-pa-none q-ma-none">
<q-resize-observer @resize="onTabsSlotResize"/>
<div class="q-py-md q-px-md overflow-hidden rounded-fix">
<div style="height: 0px;" class="row no-wrap no-pointer-events overflow-hidden">
<div
v-for="(tab, idx) in tabs"
:key="idx"
class="text-caption text-bold"
>
<q-resize-observer @resize="val => onTabResize(val, idx)"/>
{{ $t(tab.label) }}
</div>
</div>
<div
id="tabs-wrapper"
class="bg-white shadow-t rounded-fix"
:style="minWidthTab"
>
<q-tabs
v-model="tabSelect"
dense
align="center"
id="tabs"
:breakpoint="0"
class="w100 text-grey-7 "
style="z-index: 1000;"
>
<q-route-tab
v-for="tab in tabs"
:to="{ name: tab.name }"
:name="tab.name"
:key="tab.name"
:ripple="false"
no-caps
dense
:class="tabSelect === tab.name ? 'active' : ''"
>
<div class="column items-center">
<q-icon :name="tab.icon + (tabSelect === tab.name ? '' : '-outline')" :size="config?.icon"/>
<span
v-if="config?.showLabel"
class="text-caption tab-label text-bold"
>
{{ $t(tab.label) }}
</span>
</div>
</q-route-tab>
<q-resize-observer @resize="size => tabsWidth = size.width" />
</q-tabs>
</div>
<div class="bg-transparent"
style="height: var(--tg-safe-area-inset-bottom)"
/>
</div>
</div>
</template>
</pn-page-wrapper>
</template>
<script setup lang="ts">
import { ref, onBeforeMount, computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
interface Tab {
name: string
label: string
icon: string
}
const props = defineProps<{
tabs: Tab[]
}>()
const tabSelect = ref<string>()
onBeforeMount(() => {
const initialTab = props.tabs.find(t => t.name === route.name)?.name || props.tabs[0]?.name || ''
tabSelect.value = initialTab
})
const tabsSlotHeight = ref(0)
interface sizeParams {
height: number,
width: number
}
function onTabsSlotResize(size: sizeParams) {
tabsSlotHeight.value = size.height
}
// hidden icon name if overflow - with resize-observer
const tabsWidth = ref(0)
const widthTabLabel = ref(new Array(props.tabs.length).fill(0))
function onTabResize (size: sizeParams, idx: number) {
if (size.width) widthTabLabel.value[idx] = size.width
}
const config = computed(() => {
const ICON_SM = 24
const ICON_MD = 32
const availableWidth = tabsWidth.value - 2 * 8
const variants = [
{ icon: 'sm', gap: 16, showLabel: true },
{ icon: 'sm', gap: 8, showLabel: true },
{ icon: 'md', gap: 16, showLabel: false },
{ icon: 'md', gap: 8, showLabel: false },
{ icon: 'sm', gap: 16, showLabel: false },
{ icon: 'sm', gap: 8, showLabel: false }
] as const
for (const v of variants) {
const iconW = v.icon === 'sm' ? ICON_SM : ICON_MD
let totalNeeded = 0
props.tabs.forEach((_, idx) => {
const labelW = v.showLabel ? widthTabLabel.value[idx] : 0
const contentW = Math.max(iconW, labelW)
totalNeeded += contentW + (v.gap * 2)
})
if (totalNeeded <= availableWidth || v === variants[variants.length - 1]) {
return v
}
}
return variants[variants.length - 1]
})
const minWidthTab = computed(() => {
const activeIdx = props.tabs.findIndex(el => el.name === tabSelect.value)
const currentGap = config.value?.gap ?? 8
const labelW = config.value?.showLabel ? widthTabLabel.value[activeIdx] : 0
const iconW = config.value?.icon === 'sm' ? 24 : 32
const activeContentWidth = Math.max(labelW, iconW)
const PADDING_AROUND = 3
return {
'--current-gap': `${currentGap}px`,
'--min-width-tab': '1px',
'--active-tab-width': (activeContentWidth + 16 * 2) + 'px',
'--tabs-height': tabsSlotHeight.value - 32 - PADDING_AROUND * 2 + 'px',
'--padding-around': PADDING_AROUND + 'px'
}
})
</script>
<style scoped lang="scss">
#tabs-wrapper {
padding: var(--padding-around);
}
#tabs :deep(.q-tabs__content) {
justify-content: space-evenly;
padding: 0 8px;
}
#tabs :deep(.q-tab) {
padding: 0 var(--current-gap);
}
#tabs :deep(.active) {
color: var(--q-primary)
}
#tabs :deep(.q-router-link--active .q-tab__indicator) {
height: calc(var(--tabs-height) - var(--tg-safe-area-inset-bottom, 0px));
border-radius: calc(var(--tabs-height) / 2);
overflow: hidden;
opacity: 0.1;
background-color: var(--q-primary);
}
#tabs :deep(.q-focus-helper) {
border-radius: calc(var(--tabs-height) / 2);
top: var(--tabs-y-padding);
border: 0px;
}
#tabs :deep(.q-tabs__arrow) {
display: none;
}
#tabs :deep(.q-tab__content) {
min-width: var(--min-width-tab);
padding-top: 5px;
padding-bottom: 1px;
}
#tabs :deep(.q-tab__content .tab-label) {
margin-top: -2px;
}
#tabs :deep(.q-tab--active .q-tab__indicator) {
width: var(--active-tab-width);
right: inherit;
left: inherit;
}
#tabs :deep(.q-focus-helper) {
width: calc(100% - var(--current-gap) * 2 + 32px) !important;
left: calc(var(--current-gap) - 16px);
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<q-item-section avatar>
<pn-auto-avatar
:img="item.photo"
:name="item.section1"
/>
</q-item-section>
<q-item-section>
<slot/>
<q-item-label lines="1" v-if="showStatus && getUserStatus(item) && getUserStatus(item)?.status">
<q-badge :color="getUserStatus(item)?.color">
{{ $t(getUserStatus(item)?.text ?? '') }}
</q-badge>
</q-item-label>
<q-item-label lines="1" class="text-bold" v-if="item.section1">
{{item.section1}}
</q-item-label>
<q-item-label lines="2" caption v-if="item.section3">
{{item.section3}}
</q-item-label>
<q-item-label caption v-if="item.section2_1 || item.section2_2">
<pn-inline-pair
:item1="item.section2_1 ? item.section2_1 :null"
item1Class="text-bold"
item1Icon="telegram"
item1IconColor="tgcolor"
:item2="item.section2_2 ? '@' + item.section2_2 : null"
item2Class="text-blue"
/>
</q-item-label>
</q-item-section>
</template>
<script setup lang="ts">
import { getUserStatus } from 'utils/users'
import pnInlinePair from 'components/pnInlinePair.vue'
import type { User } from 'types/User'
type UserListItem = User & {
section1: string
section2_1: string
section2_2: string
section3: string
is_blocked: boolean
is_leave: boolean
companyName?: string | null
}
defineProps<{
item: UserListItem
showStatus?: boolean
}>()
</script>

View File

@@ -1,36 +1,44 @@
<template> <template>
<pn-page-card> <pn-page-template>
<template #title> <template #title>
{{ $t(title) }} {{ $t(title) }}
</template> </template>
<template #footer> <template #card-actions>
<q-btn <q-btn
rounded color="primary" rounded color="primary"
class="w100 q-mt-md q-mb-xs fix-disabled-btn" class="w100 fix-disabled-btn"
:disable="!isFormValid" :disable="!isFormValid"
@click = "emit('update')" @click = "onSubmit"
> >
{{ $t(btnText) }} {{ $t(btnText) }}
</q-btn> </q-btn>
</template> </template>
<pn-scroll-list> <div class="flex column items-center q-pa-md q-pb-sm w100">
<div class="flex column items-center q-pa-md q-pb-sm">
<pn-image-selector <pn-image-selector
v-model="modelValue.logo" v-model="projectMod.logo"
:size="100" :size="100"
:iconsize="80" :iconsize="80"
type="rounded"
class="q-pb-lg" class="q-pb-lg"
/> />
<div class="q-gutter-y-lg w100">
<q-form
ref="formRef"
@validation-success="isFormValid = true"
@validation-error="isFormValid = false"
class="q-gutter-y-lg w100"
>
<q-input <q-input
v-model="modelValue.name" v-model="projectMod.name"
no-error-icon
dense dense
filled filled
label-slot
class = "w100 fix-bottom-padding" class = "w100 fix-bottom-padding"
:rules="[rules.name]" :rules="[inputRules.require, inputRules.middleText]"
@update:model-value="formRef.validate()"
debounce="300"
no-error-icon
label-slot
> >
<template #label> <template #label>
{{ $t('project_block__project_name') }} <span class="text-red">*</span> {{ $t('project_block__project_name') }} <span class="text-red">*</span>
@@ -38,67 +46,83 @@
</q-input> </q-input>
<q-input <q-input
v-model="modelValue.description" v-model="projectMod.description"
dense dense
filled filled
autogrow autogrow
class="w100 q-pt-sm" no-error-icon
class="w100 q-pt-sm fix-bottom-padding"
:label="$t('project_block__project_description')" :label="$t('project_block__project_description')"
:rules="[inputRules.longText]"
@update:model-value="formRef.validate()"
debounce="300"
/> />
<!-- <q-checkbox <q-input
v-if="modelValue.logo" v-model="projectMod.chat_info"
v-model="modelValue.is_logo_bg"
class="w100"
dense dense
> filled
{{ $t('project_block__image_use_as_background_chats') }} autogrow
</q-checkbox> --> no-error-icon
class="w100 q-pt-sm"
:label="$t('project_block__project_chat_info')"
:rules="[inputRules.longText]"
@update:model-value="formRef.validate()"
debounce="300"
/>
</q-form>
</div> </div>
</div> </pn-page-template>
</pn-scroll-list>
</pn-page-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, computed, ref } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import pnImageSelector from 'components/pnImageSelector.vue'
import { useInputRules } from 'composables/useInputRules'
import { removeNullKeys, getFieldValue } from 'utils/helpers'
import type { ProjectParams } from 'types/Project' import type { ProjectParams } from 'types/Project'
const { t } = useI18n() const { inputRules } = useInputRules()
const modelValue = defineModel<ProjectParams>({ const props = defineProps<{
required: true
})
defineProps<{
title: string, title: string,
btnText: string btnText: string,
project?: ProjectParams
}>() }>()
const emit = defineEmits(['update']) const emit = defineEmits(['update'])
const rulesErrorMessage = { const projectMod = reactive({
name: t('project_block__error_name') name: props.project?.name ?? '',
} description: props.project?.description ?? '',
chat_info: props.project?.chat_info ?? '',
const rules = { logo: props.project?.logo ?? null
name: (val: ProjectParams['name']) => !!val?.trim() || rulesErrorMessage['name']
}
const isFormValid = computed(() => {
const validations = {
name: rules.name(modelValue.value.name) === true
}
return Object.values(validations).every(Boolean)
}) })
const initialProject = ref({} as ProjectParams) const formRef = ref()
const isFormValid = ref(false)
onMounted(() => { function onSubmit() {
initialProject.value = { ...modelValue.value } const { logo, name, description, chat_info } = projectMod
let logoValue: string | null | undefined = undefined
if (typeof logo === 'string' && logo.startsWith('data:image')) logoValue = logo
if (logo === null) logoValue = null
const outData = {
name: name.trim(),
description: getFieldValue(description.trim(), props.project?.description),
chat_info: getFieldValue(chat_info.trim(), props.project?.chat_info),
...(logoValue !== undefined && { logo: logoValue })
}
emit('update', removeNullKeys(outData, ['logo']))
}
onMounted(async () => {
await formRef.value.validate()
if (!isFormValid.value) formRef.value.resetValidation()
}) })
</script> </script>

View File

@@ -0,0 +1,81 @@
<template>
<div class="w100">
<q-btn-group spread flat class="w100 q-py-xs">
<q-btn
v-for="period in periods"
:key="period.id"
:outline="selectedPeriod === period.id"
@click="selectedPeriod = period.id"
:color="selectedPeriod === period.id ? 'primary' : ''"
no-caps dense
>
<div class="column items-center w100 self-start">
<span
class="text-caption text-grey text-no-wrap"
>
{{ $t(period.name_in_days) }}
</span>
<div
class="text-h6"
:class="selectedPeriod === period.id ? 'text-primary' : 'text-black'"
style="line-height: 1em !important;"
>
{{ $t(period.name) }}
</div>
<div
class="row no-wrap w100 q-px-sm q-pt-xs justify-center"
>
<div
v-if="getPeriodPrice(period)"
class="flex no-wrap text-caption text-grey items-center q-pa-none q-ma-none q-pr-xs"
>
<telegram-star color="gold" size="12px" class="q-mr-xs"/>
{{ formatNumber(getPeriodPrice(period)) }}
</div>
<q-badge color="red" class="q-px-xs" v-if="period.discount">
-{{ period.discount * 100 }}%
</q-badge>
</div>
</div>
</q-btn>
</q-btn-group>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import telegramStar from 'components/TelegramStar.vue'
import { formatNumber } from 'utils/helpers'
import type { TariffPeriod } from 'types/Tariff'
const props = defineProps<{
periods: TariffPeriod[],
tariffPrice: number
}>()
const emit = defineEmits(['update:selected'])
const selectedPeriod = ref<number>()
watch(() => props.periods, () => {
if (props.periods.length !== 0) {
selectedPeriod.value = props.periods[0]?.id
}
}, { immediate: true })
watch(selectedPeriod, (newVal) => {
if (newVal !== undefined) emit('update:selected', newVal)
}, { immediate: true })
function getPeriodPrice (period: TariffPeriod) {
return Math.floor(props.tariffPrice * (1 - period.discount) * period.id / 30)
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,119 @@
<template>
<div class="w100">
<div class="w100 column items-center text-center text-grey q-px-md">
<div class="text-caption">
{{ $t('subscribe_tariffs__description') }}
</div>
</div>
<q-list separator>
<q-item
v-for="item in tariffs"
:key="item.id"
clickable
v-ripple
tag="label"
:disable="disableTestTariff(item.id)"
>
<q-item-section avatar>
<q-radio
v-model="selectedTariff"
:val="item.id"
:disable="disableTestTariff(item.id)"
:keep-color="disableTestTariff(item.id)"
:color="disableTestTariff(item.id) ? 'grey-5' : ''"
/>
</q-item-section>
<q-item-section>
<div class="column">
<span class="text-bold">
{{ item.name }}
</span>
<div class="flex no-wrap items-center text-caption" style="line-height: 1em;">
<span v-if="item.chat_limit">
{{ $t('subscribe_tariffs__chats_max') + ' ' + item.chat_limit }}
</span>
<q-icon v-else name="mdi-all-inclusive" size="sm"/>
<span class="q-pl-xs">
{{ $t('subscribe_tariffs__chats')}}
</span>
</div>
</div>
</q-item-section>
<q-item-section
class="text-left"
:style="{ width: `${maxWidthStarsSection}px` }"
>
<q-resize-observer @resize="updateMaxWidthStarsSection" />
<div v-if="item.price" class="flex column justify-start">
<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_tariffs__per_month') }}</span>
</div>
</div>
<div v-else class="text-bold self-center">
{{ $t('subscribe_tariffs__free_tax') }}
</div>
</q-item-section>
</q-item>
</q-list>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import telegramStar from 'components/TelegramStar.vue'
import { formatNumber } from 'utils/helpers'
import type { Tariff } from 'types/Tariff'
const props = withDefaults(defineProps<{
tariffs: Tariff[]
currentTariffId?: number
disable?: boolean
}>(),
{
disable: false
}
)
const emit = defineEmits(['update:selected'])
const selectedTariff = ref<number>()
const testTariffId = computed(() => props.tariffs.find(el => el.price === 0)?.id)
watch(() => props.tariffs, () => {
if (props.currentTariffId !== undefined) {
selectedTariff.value = props.currentTariffId
} else if (testTariffId.value) {
selectedTariff.value = testTariffId.value
} else if (props.tariffs.length > 0) {
selectedTariff.value = props.tariffs[0]?.id
}
}, { immediate: true })
watch(() => props.currentTariffId, (newId) => {
if (newId !== undefined) selectedTariff.value = newId
})
watch(selectedTariff, (newVal) => {
if (newVal !== undefined) emit('update:selected', newVal)
}, { immediate: true })
function disableTestTariff (tariffId: number) {
if (props.disable) return true
if (!testTariffId.value) return false
return props.currentTariffId === testTariffId.value || tariffId === testTariffId.value
}
const maxWidthStarsSection = ref(0)
function updateMaxWidthStarsSection(size: { width: number, height: number }) {
if (size.width > maxWidthStarsSection.value) {
maxWidthStarsSection.value = size.width
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,255 @@
<template>
<div class="column w100 q-px-md">
<div class="flex justify-end w100">
<q-btn
flat no-caps color="grey" rounded dense
@click="showTarfiffSelectDialog = true"
>
<span class="q-px-xs text-caption">
{{$t('subscribe__dialog_select_tariff_show')}}
</span>
</q-btn>
</div>
<div class="w100 text-grey text-center q-pt-sm">
{{ t('subscribe__tariffs_interval_renewal') }}
</div>
<pn-subscribe-periods
@update:selected="selectedPeriod = $event"
:periods
:tariff-price="selectedTariffPrice"
/>
<div
class="text-caption column rounded-card q-px-md q-py-sm q-mt-md"
style="border: 1px solid var(--q-negative);"
v-if="currentTariff?.debt !== 0"
>
<div class="row justify-between item-center text-negative">
<span>
{{ $t('suscribe__credit_from') + ' ' +
(currentTariff?.end_date
? date.formatDate(currentTariff?.end_date * 1000, 'DD.MM.YYYY')
: '')
+ ': ' }}
</span>
<div class="flex no-wrap items-center q-pa-none q-ma-none">
<telegram-star color="gold" size="12px" class="q-mr-xs"/>
{{ formatNumber(currentTariff?.debt ?? '') }}
</div>
</div>
<span class="text-grey">
{{ $t('suscribe__credit_description') }}
</span>
</div>
<div class="text-center text-grey w100 text-caption q-pt-lg">
{{ $t('subscribe__promocode_description') }}
</div>
<q-input
v-model="promocode"
dense
filled
:label="$t('subscribe__promocode')"
class="q-pt-sm"
>
<template #append v-if="promocode !== ''">
<q-btn
round dense flat
color="primary"
icon="mdi-arrow-right-circle-outline"
@click="clickSendPromocode"
/>
</template>
</q-input>
</div>
<pn-bottom-sheet-dialog
v-if="tariffs && currentTariff"
title="subscribe__dialog_select_tariff_title"
v-model="showTarfiffSelectDialog"
>
<div class="w100 column">
<q-banner
v-if="!currentTariff.can_change"
class="bg-negative text-white q-mb-md"
>
<div>
{{ $t(messageBlockChange?.body ?? '') }}
</div>
<div class="text-caption" style="opacity: 0.8">
{{ $t(messageBlockChange?.description ?? '') }}
</div>
</q-banner>
<pn-subscribe-tariffs
@update:selected="selectedTariff = $event"
:tariffs
:currentTariffId="currentTariff.id"
:disable="!currentTariff.can_change"
/>
<div class="text-caption text-grey q-px-md q-py-md">
<div class="w100">
{{ $t('subscribe__dialog_select_tariff_notes_title') }}
</div>
<div class="w100">
{{ $t('subscribe__dialog_select_tariff_notes_1') }}
</div>
<div class="w100">
{{ $t('subscribe__dialog_select_tariff_notes_2') }}
</div>
<div class="w100">
{{ $t('subscribe__dialog_select_tariff_notes_3') }}
</div>
<div
class="w100 text-info cursor-pointer"
@click="router.push({ name: 'tariffs_info'})"
>
{{ $t('subscribe__dialog_select_tariff_notes_more') }}
</div>
</div>
</div>
<template #footer>
<div class="column">
<q-btn
rounded
class="w100"
color="primary"
v-close-popup
:outline="!currentTariff.can_change || currentTariff?.id === selectedTariff"
@click="clickChangeTariff"
>
{{ $t(currentTariff.can_change && currentTariff?.id !== selectedTariff
? 'subscribe__dialog_select_tariff_btn'
: 'subscribe__dialog_select_tariff_btn_cancel'
) }}
</q-btn>
<div
v-if="props.currentTariff?.id !== selectedTariff"
class="q-px-md text-caption text-negative text-center"
>
{{ $t('subscribe__dialog_select_tariff_recalc_msg') + ': ' }}
{{
typeof activePeriodMessage === 'number'
? date.formatDate(activePeriodMessage * 1000, 'DD.MM.YYYY')
: 'Error'
}}
</div>
<div style="height: 30px"/>
</div>
</template>
</pn-bottom-sheet-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { date, useQuasar } from 'quasar'
import { formatNumber } from 'utils/helpers'
import telegramStar from 'components/TelegramStar.vue'
import pnSubscribeTariffs from 'components/subscribe/pnSubscribeTariffs.vue'
import pnSubscribePeriods from 'components/subscribe/pnSubscribePeriods.vue'
import {
calcChangeTariff,
changeTariff,
calcRenewTariff,
urlRenewTariff,
sendPromocode
} from 'composables/useSubscription'
import type { Tariff, CurrentTariff, TariffPeriod } from 'types/Tariff'
const { t } = useI18n()
const router = useRouter()
const props = defineProps<{
tariffs: Tariff[]
currentTariff?: CurrentTariff
periods: TariffPeriod[]
}>()
const emit = defineEmits(['update'])
const selectedTariff = ref<number>()
const selectedPeriod = ref<number>()
const selectedTariffPrice = computed(() => props.tariffs.find((el: Tariff) => el.id === props.currentTariff?.id)?.price ?? 0)
const showTarfiffSelectDialog = ref(false)
async function clickChangeTariff () {
if (props.currentTariff &&
selectedTariff.value &&
props.currentTariff.can_change &&
props.currentTariff?.id !== selectedTariff.value)
await changeTariff(selectedTariff.value)
}
const messageBlockChange = computed(() => {
if (props.currentTariff && !props.currentTariff.can_change) {
return props.currentTariff.debt === 0
? {
body: 'subscribe__dialog_select_tariff_warn_debt_msg_1',
description: 'subscribe__dialog_select_tariff_warn_debt_msg_2'
}
: {
body: 'subscribe__dialog_select_tariff_warn_once_day_msg_1',
description: 'subscribe__dialog_select_tariff_warn_once_day_msg_2'
}
}
return {}
})
const activePeriodMessage = ref('')
watch(selectedTariff, async (newVal) => {
activePeriodMessage.value = (newVal && newVal !== props.currentTariff?.id )
? (await calcChangeTariff(newVal)).plan_end_date
: ''
})
watch(showTarfiffSelectDialog, async (newVal) => {
if (newVal) emit('update', { status: 'hide' })
else await updatePayment()
})
watch(selectedPeriod, async () => await updatePayment())
const updatePayment = async () => {
if (!props.currentTariff?.id || !selectedPeriod.value) return
const renew = await calcRenewTariff(props.currentTariff.id, selectedPeriod.value)
const textBtn = t('subscribe__pay_renew') + ' ⭐' + formatNumber(renew.amount) +
' (' + t('subscribe__tariff_exp') + ' ' + date.formatDate(renew.end_date * 1000, 'DD.MM.YYYY') + ')'
const url = urlRenewTariff(props.currentTariff.id, selectedPeriod.value)
emit('update', { status: 'show', textBtn, url })
}
// promocode
const promocode = ref('')
const $q = useQuasar()
async function clickSendPromocode () {
const result = await sendPromocode(promocode.value)
if (result) showNotify('success')
else showNotify('fail')
promocode.value = ''
}
const showNotify = (message: 'success' | 'fail') => {
$q.notify({
message: t(message === 'success' ? 'subscribe__promocode_success' : 'subscribe__promocode_fail'),
type: message === 'success' ? 'positive' : 'negative',
timeout: 1000,
multiLine: true
})
}
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,76 @@
<template>
<!-- only for test tariff -->
<div
class="column w100"
v-if="testTariffId && tariffs.length > 0 && periods.length > 0"
>
<div class="w100 text-bold text-center">
{{ $t('subscribe__tariffs') }}
</div>
<pn-subscribe-tariffs
@update:selected="selectedTariff = $event"
:tariffs
/>
<q-slide-transition>
<div v-if="selectedTariff !== testTariffId">
<div class="w100 text-bold text-center q-pt-md">
{{ $t('subscribe__tariffs_interval') }}
</div>
<pn-subscribe-periods
@update:selected="selectedPeriod = $event"
:periods
:tariff-price="selectedTariffPrice"
class="q-px-md"
/>
</div>
</q-slide-transition>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { date } from 'quasar'
import { formatNumber } from 'utils/helpers'
import pnSubscribeTariffs from 'components/subscribe/pnSubscribeTariffs.vue'
import pnSubscribePeriods from 'components/subscribe/pnSubscribePeriods.vue'
import {
calcUpgradeTariff,
urlUpgradeTariff
} from 'composables/useSubscription'
import type { Tariff, TariffPeriod } from 'types/Tariff'
const { t } = useI18n()
const props = defineProps<{
tariffs: Tariff[]
periods: TariffPeriod[]
}>()
const emit = defineEmits(['tariff-upgrade'])
const selectedTariff = ref<number>()
const selectedPeriod = ref<number>()
const testTariffId = computed(() => props.tariffs.find(el => el.price === 0)?.id ?? null)
const selectedTariffPrice = computed(() => props.tariffs.find((el: Tariff) => el.id === selectedTariff.value)?.price ?? 0)
watch([selectedTariff, selectedPeriod], async () => {
if (selectedTariff.value === testTariffId.value) {
emit('tariff-upgrade', { status: 'hide' })
return
}
if (!selectedTariff.value || !selectedPeriod.value) return
const upgrade = await calcUpgradeTariff(selectedTariff.value, selectedPeriod.value)
const textBtn = t('subscribe__pay') + ' ⭐' + formatNumber(upgrade.amount) +
' (' + t('subscribe__tariff_exp') + ' ' + date.formatDate(upgrade.end_date * 1000, 'DD.MM.YYYY') + ')'
const url = await urlUpgradeTariff(selectedTariff.value, selectedPeriod.value)
emit('tariff-upgrade', { status: 'show', textBtn, url })
})
</script>
<style lang="scss">
</style>

View File

@@ -1,23 +1,23 @@
<template> <template>
<pn-page-card> <pn-page-template>
<template #title> <template #title>
{{ $t(title) }} {{ $t(title) }}
</template> </template>
<template #footer> <template #card-actions>
<q-btn <q-btn
rounded color="primary" rounded color="primary"
class="w100 q-mt-md q-mb-xs fix-disabled-btn" class="w100 fix-disabled-btn"
:disable="!isFormValid"
@click = "onSubmit" @click = "onSubmit"
> >
{{ $t(btnText) }} {{ $t(btnText) }}
</q-btn> </q-btn>
</template> </template>
<pn-scroll-list> <div class="column items-center q-px-md q-pb-sm">
<div class="flex column items-center q-pa-md q-pb-sm"> <div class="relative-position flex justify-center w100 text-center">
<div class="relative-position">
<pn-auto-avatar <pn-auto-avatar
:img="modelValue.photo" :img="userMod.photo"
:name="tname" :name="tname"
size="100px" size="100px"
class="q-pb-lg" class="q-pb-lg"
@@ -25,30 +25,43 @@
/> />
<div <div
v-if="userStatus" v-if="userStatus"
class="absolute-center text-h4 text-bold q-pa-sm" class="flex justify-center absolute-center w100 items-center"
:class ="'status-' + userStatus.status"
> >
{{ $t(userStatus.text) }} <q-chip
square
:label="$t(userStatus.text)"
:color="userStatus.color"
text-color="white"
class="text-uppercase"
/>
</div> </div>
</div> </div>
<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">{{ tname }}</div> <div class="text-bold q-pr-xs text-center" align="center">{{ tname }}</div>
<div caption class="text-blue text-caption" align="center" v-if="modelValue.username">{{ modelValue.username }}</div> <div caption class="text-primary text-caption" align="center" v-if="user?.username">{{ user.username }}</div>
</div> </div>
</div> </div>
<div class="q-gutter-y-lg w100"> <q-form
ref="formRef"
@validation-success="isFormValid = true"
@validation-error="isFormValid = false"
class="q-gutter-y-lg w100"
>
<q-input <q-input
v-model.trim="modelValue.fullname" v-model="userMod.fullname"
dense dense
filled filled
class = "w100" class = "w100 fix-bottom-padding"
:label = "$t('user_block__name')" :label = "$t('user_block__name')"
:rules="[inputRules.middleText]"
@update:model-value="formRef.validate()"
debounce="300"
/> />
<q-select <q-select
v-model="modelValue.company_id" v-model="userMod.company_id"
:options="displayCompanies" :options="displayCompanies"
dense dense
filled filled
@@ -57,6 +70,7 @@
option-value="id" option-value="id"
emit-value emit-value
map-options map-options
@update:model-value="formRef.validate()"
> >
<template #option="scope"> <template #option="scope">
<q-item v-bind="scope.itemProps"> <q-item v-bind="scope.itemProps">
@@ -84,26 +98,35 @@
</q-select> </q-select>
<q-input <q-input
v-model.trim="modelValue.department" v-model="userMod.department"
dense dense
filled filled
class = "w100 q-pt-sm" class = "w100 fix-bottom-padding q-pt-sm"
:label = "$t('user_block__department')" :label = "$t('user_block__department')"
:rules="[inputRules.middleText]"
@update:model-value="formRef.validate()"
debounce="300"
/> />
<q-input <q-input
v-model.trim="modelValue.role" v-model="userMod.role"
dense dense
filled filled
class = "w100 q-pt-sm" class = "w100 fix-bottom-padding q-pt-sm"
:label = "$t('user_block__role')" :label = "$t('user_block__role')"
:rules="[inputRules.middleText]"
@update:model-value="formRef.validate()"
debounce="300"
/> />
<q-input <q-input
v-model.trim="modelValue.phone" v-model="userMod.phone"
dense dense
filled filled
class = "w100 q-pt-sm" class = "w100 fix-bottom-padding q-pt-sm"
:rules="[inputRules.middleText]"
@update:model-value="formRef.validate()"
debounce="300"
> >
<template #prepend> <template #prepend>
<q-icon name="mdi-phone-outline"/> <q-icon name="mdi-phone-outline"/>
@@ -111,48 +134,53 @@
</q-input> </q-input>
<q-input <q-input
v-model.trim="modelValue.email" v-model="userMod.email"
dense dense
filled filled
class = "w100 q-pt-sm" class = "w100 fix-bottom-padding q-pt-sm"
:rules="[inputRules.middleText]"
@update:model-value="formRef.validate()"
debounce="300"
> >
<template #prepend> <template #prepend>
<q-icon name="mdi-email-outline"/> <q-icon name="mdi-email-outline"/>
</template> </template>
</q-input> </q-input>
</q-form>
</div> </div>
</pn-page-template>
</div>
</pn-scroll-list>
</pn-page-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { ref, computed, reactive, onMounted } 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 { useInputRules } from 'composables/useInputRules'
import type { User } from 'types/Users' import { removeNullKeys, getFieldValue } from 'utils/helpers'
import { getUserStatus } from 'utils/users'
import type { User } from 'types/User'
const { t } = useI18n() const { t } = useI18n()
const { inputRules } = useInputRules()
const modelValue = defineModel<User>({ const props = defineProps<{
required: true
})
defineProps<{
title: string, title: string,
btnText: string btnText: string,
user?: User
}>() }>()
const emit = defineEmits(['update']) const emit = defineEmits(['update'])
function onSubmit() { const userMod = reactive({
const cleanedData = convertEmptyStringsToNull(modelValue.value) fullname: props.user?.fullname ?? '',
emit('update', cleanedData) company_id: props.user?.company_id ?? null,
} department: props.user?.department ?? '',
role: props.user?.role ?? '',
phone: props.user?.phone ?? '',
email: props.user?.email ?? '',
photo: props.user?.photo ?? null
})
const companiesStore = useCompaniesStore() const companiesStore = useCompaniesStore()
const companies = computed(() => companiesStore.companies) const companies = computed(() => companiesStore.companies)
@@ -171,15 +199,31 @@
]) ])
const tname = computed(() => const tname = computed(() =>
[modelValue.value?.firstname, modelValue.value?.lastname] [props.user?.firstname, props.user?.lastname]
.filter(Boolean) .filter(Boolean)
.join(' ') .join(' ')
) )
const userStatus = computed(() => { const userStatus = computed(() => props.user ? getUserStatus(props.user) : null)
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'} const formRef = ref()
return null const isFormValid = ref(false)
function onSubmit() {
const outData = {
fullname: getFieldValue(userMod.fullname.trim(), props.user?.fullname),
company_id: userMod.company_id,
department: getFieldValue(userMod.department.trim(), props.user?.department),
role: getFieldValue(userMod.role.trim(), props.user?.role),
phone: getFieldValue(userMod.phone.trim(), props.user?.phone),
email: getFieldValue(userMod.email.trim(), props.user?.email)
}
emit('update', removeNullKeys(outData, ['company_id']))
}
onMounted(async () => {
await formRef.value.validate()
if (!isFormValid.value) formRef.value.resetValidation()
}) })
</script> </script>
@@ -188,14 +232,4 @@
.fix-bottom-padding.q-field--with-bottom { .fix-bottom-padding.q-field--with-bottom {
padding-bottom: 0 !important padding-bottom: 0 !important
} }
.status-blocked {
border: 2px solid red;
color: red;
}
.status-leave {
border: 2px solid var(--q-primary);
color: var(--q-primary);
}
</style> </style>

View File

@@ -0,0 +1,23 @@
import { copyToClipboard, useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
export function useCopyToClipboard () {
const $q = useQuasar()
const { t } = useI18n()
const copy = async (text: string) => {
try {
await copyToClipboard(text)
$q.notify({
message: t('copy') || 'Copied',
classes: 'bg-grey-8 text-white',
timeout: 1500
})
return true
} catch {
return false
}
}
return { copy }
}

View File

@@ -0,0 +1,21 @@
import { useI18n } from 'vue-i18n'
export const useInputRules = () => {
const { t } = useI18n()
const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z]{2,})+$/
return {
inputRules: {
require: (val: string) => !!val?.trim() || t('input_rules__require'),
email: (val: string) => EMAIL_REGEX.test(val) || t('input_rules__incorrect_email'),
password: (val: string) => val.length >= 8 || t('input_rules__password_require'),
middleText: (val: string) => !val || val.length <= 60 || t('input_rules__middle_text'),
longText: (val: string) => !val || val.length <= 250 || t('input_rules__long_text'),
editorText: (val: string) => !val || val.length <= 7800 || t('input_rules__editor_text'),
editorPlainText: (val: string) => !val || val.length <= 3800 || t('input_rules__editor_text'),
isEquil: (target: string | undefined, text: string) => (val: string) => val !== target || t(text)
}
}
}

View File

@@ -1,22 +0,0 @@
import { useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
import type { ServerError } from 'boot/axios'
export type { ServerError }
export function useNotify() {
const $q = useQuasar()
const { t } = useI18n()
const notifyError = (error: ServerError) => {
$q.notify({
message: `${t(error.message)} (${t('code')}: ${error.code})`,
type: 'negative',
position: 'bottom',
timeout: 1000,
multiLine: true
})
}
return { notifyError }
}

View File

@@ -0,0 +1,49 @@
import { api } from 'boot/axios'
async function getTariffs () {
return (await api.get('/payment/plan'))?.data.data
}
async function getTariffDiscount () {
return (await api.get('payment/plan/discount'))?.data.data
}
async function calcUpgradeTariff (plan_id: number, period: number) {
return (await api.get('/payment/plan/upgrade/calc', { params: { plan_id, period } }))?.data.data
}
async function urlUpgradeTariff (plan_id: number, period: number) {
return (await api.post('/payment/plan/upgrade', { plan_id, period }))?.data.data.url
}
async function calcChangeTariff (plan_id: number) {
return (await api.get('/payment/plan/change/calc', { params: { plan_id } }))?.data.data
}
async function changeTariff (plan_id: number) {
return (await api.post('/payment/plan/change', { plan_id }))?.data.data
}
async function calcRenewTariff (plan_id: number, period: number) {
return (await api.get('/payment/plan/renew/calc', { params: { plan_id, period } }))?.data.data
}
async function urlRenewTariff (plan_id: number, period: number) {
return (await api.post('/payment/plan/renew', { plan_id, period }))?.data.data.url
}
async function sendPromocode (promocode: string) {
return (await api.post('/payment/plan/promocode', { promocode }))?.data.success
}
export {
getTariffs,
getTariffDiscount,
calcUpgradeTariff,
urlUpgradeTariff,
calcChangeTariff,
changeTariff,
calcRenewTariff,
urlRenewTariff,
sendPromocode
}

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()
@@ -11,15 +11,15 @@ export function useUserSection() {
? user.firstname + ' ' + user.lastname ? user.firstname + ' ' + user.lastname
: user.firstname : user.firstname
: user.lastname ?? '' : user.lastname ?? ''
}; }
const section1 = user.fullname ?? tname() const section1 = user.fullname && user.fullname !== '' ? user.fullname : tname()
const section2_1 = user.fullname ? tname() : '' const section2_1 = user.fullname ? tname() : ''
const section2_2 = user.username ?? '' const section2_2 = user.username ?? ''
const section3 = ( const section3 = (
user.company_id && companiesStore.companyById(user.company_id) user.company_id && companiesStore.companyById(user.company_id)
? companiesStore.companyById(user.company_id)?.name + ((user.role || user.department) ? ' - ' : '') ? companiesStore.companyById(user.company_id)?.name + ((user.role || user.department) ? ', ' : '')
: '' : ''
) + ) +
(user.department ? user.department + (user.role ? ' - ' : '') : '') + (user.department ? user.department + (user.role ? ' - ' : '') : '') +

View File

@@ -1,4 +1,40 @@
// app global css in SCSS form // app global css in SCSS form
.bg-t-primary {
background: $t-bg-color !important;
}
.bg-t-secondary {
background: $t-secondary-bg-color !important;
}
.bg-t-bottom-bar {
background: $t-bottom-bar-bg-color !important;
}
.t-section-separator {
color: $t-section-separator-color !important;
}
.t-text {
color: $t-text-color !important;
}
.t-text-hint {
color: $t-hint-color !important;
}
.t-text-subtitle {
color: $t-subtitle-text-color !important;
}
.t-text-accent {
color: $t-accent-text-color !important;
}
.t-text-section-header {
color: $t-section-header-text-color !important;
}
.text-brand { .text-brand {
color: $brand !important; color: $brand !important;
} }
@@ -7,12 +43,16 @@
background: $brand !important; background: $brand !important;
} }
.text-brand2 { .bg-base {
color: $brand2 !important; background: $base-bg !important;
} }
.bg-brand2 { .text-tgcolor {
background: $brand2 !important; color: $tgcolor !important;
}
.bg-tgcolor {
background: $tgcolor !important;
} }
$base-width: 100; $base-width: 100;
@@ -23,6 +63,7 @@ $base-width: 100;
body, html, #q-app { body, html, #q-app {
font-family: $typography-font-family; font-family: $typography-font-family;
background-color: transparent !important;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
@@ -33,14 +74,28 @@ body, html, #q-app {
:root { :root {
--body-width: 600px; --body-width: 600px;
--top-raduis: 12px; --top-radius: 12px;
--logo-color-bg-white: grey; --logo-color-bg-white: grey;
--dynamic-font-size: 16px; --dynamic-font-size: 16px;
}
#q-app { --t-bg-color: #{$t-bg-color};
padding-bottom: env(safe-area-inset-bottom, 0); --t-secondary-bg-color: #{$t-secondary-bg-color};
padding-bottom: constant(safe-area-inset-bottom, 0); // Для старых iOS --t-section-bg-color: #{$t-section-bg-color};
--t-header-bg-color: #{$t-header-bg-color};
--t-bottom-bar-bg-color: #{$t-bottom-bar-bg-color};
--t-text-color: #{$t-text-color};
--t-hint-color: #{$t-hint-color};
--t-subtitle-text-color: #{$t-subtitle-text-color};
--t-accent-text-color: #{$t-accent-text-color};
--t-section-header-text-color: #{$t-section-header-text-color};
--t-button-color: #{$t-button-color};
--t-button-text-color: #{$t-button-text-color};
--t-link-color: #{$t-link-color};
--t-destructive-text-color: #{$t-destructive-text-color};
--t-section-separator-color: #{$t-section-separator-color};
} }
body { body {
@@ -48,21 +103,26 @@ body {
} }
.main-content { .main-content {
max-width: 600px; max-width: var(--body-width);
margin: 0 auto; margin: 0 auto;
} }
.fix-fab-offset {
margin-right: calc(max((100vw - var(--body-width))/2, 0px) + 18px) !important;
}
.projects-header { .projects-header {
background-color: #eee; background-color: #eee;
} }
.top-rounded-card { .top-rounded-card {
border-top-left-radius: var(--top-raduis); border-top-left-radius: var(--top-radius) !important;
border-top-right-radius: var(--top-raduis); border-top-right-radius: var(--top-radius) !important;
}
.rounded-card {
border-radius: var(--top-radius) !important;
}
.rounded-fix {
border-radius: 1000px;
overflow: hidden;
} }
.orline { .orline {
@@ -74,20 +134,184 @@ body {
.orline:after { .orline:after {
content: ""; content: "";
flex: 1 1; flex: 1 1;
border-bottom: 1px solid grey; border-bottom: 1px solid;
margin: auto; margin: auto;
} }
@font-face {
font-family: 'myFont';
src: url(./fonts/Inter-Regular.woff2);
}
.fix-disabled-btn.q-btn[disabled]:not(.q-btn--flat) { .fix-disabled-btn.q-btn[disabled]:not(.q-btn--flat) {
background-color: $grey-5 !important; background-color: $grey-5 !important;
opacity: 1 !important;
} }
.fix-disabled-btn.q-btn.q-btn--flat[disabled] { .fix-disabled-btn.q-btn.q-btn--flat[disabled] {
color: $grey-9 !important; color: $grey-9 !important;
opacity: 1; opacity: 1;
} }
.q-field--filled {
.q-field__control {
background: $t-secondary-bg-color !important;
transition: none !important;
&:hover {
background: $t-secondary-bg-color !important;
}
}
&.q-field--focused {
.q-field__control {
background: $t-secondary-bg-color !important;
}
}
.q-field__control {
&:before {
border-bottom: none !important;
display: none !important;
}
&:after {
border-bottom: none !important;
display: none !important;
transform: none !important;
}
}
.q-field__control-container {
padding-bottom: 0 !important;
}
input, textarea {
caret-color: var(--q-primary) !important;
}
&.q-field--focused {
input, textarea {
caret-color: var(--q-primary) !important;
}
}
}
@mixin shadow-t($opacity: 0.12, $blur: 16px, $spread: -4px) {
box-shadow: 0 0 1px 0 rgba(0, 0, 0, $opacity),
0 0 $blur $spread rgba(0, 0, 0, $opacity * 0.67) !important;
}
.shadow-t {
& {
@include shadow-t;
transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
&.q-btn {
overflow: visible !important;
&:hover {
@include shadow-t(0.16, 20px, -2px);
}
&:active {
@include shadow-t(0.08, 12px, -6px);
transform: translateY(1px);
}
&.q-btn--flat,
&.q-btn--outline {
box-shadow: none;
&.shadow-t {
@include shadow-t;
}
}
}
&.q-field {
.q-field__control {
overflow: visible !important;
transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
&.q-field--focused {
.q-field__control {
@include shadow-t(0.2, 24px, 0px);
}
}
}
&.q-card {
transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
@include shadow-t(0.16, 24px, 0px);
}
}
&.div
&.q-menu,
&.q-popup,
&.q-dialog {
@include shadow-t(0.2, 24px, 0px);
}
}
@font-face {
font-family: 'myFont';
src: url(./fonts/Inter-Regular.woff2);
}
.pn-icon {
font-family: 'pn-icon';
font-style: normal;
}
@font-face {
font-family: 'pn-icon';
src: url(./fonts/pn.woff) format("woff")
}
.icon-file-default:before {
content: "\e900";
}
.icon-file-doc:before {
content: "\e901";
}
.icon-file-xls:before {
content: "\e902";
}
.icon-file-ppt:before {
content: "\e903";
}
.icon-file-vsd:before {
content: "\e904";
}
.icon-file-pdf:before {
content: "\e905";
}
.icon-file-archive:before {
content: "\e906";
}
.icon-file-img:before {
content: "\e907";
}
.icon-file-dwg:before {
content: "\e908";
}
.icon-file-skp:before {
content: "\e909";
}
.icon-file-ttf:before {
content: "\e90a";
}
.icon-file-txt:before {
content: "\e90b";
}
.icon-file-audio:before {
content: "\e90c";
}
.icon-file-video:before {
content: "\e90d";
}
.icon-file-code:before {
content: "\e90e";
}

BIN
src/css/fonts/pn.eot Normal file

Binary file not shown.

25
src/css/fonts/pn.svg Normal file
View File

@@ -0,0 +1,25 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="pn" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe900;" glyph-name="file-default" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256z" />
<glyph unicode="&#xe901;" glyph-name="file-doc" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM188.75 721h112.125l73.875-387.75 90 387.75h95.25l89.625-388.5 73.5 388.5h112.125l-121.125-546h-113.25l-88.875 365.25-88.875-365.25h-113.25l-121.125 546z" />
<glyph unicode="&#xe902;" glyph-name="file-xls" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM128 721h129.375l102.375-188.25 102.375 188.25h129.375l-159-270.75 163.125-275.25h-130.875l-105 191.25-105-191.25h-130.875l163.125 275.25-159 270.75zM640 512h128v-64h-128v64zM832 512h64v-64h-64v64zM640 384h128v-64h-128v64zM832 384h64v-64h-64v64zM640 256h128v-64h-128v64zM832 256h64v-64h-64v64z" />
<glyph unicode="&#xe903;" glyph-name="file-ppt" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM129.75 721.688h213c41 0 77-7.5 108-22.5 31.25-15 55.25-36.375 72-64.125 16.75-27.5 25.125-58.875 25.125-94.125 0-53.5-18.375-95.75-55.125-126.75-36.5-30.75-87.125-46.125-151.875-46.125h-98.625v-192.375h-112.5v546zM242.25 630.563v-171.375h100.5c29.75 0 52.375 7 67.875 21 15.75 14 23.625 34 23.625 60 0 26.75-7.875 48.375-23.625 64.875s-37.5 25-65.25 25.5h-103.125zM733.813 512c0.946-0.020 1.71-0.043 2.473-0.071l-0.286 0.008v-157h160v-2.938c0-0.001 0-0.001 0-0.002 0-88.102-71.208-159.572-159.21-159.998h-0.040c-0.206-0.001-0.45-0.002-0.694-0.002-87.861 0-159.181 70.818-159.993 158.487l-0.001 0.077c-0.004 0.432-0.007 0.942-0.007 1.453 0 87.577 70.361 158.72 157.638 159.983l0.119 0.001zM804.875 496.25c32.147-15.624 57.664-40.678 73.455-71.534l0.42-0.903h-73.875v72.437z" />
<glyph unicode="&#xe904;" glyph-name="file-vsd" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM130.25 721h124.875l123-410.625 123.75 410.625h125.25l-190.125-546h-117.375l-189.375 546zM815.875 500.563l67.875-67.875-51.75-51.75v-156.937h-96v-32h-96v96h96v-32h64v124.687l-35.375 35.313h-124.625v32h123.25l52.625 52.562z" />
<glyph unicode="&#xe905;" glyph-name="file-pdf" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM478.938 808.563c5.033-0.186 9.623-1.208 13.375-3.125 15.626-7.972 26.459-25.664 31.312-51.25 6.833-36.023 2.358-66.338-22-149.875-4.758-16.32-8.625-31.461-8.625-33.625 0-8.292 18.944-46.781 35.813-72.687 28.642-43.989 67.322-83.572 111.187-113.813 11.914-8.366 14.397-9.187 22.5-9.187 5.003 0 13.104 1.007 18 1.687 46.416 8.808 134.839 12.372 162.562 6.562 28.48-5.977 46.195-18.296 51.25-35.687 2.925-10.057 2.008-20.229-2.125-23.687-2.005-1.67-3.791-0.673-9.062 5-20.394 21.952-55.131 26.519-148.75 19.687-12.554-0.92-25.929-2.238-29.75-2.75l-6.938-0.875 4.313-3.562c5.88-4.867 39.111-20.504 60.25-28.375 26.098-9.711 42.045-13.415 64.375-14.438 22.362-1.141 34.615 1.192 48.5 9.25 14.524 8.43 17.263 3.106 5.813-11.375-8.362-10.597-21.344-19.616-35.938-25-9.085-3.288-14.163-3.946-36.875-4-23.206-0.085-28.555 0.508-44.938 4.75-37.601 9.754-84.275 31.375-129.812 60.125-6.834 4.361-8.648 4.539-18.25 3.687-25.362-2.588-99.908-18.034-137.75-28.562-55.516-15.408-94.889-27.774-99-31.062-1.414-1.141-4.954-6.452-7.813-11.75-24.864-46.23-62.903-99.388-91.75-128.187-19.778-19.749-34.872-30.812-55.375-40.625v-0.313c-13.538-6.713-16.602-7.194-31.187-7.875-14.061-0.596-17.442-0.129-26 3.5-20.293 8.587-30.624 17.985-35.875 32.875-3.946 11.185-3.157 17.609 3.625 31.187 13.429 26.843 54.267 58.31 117.5 90.563 26.963 13.732 34.312 15.957 34.312 10.25 0-4.341-8.271-11.66-29-25.75-56.875-38.655-88.952-76.293-94.75-111.125-1.45-8.757-0.465-9.134 10.125-3.75 24.522 12.444 59.103 46.529 96.937 95.5 61.876 80.087 157.92 251.5 181.875 324.562l3.562 10.75-9.687 33.313c-5.316 18.345-12.079 43.648-15.062 56.187-2.985 12.546-6.205 26.008-7.125 29.875-0.918 3.862-2.836 15.283-4.25 25.437-7.955 57.012-4.335 81.411 14.75 100 11.658 11.372 30.651 18.122 45.75 17.562zM480.063 781.563c-3.266-1.251-9.423-15.54-11.875-27.5-5.113-24.947-0.997-81.847 9.125-125.813 2.022-8.791 3.971-15.937 4.312-15.937 1.031 0 16.012 48.395 22 71.062 4.625 17.487 5.624 24.503 5.75 41.25 0.138 18.596-0.278 21.102-4.875 31.187-5.501 12.079-20.071 27.573-24.438 25.75zM471.687 516.125c-0.644-0.698-3.187-7.595-5.625-15.312-14.534-46.051-44.809-124.79-65.5-170.313-6.14-13.507-11.187-25.194-11.187-25.875 0-2.487 3.984-1.318 37.562 10.813 59.798 21.605 87.644 29.965 132.687 40 16.458 3.663 29.934 7.238 29.938 7.75 0.003 0.579-7.223 8.118-16 16.688-33.633 32.844-66.928 76.91-91.625 121.187-5.005 8.978-9.603 15.743-10.25 15.062z" />
<glyph unicode="&#xe906;" glyph-name="file-archive" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM224 896h64v-64h64v-64h-64v-64h64v-64h-64v-64h64v-64h-64v-64h-64v64h64v64h-64v64h64v64h-64v64h64v64h-64v64zM192 384h192v-320h-192v320zM256 256v-128h64v128h-64z" />
<glyph unicode="&#xe907;" glyph-name="file-img" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM288 768c53.019 0 96-42.981 96-96 0 0 0 0 0 0v0c0-53.019-42.981-96-96-96 0 0 0 0 0 0v0c-53.019 0-96 42.981-96 96 0 0 0 0 0 0v0c0 53.019 42.981 96 96 96 0 0 0 0 0 0v0zM652.813 576l256-512h-768l153.562 409.625 153.625-204.812 204.813 307.187z" />
<glyph unicode="&#xe908;" glyph-name="file-dwg" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM320 896h64v-192h64v-64h448v-64h-448v-64h-64v-512h-64v512h-64v64h-128v64h128v64h64v192zM320 640v-64h64v64h-64z" />
<glyph unicode="&#xe909;" glyph-name="file-skp" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM586.938 816.625c1.815 0 13.221-8.748 36-27.625 6.673-5.532 15.558-12.853 19.75-16.25 8.147-6.597 47.682-38.903 70.625-57.75 7.654-6.283 22.133-18.209 32.187-26.437 10.052-8.222 25.014-20.512 33.313-27.312 8.297-6.796 19.127-15.658 24.062-19.688 19.461-15.884 28.569-23.724 29.062-25.125 0.273-0.761-0.146-29.456-1.062-63.687-1.521-58.979-2.655-112.105-5.125-252.75-0.645-33.687-1.389-62.35-1.813-63.687-0.383-1.331-1.973-2.75-3.438-3.188-1.451-0.383-6.92-2.636-12.187-4.812s-18.451-7.426-29.313-11.688c-10.863-4.258-28.933-11.338-40.125-15.75-11.191-4.42-26.258-10.344-33.5-13.188-7.244-2.842-24.722-9.783-38.875-15.375-14.152-5.589-33.518-13.261-43.062-17-30.058-11.78-92.454-36.4-100.562-39.688-4.278-1.74-15.053-5.935-23.938-9.375-8.889-3.441-20.187-7.941-25.125-9.937-23.729-9.632-43.385-16.938-45.375-16.938-1.365 0-9.235 8.076-20.625 21.187h0.062c-10.126 11.653-22.235 25.516-26.875 30.813-4.638 5.304-16.441 18.747-26.187 29.938-9.748 11.192-22.467 25.792-28.25 32.375-25.475 28.995-45.119 51.532-48.437 55.562-1.977 2.405-11.095 12.89-20.312 23.313-22.424 25.361-37.972 43.403-40.312 46.938-1.347 2.034-2.111 7.073-2.625 16.938-0.282 7.718-1.532 25.363-2.5 39.187-0.956 13.824-2.086 30.245-2.5 36.5-0.381 6.255-0.966 14.021-1.188 17.313-0.191 3.298-0.98 15.121-1.625 26.313-2.49 42.216-5.421 87.646-8.625 132.312-1.295 18.104-2.855 42.882-3.5 55.063-0.646 12.178-1.75 28.835-2.375 37.063-0.433 5.703-0.608 9.541-0.562 12.062 0.051 2.521 0.298 3.735 0.812 4.25 0.756 0.761 18.683 6.543 39.75 12.875 21.064 6.34 47.262 14.238 58.125 17.562 10.863 3.327 24.809 7.556 31.063 9.438 6.255 1.882 33.744 10.269 61.062 18.625s68.817 20.973 92.187 28.062c23.368 7.091 57.713 17.565 76.313 23.25 18.601 5.684 34.627 10.312 35.625 10.312zM579.313 805.438c-2.939 0.435-6.327-0.46-12.438-2.438-2.804-0.95-11.538-3.648-19.438-6.062s-21.084-6.462-29.313-9c-8.232-2.548-40.839-12.533-72.438-22.187s-64.441-19.673-73-22.312c-8.554-2.643-17.211-5.266-19.187-5.875-1.968-0.665-10.57-3.251-19.125-5.875-17.19-5.256-53.95-16.487-88-26.937-12.178-3.736-27.283-8.383-33.625-10.312-12.1-3.659-14.847-5.528-13.688-9.188 1.161-3.631 57.506-58.933 60.062-58.937 1.261-0.003 12.492 3.239 25 7.25 24.333 7.785 38.336 12.217 64 20.25 8.889 2.785 23.426 7.416 32.313 10.25 8.889 2.833 26.43 8.37 38.938 12.313 12.508 3.945 34.874 11.085 49.687 15.812 32.784 10.461 47.167 14.922 57.375 17.875l7.812 2.25 3.625-2.75c1.986-1.512 5.21-4.354 7.187-6.312 1.975-1.949 15.393-13.702 29.875-26.125 14.481-12.425 26.67-22.922 27-23.312 0.645-0.761 40.136-35.363 56.75-49.75 37.902-32.809 37.856-32.75 41.813-32.75 2.129 0 8.376 1.527 13.938 3.438 5.551 1.91 29.036 9.93 52.125 17.812 23.088 7.869 43.128 14.922 44.562 15.687 5.19 2.785 2.806 6.276-12.875 18.688-4.161 3.298-12.737 10.337-19.062 15.625-12.235 10.221-83.14 68.893-91.75 75.937-2.813 2.31-10.158 8.331-16.313 13.375-6.16 5.038-20.075 16.478-30.938 25.5-19.222 15.965-35.298 29.302-51.187 42.375-4.272 3.517-6.749 5.253-9.687 5.688zM527 567.875c-6.242-0.992-23.435-6.689-79.125-25.313-43.988-14.71-99.956-33.425-124.313-41.562-40.53-13.542-47.25-16.305-47.25-19.75 0-0.665 7.978-9.282 17.688-19.312 9.706-10.031 23.278-24.248 30.125-31.625 6.854-7.376 13.26-13.667 14.313-14 1.055-0.286 17.006 4.551 35.438 10.875 18.432 6.331 40.209 13.814 48.438 16.625 8.232 2.814 23.593 8.16 34.125 11.875 10.533 3.717 20.339 6.619 21.812 6.438 1.572-0.189 10.5-8.044 21.563-18.938 49.844-49.087 58.673-57.437 60.938-57.437 1.295 0 6.133 1.34 10.687 2.937 7.871 2.757 12.476 4.38 44.812 16.188 16.581 6.055 22.089 8.055 37.688 13.5 22.143 7.747 24.322 8.988 22.687 13.25-0.272 0.857-5.863 6.32-12.25 12.125-19.527 17.729-37.184 34.12-51.688 48-7.604 7.281-20.817 19.618-29.375 27.437-8.555 7.823-23.245 21.574-32.688 30.5-9.448 8.925-18.463 16.991-20 17.938-0.655 0.403-1.544 0.58-3.625 0.25zM475.187 318.063l-5.75-1.375c-3.173-0.766-13.050-4.033-21.938-7.312-44.493-16.44-76.602-28.132-81.687-29.813-3.117-1.044-6.593-2.846-7.688-4.062-1.842-2.034-1.76-2.401 0.25-5.5 3.051-4.677 50.353-57.225 57.687-64.062l6.125-5.625 4.5 1.625c2.519 0.947 23.164 8.686 45.875 17.25 58.911 22.225 63.765 24.060 65.5 25.5 4.154 3.46 1.78 6.731-24.5 33.625-14.322 14.658-28.783 29.592-32.187 33.187l-6.188 6.562z" />
<glyph unicode="&#xe90a;" glyph-name="file-ttf" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM192 768h320v-64h-128v-384h-64v384h-128v64zM512 576h320v-64h-128v-384h-64v384h-128v64z" />
<glyph unicode="&#xe90b;" glyph-name="file-txt" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM292.188 694h92.625l181.687-485.312h-106.313l-33.687 100h-175.312l-33.375-100h-106.312l180.687 485.312zM753.187 576c44.444 0 79.625-11.243 105.625-33.687 26.222-22.222 39.375-53.556 39.375-94v-156.313c0.222-34.222 4.979-60.132 14.313-77.687v-5.625h-97.313c-4.444 8.667-7.687 19.424-9.687 32.313-23.333-26-53.667-39-91-39-35.333 0-64.667 10.243-88 30.687-23.111 20.444-34.687 46.201-34.687 77.313 0 38.222 14.153 67.556 42.375 88 28.444 20.444 69.444 30.778 123 31h44.313v20.687c0 16.667-4.333 30-13 40-8.444 10-21.868 15-40.313 15-16.222 0-29.042-3.91-38.375-11.687-9.111-7.778-13.625-18.444-13.625-32h-96.375c0 20.889 6.486 40.222 19.375 58s31.069 31.687 54.625 41.687c23.556 10.222 50.042 15.313 79.375 15.313zM338.5 571.313l-60.312-181.625h121.312l-61 181.625zM765.5 372.688c-48.222 0-73.889-16.667-77-50l-0.313-5.687c0-12 4.181-21.91 12.625-29.687s20.021-11.625 34.687-11.625c14.222 0 27.313 3.090 39.313 9.313 12 6.444 20.91 15.021 26.687 25.687v62h-36z" />
<glyph unicode="&#xe90c;" glyph-name="file-audio" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM799.937 736v-377.312c1.071-26.653-25.792-59.237-67.251-80.562l-0.374-0.188c-2.538-1.311-5.523-2.755-8.576-4.137-50.502-22.867-101.501-20.708-118.419 3.935l-0.129 0.202c-0.931 1.348-1.854 2.985-2.635 4.71-12.413 27.415 15.658 67.296 63.483 92.211l0.402 0.204c2.75 1.436 5.985 3.010 9.3 4.51 26.201 11.864 52.535 16.991 74.175 15.738l-0.037 0.002v222.5l-325.562-50.313v-306.687c1.048-26.661-25.837-59.248-67.316-80.564l-0.372-0.186c-2.515-1.298-5.474-2.728-8.498-4.098-50.518-22.874-101.533-20.707-118.435 3.959l-0.128 0.202c-0.937 1.355-1.867 3-2.652 4.734-12.409 27.406 15.639 67.269 63.436 92.186l0.404 0.205c2.72 1.418 5.92 2.973 9.197 4.457 26.266 11.893 52.666 17.017 74.336 15.728l-0.034 0.002v372.812l425.688 65.75z" />
<glyph unicode="&#xe90d;" glyph-name="file-video" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM507.625 768c1.3 0.019 2.835 0.030 4.371 0.030 174.697 0 316.696-139.992 319.937-313.914l0.004-0.303 0.062-5.813c0-0.003 0-0.006 0-0.010 0-176.224-142.451-319.177-318.485-319.99h-0.078c-0.433-0.002-0.945-0.003-1.458-0.003-175.698 0-318.323 141.602-319.979 316.909l-0.001 0.157c-0.009 0.884-0.014 1.928-0.014 2.974 0 175.196 140.794 317.508 315.408 319.961l0.231 0.003zM324.313 675.563v-455.125l472.125 227.562-472.125 227.562z" />
<glyph unicode="&#xe90e;" glyph-name="file-code" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM552.875 576h32.813l-133.75-512h-32.25l133.187 512zM375.25 485.438v-49.562l-169.75-108.688 169.75-108.75v-49.562l-215.25 141.187v34.188l215.25 141.187zM648.75 485.438l215.25-141.187v-34.188l-215.25-141.187v49.562l169.75 108.75-169.75 108.688v49.562z" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/css/fonts/pn.ttf Normal file

Binary file not shown.

BIN
src/css/fonts/pn.woff Normal file

Binary file not shown.

View File

@@ -1,4 +1,45 @@
$primary : #3390ec; /* telegram theme on Android 2026
bg-color: #ffffff
secondary-bg-color: #f1f1f3
section-bg-color: #ffffff
header-bg-color: #ffffff
bottom-bar-bg-color: #f1f1f3
text-color: #1a1d21
hint-color: #a8a8a8
subtitle-text-color: #82868a
accent-text-color: #1c93e3
section-header-text-color: #298acf
button-color: #229af0
button-text-color: #ffffff
link-color: #2678b6
destructive-text-color: #cc2929
section-separator-color: #d9d9d9
*/
$t-bg-color: #ffffff;
$t-secondary-bg-color: #f1f1f3;
$t-section-bg-color: #ffffff;
$t-header-bg-color: #ffffff;
$t-bottom-bar-bg-color: #f1f1f3;
$t-text-color: #1a1d21;
$t-hint-color: #a8a8a8;
$t-subtitle-text-color: #82868a;
$t-accent-text-color: #1c93e3;
$t-section-header-text-color: #298acf;
$t-button-color: #229af0;
$t-button-text-color: #ffffff;
$t-link-color: #2678b6;
$t-destructive-text-color: #cc2929;
$t-section-separator-color: #d9d9d9;
$primary : $t-button-color;
$secondary : #26A69A; $secondary : #26A69A;
$accent : #9C27B0; $accent : #9C27B0;
@@ -6,14 +47,16 @@ $dark : #1D1D1D;
$dark-page : #121212; $dark-page : #121212;
$positive : #21BA45; $positive : #21BA45;
$negative : #C10015; $negative : $t-destructive-text-color;
$info : #31CCEC; $info : #31CCEC;
$warning : #F2C037; $warning : #F2C037;
$lightgrey : #DCDCDC; $lightgrey : $t-secondary-bg-color;
$brand: #3390ec; $brand: #419FD9;
$brand2: #F36D3A; $base-bg: #517DA2;
$tgcolor: #419FD9;
$body-font-size: var(--dynamic-font-size); $body-font-size: var(--dynamic-font-size);

View File

@@ -1,113 +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 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
}
// Функция для преобразования пустых строк в null
function convertEmptyStringsToNull<T extends Record<string, unknown>>(obj: T): T {
const result = { ...obj } as Record<string, unknown>
Object.keys(result).forEach(key => {
if (result[key] === '') {
result[key] = null
}
})
return result as T
}
export {
isDirty,
parseIntString,
convertEmptyStringsToNull
}

View File

@@ -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'
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,4 @@
<template> <template>
<q-layout <q-layout
view="lHr lpr lFr" view="lHr lpr lFr"
class="no-scroll bg-transparent" class="no-scroll bg-transparent"
@@ -7,18 +6,12 @@
<q-page-container <q-page-container
class="main-content q-pa-none q-ma-none no-scroll bg-transparent" class="main-content q-pa-none q-ma-none no-scroll bg-transparent"
> >
<q-page class="no-scroll column"> <q-page>
<router-view /> <router-view />
</q-page> </q-page>
</q-page-container> </q-page-container>
</q-layout> </q-layout>
<meshBackground/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import meshBackground from 'components/meshBackground.vue'
</script> </script>
<style>
</style>

View File

@@ -1,264 +0,0 @@
<template>
<pn-page-card>
<template #title>
{{$t('account_change_email__title')}}
</template>
<pn-scroll-list>
<q-stepper
v-model="step"
vertical
color="primary"
animated
flat
class="bg-transparent"
>
<q-step
:name="1"
:title="$t('account_change_email__current_email')"
:done="step > 1"
>
<q-input
v-model="login"
autofocus
dense
filled
:label = "$t('account_change_email__current_email')"
disable
/>
<q-stepper-navigation>
<q-btn
@click="handleSubmit"
color="primary"
:label="$t('continue')"
/>
</q-stepper-navigation>
</q-step>
<q-step
:name="2"
:title="$t('account_change_email__confirm_current_email')"
:done="step > 2"
>
<div class="q-pb-md">{{$t('account_change_email__confirm_email_message')}}</div>
<q-input
v-model="code"
dense
filled
autofocus
hide-bottom-space
:label = "$t('account_change_email__code')"
num="30"
/>
<q-stepper-navigation>
<q-btn
@click="handleSubmit"
color="primary"
:label="$t('continue')"
:disable="code.length === 0"
/>
<q-btn
flat
@click="step = 1"
color="primary"
:label="$t('back')"
class="q-ml-sm"
/>
</q-stepper-navigation>
</q-step>
<q-step
:name="3"
:title="$t('account_change_email__new_email')"
:done="step > 2"
>
<q-input
v-model="newLogin"
autofocus
dense
filled
:label = "$t('account_change_email__new_email')"
:rules="validationRules.email"
lazy-rules
no-error-icon
@focus="($refs.emailInput as typeof QInput)?.resetValidation()"
ref="emailInput"
/>
<q-stepper-navigation>
<q-btn
@click="handleSubmit"
color="primary"
:label="$t('continue')"
:disabled="!isEmailValid"
/>
<q-btn
flat
@click="step = 2"
color="primary"
:label="$t('back')"
class="q-ml-sm"
/>
</q-stepper-navigation>
</q-step>
<q-step
:name="4"
:title="$t('account_change_email__confirm_new_email')"
:done="step > 3"
>
<div class="q-pb-md">{{$t('account_change_email__confirm_email_message')}}</div>
<q-input
v-model="newCode"
dense
filled
autofocus
hide-bottom-space
:label = "$t('account_change_email__code')"
num="30"
/>
<q-stepper-navigation>
<q-btn
@click="handleSubmit"
color="primary"
:label="$t('continue')"
:disable="newCode.length === 0"
/>
<q-btn
flat
@click="step = 3"
color="primary"
:label="$t('back')"
class="q-ml-sm"
/>
</q-stepper-navigation>
</q-step>
<q-step
:name="5"
:title="$t('account_change_email__set_password')"
>
<q-input
v-model="password"
dense
filled
:label = "$t('account_change_email__password')"
:type="isPwd ? 'password' : 'text'"
hide-hint
:hint="passwordHint"
:rules="validationRules.password"
lazy-rules
no-error-icon
@focus="($refs.passwordInput as typeof QInput)?.resetValidation()"
ref="passwordInput"
>
<template #append>
<q-icon
color="grey-5"
:name="isPwd ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
<q-stepper-navigation>
<q-btn
@click="handleSubmit"
color="primary"
:label="$t('account_change_email__finish')"
:disabled = "!isPasswordValid"
/>
<q-btn
flat
@click="step = 4"
color="primary"
:label="$t('back')"
class="q-ml-sm"
/>
</q-stepper-navigation>
</q-step>
</q-stepper>
<pn-magic-overlay
v-if="showSuccessOverlay"
icon="mdi-check-circle-outline"
message1="account_change_email__ok_message1"
message2="account_change_email__ok_message2"
route-name="account"
/>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { AxiosError } from 'axios'
import { useI18n } from "vue-i18n"
import { QInput } from 'quasar'
import { useAuthStore } from 'stores/auth'
const { t } = useI18n()
const authStore = useAuthStore()
type ValidationRule = (val: string) => boolean | string
type Step = 1 | 2 | 3 | 4 | 5
const step = ref<Step>(1)
const login = authStore.customer?.email
const code = ref<string>('')
const newLogin = ref<string>('')
const newCode = ref<string>('')
const password = ref<string>('')
const showSuccessOverlay = ref(false)
const isPwd = ref<boolean>(true)
const validationRules = {
email: [(val: string) => /.+@.+\..+/.test(val) || t('login__incorrect_email')] as [ValidationRule],
password: [(val: string) => val.length >= 8 || t('login__password_require')] as [ValidationRule]
}
const isEmailValid = computed(() =>
validationRules.email.every(f => f(newLogin.value) === true)
)
const isPasswordValid = computed(() =>
validationRules.password.every(f => f(password.value) === true)
)
const passwordHint = computed(() => {
const result = validationRules.password[0](password.value)
return typeof result === 'string' ? result : ''
})
const stepActions: Record<Step, () => Promise<void>> = {
1: async () => {
await authStore.getCodeCurrentEmail()
},
2: async () => {
await authStore.confirmCurrentEmailCode(code.value)
console.log(code.value)
},
3: async () => {
await authStore.getCodeNewEmail(code.value, newLogin.value)
},
4: async () => {
await authStore.confirmNewEmailCode(code.value, newCode.value, newLogin.value,)
},
5: async () => {
await authStore.setNewEmailPassword(code.value, newCode.value, newLogin.value, password.value)
await authStore.loginWithCredentials(newLogin.value, password.value)
}
}
const handleSubmit = async () => {
try {
await stepActions[step.value]()
if (step.value < 5) {
step.value++
} else {
showSuccessOverlay.value = true
}
} catch (error) {
console.error(error as AxiosError)
}
}
</script>

View File

@@ -1,12 +1,10 @@
<template> <template>
<pn-page-card> <pn-page-template>
<template #title> <template #title>
{{$t('login__register_title')}} {{$t('login__register_title')}}
</template> </template>
<pn-scroll-list>
<account-helper :type :email/> <account-helper :type :email/>
</pn-scroll-list> </pn-page-template>
</pn-page-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -14,9 +12,10 @@
import accountHelper from 'components/accountHelper.vue' import accountHelper from 'components/accountHelper.vue'
const type = 'register' const type = 'register'
const email = ref(sessionStorage.getItem('pendingLogin') || '') const email = ref('')
onMounted(() => { onMounted(() => {
email.value = sessionStorage.getItem('pendingLogin') ?? ''
sessionStorage.removeItem('pendingLogin') sessionStorage.removeItem('pendingLogin')
}) })

View File

@@ -1,12 +1,10 @@
<template> <template>
<pn-page-card> <pn-page-template>
<template #title> <template #title>
{{$t('forgot_password__password_recovery')}} {{$t('forgot_password__password_recovery')}}
</template> </template>
<pn-scroll-list>
<account-helper :type :email/> <account-helper :type :email/>
</pn-scroll-list> </pn-page-template>
</pn-page-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

Some files were not shown because too many files have changed in this diff Show More