12
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/docs/triggers_v2.xlsx
Normal file
BIN
backend/docs/triggers_v2.xlsx
Normal file
Binary file not shown.
BIN
i18n-2.xlsm
BIN
i18n-2.xlsm
Binary file not shown.
@@ -1,30 +0,0 @@
|
||||
Software SURVy (the “Tool”) to facilitate your selection CCTV products based on your needs.
|
||||
Before you start using the Tool, we ask you to carefully read through the below terms of use ("Terms of Use") and make sure that you have understood them prior to any use of the Tool. By downloading, installing, activating, accessing or otherwise using the Tool, you agree to be bound by the terms and conditions of the Terms of Use. If you are executing the Terms of Use on behalf of an entity, you represent that you have authority to legally bind that entity. If you do not have such authority or you do not agree to the terms and conditions of the Terms of Use, neither you nor the entity is permitted to and must not download, install, access or use the Tool.
|
||||
The Tool developer ("Developer") - Individual Entrepreneur/Sole Proprietor Martyshkin Alexey Alexandrovich (Russia, Moscow, PSRNSP/Primary State Registration Number of the Sole Proprietor 318774600262084, ITN/Individual Taxpayer Number 366316608346). All rights to software SURVy belong to Developer.
|
||||
By "Developer Representatives" in this document means to the circle of persons involved by Developer for development and support of the Tool.
|
||||
|
||||
TERMS OF USE
|
||||
The Tool is provided for guidance only. The estimates, recommendations and calculation results (collectively the "Deliverables") produced by the use of the Tool are only orientational.
|
||||
DEVELOPER AND/OR ITS REPRESENTATIVES WILL IN NO EVENT BE RESPONSIBLE FOR DAMAGES OF ANY NATURE WHATSOEVER RESULTING FROM THE USE OF, OR RELIANCE UPON, THE TOOL AND THE DELIVERABLES.
|
||||
The Tool for authorization use HttpOnly cookies-files. Also in local memory of your web browser (known as "local storage") stored language settings.
|
||||
|
||||
Consent to Use of Data
|
||||
Your projects will be stored on Developer servers. By agreeing to these terms of use, you accept that your project data will be used by Developer and/or its representatives for internal purposes (such as subsequent improving the Tool).
|
||||
Rest assured that will Developer and/or its representatives intentionally not share or transfer about your projects to anyone.
|
||||
Developer can use email address specified in the account to send notifications about changes to these Terms, request feedback on the use of the Tool and provide technical support.
|
||||
|
||||
Restrictions
|
||||
You may not (and you may not allow anyone else to):
|
||||
(i) reverse engineer, decompile, disassemble or otherwise attempt to derive access to the source code of the Tool, or any part thereof,
|
||||
(ii) misuse the Tool by interfering with its normal operation, or attempting to access it using a method other than through the interfaces and instructions that provide,
|
||||
(iii) submit or upload any data or content that is illegal or violates these Terms of Use,
|
||||
(iv) use the Tool on the territory of Russian Federation for objects of state-owned enterprises/institutions (including companies with state participation), as well as for those objects whose data can be identified as classified information.
|
||||
The data submitted by you by use of the Tool may not exceed 1 GB.
|
||||
Developer reserves the right to delete your data (account and projects) in its sole discretion.
|
||||
You undertake to indemnify and hold Developer and/or its representatives harmless for all damages and losses incurred due to any breach of the undertakings in this Section.
|
||||
|
||||
DISCLAIMER
|
||||
THE TOOL AND ANY DELIVERABLES ARE DELIVERED FREE OF CHARGE AND 'AS IS' WITHOUT WARRANTY OF ANY KIND. THE ENTIRE RISK AS TO THE RESULTS AND PERFORMANCE OF THE TOOL AND THE DELIVERABLES IS ASSUMED BY YOU/THE USER. DEVELOPER DISCLAIMS ALL WARRANTIES, WHETHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT, OR ANY WARRANTY ARISING OUT OF ANY PROPOSAL, SPECIFICATION OR SAMPLE WITH RESPECT TO THE TOOL AND DELIVERABLES. DEVELOPER AND/OR ITS REPRESENTATIVES SHALL NOT BE LIABLE FOR LOSS OF DATA, LOSS OF PRODUCTION, LOSS OF PROFIT, LOSS OF USE, LOSS OF CONTRACTS OR FOR ANY OTHER CONSEQUENTIAL, ECONOMIC OR INDIRECT LOSS WHATSOEVER IN RESPECT OF DELIVERY, USE OR DISPOSITION OF THE TOOL AND THE DELIVERABLES. DEVELOPER AND/OR ITS REPRESENTATIVES TOTAL LIABILITY FOR ANY AND ALL CLAIMS, DAMAGES AND LIABILITY IN ACCORDANCE WITH THE DELIVERY AND USE OF THE TOOL AND THE DELIVERABLES SHALL NOT EXCEED THE PRICE PAID FOR THE TOOL.
|
||||
|
||||
Governing law and dispute resolution
|
||||
These Terms of Use shall be deemed performed in and shall be construed and governed by the laws of Russian Federation.
|
||||
1
public/telegram_star.svg
Normal file
1
public/telegram_star.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="14" height="15" viewBox="0 0 14 15" fill="000" 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" fill="000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,5 +1,6 @@
|
||||
import { defineBoot } from '#q-app/wrappers'
|
||||
import axios, { type AxiosError } from 'axios'
|
||||
import { Notify } from 'quasar'
|
||||
|
||||
class ServerError extends Error {
|
||||
constructor(
|
||||
@@ -19,6 +20,7 @@ const api = axios.create({
|
||||
api.interceptors.response.use(
|
||||
response => response,
|
||||
async (error: AxiosError<{ error?: { code: string; message: string } }>) => {
|
||||
console.log(error)
|
||||
const errorData = error.response?.data?.error || {
|
||||
code: 'ZERO',
|
||||
message: error.message || 'Unknown error'
|
||||
@@ -29,6 +31,12 @@ api.interceptors.response.use(
|
||||
errorData.message
|
||||
)
|
||||
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: errorData.code + ': ' + errorData.message,
|
||||
icon: 'mdi-alert-outline',
|
||||
position: 'bottom'
|
||||
})
|
||||
return Promise.reject(serverError)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
<template>
|
||||
<div class="q-pt-md" v-if="mapUsers.length !==0 ">
|
||||
<span class="q-pl-md text-h6">
|
||||
{{ $t('company_info__users') }}
|
||||
</span>
|
||||
|
||||
<q-list separator>
|
||||
<q-item
|
||||
v-for="item in mapUsers"
|
||||
:key="item.id"
|
||||
v-ripple
|
||||
clickable
|
||||
@click="goPersonInfo(item.id)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<pn-auto-avatar
|
||||
:img="item.photo"
|
||||
:name="item.section1"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label lines="1" class="text-bold" v-if="item.section1">
|
||||
{{item.section1}}
|
||||
</q-item-label>
|
||||
<q-item-label caption lines="2">
|
||||
<span v-if="item.section2_1" class="q-mr-sm">{{item.section2_1}}</span>
|
||||
<span class="text-blue" v-if="item.section2_2">{{'@' + item.section2_2}}</span>
|
||||
</q-item-label>
|
||||
<q-item-label lines="1" v-if="item.section3">
|
||||
{{item.section3}}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useUsersStore } from 'stores/users'
|
||||
import type { User } from 'types/Users'
|
||||
const usersStore = useUsersStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const currentCompanyId = Number(route.params.companyId)
|
||||
|
||||
const users = usersStore.users
|
||||
|
||||
const mapUsers = users
|
||||
.filter(el => el.company_id === currentCompanyId)
|
||||
.map(el => ({...el, ...userSection(el)}))
|
||||
|
||||
|
||||
async function goPersonInfo (userId: number) {
|
||||
await router.push({ name: 'user_info', params: { id: route.params.id, userId }})
|
||||
}
|
||||
|
||||
// copy from 'pages/project-page/ProjectPageUsers.vue' кроме company.name
|
||||
function userSection (user: User) {
|
||||
const tname = () => {
|
||||
return user.firstname
|
||||
? user.lastname
|
||||
? user.firstname + ' ' + user.lastname
|
||||
: user.firstname
|
||||
: user.lastname ?? ''
|
||||
}
|
||||
|
||||
const section1 = user.name
|
||||
? user.name
|
||||
: tname()
|
||||
|
||||
const section2_1 = user.name
|
||||
? tname()
|
||||
: ''
|
||||
|
||||
const section2_2 = user.username ?? ''
|
||||
|
||||
const section3 = (
|
||||
user.department
|
||||
? user.department + ' '
|
||||
: ''
|
||||
) + (
|
||||
user.role ?? ''
|
||||
)
|
||||
|
||||
return {
|
||||
section1,
|
||||
section2_1, section2_2,
|
||||
section3
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -34,37 +34,46 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useSettingsStore } from 'stores/settings'
|
||||
|
||||
const fileText = ref<string>('')
|
||||
const isLoading = ref<boolean>(true)
|
||||
const error = ref<boolean>(false)
|
||||
const settingsStore = useSettingsStore()
|
||||
const lang = ref<string>('EN')
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'terms_of_use' | 'privacy'
|
||||
}>()
|
||||
|
||||
function parseLocale(locale: string): string {
|
||||
return locale.split(/[-_]/)[0] ?? ''
|
||||
const settingsStore = useSettingsStore()
|
||||
const fileText = ref<string | null>('')
|
||||
const isLoading = ref(true)
|
||||
const DEFAULT_LANG = 'ru'
|
||||
|
||||
const baseDocName = props.type === 'terms_of_use'
|
||||
? 'Terms_of_use'
|
||||
: 'Privacy'
|
||||
|
||||
const parseLocale = (locale: string) => locale.split(/[-_]/)[0] || DEFAULT_LANG
|
||||
|
||||
const fetchDocument = async (language: string) => {
|
||||
try {
|
||||
const response = await fetch(`/admin/doc/${baseDocName}_${language}.txt`)
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||
return await response.text()
|
||||
} catch (error) {
|
||||
console.error(`Failed to load ${language} version:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const baseDocName =
|
||||
props.type ==='terms_of_use' ? 'Terms_of_use' : 'Privacy'
|
||||
|
||||
onMounted(async () => {
|
||||
const locale = settingsStore.settings.locale
|
||||
lang.value = parseLocale(locale)
|
||||
try {
|
||||
const response = await fetch('/admin/doc/' + baseDocName + '_' + lang.value +'.txt')
|
||||
const lang = parseLocale(settingsStore.settings.locale)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`)
|
||||
fileText.value = await fetchDocument(lang)
|
||||
|
||||
if (!fileText.value && lang !== DEFAULT_LANG) {
|
||||
fileText.value = await fetchDocument(DEFAULT_LANG)
|
||||
}
|
||||
|
||||
fileText.value = await response.text()
|
||||
} catch (err) {
|
||||
console.error('File load error:', err)
|
||||
error.value = true
|
||||
if (!fileText.value) throw new Error('All loading attempts failed')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Document loading failed:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
125
src/components/pnItemBtmDialog.vue
Normal file
125
src/components/pnItemBtmDialog.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<q-item
|
||||
@click="showDialog=true"
|
||||
clickable
|
||||
v-ripple
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar
|
||||
rounded
|
||||
text-color="white"
|
||||
:icon
|
||||
:color="iconColor"
|
||||
size="lg"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
{{ $t(title) }}
|
||||
</q-item-label>
|
||||
<q-item-label caption>
|
||||
<slot name="value"/>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="mdi-chevron-right" color="grey"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-dialog
|
||||
v-model="showDialog"
|
||||
maximized
|
||||
transition-show="slide-up"
|
||||
transition-hide="slide-down"
|
||||
position="bottom"
|
||||
>
|
||||
<q-card
|
||||
class="fix-card-width flex column no-scroll no-wrap q-px-none"
|
||||
style="
|
||||
border-top-left-radius: var(--top-raduis) !important;
|
||||
border-top-right-radius: var(--top-raduis) !important;
|
||||
"
|
||||
>
|
||||
<div
|
||||
ref="cardHeaderRef"
|
||||
class="flex items-center no-wrap justify-between w100 q-my-none q-pa-md"
|
||||
>
|
||||
<div>
|
||||
<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/>
|
||||
</div>
|
||||
</pn-shadow-scroll>
|
||||
</div>
|
||||
<div
|
||||
ref="cardFooterRef"
|
||||
class="q-pa-md"
|
||||
>
|
||||
<slot name="footer"/>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useSlots } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
caption?: string
|
||||
icon: string
|
||||
iconColor: string
|
||||
}>()
|
||||
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.fix-card-width {
|
||||
width: var(--body-width) !important;
|
||||
}
|
||||
</style>
|
||||
53
src/components/pnListSelector.vue
Normal file
53
src/components/pnListSelector.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<q-list class="q-gutter-y-sm">
|
||||
<q-btn
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
flat
|
||||
no-caps dense
|
||||
class="w100"
|
||||
align="left"
|
||||
@click="selectItem(option)"
|
||||
:class="isSelected(option.value) ? 'text-primary' : ''"
|
||||
>
|
||||
<q-icon
|
||||
:name="isSelected(option.value) ? 'mdi-check' : ''"
|
||||
color="primary"
|
||||
class="q-pr-sm"
|
||||
/>
|
||||
<span
|
||||
:class="!isSelected(option.value) ? 'text-weight-regular' : ''"
|
||||
>
|
||||
{{ $te(option.label) ? $t(option.label) : option.label }}
|
||||
</span>
|
||||
</q-btn>
|
||||
</q-list>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface ListOption {
|
||||
label: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
options: ListOption[]
|
||||
}>()
|
||||
|
||||
const model = defineModel<string | number | null>({
|
||||
required: true
|
||||
})
|
||||
|
||||
const isSelected = computed(() => (value: string | number) => {
|
||||
return model.value === value
|
||||
})
|
||||
|
||||
const selectItem = (option: ListOption) => {
|
||||
model.value = option.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<q-dialog
|
||||
v-model="modelValue"
|
||||
>
|
||||
<q-card class="q-pa-none q-ma-none w100 no-scroll" align="center">
|
||||
<q-dialog v-model="modelValue">
|
||||
<q-card
|
||||
class="q-pa-none q-ma-none w100 no-scroll"
|
||||
align="center"
|
||||
>
|
||||
<q-card-section>
|
||||
<q-avatar :color :icon size="60px" font-size="45px" text-color="white"/>
|
||||
</q-card-section>
|
||||
@@ -12,50 +13,55 @@
|
||||
style="overflow-wrap: break-word"
|
||||
>
|
||||
<div class="text-h6 text-bold ">
|
||||
{{ $t(title)}}
|
||||
{{ $t(title) }}
|
||||
</div>
|
||||
<div v-if="message1">
|
||||
{{ $t(message1)}}
|
||||
{{ $t(message1) }}
|
||||
</div>
|
||||
<div v-if="message2">
|
||||
{{ $t(message2)}}
|
||||
{{ $t(message2) }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="center" vertical>
|
||||
<div class="flex q-mt-lg no-wrap w100 justify-center q-gutter-x-md">
|
||||
<q-btn
|
||||
v-if="auxBtnLabel"
|
||||
:label="$t(auxBtnLabel)"
|
||||
outline
|
||||
color="grey"
|
||||
v-close-popup
|
||||
rounded
|
||||
class="w50"
|
||||
@click="emit('clickAuxBtn')"
|
||||
/>
|
||||
<q-card-section>
|
||||
<div class="flex column w100 q-mt-lg q-px-sm">
|
||||
<div class="flex q-gutter-md">
|
||||
<div class="col-grow" v-if="auxBtnLabel">
|
||||
<q-btn
|
||||
:label="$t(auxBtnLabel)"
|
||||
outline
|
||||
color="grey"
|
||||
class="w100"
|
||||
v-close-popup
|
||||
rounded
|
||||
@click="emit('clickAuxBtn')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-grow">
|
||||
<q-btn
|
||||
:label="$t(mainBtnLabel)"
|
||||
:color="color"
|
||||
class="w100"
|
||||
v-close-popup
|
||||
rounded
|
||||
@click="emit('clickMainBtn')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
:label="$t(mainBtnLabel)"
|
||||
:color="color"
|
||||
v-close-popup
|
||||
rounded
|
||||
:class="auxBtnLabel ? 'w50' : 'w80'"
|
||||
@click="emit('clickMainBtn')"
|
||||
/>
|
||||
class="w100 q-mt-md q-mb-sm" flat
|
||||
v-close-popup rounded
|
||||
@click="emit('close')"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<q-icon name="close"/>
|
||||
{{$t('close')}}
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
class="w80 q-mt-md q-mb-sm" flat
|
||||
v-close-popup rounded
|
||||
@click="emit('close')"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<q-icon name="close"/>
|
||||
{{$t('cancel')}}
|
||||
</div>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
@@ -83,5 +89,5 @@
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
140
src/components/pnTimeZoneSelector.vue
Normal file
140
src/components/pnTimeZoneSelector.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="w100 flex column">
|
||||
<div class="flex row w100">
|
||||
<q-input
|
||||
v-model="search"
|
||||
clearable
|
||||
clear-icon="close"
|
||||
:placeholder="$t('settings__timezone_search')"
|
||||
dense
|
||||
class="col-grow"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<q-list class="q-gutter-y-sm q-my-sm">
|
||||
<q-btn
|
||||
v-for="tz in displayTimeZones"
|
||||
:key="tz.value"
|
||||
flat
|
||||
no-caps
|
||||
dense
|
||||
class="w100"
|
||||
align="left"
|
||||
@click="selectTz(tz)"
|
||||
:class="isSelected(tz.value) ? 'text-primary' : ''"
|
||||
>
|
||||
<div class="flex w100 no-wrap">
|
||||
<q-icon
|
||||
:name="isSelected(tz.value) ? 'mdi-check' : ''"
|
||||
color="primary"
|
||||
class="q-pr-sm"
|
||||
/>
|
||||
<div class="flex row no-wrap justify-between w100">
|
||||
<div>
|
||||
<span class="text-grey text-weight-regular">{{ tz.continent + ' / ' }}</span>
|
||||
<span>{{ tz.city + ' ' }}</span>
|
||||
</div>
|
||||
<span class="text-grey text-weight-regular">{{ tz.offsetDisplay }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-btn>
|
||||
</q-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watchEffect } from 'vue'
|
||||
|
||||
interface TimeZone {
|
||||
value: string
|
||||
continent: string
|
||||
city: string
|
||||
offsetDisplay: string
|
||||
offsetHours: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
locale: string
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<{ tz: string; offset: number, offsetString: string }>({
|
||||
required: true
|
||||
})
|
||||
|
||||
const search = ref('')
|
||||
const timeZonesBase = Intl.supportedValuesOf('timeZone')
|
||||
const timeZones = ref<TimeZone[]>([])
|
||||
|
||||
watchEffect(() => {
|
||||
const date = Date.now()
|
||||
|
||||
timeZones.value = timeZonesBase.map((tz) => {
|
||||
const [continent, ...cityParts] = tz.split('/')
|
||||
const city = cityParts.join('/').replace(/_/g, ' ')
|
||||
|
||||
const formatter = new Intl.DateTimeFormat('en', {
|
||||
timeZone: tz,
|
||||
timeZoneName: 'longOffset'
|
||||
})
|
||||
|
||||
const offsetPart = formatter
|
||||
.formatToParts(date)
|
||||
.find(part => part.type === 'timeZoneName')?.value || 'UTC'
|
||||
|
||||
const cleanOffset = offsetPart.replace(/^GMT|^UTC/, '') || '+00:00'
|
||||
let sign = 1
|
||||
let offsetString = cleanOffset
|
||||
|
||||
if (cleanOffset.startsWith('-')) {
|
||||
sign = -1
|
||||
offsetString = cleanOffset.substring(1)
|
||||
} else if (cleanOffset.startsWith('+')) {
|
||||
offsetString = cleanOffset.substring(1)
|
||||
}
|
||||
|
||||
const [hours = '0', minutes = '0'] = offsetString.split(':')
|
||||
const totalHours = sign * (parseInt(hours) + parseInt(minutes) / 60)
|
||||
|
||||
const absHours = Math.abs(parseInt(hours))
|
||||
const absMinutes = Math.abs(parseInt(minutes))
|
||||
const displaySign = sign === -1 ? '-' : '+'
|
||||
const offsetDisplay = `${displaySign}${String(absHours).padStart(2, '0')}:${String(absMinutes).padStart(2, '0')}`
|
||||
|
||||
return {
|
||||
value: tz,
|
||||
continent: continent?.replace(/_/g, ' ') || '',
|
||||
city: city,
|
||||
offsetDisplay,
|
||||
offsetHours: totalHours
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const displayTimeZones = computed(() => {
|
||||
if (!search.value || !(search.value && search.value.trim())) return timeZones.value
|
||||
const searchValue = search.value.trim().toLowerCase()
|
||||
|
||||
return timeZones.value.filter(
|
||||
el =>
|
||||
el.continent.toLowerCase().includes(searchValue) ||
|
||||
el.city.toLowerCase().includes(searchValue) ||
|
||||
el.offsetDisplay.includes(searchValue) ||
|
||||
el.offsetDisplay.replace(':', '').includes(searchValue)
|
||||
)
|
||||
})
|
||||
|
||||
const isSelected = (value: string) => {
|
||||
return modelValue.value.tz === value
|
||||
}
|
||||
|
||||
const selectTz = (tz: TimeZone) => {
|
||||
modelValue.value = { tz: tz.value, offset: tz.offsetHours, offsetString: tz.offsetDisplay }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -68,7 +68,6 @@
|
||||
{ id: 5, name: 'account__company_data', icon: 'mdi-account-group-outline', description: 'account__company_data_description', pathName: 'your_company' },
|
||||
{ id: 6, name: 'account__settings', icon: 'mdi-cog-outline', description: 'account__settings_description', iconColor: 'info', pathName: 'settings' },
|
||||
{ id: 7, name: 'account__support', icon: 'mdi-lifebuoy', description: 'account__support_description', iconColor: 'info', pathName: 'support' },
|
||||
{ id: 8, name: 'account__3rd_party_software', icon: 'mdi-scale-balance', description: 'account__3rd_party_software_description', iconColor: 'grey', pathName: '3software' },
|
||||
{ id: 9, name: 'account__terms_of_use', icon: 'mdi-book-open-variant-outline', description: '', iconColor: 'grey', pathName: 'terms' },
|
||||
{ id: 10, name: 'account__privacy', icon: 'mdi-lock-outline', description: '', iconColor: 'grey', pathName: 'privacy' }
|
||||
]))
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<company-block
|
||||
v-model="newCompany"
|
||||
title="company_create__title_card"
|
||||
btnText="company_create__btn"
|
||||
title="company_add__title_card"
|
||||
btnText="company_add__btn"
|
||||
@update = "addCompany"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
v-model="searchProject"
|
||||
clearable
|
||||
clear-icon="close"
|
||||
:placeholder="$t('project_chats__search')"
|
||||
:placeholder="$t('projects__search')"
|
||||
dense
|
||||
class="col-grow q-px-md q-py-md"
|
||||
>
|
||||
@@ -91,22 +91,22 @@
|
||||
class="flex column items-center w100"
|
||||
:class="showArchive ? 'bg-grey-12' : ''"
|
||||
>
|
||||
<q-btn-dropdown
|
||||
class="w100 fix-rotate-arrow"
|
||||
<q-btn
|
||||
class="w100 rotate-icon-btn"
|
||||
color="grey"
|
||||
flat no-caps
|
||||
flat
|
||||
no-caps
|
||||
@click="showArchive=!showArchive"
|
||||
dropdown-icon="arrow_drop_up"
|
||||
icon-right="arrow_drop_down"
|
||||
:class="{ 'rotate-icon': showArchive }"
|
||||
>
|
||||
<template #label>
|
||||
<span class="text-caption">
|
||||
{{ !showArchive
|
||||
? $t('projects__show_archive') + ' (' + archiveProjects.length +')'
|
||||
: $t('projects__hide_archive')
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</q-btn-dropdown>
|
||||
<span class="text-caption">
|
||||
{{ !showArchive
|
||||
? $t('projects__show_archive') + ' (' + archiveProjects.length +')'
|
||||
: $t('projects__hide_archive')
|
||||
}}
|
||||
</span>
|
||||
</q-btn>
|
||||
|
||||
<div class="w100" style="overflow: hidden">
|
||||
<transition
|
||||
@@ -186,6 +186,7 @@
|
||||
color="negative"
|
||||
title="projects__dialog_archive_title"
|
||||
message1="projects__dialog_archive_message"
|
||||
message2="projects__dialog_archive_message2"
|
||||
mainBtnLabel="projects__dialog_archive_ok"
|
||||
@clickMainBtn="onConfirmArchiveProject()"
|
||||
@close="onCancel()"
|
||||
@@ -198,7 +199,8 @@
|
||||
color="green"
|
||||
title="projects__dialog_restore_title"
|
||||
message1="projects__dialog_restore_message"
|
||||
mainBtnLabel="projects__dialog_cancel_ok"
|
||||
message2="projects__dialog_restore_message2"
|
||||
mainBtnLabel="projects__dialog_restore_ok"
|
||||
@clickMainBtn="restoreFromArchive()"
|
||||
/>
|
||||
</template>
|
||||
@@ -319,7 +321,17 @@
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.fix-rotate-arrow :deep(.q-btn-dropdown--simple) {
|
||||
margin-left: 0;
|
||||
.rotate-icon-btn {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.rotate-icon-btn.rotate-icon :deep(.q-icon) {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||
}
|
||||
|
||||
.rotate-icon-btn :deep(.q-icon) {
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,75 +6,120 @@
|
||||
|
||||
<pn-scroll-list>
|
||||
<q-list separator>
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-avatar color="primary" rounded text-color="white" icon="mdi-translate" size="md" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<span>{{ $t('settings__language') }}</span>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-select
|
||||
class="fix-input-right text-body1"
|
||||
v-model="locale"
|
||||
:options="localeOptions"
|
||||
dense
|
||||
borderless
|
||||
emit-value
|
||||
map-options
|
||||
hide-bottom-space
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-avatar color="primary" rounded text-color="white" icon="mdi-format-size" size="md" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<span>{{ $t('settings__font_size') }}</span>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<div class="flex justify-end">
|
||||
<q-btn
|
||||
@click="settingsStore.decreaseFontSize()"
|
||||
color="negative" flat
|
||||
icon="mdi-format-font-size-decrease"
|
||||
class="q-pa-sm q-mx-xs"
|
||||
:disable="!settingsStore.canDecrease"
|
||||
/>
|
||||
<q-btn
|
||||
@click="settingsStore.increaseFontSize()"
|
||||
color="positive" flat
|
||||
icon="mdi-format-font-size-increase"
|
||||
class="q-pa-sm q-mx-xs"
|
||||
:disable="!settingsStore.canIncrease"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item-label header>{{ $t('settings__software_title') }}</q-item-label>
|
||||
|
||||
<pn-item-btm-dialog
|
||||
title="settings__language"
|
||||
caption="settings__software_title"
|
||||
icon="mdi-translate"
|
||||
iconColor="primary"
|
||||
>
|
||||
<template #value>
|
||||
{{ localeOptions.find(el => el.value === locale)?.label }}
|
||||
</template>
|
||||
<pn-list-selector
|
||||
v-model="locale"
|
||||
:options="localeOptions"
|
||||
/>
|
||||
</pn-item-btm-dialog>
|
||||
|
||||
<pn-item-btm-dialog
|
||||
title="settings__font_size"
|
||||
caption="settings__software_title"
|
||||
icon="mdi-format-size"
|
||||
iconColor="primary"
|
||||
>
|
||||
<template #value>
|
||||
{{ $t(fontSizeLabel) }}
|
||||
</template>
|
||||
<pn-list-selector
|
||||
v-model="fontSize"
|
||||
:options="fontSizeOptions"
|
||||
/>
|
||||
</pn-item-btm-dialog>
|
||||
|
||||
<q-item-label class="q-mt-md" header>{{ $t('settings__bot_title') }}</q-item-label>
|
||||
|
||||
<pn-item-btm-dialog
|
||||
title="settings__language"
|
||||
caption="settings__bot_title"
|
||||
icon="mdi-translate"
|
||||
iconColor="primary"
|
||||
>
|
||||
<template #value>
|
||||
{{ localeOptions.find(el => el.value === localeBot)?.label }}
|
||||
</template>
|
||||
<pn-list-selector
|
||||
v-model="localeBot"
|
||||
:options="localeOptions"
|
||||
/>
|
||||
</pn-item-btm-dialog>
|
||||
<pn-item-btm-dialog
|
||||
title="settings__timezone"
|
||||
caption="settings__bot_title"
|
||||
icon="mdi-map-clock-outline"
|
||||
iconColor="primary"
|
||||
>
|
||||
<template #value>
|
||||
{{ timeZoneBot.tz }}
|
||||
{{ timeZoneBot.offsetString }}
|
||||
</template>
|
||||
|
||||
<pn-time-zone-selector
|
||||
v-model="timeZoneBot"
|
||||
:locale
|
||||
/>
|
||||
</pn-item-btm-dialog>
|
||||
</q-list>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ref, watch, computed, onMounted } from 'vue'
|
||||
import { useSettingsStore } from 'stores/settings'
|
||||
import pnItemBtmDialog from 'components/pnItemBtmDialog.vue'
|
||||
import pnListSelector from 'components/pnListSelector.vue'
|
||||
import pnTimeZoneSelector from 'components/pnTimeZoneSelector.vue'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const locale = ref('')
|
||||
const localeBot = ref('')
|
||||
const localeOptions = settingsStore.supportLocale
|
||||
|
||||
const locale = computed({
|
||||
get: () => settingsStore.settings.locale,
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
set: (value: string) => settingsStore.updateLocale(value)
|
||||
watch(locale, async (newValue) => {
|
||||
await settingsStore.updateSettings({ locale: newValue })
|
||||
})
|
||||
|
||||
watch(localeBot, async (newValue) => {
|
||||
await settingsStore.updateSettings({ localeBot: newValue })
|
||||
})
|
||||
|
||||
const fontSize = ref(14)
|
||||
const fontSizeOptions = settingsStore.supportFontSizes
|
||||
const fontSizeLabel = computed(() =>
|
||||
fontSizeOptions.find(el => el.value === fontSize.value)?.label ?? ''
|
||||
)
|
||||
|
||||
watch(fontSize, async (newValue) => {
|
||||
await settingsStore.updateSettings({ fontSize: newValue })
|
||||
})
|
||||
|
||||
const timeZoneBot = ref<{ tz: string, offset: number }>({ tz: '', offset: 1 })
|
||||
|
||||
watch(timeZoneBot, async (newValue) => {
|
||||
if (newValue) await settingsStore.updateSettings({ timeZoneBot: newValue })
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
locale.value = settingsStore.settings.locale
|
||||
localeBot.value = settingsStore.settings.localeBot
|
||||
fontSize.value = settingsStore.settings.fontSize
|
||||
timeZoneBot.value = settingsStore.settings.timeZoneBot
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fix-input-right :deep(.q-field__native) {
|
||||
justify-content: end;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
v-model="search"
|
||||
clearable
|
||||
clear-icon="close"
|
||||
:placeholder="$t('project_chats__search')"
|
||||
:placeholder="$t('chats__search')"
|
||||
dense
|
||||
class="col-grow"
|
||||
v-if="chats.length !== 0"
|
||||
@@ -71,8 +71,8 @@
|
||||
<pn-onboard-btn
|
||||
v-if="chats.length === 0 && chatsInit"
|
||||
icon="mdi-chat-plus-outline"
|
||||
:message1="$t('project_chat__onboard_msg1')"
|
||||
:message2="$t('project_chat__onboard_msg2')"
|
||||
:message1="$t('chats__onboard_msg1')"
|
||||
:message2="$t('chats__onboard_msg2')"
|
||||
@click="showOverlay=true; fabState=true"
|
||||
/>
|
||||
<div
|
||||
@@ -110,7 +110,7 @@
|
||||
anchor="center left" self="center end"
|
||||
style="width: calc(min(100vw, var(--body-width)) - 102px) !important;"
|
||||
>
|
||||
{{ $t('project_chats_disabled_FAB')}}
|
||||
{{ $t('chats_disabled_FAB')}}
|
||||
</q-tooltip>
|
||||
</template>
|
||||
<q-fab-action
|
||||
@@ -152,9 +152,10 @@
|
||||
v-model="showDialogDeleteChat"
|
||||
icon="mdi-link-off"
|
||||
color="negative"
|
||||
title="project_chat__delete_warning"
|
||||
message1="project_chat__delete_warning_message"
|
||||
mainBtnLabel="project_chat__dialog_cancel_ok"
|
||||
title="chats__dialog_unlink_title"
|
||||
message1="chats__dialog_unlink_message"
|
||||
message2="chats__dialog_unlink_message2"
|
||||
mainBtnLabel="chats__dialog_unlink_ok"
|
||||
@clickMainBtn="onConfirm()"
|
||||
@close="onCancel()"
|
||||
@before-hide="onDialogBeforeHide()"
|
||||
@@ -190,8 +191,8 @@
|
||||
const chatsInit = computed(() => chatsStore.isInit)
|
||||
|
||||
const fabMenu = [
|
||||
{id: 1, icon: 'mdi-chat-plus-outline', name: 'project_chats__attach_chat', description: 'project_chats__attach_chat_description', func: attachChat},
|
||||
{id: 2, icon: 'mdi-share-outline', name: 'project_chats__send_chat', description: 'project_chats__send_chat_description', func: sendChat},
|
||||
{id: 1, icon: 'mdi-chat-plus-outline', name: 'chats__attach_chat', description: 'chats__attach_chat_description', func: attachChat},
|
||||
{id: 2, icon: 'mdi-share-outline', name: 'chats__send_chat', description: 'chats__send_chat_description', func: sendChat},
|
||||
]
|
||||
|
||||
const displayChats = computed(() => {
|
||||
@@ -253,7 +254,7 @@
|
||||
const key = await chatsStore.getKey()
|
||||
const message = urlAdmin + key + urlAdminPermission
|
||||
const tgShareUrl = 'https://t.me/share/url?url=' +
|
||||
encodeURIComponent( t('project_chats__send_chat_title')) +
|
||||
encodeURIComponent( t('chats__send_chat_title')) +
|
||||
'&text=' + `${encodeURIComponent(message)}`
|
||||
tg.openTelegramLink(tgShareUrl)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:color="companies.length <= 2 ? 'grey-6' : 'primary'"
|
||||
flat
|
||||
no-caps
|
||||
@click="maskCompany()"
|
||||
@click="maskCompany"
|
||||
:disable="companies.length <= 2"
|
||||
class="q-pr-md"
|
||||
rounded
|
||||
@@ -16,9 +16,10 @@
|
||||
left
|
||||
size="sm"
|
||||
name="mdi-domino-mask"
|
||||
class="q-mr-xs"
|
||||
/>
|
||||
<div>
|
||||
{{ $t('company__mask')}}
|
||||
{{ $t('companies__mask')}}
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
@@ -53,7 +54,7 @@
|
||||
<q-item-label lines="1" class="text-caption text-amber-10" v-if="item.id === myCompany?.id">
|
||||
<div class="flex items-center">
|
||||
<q-icon name="star" class="q-pr-xs"/>
|
||||
{{ $t('company__my_company') }}
|
||||
{{ $t('companies__my_company') }}
|
||||
</div>
|
||||
</q-item-label>
|
||||
<q-item-label lines="1" class="text-bold">{{ item.name }}</q-item-label>
|
||||
@@ -93,9 +94,9 @@
|
||||
<pn-onboard-btn
|
||||
v-if="companies.length <= 1 && companiesInit"
|
||||
icon="mdi-account-multiple-plus-outline"
|
||||
:message1="$t('company__onboard_msg1')"
|
||||
:message2="$t('company__onboard_msg2')"
|
||||
@btn-click="createCompany()"
|
||||
:message1="$t('companies__onboard_msg1')"
|
||||
:message2="$t('companies__onboard_msg2')"
|
||||
@btn-click="createCompany"
|
||||
/>
|
||||
<div
|
||||
class="flex column justify-center items-center w100"
|
||||
@@ -130,9 +131,10 @@
|
||||
v-model="showDialogDeleteCompany"
|
||||
icon="mdi-account-multiple-minus-outline"
|
||||
color="negative"
|
||||
title="company__dialog_delete_title"
|
||||
message1="company__dialog_delete_message"
|
||||
mainBtnLabel="company__dialog_delete_ok"
|
||||
title="companies__dialog_delete_title"
|
||||
message1="companies__dialog_delete_message"
|
||||
message2="companies__dialog_delete_message2"
|
||||
mainBtnLabel="companies__dialog_delete_ok"
|
||||
@clickMainBtn="onConfirm()"
|
||||
@close="onCancel()"
|
||||
@before-hide="onDialogBeforeHide()"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
v-model="search"
|
||||
clearable
|
||||
clear-icon="close"
|
||||
:placeholder="$t('project_users__search')"
|
||||
:placeholder="$t('users__search')"
|
||||
dense
|
||||
class="col-grow"
|
||||
>
|
||||
@@ -60,27 +60,92 @@
|
||||
</q-slide-item>
|
||||
</q-list>
|
||||
|
||||
<!-- LEAVE USERS SECTION -->
|
||||
<div
|
||||
v-if="leaveUsers.length!==0"
|
||||
class="flex column items-center w100"
|
||||
:class="showLeaveUsers ? 'bg-grey-12' : ''"
|
||||
>
|
||||
<q-btn
|
||||
class="w100 rotate-icon-btn"
|
||||
color="grey"
|
||||
flat
|
||||
no-caps
|
||||
@click="showLeaveUsers=!showLeaveUsers"
|
||||
icon-right="arrow_drop_down"
|
||||
:class="{ 'rotate-icon': showLeaveUsers }"
|
||||
>
|
||||
<span class="text-caption">
|
||||
{{ !showLeaveUsers
|
||||
? $t('users__show_left_users') + ' (' + leaveUsers.length +')'
|
||||
: $t('users__hide_left_users')
|
||||
}}
|
||||
</span>
|
||||
</q-btn>
|
||||
|
||||
<div class="w100" style="overflow: hidden">
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated slideInDown"
|
||||
leave-active-class="animated slideOutUp"
|
||||
>
|
||||
<q-list separator v-if="showLeaveUsers" class="w100">
|
||||
<q-item
|
||||
v-for = "item in leaveUsers"
|
||||
:key="item.id"
|
||||
class="w100 text-grey"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<pn-auto-avatar
|
||||
:img="item.photo"
|
||||
:name="item.section1"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label lines="1" class="text-bold" v-if="item.section1">
|
||||
{{item.section1}}
|
||||
</q-item-label>
|
||||
<q-item-label lines="1" caption v-if="item.section3">
|
||||
{{item.section3}}
|
||||
</q-item-label>
|
||||
<q-item-label caption lines="2">
|
||||
<div class="flex items-center">
|
||||
<q-icon name="telegram" v-if="item.section2_1 || item.section2_2" class="q-pr-xs" style="color: #27a7e7"/>
|
||||
<div v-if="item.section2_1" class="q-mr-sm text-bold">{{item.section2_1}}</div>
|
||||
<div class="text-blue" v-if="item.section2_2">{{'@' + item.section2_2}}</div>
|
||||
</div>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END LEAVE USERS SECTION -->
|
||||
|
||||
|
||||
<!-- BLOCKED USERS SECTION -->
|
||||
<div
|
||||
v-if="blockedUsers.length!==0"
|
||||
class="flex column items-center w100"
|
||||
:class="showBlockedUsers ? 'bg-grey-12' : ''"
|
||||
>
|
||||
<q-btn-dropdown
|
||||
class="w100 fix-rotate-arrow"
|
||||
<q-btn
|
||||
class="w100 rotate-icon-btn"
|
||||
color="grey"
|
||||
flat no-caps
|
||||
flat
|
||||
no-caps
|
||||
@click="showBlockedUsers=!showBlockedUsers"
|
||||
dropdown-icon="arrow_drop_up"
|
||||
icon-right="arrow_drop_down"
|
||||
:class="{ 'rotate-icon': showBlockedUsers }"
|
||||
>
|
||||
<template #label>
|
||||
<span class="text-caption">
|
||||
{{ !showBlockedUsers
|
||||
? $t('users__show_archive') + ' (' + blockedUsers.length +')'
|
||||
: $t('user__hide_archive')
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</q-btn-dropdown>
|
||||
<span class="text-caption">
|
||||
{{ !showBlockedUsers
|
||||
? $t('users__show_blocked_users') + ' (' + blockedUsers.length +')'
|
||||
: $t('users__hide_blocked_users')
|
||||
}}
|
||||
</span>
|
||||
</q-btn>
|
||||
|
||||
<div class="w100" style="overflow: hidden">
|
||||
<transition
|
||||
@@ -123,12 +188,13 @@
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END BLOCKED USERS SECTION -->
|
||||
|
||||
<pn-onboard-btn
|
||||
v-if="users.length === 0 && usersInit"
|
||||
icon="mdi-account-outline"
|
||||
:message1="$t('project_users__onboard_msg1')"
|
||||
:message2="$t('project_users__onboard_msg2')"
|
||||
:message1="$t('users__onboard_msg1')"
|
||||
:message2="$t('users__onboard_msg2')"
|
||||
noBtn
|
||||
/>
|
||||
<div
|
||||
@@ -142,13 +208,14 @@
|
||||
</div>
|
||||
|
||||
<pn-small-dialog
|
||||
v-model="showDialogDeleteUser"
|
||||
v-model="showDialogBlockUser"
|
||||
icon="mdi-account-remove-outline"
|
||||
color="negative"
|
||||
title="user__dialog_delete_title"
|
||||
message1="user__dialog_delete_message"
|
||||
mainBtnLabel="user__dialog_delete_ok"
|
||||
@clickMainBtn="onConfirmDeleteUser()"
|
||||
title="users__dialog_block_title"
|
||||
message1="users__dialog_block_message"
|
||||
message2="users__dialog_block_message2"
|
||||
mainBtnLabel="users__dialog_block_ok"
|
||||
@clickMainBtn="onConfirmBlockUser()"
|
||||
@close="onCancel()"
|
||||
@before-hide="onDialogBeforeHide()"
|
||||
/>
|
||||
@@ -157,9 +224,10 @@
|
||||
v-model="showDialogRestoreUser"
|
||||
icon="mdi-account-reactivate-outline"
|
||||
color="green"
|
||||
title="user__dialog_restore_title"
|
||||
message1="user__dialog_restore_message"
|
||||
mainBtnLabel="user__dialog_restore_ok"
|
||||
title="users__dialog_restore_title"
|
||||
message1="users__dialog_restore_message"
|
||||
message2="users__dialog_restore_message2"
|
||||
mainBtnLabel="users__dialog_restore_ok"
|
||||
@clickMainBtn="onConfirmRestoreUser()"
|
||||
/>
|
||||
|
||||
@@ -183,12 +251,18 @@
|
||||
const users = usersStore.getUsers
|
||||
const usersInit = computed(() => usersStore.isInit)
|
||||
|
||||
const deleteUserId = ref<number | undefined>(undefined)
|
||||
const showDialogDeleteUser = ref<boolean>(false)
|
||||
const blockUserId = ref<number | undefined>(undefined)
|
||||
const showDialogBlockUser = ref<boolean>(false)
|
||||
const currentSlideEvent = ref<SlideEvent | null>(null)
|
||||
const closedByUserAction = ref(false)
|
||||
|
||||
const mapUsers = computed(() => users.map(el => ({...el, ...userSection(el)})))
|
||||
const mapUsers = computed(() => users.map(el => ({
|
||||
...el,
|
||||
...userSection(el),
|
||||
companyName: el.company_id && companiesStore.companyById(el.company_id)
|
||||
? companiesStore.companyById(el.company_id)?.name
|
||||
: null
|
||||
})))
|
||||
|
||||
interface SlideEvent {
|
||||
reset: () => void
|
||||
@@ -202,12 +276,13 @@
|
||||
el.section1.toLowerCase().includes(searchValue) ||
|
||||
el.section2_1.toLowerCase().includes(searchValue) ||
|
||||
el.section2_2.toLowerCase().includes(searchValue) ||
|
||||
el.section3.toLowerCase().includes(searchValue)
|
||||
el.section3.toLowerCase().includes(searchValue) ||
|
||||
el.companyName && el.companyName.toLowerCase().includes(searchValue)
|
||||
)
|
||||
return arrOut
|
||||
})
|
||||
|
||||
const displayUsers = computed(() => displayUsersAll.value.filter(el => !el.is_block))
|
||||
const displayUsers = computed(() => displayUsersAll.value.filter(el => !el.is_blocked))
|
||||
|
||||
function userSection (user: User) {
|
||||
const tname = () => {
|
||||
@@ -244,8 +319,8 @@
|
||||
|
||||
function handleSlide (event: SlideEvent, id: number) {
|
||||
currentSlideEvent.value = event
|
||||
showDialogDeleteUser.value = true
|
||||
deleteUserId.value = id
|
||||
showDialogBlockUser.value = true
|
||||
blockUserId.value = id
|
||||
}
|
||||
|
||||
function onDialogBeforeHide () {
|
||||
@@ -263,26 +338,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function onConfirmDeleteUser() {
|
||||
async function onConfirmBlockUser() {
|
||||
closedByUserAction.value = true
|
||||
if (deleteUserId.value) {
|
||||
await usersStore.blockUser(deleteUserId.value)
|
||||
if (blockUserId.value) {
|
||||
await usersStore.blockUser(blockUserId.value)
|
||||
}
|
||||
currentSlideEvent.value = null
|
||||
}
|
||||
|
||||
const showBlockedUsers = ref(false)
|
||||
const blockedUsers = computed(() => displayUsersAll.value.filter(el => el.is_block))
|
||||
const blockedUsers = computed(() => displayUsersAll.value.filter(el => el.is_blocked))
|
||||
const unblockUserId = ref<number | undefined> (undefined)
|
||||
const showDialogRestoreUser = ref(false)
|
||||
|
||||
const showLeaveUsers = ref(false)
|
||||
const leaveUsers = computed(() => displayUsersAll.value.filter(el => el.is_leave))
|
||||
|
||||
function handleUnblockUser (id: number) {
|
||||
showDialogRestoreUser.value = true
|
||||
unblockUserId.value = id
|
||||
}
|
||||
|
||||
async function onConfirmRestoreUser () {
|
||||
if (unblockUserId.value) await usersStore.restore(unblockUserId.value)
|
||||
if (unblockUserId.value) await usersStore.unblockUser(unblockUserId.value)
|
||||
}
|
||||
|
||||
watch(showDialogRestoreUser, (newD :boolean) => {
|
||||
@@ -293,5 +371,18 @@
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
.rotate-icon-btn {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.rotate-icon-btn.rotate-icon :deep(.q-icon) {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||
}
|
||||
|
||||
.rotate-icon-btn :deep(.q-icon) {
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -129,11 +129,6 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('pages/account/SubscribePage.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
name: '3software',
|
||||
path: '/3software',
|
||||
component: () => import('pages/account/3Software.vue'),
|
||||
},
|
||||
{
|
||||
name: 'terms',
|
||||
path: '/terms-of-use',
|
||||
|
||||
@@ -14,6 +14,14 @@ interface Customer {
|
||||
company?: CompanyParams
|
||||
}
|
||||
|
||||
interface WSMessage {
|
||||
id: number
|
||||
action: string
|
||||
entity: string
|
||||
entity_id?: number
|
||||
needUpdateStores: string[]
|
||||
}
|
||||
|
||||
const ENDPOINT_MAP = {
|
||||
register: '/auth/email/register',
|
||||
forgot: '/auth/forgot',
|
||||
@@ -25,6 +33,7 @@ export type AuthFlowType = keyof typeof ENDPOINT_MAP
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const customer = ref<Customer | null>(null)
|
||||
const wsEvent = ref<WSMessage | null>(null)
|
||||
const projectsStore = useProjectsStore()
|
||||
|
||||
const isAuth = computed(() => !!customer.value)
|
||||
@@ -35,29 +44,26 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
customer.value = data.data
|
||||
const socket = new WebSocket("wss://946gp81j-9000.euw.devtunnels.ms/api/admin")
|
||||
console.log(socket)
|
||||
socket.onopen = function() {
|
||||
console.log("Соединение установлено.");
|
||||
};
|
||||
socket.onopen = () => console.log("Connection ws create.")
|
||||
|
||||
socket.onclose = function(event) {
|
||||
if (event.wasClean) {
|
||||
console.log('Соединение закрыто чисто');
|
||||
} else {
|
||||
console.log('Обрыв соединения'); // например, "убит" процесс сервера
|
||||
socket.onclose = (event) => {
|
||||
if (event.wasClean) console.log('Connection ws close.')
|
||||
else console.log('Connection ws failure!')
|
||||
console.log('Code: ' + event.code + ', reason : ' + event.reason)
|
||||
}
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
if (wsEvent.value) {
|
||||
wsEvent.value.needUpdateStores = []
|
||||
wsEvent.value = JSON.parse(event.data)
|
||||
if (wsEvent.value?.entity === 'chat') wsEvent.value.needUpdateStores = ['users']
|
||||
}
|
||||
console.log('Код: ' + event.code + ' причина: ' + event.reason);
|
||||
};
|
||||
|
||||
socket.onmessage = function(event) {
|
||||
console.log("Получены данные " + event.data);
|
||||
};
|
||||
|
||||
socket.onerror = function(error) {
|
||||
console.log("Ошибка " + error.message);
|
||||
};
|
||||
socket.onerror = (event) => console.error("Ошибка ", event)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (isAuth.value) console.log(error)
|
||||
if (isAuth.value) console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,9 +87,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
if (flowType !== 'changePwd')
|
||||
await api.post(ENDPOINT_MAP[flowType], { email })
|
||||
else
|
||||
{console.log(222)
|
||||
await api.post(ENDPOINT_MAP[flowType])
|
||||
}
|
||||
}
|
||||
|
||||
const confirmCode = async (flowType: AuthFlowType, email: string, code: string) => {
|
||||
@@ -100,36 +104,32 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
|
||||
//change email of account
|
||||
const getCodeCurrentEmail = async () => {
|
||||
const getCodeCurrentEmail = async () =>
|
||||
await api.post('/auth/email/change-email')
|
||||
}
|
||||
|
||||
const confirmCurrentEmailCode = async (code: string) => {
|
||||
|
||||
const confirmCurrentEmailCode = async (code: string) =>
|
||||
await api.post('/auth/email/change-email', { code })
|
||||
}
|
||||
|
||||
const getCodeNewEmail = async (code: string, email: string) => {
|
||||
const getCodeNewEmail = async (code: string, email: string) =>
|
||||
await api.post('/auth/email/change-email', { code, email })
|
||||
}
|
||||
|
||||
const confirmNewEmailCode = async (code: string, code2: string, email: string) => {
|
||||
const confirmNewEmailCode = async (code: string, code2: string, email: string) =>
|
||||
await api.post('/auth/email/change-email', { code, code2, email })
|
||||
}
|
||||
|
||||
const setNewEmailPassword = async (code: string, code2: string, email: string, password: string) => {
|
||||
const setNewEmailPassword = async (code: string, code2: string, email: string, password: string) =>
|
||||
await api.post('/auth/email/change-email', { code, code2, email, password })
|
||||
}
|
||||
|
||||
// user data company
|
||||
const updateMyCompany = async (companyData: CompanyParams) => {
|
||||
const response = await api.put('/customer/profile', { company: companyData })
|
||||
console.log(response)
|
||||
if (response.status === 200 && customer.value) customer.value.company = companyData
|
||||
}
|
||||
|
||||
return {
|
||||
customer,
|
||||
isAuth,
|
||||
wsEvent,
|
||||
initialize,
|
||||
loginWithCredentials,
|
||||
loginWithTelegram,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from 'boot/axios'
|
||||
import type { Chat } from 'types/Chat'
|
||||
import { useProjectsStore } from 'stores/projects'
|
||||
import { useAuthStore } from 'stores/auth'
|
||||
|
||||
export const useChatsStore = defineStore('chats', () => {
|
||||
const projectsStore = useProjectsStore()
|
||||
@@ -12,8 +13,9 @@ export const useChatsStore = defineStore('chats', () => {
|
||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
||||
|
||||
async function init () {
|
||||
const response = await api.get('/project/' + currentProjectId.value + '/chat')
|
||||
const chatsAPI = response.data.data
|
||||
reset()
|
||||
const { data } = await api.get('/project/' + currentProjectId.value + '/chat')
|
||||
const chatsAPI = data.data
|
||||
chats.value.push(...chatsAPI)
|
||||
isInit.value = true
|
||||
}
|
||||
@@ -42,6 +44,12 @@ export const useChatsStore = defineStore('chats', () => {
|
||||
return chats.value.find(el =>el.id === id)
|
||||
}
|
||||
|
||||
const authStore = useAuthStore()
|
||||
watch(() => authStore.wsEvent, async (event) => {
|
||||
if (!event || event.entity !== 'chat' || !event.needUpdateStores.includes('chats')) return
|
||||
await init()
|
||||
}, { deep: true })
|
||||
|
||||
return {
|
||||
chats,
|
||||
isInit,
|
||||
|
||||
@@ -14,6 +14,7 @@ export const useCompaniesStore = defineStore('companies', () => {
|
||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
||||
|
||||
async function init () {
|
||||
reset()
|
||||
const { data }= await api.get('/project/' + currentProjectId.value + '/company')
|
||||
const companiesAPI = data.data
|
||||
companies.value.push(...(companiesAPI.sort((a: Company, b: Company) => (a.id - b.id))))
|
||||
|
||||
@@ -9,16 +9,17 @@ import type { WebApp } from '@twa-dev/types'
|
||||
interface AppSettings {
|
||||
fontSize: number
|
||||
locale: string
|
||||
timeZoneBot: { tz: string, offset: number, offsetString: string }
|
||||
localeBot: string
|
||||
}
|
||||
|
||||
const defaultFontSize = 16
|
||||
const minFontSize = 10
|
||||
const maxFontSize = 22
|
||||
const fontSizeStep = 2
|
||||
|
||||
const defaultSettings: AppSettings = {
|
||||
fontSize: defaultFontSize,
|
||||
locale: 'en-US'
|
||||
locale: 'en-US',
|
||||
timeZoneBot: { tz: 'Europe/Moscow', offset: 3, offsetString: '+03:00' },
|
||||
localeBot: 'en-US'
|
||||
}
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
@@ -30,14 +31,18 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
const isInit = ref(false)
|
||||
|
||||
const currentFontSize = computed(() => settings.value?.fontSize ?? defaultFontSize)
|
||||
const canIncrease = computed(() => currentFontSize.value < maxFontSize)
|
||||
const canDecrease = computed(() => currentFontSize.value > minFontSize)
|
||||
|
||||
const supportLocale = [
|
||||
{ value: 'en-US', label: 'English' },
|
||||
{ value: 'ru-RU', label: 'Русский' }
|
||||
]
|
||||
|
||||
const supportFontSizes = [
|
||||
{ value: 12, label: 'settings__fontsize_small' },
|
||||
{ value: 16, label: 'settings__fontsize_medium' },
|
||||
{ value: 20, label: 'settings__fontsize_large' }
|
||||
]
|
||||
|
||||
const detectLocale = (): string => {
|
||||
const localeMap = {
|
||||
ru: 'ru-RU',
|
||||
@@ -74,10 +79,12 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
const init = async () => {
|
||||
if (authStore.isAuth) {
|
||||
try {
|
||||
const response = await api.get('/customer/settings')
|
||||
const { data } = await api.get('/customer/settings')
|
||||
settings.value = {
|
||||
fontSize: response.data.data.settings.fontSize || defaultSettings.fontSize,
|
||||
locale: response.data.data.settings.locale || detectLocale()
|
||||
fontSize: data.data.settings.fontSize || defaultSettings.fontSize,
|
||||
locale: data.data.settings.locale || detectLocale(),
|
||||
timeZoneBot: data.data.settings.timeZone || defaultSettings.timeZoneBot,
|
||||
localeBot: data.data.settings.localeBot || detectLocale()
|
||||
}
|
||||
} catch {
|
||||
settings.value.locale = detectLocale()
|
||||
@@ -93,12 +100,6 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
isInit.value = true
|
||||
}
|
||||
|
||||
const updateLocale = async (newLocale: string) => {
|
||||
settings.value.locale = newLocale
|
||||
applyLocale()
|
||||
await saveSettings()
|
||||
}
|
||||
|
||||
const saveSettings = async () => {
|
||||
await api.put('/customer/settings', { settings: settings.value })
|
||||
}
|
||||
@@ -110,19 +111,6 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
await saveSettings()
|
||||
}
|
||||
|
||||
const clampFontSize = (size: number) =>
|
||||
Math.max(minFontSize, Math.min(size, maxFontSize))
|
||||
|
||||
const increaseFontSize = async () => {
|
||||
const newSize = clampFontSize(currentFontSize.value + fontSizeStep)
|
||||
await updateSettings({ fontSize: newSize })
|
||||
}
|
||||
|
||||
const decreaseFontSize = async () => {
|
||||
const newSize = clampFontSize(currentFontSize.value - fontSizeStep)
|
||||
await updateSettings({ fontSize: newSize })
|
||||
}
|
||||
|
||||
watch(() => authStore.isAuth, (newVal) => {
|
||||
if (newVal !== undefined) void init()
|
||||
}, { immediate: true })
|
||||
@@ -130,14 +118,10 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
return {
|
||||
settings,
|
||||
supportLocale,
|
||||
supportFontSizes,
|
||||
isInit,
|
||||
currentFontSize,
|
||||
canIncrease,
|
||||
canDecrease,
|
||||
init,
|
||||
increaseFontSize,
|
||||
decreaseFontSize,
|
||||
updateSettings,
|
||||
updateLocale
|
||||
updateSettings
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from 'boot/axios'
|
||||
import type { User, UserParams } from 'types/Users'
|
||||
import { useProjectsStore } from 'stores/projects'
|
||||
import { useAuthStore } from 'stores/auth'
|
||||
|
||||
export const useUsersStore = defineStore('users', () => {
|
||||
const projectsStore = useProjectsStore()
|
||||
@@ -12,6 +13,7 @@ export const useUsersStore = defineStore('users', () => {
|
||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
||||
|
||||
async function init () {
|
||||
reset()
|
||||
const { data } = await api.get('/project/' + currentProjectId.value + '/user')
|
||||
const usersAPI = data.data
|
||||
users.value.push(...usersAPI)
|
||||
@@ -38,6 +40,13 @@ export const useUsersStore = defineStore('users', () => {
|
||||
if (users.value[idx]) Object.assign(users.value[idx], userAPI)
|
||||
}
|
||||
|
||||
async function unblockUser (userId: number) {
|
||||
const { data } = await api.put('/project/' + currentProjectId.value + '/user/' + userId, { is_blocked: false })
|
||||
const userAPI = data.data
|
||||
const idx = users.value.findIndex(item => item.id === userAPI.id)
|
||||
if (users.value[idx]) Object.assign(users.value[idx], userAPI)
|
||||
}
|
||||
|
||||
function userById (id: number) {
|
||||
return users.value.find(el =>el.id === id)
|
||||
}
|
||||
@@ -52,6 +61,12 @@ export const useUsersStore = defineStore('users', () => {
|
||||
|
||||
const getUsers = computed(() => users.value)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
watch(() => authStore.wsEvent, async (event) => {
|
||||
if (!event || event.entity !== 'user' || !event.needUpdateStores.includes('users')) return
|
||||
await init()
|
||||
}, { deep: true })
|
||||
|
||||
return {
|
||||
users,
|
||||
isInit,
|
||||
@@ -59,6 +74,7 @@ export const useUsersStore = defineStore('users', () => {
|
||||
reset,
|
||||
update,
|
||||
blockUser,
|
||||
unblockUser,
|
||||
userById,
|
||||
userNameById,
|
||||
getUsers
|
||||
|
||||
40
todo_all.txt
Normal file
40
todo_all.txt
Normal file
@@ -0,0 +1,40 @@
|
||||
1. Приложение Администратора:
|
||||
1.1. Внедрение web-socket
|
||||
1.2. Страница подписки и внедрение оплаты звездами
|
||||
1.3. Юридические документы:
|
||||
- Лицензионное соглашение (EN / RU)
|
||||
- Обработка персональных данных (RU)
|
||||
- Privacy police (EN)
|
||||
1.4. Маскировка компаний
|
||||
1.5. Взаимодействие с архивным чатом
|
||||
|
||||
2. Приложение Пользователя:
|
||||
2.1. Авторизация с флагом принятия документов
|
||||
2.2. При выборе пользователя в задаче или совещании - предоставлять
|
||||
только список пользователей в чате
|
||||
2.3. Отображение владельца чата (?техническая возможность)
|
||||
2.4. В файлах добавить новую сущность URL
|
||||
2.5. Работа с архивом задач
|
||||
2.6. Механизм передачи vCard (через медиашаринг https://telegram.tips/blog/mini-apps-media-sharing/ )
|
||||
2.7. Внедрение web-socket
|
||||
|
||||
3. Чаты:
|
||||
3.1. Внедрение inline-режима создания форматированного сообщения, задачи и совещания
|
||||
3.2. Проверить реализацию всех фич.
|
||||
|
||||
4. Сайт:
|
||||
4.1. Создание сайта-лендинга
|
||||
4.2. Оплата адреса
|
||||
|
||||
5. Инфраструктура:
|
||||
5.1. Разворачивание гипервизора
|
||||
5.2. Развертывание виртуальных машин
|
||||
5.3. Бекапирование
|
||||
5.4. Настройка nginx
|
||||
|
||||
6. Прочее:
|
||||
6.1. Подача заявления в Роскомнадзор о работе с персональными данными.
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user