before delete 3software

This commit is contained in:
2025-06-29 18:55:59 +03:00
parent ebd77a3e66
commit b51a472738
147 changed files with 257326 additions and 3151 deletions

View File

@@ -0,0 +1,20 @@
<template>
<pn-page-card>
<template #title>
{{$t('account__change_auth_method')}}
</template>
<pn-scroll-list>
<account-helper :type/>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import accountHelper from 'components/accountHelper.vue'
import { useAuthStore } from 'stores/auth'
const authStore= useAuthStore()
const type = 'changeMethod'
</script>

View File

@@ -1,17 +1,283 @@
<template>
<pn-page-card>
<template #title>
<div class="col-grow">
{{$t('account__change_password')}}
</div>
{{$t('account_change_email__title')}}
</template>
<pn-scroll-list>
<account-helper :type />
<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 accountHelper from 'src/components/accountHelper.vue'
const type = 'forgotPwd'
import { ref, computed } from 'vue'
import type { AxiosError } from 'axios'
import { useQuasar } from 'quasar'
import { useI18n } from "vue-i18n"
import { QInput } from 'quasar'
import { useAuthStore } from 'stores/auth'
const $q = useQuasar()
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 handleError = (err: AxiosError) => {
const error = err as AxiosError<{ error?: { message?: string } }>
const message = error.response?.data?.error?.message || t('unknown_error')
$q.notify({
message: `${t('error')}: ${message}`,
type: 'negative',
position: 'bottom',
timeout: 2500
})
if (step.value > 1) {
code.value = ''
password.value = ''
}
}
const handleSubmit = async () => {
try {
await stepActions[step.value]()
if (step.value < 5) {
step.value++
} else {
showSuccessOverlay.value = true
}
} catch (error) {
handleError(error as AxiosError)
}
}
</script>

View File

@@ -1,17 +1,21 @@
<template>
<pn-page-card>
<template #title>
<div class="col-grow">
{{$t('account__change_password')}}
</div>
{{$t('account__change_password')}}
</template>
<pn-scroll-list>
<account-helper :type />
<account-helper :type :email/>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import accountHelper from 'src/components/accountHelper.vue'
const type = 'change'
import accountHelper from 'components/accountHelper.vue'
import { useAuthStore } from 'stores/auth'
const authStore= useAuthStore()
const type = 'changePwd'
const email = authStore.customer?.email ? authStore.customer?.email : '???'
</script>

View File

@@ -1,9 +1,7 @@
<template>
<pn-page-card>
<template #title>
<div class="col-grow">
{{$t('login__register')}}
</div>
{{$t('login__register')}}
</template>
<pn-scroll-list>
<account-helper :type :email/>
@@ -13,7 +11,7 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import accountHelper from 'src/components/accountHelper.vue'
import accountHelper from 'components/accountHelper.vue'
const type = 'register'
const email = ref(sessionStorage.getItem('pendingLogin') || '')

View File

@@ -1,9 +1,7 @@
<template>
<pn-page-card>
<template #title>
<div class="col-grow">
{{$t('forgot_password__password_recovery')}}
</div>
{{$t('forgot_password__password_recovery')}}
</template>
<pn-scroll-list>
<account-helper :type :email/>
@@ -13,7 +11,7 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import accountHelper from 'src/components/accountHelper.vue'
import accountHelper from 'components/accountHelper.vue'
const type = 'forgotPwd'
const email = ref(sessionStorage.getItem('pendingLogin') || '')

View File

@@ -1,22 +1,19 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center no-wrap w100">
<pn-account-block-name/>
<q-btn
@click="logout()"
flat
round
icon="mdi-logout"
class="q-ml-md"
/>
</div>
<pn-account-block-name/>
<q-btn
@click="logout()"
flat
round
icon="mdi-logout"
/>
</template>
<pn-scroll-list>
<q-list separator>
<q-item
v-for="item in items"
v-for="item in displayItems"
:key="item.id"
@click="goTo(item.pathName)"
clickable
@@ -35,7 +32,7 @@
<q-item-label>
{{ $t(item.name) }}
</q-item-label>
<q-item-label class="text-caption">
<q-item-label class="text-caption" v-if="$te(item.description)">
{{ $t(item.description) }}
</q-item-label>
</q-item-section>
@@ -53,18 +50,33 @@
const router = useRouter()
const authStore = useAuthStore()
interface ItemList {
id: number
name: string
description?: string
icon: string
iconColor?: string
pathName: string
display?: boolean
}
const items = computed(() => ([
{ id: 1, name: 'account__subscribe', description: 'account__subscribe_description', icon: 'mdi-crown-circle-outline', iconColor: 'orange', pathName: 'subscribe' },
{ id: 2, name: 'account__auth_change_method', description: 'account__auth_change_method_description', icon: 'mdi-account-sync-outline', iconColor: 'primary', pathName: '' },
{ id: 3, name: 'account__auth_change_password', description: 'account__auth_change_password_description', icon: 'mdi-account-key-outline', iconColor: 'primary', pathName: 'change_account_password' },
{ id: 4, name: 'account__auth_change_account', description: 'account__auth_change_account_description', icon: 'mdi-account-switch-outline', iconColor: 'primary', pathName: 'change_account_email' },
{ id: 2, name: 'account__auth_change_method', description: 'account__auth_change_method_description', icon: 'mdi-account-sync-outline', iconColor: 'primary', pathName: 'change_account_auth_method', display: !authStore.customer?.email },
{ id: 3, name: 'account__auth_change_password', description: 'account__auth_change_password_description', icon: 'mdi-account-key-outline', iconColor: 'primary', pathName: 'change_account_password', display: !!authStore.customer?.email },
{ id: 4, name: 'account__auth_change_account', description: 'account__auth_change_account_description', icon: 'mdi-account-switch-outline', iconColor: 'primary', pathName: 'change_account_email', display: !!authStore.customer?.email },
{ 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__terms_of_use', icon: 'mdi-book-open-variant-outline', description: '', iconColor: 'grey', pathName: 'terms' },
{ id: 9, name: 'account__privacy', icon: 'mdi-lock-outline', description: '', iconColor: 'grey', pathName: 'privacy' }
{ 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' }
]))
const displayItems = computed(() => (
items.value.filter((item: ItemList) => !('display' in item) || item.display === true)
))
async function goTo (path: string) {
await router.push({ name: path })
}

View File

@@ -0,0 +1,35 @@
<template>
<company-block
v-model="newCompany"
title="company_create__title_card"
btnText="company_create__btn"
@update = "addCompany"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import companyBlock from 'components/companyBlock.vue'
import { useCompaniesStore } from 'stores/companies'
import type { CompanyParams } from 'types/Company'
const router = useRouter()
const companiesStore = useCompaniesStore()
const newCompany = ref(<CompanyParams>{
name: '',
logo: '',
description: '',
site: '',
address: '',
phone: '',
email: ''
})
async function addCompany () {
await companiesStore.add(newCompany.value)
router.go(-1)
}
</script>

View File

@@ -1,39 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{$t('company_create__title_card')}}
</div>
<q-btn
v-if="(Object.keys(companyMod).length !== 0)"
@click = "addCompany(companyMod)"
flat round
icon="mdi-check"
/>
</div>
</template>
<pn-scroll-list>
<company-info-block v-model="companyMod"/>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import companyInfoBlock from 'src/components/companyInfoBlock.vue'
import { useCompaniesStore } from 'stores/companies'
import type { Company } from 'src/types'
const router = useRouter()
const companiesStore = useCompaniesStore()
const companyMod = ref(<Company>{})
function addCompany (data: Company) {
companiesStore.addCompany(data)
router.go(-1)
}
</script>

View File

@@ -0,0 +1,58 @@
<template>
<company-block
v-if="companyMod"
v-model="companyMod"
title="company_edit__title_card"
btnText="company_edit__btn"
@update="updateCompany"
>
<template #myCompany v-if="companyMod.is_own">
<div class="q-mb-md flex w100 justify-center">
<div class="flex items-center text-amber-10">
<q-icon name="star" class="q-pr-xs"/>
{{ $t('company_edit__my_company') }}
</div>
<div class="text-caption" align="center" style="white-space: pre-wrap;">
{{ $t('company_edit__my_company_hint') }}
</div>
</div>
</template>
</company-block>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import companyBlock from 'components/companyBlock.vue'
import { useCompaniesStore } from 'stores/companies'
import { parseIntString } from 'helpers/helpers'
import type { CompanyParams } from 'types/Company'
const router = useRouter()
const route = useRoute()
const companiesStore = useCompaniesStore()
const companyMod = ref<CompanyParams | null>(null)
const companyId = computed(() => parseIntString(route.params.companyId))
if (companiesStore.isInit) {
companyMod.value = companyId.value
? { ...companiesStore.companyById(companyId.value) } as CompanyParams
: null
}
watch(() => companiesStore.isInit, (isInit) => {
if (isInit && companyId.value && !companyMod.value) {
companyMod.value = { ...companiesStore.companyById(companyId.value) as CompanyParams }
}
})
async function updateCompany () {
if (companyId.value && companyMod.value) {
await companiesStore.update(companyId.value, companyMod.value)
router.go(-1)
}
}
</script>

View File

@@ -1,74 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{$t('company_info__title_card')}}
</div>
<q-btn
v-if="isFormValid && isDirty()"
@click = "updateCompany()"
flat round
icon="mdi-check"
/>
</div>
</template>
<pn-scroll-list>
<company-info-block
v-if="company"
v-model="company"
@valid="isFormValid = $event"
/>
<company-info-persons/>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import companyInfoBlock from 'src/components/companyInfoBlock.vue'
import companyInfoPersons from 'src/components/companyInfoPersons.vue'
import { useCompaniesStore } from 'stores/companies'
import type { Company } from 'src/types'
import { parseIntString, isObjEqual } from 'boot/helpers'
const router = useRouter()
const route = useRoute()
const companiesStore = useCompaniesStore()
const company = ref<Company>()
const companyId = parseIntString(route.params.companyId)
const isFormValid = ref(false)
const originalCompany = ref<Company>({} as Company)
const isDirty = () => {
return company.value && !isObjEqual(originalCompany.value, company.value)
}
onMounted(async () => {
if (companyId && companiesStore.companyById(companyId)) {
const initial = companiesStore.companyById(companyId)
company.value = { ...initial } as Company
originalCompany.value = JSON.parse(JSON.stringify(company.value))
} else {
await abort()
}
})
function updateCompany () {
if (companyId && company.value) {
companiesStore.updateCompany(companyId, company.value)
router.back()
}
}
async function abort () {
await router.replace({name: 'projects'})
}
</script>

View File

@@ -1,102 +1,150 @@
<template>
<pn-page-card>
<template #title>
<div class="col-grow">
{{$t('company__mask')}}
</div>
{{$t('company__mask')}}
<q-btn
v-if="!checkIsDirty(mask, originalMask)"
@click = "updateCompanies()"
flat round
icon="mdi-check"
/>
</template>
<pn-scroll-list>
<template #card-body-header>
<div style="min-height: var(--top-raduis);"/>
</template>
<q-list
separator
<q-table
flat
:rows="displayCompanies"
:columns="tableColumns"
hide-pagination
:pagination="{ rowsPerPage: 0 }"
dense
row-key="id"
style="max-width: 100% !important"
>
<q-item>
<q-item-section>
<div>
<q-btn flat round color="primary" icon="mdi-help-circle-outline" @click="showDialogHelp = true" />
</div>
</q-item-section>
<q-item-section></q-item-section>
<q-item-section align="end" class="col-grow">
{{ $t('mask__title_table') }}
</q-item-section>
</q-item>
<q-item
v-for = "company in companies"
:key="company.id"
class="w100"
>
<q-item-section>
<q-checkbox
v-model="company.masked"
:label="company.name"
unchecked-icon="mdi-drama-masks"
checked-icon="mdi-drama-masks"
:class="company.masked ? 'masked' : 'unmasked'"
/>
</q-item-section>
<q-item-section>
<q-select
v-if="company.masked"
v-model="company.unmasked"
multiple
:options="companiesSelect(company.id)"
dense
borderless
dropdown-icon="mdi-plus"
class="fix-select"
>
<template #selected>
<div
v-for="(comp, idx) in company.unmasked"
:key=idx
class="q-pa-xs"
>
<q-avatar rounded size="md">
<img v-if="comp['logo']" :src="comp['logo']"/>
<pn-auto-avatar v-else :name="comp['name']"/>
</q-avatar>
</div>
<template #header>
<q-tr>
<q-th style="width: 10%">
<q-icon name="mdi-domino-mask" size="sm"/>
</q-th>
<q-th style="width: 45%">
<span class="text-bold">
{{ $t('mask__table_header_company') }}
</span>
</q-th>
<q-th style="width: 45%">
<span class="text-bold">
{{ $t('mask__table_header_visible') }}
<q-btn
flat
@click="showDialogHelp=true"
color="primary"
dense round
size="sm"
icon="mdi-help-circle-outline"
/>
</span>
</q-th>
</q-tr>
</template>
<template #body="props">
<q-tr :props="props">
<q-td key="checkbox" :props="props">
<q-toggle
v-model="props.row.masked"
size="sm"
:color="props.row.masked ? 'red' : 'brand'"
/>
</q-td>
<q-td key="name" :props="props">
{{ props.row.name }}
</q-td>
<q-td key="visible" :props="props">
<q-select
v-if="props.row.masked"
v-model="props.row.company_list"
multiple
:options="companiesSelect(props.row.id)"
option-value="id"
emit-value
dense
borderless
dropdown-icon="mdi-plus"
class="fix-select"
:label-slot="!props.row.hasFocus && !props.row.isMenuOpen && (props.row.company_list.length === 0)"
@popup-show="props.row.isMenuOpen = true"
@popup-hide="props.row.isMenuOpen = false"
@focus="props.row.hasFocus = true"
@blur="props.row.hasFocus = false"
>
<template #label>
<span
v-if="(!props.row.hasFocus || !props.row.isMenuOpen) && (props.row.company_list.length === 0)"
class="q-field__label"
>
{{ $t('mask__table_visible_none') }}
</span>
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-avatar rounded size="md">
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
<pn-auto-avatar v-else :name="scope.opt.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</q-item-section>
</q-item>
<q-item
class="q-pa-none q-ma-none"
style="min-height: 18px"
>
<div class="q-py-none flex column w100"/>
</q-item>
</q-list>
<template #selected>
<div
v-for="id in props.row.company_list"
:key=id
class="q-pa-xs"
>
<pn-auto-avatar
:img="getCompanyById(id)?.logo"
:name="getCompanyById(id)?.name"
size="md"
type="rounded"
/>
</div>
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<pn-auto-avatar
:img="scope.opt.logo"
:name="scope.opt.name"
size="md"
type="rounded"
/>
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
<span
v-else
>
{{ $t('mask__table_visible_all') }}
</span>
</q-td>
</q-tr>
</template>
</q-table>
<div class="q-py-none flex column w100" style="min-height: 18px"/>
</pn-scroll-list>
<q-dialog v-model="showDialogHelp">
<q-card class="q-ma-sm w100">
<q-card-section class="row items-center q-pb-none">
<span class="text-h6">
<q-icon name="mdi-drama-masks"/>
<q-icon name="mdi-domino-mask"/>
{{ $t('mask__help_title')}}
</span>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-card-section class="q-pt-sm">
{{ $t('mask__help_message')}}
<p>{{ $t('mask__help_message1')}}</p>
<p>{{ $t('mask__help_message2')}}</p>
<p>{{ $t('mask__help_message3')}}</p>
</q-card-section>
</q-card>
</q-dialog>
@@ -104,26 +152,53 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
// import { useCompaniesStore } from 'src/stores/companies'
import { ref, computed, onMounted } from 'vue'
import { useCompaniesStore } from 'stores/companies'
import type { CompanyMask } from 'types/Company'
import { useRouter } from 'vue-router'
const showDialogHelp = ref<boolean>(false)
// const companiesStore = useCompaniesStore()
const companiesStore = useCompaniesStore()
const router = useRouter()
// const companies = computed(() => companiesStore.companies)
const companies = computed(() => companiesStore.companies)
const companiesMask = computed(() => companiesStore.companiesMask)
const companies = ref([
{id: "com11", name: 'Рога и копытца1', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: [] },
{id: "com21", name: 'ООО "Василек1"', logo: 'https://cdn.quasar.dev/img/avatar5.jpg', qtyPersons: 2, masked: true, unmasked: [] },
{id: "ch13", name: 'Откат и деньги1', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
{id: "ch14", name: 'Откат и деньги2', logo: '', description: 'Договариваются о чем-то', qtyPersons: 5, masked: false, unmasked: [] },
{id: "com111", name: 'Рога и копытца2', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: []},
{id: "com211", name: 'ООО "Василек2"', logo: '', qtyPersons: 2, masked: true, unmasked: [] },
{id: "ch131", name: 'Откат и деньги3', logo: '', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
])
interface DisplayCompany {
id: number
name: string
logo?: string
masked: boolean
company_list: number[],
isMenuOpen: boolean
hasFocus: boolean
}
function companiesSelect (id :string) {
return companies.value
//
const displayCompanies = ref<DisplayCompany[]>([])
const originalMask = ref<CompanyMask[]>([])
const tableColumns = [
{ name: 'checkbox', field: 'checkbox', label: 'checkbox', sortable: false, style: "width: 10%" },
{ name: 'name', field: 'name', align: 'left' as const, label: 'name', sortable: false, style: "width: 45% !important; text-wrap: auto" },
{ name: 'visible', field: 'visible', align: 'center' as const, label: 'visible', sortable: false, style: "width: 45% !important" }
]
const mask = computed(() => displayCompanies.value
.filter(el => el.masked)
.map(el => ({
company_id: el.id,
company_list: el.company_list.sort(compareNumbers)
}))
.sort((a, b) => compareNumbers(a.company_id, b.company_id))
)
function getCompanyById(id: number) {
return companies.value.find(c => c.id === id);
}
function companiesSelect (id :number) {
return displayCompanies.value
.map(el => ({
id: el.id,
name: el.name,
@@ -132,26 +207,103 @@
.filter(el => el.id !== id)
}
async function updateCompanies () {
await companiesStore.updateMask(mask.value)
router.back()
}
function compareNumbers (a: number, b: number): number {
if (Number.isNaN(a)) return Number.isNaN(b) ? 0 : 1
if (Number.isNaN(b)) return -1
if (a === b) return 0
return a < b ? -1 : 1
}
function checkIsDirty (arr1: CompanyMask[], arr2: CompanyMask[]): boolean {
// Проверка длины массивов
if (arr1.length !== arr2.length) return false
// Функция для нормализации объекта в строковый ключ
const getKey = (obj: { company_id: number; company_list: number[] }): string => {
const sortedList = [...obj.company_list].sort(compareNumbers)
return `${obj.company_id}|${sortedList.join(',')}`
}
// Создаем Map для подсчета объектов в первом массиве
const countMap = new Map<string, number>()
for (const item of arr1) {
const key = getKey(item)
countMap.set(key, (countMap.get(key) || 0) + 1)
}
// Проверяем объекты второго массива
for (const item of arr2) {
const key = getKey(item)
const count = countMap.get(key) || 0
if (count === 0) return false
countMap.set(key, count - 1)
}
return true
}
onMounted(() => {
function getList (companyId: number) {
const company = companiesMask.value.find(el => el.company_id === companyId)
return company?.company_list ?? []
}
displayCompanies.value = companies.value
.filter(el => !el.is_own)
.map(el => ({
id: el.id,
name: el.name,
logo: el?.logo ?? '',
masked: companiesStore.checkCompanyMasked(el.id),
company_list: getList(el.id),
isMenuOpen: false,
hasFocus: false
}))
console.log(companies.value)
console.log(displayCompanies.value)
originalMask.value = [ ...(companiesMask.value.sort((a, b) => compareNumbers(a.company_id, b.company_id))) ]
})
</script>
<style scoped lang="scss">
:deep(.fix-select .q-field__control) {
.fix-select :deep(.q-field__control) {
align-items: center;
}
.fix-select :deep(.q-field__native) {
justify-content: flex-end;
}
.fix-select :deep(.q-field__label.no-pointer-events) {
left: 50% !important;
transform: translateX(-50%) !important;
}
:deep(.fix-select .q-icon) {
color: $green-14 !important;
}
:deep(.masked .q-icon) {
color: red;
opacity: 1;
}
:deep(.unmasked .q-icon) {
color: grey;
opacity: 0.5;
color: 'brand';
}
</style>

View File

@@ -1,39 +1,46 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{$t('Your_company__title_card')}}
</div>
<q-btn
v-if="(Object.keys(companyMod).length !== 0)"
@click = "addCompany(companyMod)"
flat round
icon="mdi-check"
/>
</div>
</template>
<pn-scroll-list>
<company-info-block v-model="companyMod"/>
</pn-scroll-list>
</pn-page-card>
<company-block
v-if="companyMod"
v-model="companyMod"
title="account_company__title_card"
btnText="account_company__btn"
@update="updateMyCompany"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import companyInfoBlock from 'src/components/companyInfoBlock.vue'
import { useCompaniesStore } from 'stores/companies'
import type { Company } from 'src/types'
import companyBlock from 'components/companyBlock.vue'
import { useAuthStore } from 'stores/auth'
import type { CompanyParams } from 'types/Company'
const router = useRouter()
const companiesStore = useCompaniesStore()
const authStore = useAuthStore()
const companyMod = ref(<Company>{})
const companyMod = ref<CompanyParams | null>(null)
function addCompany (data: Company) {
companiesStore.addCompany(data)
router.go(-1)
async function updateMyCompany () {
if (companyMod.value) {
await authStore.updateMyCompany(companyMod.value)
router.go(-1)
}
}
onMounted(() => {
if (authStore.customer?.company) {
companyMod.value = authStore.customer?.company as CompanyParams
} else {
companyMod.value = {
name: '',
logo: '',
description: '',
site: '',
address: '',
phone: '',
email: ''
}
}
})
</script>

View File

@@ -6,22 +6,11 @@
</div>
<div class="text-h2" style="opacity:.4">
Oops. Nothing here...
{{ $t('error404') }}
</div>
<q-btn
class="q-mt-xl"
color="white"
text-color="blue"
unelevated
to="/"
label="Go Home"
no-caps
/>
</div>
</div>
</template>
<script setup lang="ts">
//
</script>

View File

@@ -274,19 +274,6 @@
max-width: 300px;
}
.orline {
display: flex;
flex-direction: row;
}
.orline:before,
.orline:after {
content: "";
flex: 1 1;
border-bottom: 1px solid;
margin: auto;
}
.login-card {
opacity: 0.9 !important;
border-radius: var(--top-raduis);

View File

@@ -1,113 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{ $t('person_card__title') }}
</div>
<q-btn
@click = "goProject()"
flat round
icon="mdi-check"
/>
</div>
</template>
<pn-scroll-list>
<div class="flex column items-center q-ma-lg">
<q-avatar size="100px">
<q-img :src="person.logo"/>
</q-avatar>
<div class="flex row items-start justify-center no-wrap q-pb-lg">
<div class="flex column justify-center">
<div class="text-bold q-pr-xs text-center">{{ person.tname }}</div>
<div caption class="text-blue text-caption">{{ person.tusername }}</div>
</div>
</div>
<div class="q-gutter-y-lg w100">
<q-input
v-model.trim="person.name"
dense
filled
class = "w100"
:label = "$t('person_card__name')"
/>
<q-select
v-if="companies"
v-model="person.company"
:options="companies"
dense
filled
class="w100"
:label = "$t('person_card__company')"
>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-avatar rounded size="md">
<img v-if="scope.opt.logo" :src="scope.opt.logo"/>
<pn-auto-avatar v-else :name="scope.opt.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
<template #selected>
{{ JSON.parse(JSON.stringify(person.company)).name }}
</template>
</q-select>
<q-input
v-model.trim="person.department"
dense
filled
class = "w100"
:label = "$t('person_card__department')"
/>
<q-input
v-model.trim="person.role"
dense
filled
class = "w100"
:label = "$t('person_card__role')"
/>
</div>
</div>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const person = ref({id: "p1", name: 'Кирюшкин Андрей', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', tname: 'Kir_AA', tusername: '@kiruha90', role: 'DevOps', company: '', department: 'test' })
const companies = ref([
{id: "com11", value: "com11", name: 'Рога и копытца1', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: [] },
{id: "com21", name: 'ООО "Василек1"', logo: 'https://cdn.quasar.dev/img/avatar5.jpg', qtyPersons: 2, masked: true, unmasked: [] },
{id: "ch13", name: 'Откат и деньги1', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
{id: "ch14", name: 'Откат и деньги2', logo: '', description: 'Договариваются о чем-то', qtyPersons: 5, masked: false, unmasked: [] },
{id: "com111", name: 'Рога и копытца2', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false, unmasked: []},
{id: "com211", name: 'ООО "Василек2"', logo: '', qtyPersons: 2, masked: true, unmasked: [] },
{id: "ch131", name: 'Откат и деньги3', logo: '', description: 'Договариваются с администрацией', qtyPersons: 5, masked: false, unmasked: [] },
])
async function goProject () {
await router.push({ name: 'project' })
}
</script>
<style>
</style>

View File

@@ -1,22 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{ $t('privacy__title') }}
</div>
</div>
</template>
<pn-scroll-list>
{{ $t('under_construction') }}
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
</script>
<style>
</style>

View File

@@ -0,0 +1,32 @@
<template>
<project-block
v-model="newProject"
title="project_create__title_card"
btnText="project_create__btn"
@update="addProject"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import projectBlock from 'components/projectBlock.vue'
import { useProjectsStore } from 'stores/projects'
import type { ProjectParams } from 'types/Project'
const router = useRouter()
const projectsStore = useProjectsStore()
const newProject = ref(<ProjectParams>{
name: '',
logo: '',
description: '',
is_logo_bg: false
})
async function addProject () {
const newDataProject = await projectsStore.add(newProject.value)
await router.replace({ name: 'chats', params: { id: newDataProject.id }})
}
</script>

View File

@@ -1,66 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{$t('project_card__add_project')}}
</div>
<q-btn
v-if="isFormValid && isDirty"
@click = "addProject(project)"
flat round
icon="mdi-check"
/>
</div>
</template>
<pn-scroll-list>
<project-info-block
v-model="project"
@valid="isFormValid = $event"
/>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import projectInfoBlock from 'components/projectInfoBlock.vue'
import { useProjectsStore } from 'stores/projects'
import type { ProjectParams } from 'types/Project'
import { useNotify, type ServerError } from 'composables/useNotify'
const { notifyError } = useNotify()
const router = useRouter()
const projectsStore = useProjectsStore()
const initialProject: ProjectParams = {
name: '',
logo: '',
description: '',
is_logo_bg: false
}
const project = ref<ProjectParams>({ ...initialProject })
const isFormValid = ref(false)
const isDirty = computed(() => {
return (
project.value.name !== initialProject.name ||
project.value.logo !== initialProject.logo ||
project.value.description !== initialProject.description ||
project.value.is_logo_bg !== initialProject.is_logo_bg
)
})
async function addProject (data: ProjectParams) {
try {
const newProject = await projectsStore.add(data)
await router.replace({ name: 'chats', params: { id: newProject.id }})
console.log(newProject)
} catch (error) {
notifyError(error as ServerError)
}
}
</script>

View File

@@ -0,0 +1,43 @@
<template>
<project-block
v-if="projectMod"
v-model="projectMod"
title="project_edit__title_card"
btnText="project_edit__btn"
@update="updateProject"
/>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import projectBlock from 'components/projectBlock.vue'
import { useProjectsStore } from 'stores/projects'
import type { ProjectParams } from 'types/Project'
const router = useRouter()
const route = useRoute()
const projectsStore = useProjectsStore()
const projectId = computed(() => projectsStore.currentProjectId)
const projectMod = ref<ProjectParams | null>(null)
if (projectsStore.isInit) {
projectMod.value = projectId.value
? { ...projectsStore.projectById(projectId.value) } as ProjectParams
: null
}
watch(() => projectsStore.isInit, (isInit) => {
if (isInit && projectId.value && !projectMod.value) {
projectMod.value = { ...projectsStore.projectById(projectId.value) as ProjectParams }
}
})
const updateProject = async () => {
if (!projectId.value || !projectMod.value) return
await projectsStore.update(projectId.value, projectMod.value)
router.go(-1)
}
</script>

View File

@@ -1,69 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
<span>{{ $t('project_card__project_card') }}</span>
</div>
<q-btn
v-if="isFormValid && (!isDirty(originalProject, project))"
@click="updateProject()"
flat
round
icon="mdi-check"
/>
</div>
</template>
<pn-scroll-list>
<project-info-block
v-if="project"
v-model="project"
@valid="isFormValid = $event"
/>
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useProjectsStore } from 'stores/projects'
import projectInfoBlock from 'components/projectInfoBlock.vue'
import { parseIntString, isDirty } from 'boot/helpers'
import type { ProjectParams } from 'types/Project'
const router = useRouter()
const route = useRoute()
const projectsStore = useProjectsStore()
const project = ref<ProjectParams>()
const originalProject = ref<ProjectParams>()
const id = parseIntString(route.params.id)
const isFormValid = ref(false)
onMounted(async () => {
if (id && projectsStore.projectById(id)) {
const initial = projectsStore.projectById(id)
project.value = { ...initial } as ProjectParams
originalProject.value = {...project.value}
} else {
await abort()
}
})
async function updateProject () {
if (id && project.value) {
await projectsStore.update(id, project.value)
router.back()
}
}
async function abort () {
await router.replace({name: 'projects'})
}
</script>
<style>
</style>

View File

@@ -6,7 +6,6 @@
<q-tab-panels
v-model="tabSelect"
animated
keep-alive
class="tab-panel-color full-height-panel w100 flex column col-grow no-scroll"
>
@@ -20,9 +19,8 @@
<router-view/>
</q-tab-panel>
</q-tab-panels>
<template #footer>
<q-footer class="bg-grey-1 text-grey">
<q-footer class="bg-grey-1 text-grey main-content">
<q-tabs
style = "z-index: 1000"
v-model="tabSelect"
@@ -38,23 +36,25 @@
no-caps
dense
:class="tabSelect === tab.name ? 'active' : ''"
class="w100 flex column"
class="flex column w100"
>
<template #default>
<div class="flex column items-center">
<q-icon :name="tab.icon" size="sm">
<q-badge
color="brand" align="top"
rounded floating
style="font-style: normal;"
>
{{ currentProject?.[tab.name as keyof typeof currentProject] ?? 0 }}
</q-badge>
</q-icon>
<span class="text-caption">{{$t(tab.label)}}</span>
<q-icon :name="tab.icon" :size="maxTabWidth < baseTabWidth ? 'sm' : 'md'"/>
<div
class="text-caption flex justify-center"
:style="{ width: (baseTabWidth - 32) + 'px'}"
v-if="maxTabWidth < baseTabWidth"
>
<span>
<q-resize-observer @resize="size => tab.width = size.width"/>
{{$t(tab.label)}}
</span>
</div>
</div>
</template>
</q-route-tab>
<q-resize-observer @resize="size => tabsWidth = size.width" />
</q-tabs>
</q-footer>
</template>
@@ -63,25 +63,37 @@
<script setup lang="ts">
import { ref, onBeforeMount, computed } from 'vue'
import { useProjectsStore } from 'stores/projects'
import { useChatsStore } from 'stores/chats'
import { useCompaniesStore } from 'stores/companies'
import { useUsersStore } from 'stores/users'
import projectPageHeader from 'pages/project-page/ProjectPageHeader.vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const projectStore = useProjectsStore()
const currentProject = computed(() => projectStore.getCurrentProject() )
const chatsStore = useChatsStore()
const usersStore = useUsersStore()
const companiesStore = useCompaniesStore()
const tabs = [
{name: 'chats', label: 'project__chats', icon: 'mdi-chat-outline', to: { name: 'chats'} },
{name: 'persons', label: 'project__persons', icon: 'mdi-account-outline', to: { name: 'persons'} },
{name: 'companies', label: 'project__companies', icon: 'mdi-account-group-outline', to: { name: 'companies'} },
]
const chatsQty = computed(() => chatsStore.getChats.length)
const usersQty = computed(() => usersStore.getUsers.length)
const companiesQty = computed(() => companiesStore.getCompanies.length)
const tabs = ref([
{name: 'chats', label: 'project__chats', icon: 'mdi-chat-outline', to: { name: 'chats'}, qty: chatsQty.value, width: 0 },
{name: 'users', label: 'project__users', icon: 'mdi-account-outline', to: { name: 'users'}, qty: usersQty.value, width: 0 },
{name: 'companies', label: 'project__companies', icon: 'mdi-account-group-outline', to: { name: 'companies'}, qty: companiesQty.value, width: 0 }
])
// hidden icon name if overflow - with resize-observer
const tabsWidth = ref(0)
const baseTabWidth = computed(() => Math.floor(tabsWidth.value / 3))
const maxTabWidth = computed(() => Math.max(...tabs.value.map(el => el.width)))
const tabSelect = ref<string>()
onBeforeMount(() => {
const initialTab = tabs.find(t => t.to.name === route.name)?.name || tabs[0]?.name || ''
const initialTab = tabs.value.find(t => t.to.name === route.name)?.name || tabs.value[0]?.name || ''
tabSelect.value = initialTab
})

View File

@@ -1,30 +1,27 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>{{ $t('projects__projects') }}</div>
<div class="flex items-center">
<q-btn
@click="goAccount()"
flat
no-caps
icon-right="mdi-chevron-right"
align="right"
dense
class="fix-btn"
>
<pn-account-block-name/>
</q-btn>
</div>
</div>
{{ $t('projects__projects') }}
<q-btn
@click="goAccount()"
flat rounded
no-caps
icon-right="mdi-chevron-right"
align="right"
dense
class="fix-btn ellipsis"
>
<pn-account-block-name/>
</q-btn>
</template>
<pn-scroll-list>
<template #card-body-header v-if="projects.length !== 0">
<template
#card-body-header
v-if="projects.length !== 0 || archiveProjects.length !== 0"
>
<q-input
v-if="projects.length !== 0"
v-model="searchProject"
clearable
clear-icon="close"
@@ -38,155 +35,189 @@
</q-input>
</template>
<div id="projects-wrapper">
<q-list separator v-if="projects.length !== 0">
<q-item
v-for = "item in activeProjects"
:key="item.id"
<q-list separator v-if="projects.length !== 0">
<template
v-for = "item in activeProjects"
:key="item.id"
>
<q-slide-item
@right="handleSlideRight($event, item.id)"
clickable
v-ripple
@click="goProject(item.id)"
class="w100"
right-color="red"
left-color="green"
>
<template #right>
<q-icon size="lg" name="mdi-briefcase-remove-outline"/>
</template>
<q-item>
<q-item-section avatar>
<q-avatar rounded >
<q-img v-if="item.logo" :src="item.logo" fit="cover" style="height: 40px"/>
<pn-auto-avatar v-else :name="item.name"/>
</q-avatar>
<pn-auto-avatar
:img="item.logo"
:name="item.name"
type="rounded"
size="lg"
/>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-bold">{{ item.name }}</q-item-label>
<q-item-label caption lines="2">{{item.description}}</q-item-label>
</q-item-section>
<q-item-section side class="text-caption ">
<div class="flex items-center column">
<div class="flex items-center">
<q-icon name="mdi-chat-outline"/>
<span>{{ item.chat_count }} </span>
</div>
<div class="flex items-center">
<q-icon name="mdi-account-outline"/>
<span>{{ item.user_count }}</span>
</div>
</div>
</q-item-section>
</q-item>
</q-list>
<div v-if="archiveProjects.length !== 0" class="flex column items-center w100" :class="showArchive ? 'bg-grey-12' : ''">
<div id="btn_show_archive">
<q-btn-dropdown color="grey" flat no-caps @click="showArchive = !showArchive" dropdown-icon="arrow_drop_up">
<template #label>
<span class="text-caption">
<span v-if="!showArchive">{{ $t('projects__show_archive') }}</span>
<span v-else>{{ $t('projects__hide_archive') }}</span>
</span>
</template>
</q-btn-dropdown>
</div>
<q-list separator v-if="showArchive" class="w100">
<q-item
v-for = "item in archiveProjects"
:key="item.id"
clickable
v-ripple
@click="handleArchiveList(item.id)"
class="w100 text-grey"
>
<q-item-section avatar>
<q-avatar rounded >
<q-img v-if="item.logo" :src="item.logo" fit="cover" style="height: 40px"/>
<pn-auto-avatar v-else :name="item.name"/>
</q-avatar>
<q-item-label
caption lines="2"
style="max-width: -webkit-fill-available; white-space: pre-line"
>
{{item.description}}
</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-bold">{{ item.name }}</q-item-label>
<q-item-label caption lines="2">{{item.description}}</q-item-label>
<q-item-section side class="text-caption ">
<div class="flex items-center column">
<div class="flex items-center">
<q-icon name="mdi-chat-outline"/>
<span>{{ item.chat_count }} </span>
</div>
<div class="flex items-center">
<q-icon name="mdi-account-outline"/>
<span>{{ item.user_count }}</span>
</div>
</div>
</q-item-section>
</q-item>
</q-list>
</q-slide-item>
</template>
</q-list>
<div
v-if="archiveProjects.length!==0"
class="flex column items-center w100"
:class="showArchive ? 'bg-grey-12' : ''"
>
<q-btn-dropdown
class="w100 fix-rotate-arrow"
color="grey"
flat no-caps
@click="showArchive=!showArchive"
dropdown-icon="arrow_drop_up"
>
<template #label>
<span class="text-caption">
{{ !showArchive
? $t('projects__show_archive') + ' (' + archiveProjects.length +')'
: $t('projects__hide_archive')
}}
</span>
</template>
</q-btn-dropdown>
<div class="w100" style="overflow: hidden">
<transition
appear
enter-active-class="animated slideInDown"
leave-active-class="animated slideOutUp"
>
<q-list separator v-if="showArchive" class="w100">
<q-item
v-for = "item in archiveProjects"
:key="item.id"
clickable
v-ripple
@click="handleArchiveList(item.id)"
class="w100 text-grey"
>
<q-item-section avatar>
<pn-auto-avatar
:img="item.logo"
:name="item.name"
type="rounded"
size="lg"
/>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-bold">{{ item.name }}</q-item-label>
<q-item-label
caption lines="2"
style="max-width: -webkit-fill-available; white-space: pre-line"
>
{{item.description}}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</transition>
</div>
</div>
<pn-onboard-btn
v-if="projects.length === 0 && projectsInit"
icon="mdi-briefcase-plus-outline"
:message1="$t('projects__lets_start')"
:message2="$t('projects__lets_start_description')"
@btn-click="createNewProject"
/>
<div
v-if="projects.length === 0"
class="flex w100 column q-pt-xl q-pa-md"
>
<div class="flex column justify-center col-grow items-center text-grey">
<q-btn flat no-caps @click="createNewProject">
<div class="flex column justify-center col-grow items-center">
<q-icon name="mdi-briefcase-plus-outline" size="160px" class="q-pb-md"/>
<div class="text-h6 text-brand">
{{$t('projects__lets_start')}}
</div>
</div>
</q-btn>
<div class="text-caption" align="center">
{{$t('projects__lets_start_description')}}
</div>
</div>
</div>
class="flex column justify-center items-center w100"
style="position: absolute; bottom: 0;"
v-if="!projectsInit"
>
<q-linear-progress indeterminate />
</div>
</pn-scroll-list>
<q-page-sticky
position="bottom-right"
:offset="[18, 18]"
:offset="[0, 18]"
class="fix-fab-offset"
>
<q-btn
fab
icon="add"
color="brand"
@click="createNewProject"
<transition
appear
enter-active-class="animated zoomIn"
>
<q-btn
fab
icon="add"
color="brand"
@click="createNewProject"
/>
</transition>
</q-page-sticky>
</pn-page-card>
<q-dialog v-model="showDialogArchive">
<q-card class="q-pa-none q-ma-none">
<q-card-section align="center">
<div class="text-h6 text-negative ">{{ $t('projects__restore_archive_warning') }}</div>
</q-card-section>
<q-card-section class="q-pt-none" align="center">
{{ $t('projects__restore_archive_warning_message') }}
</q-card-section>
<pn-small-dialog
v-model="showDialogArchiveProject"
icon="mdi-briefcase-remove-outline"
color="negative"
title="projects__dialog_archive_title"
message1="projects__dialog_archive_message"
mainBtnLabel="projects__dialog_archive_ok"
@clickMainBtn="onConfirmArchiveProject()"
@close="onCancel()"
@before-hide="onDialogBeforeHide()"
/>
<q-card-actions align="center">
<q-btn
flat
:label="$t('back')"
color="primary"
v-close-popup
/>
<q-btn
flat
:label="$t('continue')"
color="primary"
v-close-popup
@click="restoreFromArchive()"
/>
</q-card-actions>
</q-card>
</q-dialog>
<pn-small-dialog
v-model="showDialogRestoreArchive"
icon="mdi-briefcase-upload-outline"
color="green"
title="projects__dialog_restore_title"
message1="projects__dialog_restore_message"
mainBtnLabel="projects__dialog_cancel_ok"
@clickMainBtn="restoreFromArchive()"
/>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useProjectsStore } from 'stores/projects'
import { useSettingsStore } from 'stores/settings'
import type { Project } from 'types/Project'
const router = useRouter()
const projectsStore = useProjectsStore()
const settingsStore = useSettingsStore()
const projects = projectsStore.projects
const projects = projectsStore.getProjects
const projectsInit = computed(() => projectsStore.isInit)
const searchProject = ref('')
const showArchive = ref(false)
const showDialogArchive = ref(false)
const archiveProjectId = ref<number | undefined> (undefined)
const showDialogRestoreArchive = ref(false)
const restoreProjectId = ref<number | undefined> (undefined)
async function goProject (id: number) {
await router.push({ name: 'chats', params: { id }})
@@ -201,12 +232,12 @@
}
function handleArchiveList (id: number) {
showDialogArchive.value = true
archiveProjectId.value = id
showDialogRestoreArchive.value = true
restoreProjectId.value = id
}
function restoreFromArchive () {
if (archiveProjectId.value) projectsStore.restore(archiveProjectId.value)
async function restoreFromArchive () {
if (restoreProjectId.value) await projectsStore.restore(restoreProjectId.value)
}
const displayProjects = computed(() => {
@@ -232,19 +263,63 @@
if (!projectsStore.isInit) {
await projectsStore.init()
}
if (!settingsStore.isInit) {
await settingsStore.init()
}
})
watch(showDialogArchive, (newD :boolean) => {
if (!newD) archiveProjectId.value = undefined
watch(showDialogRestoreArchive, (newD :boolean) => {
if (!newD) restoreProjectId.value = undefined
})
interface SlideEvent {
reset: () => void
}
const currentSlideEvent = ref<SlideEvent | null>(null)
const showDialogArchiveProject = ref<boolean>(false)
const archiveProjectId = ref<number | undefined>(undefined)
const closedByUserAction = ref(false)
function handleSlideRight (event: SlideEvent, id: number) {
currentSlideEvent.value = event
showDialogArchiveProject.value = true
archiveProjectId.value = id
}
function onDialogBeforeHide () {
if (!closedByUserAction.value) {
onCancel()
}
closedByUserAction.value = false
}
async function onConfirmArchiveProject () {
closedByUserAction.value = true
if (archiveProjectId.value) {
await projectsStore.archive(archiveProjectId.value)
}
currentSlideEvent.value = null
}
function onCancel() {
closedByUserAction.value = true
if (currentSlideEvent.value) {
currentSlideEvent.value.reset()
currentSlideEvent.value = null
}
}
</script>
<style scoped>
:deep(.q-slide-item__right) {
align-self: center;
height: 98%;
}
.fix-btn :deep(.q-btn__content) {
flex-wrap: nowrap;
}
.fix-rotate-arrow :deep(.q-btn-dropdown--simple) {
margin-left: 0;
}
</style>

View File

@@ -1,22 +0,0 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{ $t('terms__title') }}
</div>
</div>
</template>
<pn-scroll-list>
{{ $t('under_construction') }}
</pn-scroll-list>
</pn-page-card>
</template>
<script setup lang="ts">
</script>
<style>
</style>

View File

@@ -0,0 +1,45 @@
<template>
<user-block
v-if="userMod"
v-model="userMod"
title="user_edit__title_card"
btnText="user_edit__btn"
@update="updateUser"
/>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import userBlock from 'components/userBlock.vue'
import { useUsersStore } from 'stores/users'
import { parseIntString } from 'helpers/helpers'
import type { User } from 'types/Users'
const router = useRouter()
const route = useRoute()
const usersStore = useUsersStore()
const userMod = ref<User | null>(null)
const userId = computed(() => parseIntString(route.params.userId))
if (usersStore.isInit) {
userMod.value = userId.value
? { ...usersStore.userById(userId.value) } as User
: null
}
watch(() => usersStore.isInit, (isInit) => {
if (isInit && userId.value && !userMod.value) {
userMod.value = { ...usersStore.userById(userId.value) as User }
}
})
async function updateUser () {
if (userId.value && userMod.value) {
await usersStore.update(userId.value, userMod.value)
router.go(-1)
}
}
</script>

View File

@@ -0,0 +1,234 @@
<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>

View File

@@ -0,0 +1,10 @@
<template>
<doc-block type="privacy"/>
</template>
<script setup lang="ts">
import docBlock from 'components/docBlock.vue'
</script>
<style>
</style>

View File

@@ -1,11 +1,7 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{ $t('settings__title') }}
</div>
</div>
{{ $t('settings__title') }}
</template>
<pn-scroll-list>
@@ -62,13 +58,12 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { computed } from 'vue'
import { useSettingsStore } from 'stores/settings'
const localeOptions = ref([
{ value: 'en-US', label: 'English' },
{ value: 'ru-RU', label: 'Русский' }
])
const settingsStore = useSettingsStore()
const localeOptions = settingsStore.supportLocale
const locale = computed({
get: () => settingsStore.settings.locale,
@@ -76,8 +71,6 @@
set: (value: string) => settingsStore.updateLocale(value)
})
const settingsStore = useSettingsStore()
</script>
<style scoped>

View File

@@ -1,11 +1,7 @@
<template>
<pn-page-card>
<template #title>
<div class="flex items-center justify-between col-grow">
<div>
{{$t('subscribe__title')}}
</div>
</div>
{{$t('subscribe__title')}}
</template>
<pn-scroll-list class="q-px-md">

View File

@@ -0,0 +1,64 @@
<template>
<pn-page-card>
<template #title>
{{ $t('support__title') }}
</template>
<pn-scroll-list>
<div class="q-pa-md flex column w100 items-center">
<div align="center" >
<div>{{ $t('support__work_time_text') }}</div>
<div class="flex items-center justify-center text-caption">
<q-icon name="mdi-clock-time-four-outline"/>
<div>{{ $t('support__work_time_time') }}</div>
</div>
</div>
<q-btn
no-caps
icon="telegram"
color="primary"
rounded
@click="openSupport"
class="q-ma-lg"
>
<span class="q-pl-xs no-wrap">{{ $t('support__ask_question') }}</span>
</q-btn>
<div class="orline w100 text-grey">
<span class="q-mx-sm text-caption">{{ $t('support__or') }}</span>
</div>
<div class="flex items-center justify-center text-primary q-ma-md">
<q-icon name="mail" class="q-mr-xs"/>
{{ supportEmailAddress }}
</div>
</div>
</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 supportEmailAddress = 'a-mart@ya.ru'
const supportTelegramUser = 'alexmart80'
const telegramUrl = `https://t.me/${supportTelegramUser}`
const openSupport = () => {
if (tg?.platform !== 'unknown') {
tg?.openLink(telegramUrl, { try_instant_view: true })
} else {
window.open(telegramUrl, '_blank')
}
}
</script>
<style scope>
.hidden-href{
display: none;
}
</style>

View File

@@ -0,0 +1,10 @@
<template>
<doc-block type="terms_of_use"/>
</template>
<script setup lang="ts">
import docBlock from 'components/docBlock.vue'
</script>
<style>
</style>

View File

@@ -3,7 +3,7 @@
<pn-scroll-list>
<template #card-body-header>
<div class="flex row q-ma-md justify-between">
<div class="flex row q-ma-md justify-between" v-if="chats.length !== 0">
<q-input
v-model="search"
clearable
@@ -11,6 +11,7 @@
:placeholder="$t('project_chats__search')"
dense
class="col-grow"
v-if="chats.length !== 0"
>
<template #prepend>
<q-icon name="mdi-magnify" />
@@ -18,26 +19,30 @@
</q-input>
</div>
</template>
<q-list bordered separator>
<q-list separator v-if="chats.length !== 0">
<q-slide-item
v-for="item in displayChats"
:key="item.id"
@right="handleSlide($event, item.id)"
right-color="red"
@click="goChat(item.invite_link)"
>
<template #right>
<q-icon size="lg" name="mdi-link-off"/>
</template>
<q-item
:key="item.id"
:clickable="false"
clickable
v-ripple
>
<q-item-section avatar>
<q-avatar rounded>
<q-img v-if="item.logo" :src="item.logo"/>
<pn-auto-avatar v-else :name="item.name"/>
</q-avatar>
<pn-auto-avatar
:img="item.logo"
:name="item.name"
type="rounded"
size="lg"
/>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-bold">
@@ -47,14 +52,14 @@
{{ item.description }}
</q-item-label>
<q-item-label caption lines="1">
<div class = "flex justify-start items-center">
<div class="q-mr-sm flex items-center">
<q-icon name="mdi-account-outline" class="q-mx-sm"/>
<span>{{ item.persons }}</span>
<div class = "flex justify-start items-center no-wrap">
<div class="q-mr-sm flex items-center no-wrap">
<q-icon name="mdi-account-multiple-outline" class="q-mr-xs"/>
<span>{{ item.user_count }}</span>
</div>
<div class="q-mx-sm flex items-center">
<q-icon name="mdi-key" class="q-mr-sm"/>
<span>{{ item.owner_id }} </span>
<div class="q-mx-sm flex items-center no-wrap ellipsis" v-if="item.owner_id">
<q-icon name="mdi-key" class="q-mr-xs"/>
<span class="ellipsis">{{ usersStore.userNameById(item.owner_id) }} </span>
</div>
</div>
</q-item-label>
@@ -63,111 +68,130 @@
</q-slide-item>
</q-list>
<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')"
@click="showOverlay=true; fabState=true"
/>
<div
class="flex column justify-center items-center w100"
style="position: absolute; bottom: 0;"
v-if="!chatsInit"
>
<q-linear-progress indeterminate />
</div>
</pn-scroll-list>
</div>
<q-page-sticky
:style="{ zIndex: !showOverlay ? 'inherit' : '5100 !important' }"
position="bottom-right"
:offset="[18, 18]"
:style="{ zIndex: !showOverlay ? 'inherit' : '5100 !important' }"
position="bottom-right"
:offset="[0, 18]"
class="fix-fab-offset"
>
<transition
appear
enter-active-class="animated zoomIn"
>
<transition
appear
enter-active-class="animated slideInUp"
<q-fab
v-model="fabState"
v-if="showFab"
icon="add"
color="brand"
direction="up"
vertical-actions-align="right"
@click="showOverlay = !showOverlay"
:disable="!tg.initData"
>
<q-fab
v-if="showFab"
icon="add"
color="brand"
direction="up"
vertical-actions-align="right"
@click="showOverlay = !showOverlay"
<template #tooltip>
<q-tooltip
v-if="!tg.initData"
anchor="center left" self="center end"
style="width: calc(min(100vw, var(--body-width)) - 102px) !important;"
>
<q-fab-action
v-for="item in fabMenu"
:key="item.id"
square
clickable
v-ripple
class="bg-white change-fab-action"
>
<template #icon>
<q-item class="q-pa-xs w100">
<q-item-section avatar class="items-center">
<q-avatar color="brand" rounded text-color="white" :icon="item.icon" />
</q-item-section>
{{ $t('project_chats_disabled_FAB')}}
</q-tooltip>
</template>
<q-fab-action
v-for="item in fabMenu"
:key="item.id"
square
clickable
v-ripple
class="bg-white change-fab-action"
@click="item.func()"
>
<template #icon>
<q-item class="q-pa-xs w100">
<q-item-section avatar class="items-center">
<q-avatar color="brand" rounded text-color="white" :icon="item.icon" />
</q-item-section>
<q-item-section class="items-start">
<q-item-label class="fab-action-item">
{{ $t(item.name) }}
</q-item-label>
<q-item-label caption class="fab-action-item">
{{ $t(item.description) }}
</q-item-label>
<q-item-section class="items-start">
<q-item-label class="fab-action-item">
{{ $t(item.name) }}
</q-item-label>
<q-item-label caption class="fab-action-item">
{{ $t(item.description) }}
</q-item-label>
</q-item-section>
</q-item>
</template>
</q-fab-action>
</q-fab>
</transition>
</q-page-sticky>
</q-item-section>
</q-item>
</template>
</q-fab-action>
</q-fab>
</transition>
</q-page-sticky>
</div>
<pn-overlay v-if="showOverlay"/>
<q-dialog v-model="showDialogDeleteChat" @before-hide="onDialogBeforeHide()">
<q-card class="q-pa-none q-ma-none">
<q-card-section align="center">
<div class="text-h6 text-negative ">{{ $t('project_chat__delete_warning') }}</div>
</q-card-section>
<pn-overlay v-if="showOverlay"/>
<q-card-section class="q-pt-none" align="center">
{{ $t('project_chat__delete_warning_message') }}
</q-card-section>
<pn-small-dialog
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"
@clickMainBtn="onConfirm()"
@close="onCancel()"
@before-hide="onDialogBeforeHide()"
/>
<q-card-actions align="center">
<q-btn
flat
:label="$t('back')"
color="primary"
v-close-popup
@click="onCancel()"
/>
<q-btn
flat
:label="$t('delete')"
color="primary"
v-close-popup
@click="onConfirm()"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, computed, onActivated, onDeactivated, onBeforeUnmount } from 'vue'
import { ref, computed, onActivated, onDeactivated, onBeforeUnmount, inject } from 'vue'
import { useChatsStore } from 'stores/chats'
import { useUsersStore } from 'stores/users'
import type { WebApp } from '@twa-dev/types'
import { useI18n } from "vue-i18n"
const { t } = useI18n()
const search = ref('')
const showOverlay = ref<boolean>(false)
const chatsStore = useChatsStore()
const usersStore = useUsersStore()
const showDialogDeleteChat = ref<boolean>(false)
const deleteChatId = ref<number | undefined>(undefined)
const currentSlideEvent = ref<SlideEvent | null>(null)
const closedByUserAction = ref(false)
const tg = inject('tg') as WebApp
const fabState = ref(false)
interface SlideEvent {
reset: () => void
}
const chats = chatsStore.chats
const chats = chatsStore.getChats
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: '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},
]
const displayChats = computed(() => {
@@ -181,7 +205,7 @@
return arrOut
})
function handleSlide (event: SlideEvent, id: number) {
function handleSlide (event: SlideEvent, id: number) {
currentSlideEvent.value = event
showDialogDeleteChat.value = true
deleteChatId.value = id
@@ -202,14 +226,42 @@
}
}
function onConfirm() {
async function onConfirm() {
closedByUserAction.value = true
if (deleteChatId.value) {
chatsStore.deleteChat(deleteChatId.value)
await chatsStore.unlink(deleteChatId.value)
}
currentSlideEvent.value = null
}
const botName = 'ready_or_not_2025_bot'
const urlAdmin = 'https://t.me/' + botName + '?startgroup='
const urlAdminPermission='&admin=' +
'post_messages+' +
'edit_messages+' +
'delete_messages+' +
'pin_messages+' +
'restrict_members+' +
'invite_users'
async function attachChat () {
const key = await chatsStore.getKey()
tg.openTelegramLink(urlAdmin + key + urlAdminPermission)
}
async function sendChat () {
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')) +
'&text=' + `${encodeURIComponent(message)}`
tg.openTelegramLink(tgShareUrl)
}
function goChat (invite: string) {
tg.openTelegramLink(invite)
}
// fix fab jumping
const showFab = ref(false)
const timerId = ref<ReturnType<typeof setTimeout> | null>(null)
@@ -236,6 +288,7 @@
</script>
<style scoped>
/* width of choose element */
.change-fab-action .q-fab__label--internal {
max-height: none;
}
@@ -255,10 +308,4 @@
align-self: center;
height: 98%;
}
.fix-fab {
top: calc(100vh - 92px);
left: calc(100vw - 92px);
padding: 18px;
}
</style>

View File

@@ -2,29 +2,37 @@
<div class="q-pa-none flex column col-grow no-scroll">
<pn-scroll-list>
<template #card-body-header>
<div class="w100 flex items-center justify-end q-pa-sm">
<q-btn color="primary" flat no-caps dense @click="maskCompany()">
<q-icon
left
size="sm"
name="mdi-drama-masks"
/>
<div>
{{ $t('company__mask')}}
</div>
</q-btn>
</div>
</template>
<div class="flex items-center justify-end q-pa-sm w100" v-if="companies.length !== 0">
<q-btn
:color="companies.length <= 2 ? 'grey-6' : 'primary'"
flat
no-caps
@click="maskCompany()"
:disable="companies.length <= 2"
class="q-pr-md"
rounded
>
<q-icon
left
size="sm"
name="mdi-domino-mask"
/>
<div>
{{ $t('company__mask')}}
</div>
</q-btn>
</div>
</template>
<q-list separator>
<q-list separator v-if="companies.length !== 0">
<q-slide-item
v-for="item in companies"
v-for="item in displayCompanies"
:key="item.id"
@right="handleSlide($event, item.id)"
right-color="red"
>
<template #right>
<q-icon size="lg" name="mdi-delete-outline"/>
<template #right v-if="item.id !== myCompany?.id">
<q-icon size="lg" name="mdi-account-multiple-minus-outline"/>
</template>
<q-item
:key="item.id"
@@ -33,34 +41,79 @@
class="w100"
@click="goCompanyInfo(item.id)"
>
<q-item-section avatar>
<q-avatar rounded>
<q-img v-if="item.logo" :src="item.logo" fit="cover" style="max-width: unset; height:40px;"/>
<pn-auto-avatar v-else :name="item.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-bold">{{ item.name }}</q-item-label>
<q-item-label caption lines="2">{{ item.description }}</q-item-label>
</q-item-section>
<!-- <q-item-section side top>
<div class="flex items-center">
<q-icon v-if="item.masked" name="mdi-drama-masks" color="black" size="sm"/>
<q-item-section avatar>
<pn-auto-avatar
:img="item.logo"
:name="item.name"
type="rounded"
size="lg"
/>
</q-item-section>
<q-item-section>
<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') }}
</div>
</q-item-label>
<q-item-label lines="1" class="text-bold">{{ item.name }}</q-item-label>
<q-item-label
caption lines="2"
style="max-width: -webkit-fill-available; white-space: pre-line"
>
{{ item.description }}
</q-item-label>
</q-item-section>
<q-item-section side top>
<div class="flex items-end column">
<span class="text-caption flex items-center">
<q-icon name="mdi-account-outline" color="grey" />
<span>{{ item.qtyPersons }}</span>
<span>{{ getQtyUsers(item.id) }}</span>
</span>
<q-icon
v-if="companiesStore.checkCompanyMasked(item.id)"
name="mdi-domino-mask"
color="grey"
size="xs"
/>
<div class="flex items-center row text-caption">
<q-icon v-if="item.site" name="mdi-web"/>
<q-icon v-if="item.address" name="mdi-map-marker-outline"/>
<q-icon v-if="item.phone" name="mdi-phone-outline"/>
<q-icon v-if="item.email" name="mdi-email-outline"/>
</div>
</q-item-section> -->
</div>
</q-item-section>
</q-item>
</q-slide-item>
</q-list>
<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()"
/>
<div
class="flex column justify-center items-center w100"
style="position: absolute; bottom: 0;"
v-if="!companiesInit"
>
<q-linear-progress indeterminate />
</div>
</pn-scroll-list>
<q-page-sticky
position="bottom-right"
:offset="[18, 18]"
:offset="[0, 18]"
class="fix-fab-offset"
>
<transition
appear
enter-active-class="animated slideInUp"
enter-active-class="animated zoomIn"
>
<q-btn
v-if="showFab"
@@ -72,63 +125,58 @@
</transition>
</q-page-sticky>
</div>
<q-dialog v-model="showDialogDeleteCompany" @before-hide="onDialogBeforeHide()">
<q-card class="q-pa-none q-ma-none">
<q-card-section align="center">
<div class="text-h6 text-negative ">{{ $t('company__delete_warning') }}</div>
</q-card-section>
<q-card-section class="q-pt-none" align="center">
{{ $t('company__delete_warning_message') }}
</q-card-section>
<q-card-actions align="center">
<q-btn
flat
:label="$t('back')"
color="primary"
v-close-popup
@click="onCancel()"
/>
<q-btn
flat
:label="$t('delete')"
color="primary"
v-close-popup
@click="onConfirm()"
/>
</q-card-actions>
</q-card>
</q-dialog>
<pn-small-dialog
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"
@clickMainBtn="onConfirm()"
@close="onCancel()"
@before-hide="onDialogBeforeHide()"
/>
</template>
<script setup lang="ts">
import { ref, computed, onActivated, onDeactivated, onBeforeUnmount } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useRouter, useRoute } from 'vue-router'
import { useCompaniesStore } from 'stores/companies'
import { parseIntString } from 'boot/helpers'
import { useUsersStore } from 'stores/users'
const router = useRouter()
const route = useRoute()
const companiesStore = useCompaniesStore()
const usersStore = useUsersStore()
const showDialogDeleteCompany = ref<boolean>(false)
const deleteCompanyId = ref<number | undefined>(undefined)
const currentSlideEvent = ref<SlideEvent | null>(null)
const closedByUserAction = ref(false)
const projectId = computed(() => parseIntString(route.params.id))
interface SlideEvent {
reset: () => void
}
const companies = companiesStore.companies
const users = computed(() => usersStore.users)
const companies = companiesStore.getCompanies
const companiesInit = computed(() => companiesStore.isInit)
const myCompany = computed(() => companies.find(el => el.is_own))
const displayCompanies = computed(() => {
const otherComp = companies.filter(el => !el.is_own)
return myCompany.value
? [myCompany.value, ...otherComp]
: otherComp
})
async function maskCompany () {
await router.push({ name: 'company_mask' })
}
async function goCompanyInfo (id :number) {
await router.push({ name: 'company_info', params: { id: projectId.value, companyId: id }})
async function goCompanyInfo (companyId: number) {
await router.push({ name: 'company_info', params: { id: route.params.id, companyId }})
}
async function createCompany () {
@@ -156,14 +204,19 @@
}
}
function onConfirm() {
async function onConfirm() {
closedByUserAction.value = true
if (deleteCompanyId.value) {
companiesStore.deleteCompany(deleteCompanyId.value)
await companiesStore.remove(deleteCompanyId.value)
}
currentSlideEvent.value = null
}
function getQtyUsers (companyId: number) {
const arr = users.value.filter(el => el.company_id === companyId)
return arr.length
}
// fix fab jumping
const showFab = ref(false)
const timerId = ref<ReturnType<typeof setTimeout> | null>(null)
@@ -195,5 +248,5 @@
{
align-self: center;
}
</style>
</style>

View File

@@ -1,189 +1,119 @@
<template>
<div
id="project-info"
:style="{ height: headerHeight + 'px' }"
class="flex row items-center justify-between no-wrap q-my-sm w100"
style="overflow: hidden; transition: height 0.3s ease-in-out;"
id="project-info"
:style="{ height: headerHeight + 'px', minHeight: '48px' }"
class="flex row items-center justify-between no-wrap w100 q-gutter-x-sm"
style="overflow: hidden; transition: height 0.3s ease-in-out; margin-left: 0"
>
<div class="ellipsis overflow-hidden">
<div class="ellipsis overflow-hidden q-pa-none q-ma-none">
<q-resize-observer @resize="onResize" />
<transition
enter-active-class="animated slideInUp"
leave-active-class="animated slideOutUp"
mode="out-in"
>
>
<div
v-if="!expandProjectInfo"
@click="toggleExpand"
class="text-h6 ellipsis no-wrap w100"
class="text-h6 ellipsis no-wrap w100 q-pa-none q-ma-none"
key="compact"
>
{{project.name}}
>
{{ project.name }}
</div>
<div
v-else
class="flex items-center no-wrap q-hoverable q-animate--slideUp"
class="flex items-center no-wrap q-hoverable q-animate--slideUp q-py-sm"
@click="toggleExpand"
key="expanded"
>
<q-avatar rounded>
<q-img v-if="project.logo" :src="project.logo" fit="cover" style="height: 100%;"/>
<pn-auto-avatar v-else :name="project.name"/>
</q-avatar>
>
<pn-auto-avatar
:img="project.logo"
:name="project.name"
type="rounded"
size="lg"
/>
<div class="q-px-md flex column text-white fit">
<div class="flex column text-white fit">
<div
class="text-h6"
:style="{ maxWidth: '-webkit-fill-available', whiteSpace: 'normal' }"
>
{{project.name}}
class="text-h6 q-pl-sm text-field"
>
{{ project.name }}
</div>
<div class="text-caption" :style="{ maxWidth: '-webkit-fill-available', whiteSpace: 'normal' }">
{{project.description}}
<div
class="text-caption q-pl-sm text-field"
>
{{ project.description }}
</div>
</div>
</div>
</transition>
</div>
<q-btn flat round color="white" icon="mdi-pencil" size="sm" class="q-ml-xl q-mr-sm">
<q-menu anchor="bottom right" self="top right">
<q-list>
<q-item
v-for="item in menuItems"
:key="item.id"
@click="item.func"
clickable
v-close-popup
class="flex items-center"
>
<q-icon :name="item.icon" size="sm" :color="item.iconColor"/>
<span class="q-ml-xs">{{ $t(item.title) }}</span>
</q-item>
</q-list>
</q-menu>
</q-btn>
<q-btn
@click="editProject"
flat round
color="white"
icon="edit"
size="md"
/>
</div>
<q-dialog v-model="showDialog">
<q-card class="q-pa-none q-ma-none">
<q-card-section align="center">
<div class="text-h6 text-negative ">
{{ $t('project__archive_warning')}}
</div>
</q-card-section>
<q-card-section class="q-pt-none" align="center">
{{ $t('project__archive_warning_message')}}
</q-card-section>
<q-card-actions align="center">
<q-btn
flat
:label="$t('back')"
color="primary"
v-close-popup
/>
<q-btn
flat
:label="$t('project__archive')"
color="negative"
v-close-popup
@click="archiveProject"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useProjectsStore } from 'stores/projects'
import { parseIntString } from 'boot/helpers'
const router = useRouter()
const route = useRoute()
const projectsStore = useProjectsStore()
const expandProjectInfo = ref<boolean>(false)
const showDialog = ref<boolean>(false)
const dialogType = ref<null | 'archive'>(null)
const headerHeight = ref<number>(0)
const menuItems = [
{ id: 1, title: 'project__edit', icon: 'mdi-square-edit-outline', iconColor: '', func: editProject },
// { id: 2, title: 'project__backup', icon: 'mdi-content-save-outline', iconColor: '', func: () => {} },
{ id: 3, title: 'project__archive', icon: 'mdi-archive-outline', iconColor: 'red', func: () => { showDialog.value = true; dialogType.value = 'archive' }}
]
const projectId = computed(() => parseIntString(route.params.id))
const project =ref({
name: '',
description: '',
logo: '',
is_logo_bg: false
const currentProjectId = computed(() => projectsStore.currentProjectId)
const project = computed(() => {
const currentProject =
currentProjectId.value && projectsStore.projectById(currentProjectId.value)
return currentProject
? {
name: currentProject.name,
description: currentProject.description ?? '',
logo: currentProject.logo ?? ''
}
: {
name: '',
description: '',
logo: ''
}
})
const loadProjectData = async () => {
if (!projectId.value) {
await abort()
return
} else {
const projectFromStore = projectsStore.projectById(projectId.value)
if (!projectFromStore) {
await abort()
return
}
project.value = {
name: projectFromStore.name,
description: projectFromStore.description || '',
logo: projectFromStore.logo || '',
is_logo_bg: projectFromStore.is_logo_bg || false
}
function toggleExpand () {
expandProjectInfo.value = !expandProjectInfo.value
}
}
async function abort () {
await router.replace({ name: 'projects' })
}
async function editProject () {
if (currentProjectId.value)
await router.push({ name: 'project_info', params: { id: currentProjectId.value } })
}
async function editProject () {
if (projectId.value) void projectsStore.update(projectId.value, project.value)
await router.push({ name: 'project_info' })
}
async function archiveProject () {
if (projectId.value) void projectsStore.archive(projectId.value)
await router.replace({ name: 'projects' })
}
function toggleExpand () {
expandProjectInfo.value = !expandProjectInfo.value
}
interface sizeParams {
interface sizeParams {
height: number,
width: number
}
function onResize (size :sizeParams) {
headerHeight.value = size.height
}
const headerHeight = ref<number>(0)
watch(projectId, loadProjectData)
watch(showDialog, () => {
if (showDialog.value === false) dialogType.value = null
})
onMounted(() => loadProjectData())
function onResize (size :sizeParams) {
headerHeight.value = size.height
}
</script>
<style>
<style scoped>
.text-field {
max-width: -webkit-fill-available;
white-space: pre-line;
}
</style>

View File

@@ -1,90 +0,0 @@
<template>
<div class="q-pa-none flex column col-grow no-scroll">
<pn-scroll-list>
<template #card-body-header>
<div class="flex row q-ma-md justify-between">
<q-input
v-model="search"
clearable
clear-icon="close"
:placeholder="$t('project_persons__search')"
dense
class="col-grow"
>
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
</q-input>
</div>
</template>
<q-list separator>
<q-item
v-for="item in displayPersons"
:key="item.id"
v-ripple
clickable
@click="goPersonInfo()"
>
<q-item-section avatar>
<q-avatar>
<img v-if="item.logo" :src="item.logo"/>
<pn-auto-avatar v-else :name="item.name"/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-bold">
{{item.name}}
</q-item-label>
<q-item-label caption lines="2">
<span>{{item.tname}}</span>
<span class="text-blue q-ml-sm">{{item.tusername}}</span>
</q-item-label>
<q-item-label lines="1">
{{ item.company.name +', ' + item.role }}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</pn-scroll-list>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
defineOptions({ inheritAttrs: false })
const router = useRouter()
const search = ref('')
const persons = [
{id: "p1", name: 'Кирюшкин Андрей', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', tname: 'Kir_AA', tusername: '@kiruha90', role: 'DevOps', company: {id: "com11", name: 'Рога и копытца', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false }},
{id: "p2", name: 'Пупкин Василий Александрович', logo: '', tname: 'Pupkin', tusername: '@super_pupkin', role: 'Руководитель проекта', company: {id: "com11", name: 'Рога и копытца', logo: '', description: 'Монтажники вывески', qtyPersons: 3, masked: false }},
{id: "p3", name: 'Макарова Полина', logo: 'https://cdn.quasar.dev/img/avatar6.jpg', tname: 'Unikorn', tusername: '@unicorn_stars', role: 'Администратор', company: {id: "com21", name: 'ООО "Василек"', logo: '', qtyPersons: 2, masked: true }},
{id: "p4", name: 'Жабов Максим', logo: '', tname: 'Zhaba', tusername: '@Zhabchenko', role: 'Аналитик', company: {id: "com21", name: 'ООО "Василек"', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', qtyPersons: 2, masked: true }},
]
const displayPersons = computed(() => {
if (!search.value || !(search.value && search.value.trim())) return persons
const searchValue = search.value.trim().toLowerCase()
const arrOut = persons
.filter(el =>
el.name.toLowerCase().includes(searchValue) ||
el.tname && el.tname.toLowerCase().includes(searchValue) ||
el.tusername && el.tusername.toLowerCase().includes(searchValue) ||
el.role && el.role.toLowerCase().includes(searchValue) ||
el.company.name && el.company.name.toLowerCase().includes(searchValue)
)
return arrOut
})
async function goPersonInfo () {
console.log('update')
await router.push({ name: 'person_info' })
}
</script>
<style>
</style>

View File

@@ -0,0 +1,297 @@
<template>
<div class="q-pa-none flex column col-grow no-scroll">
<pn-scroll-list>
<template #card-body-header>
<div class="flex row q-ma-md justify-between" v-if="users.length !== 0">
<q-input
v-model="search"
clearable
clear-icon="close"
:placeholder="$t('project_users__search')"
dense
class="col-grow"
>
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
</q-input>
</div>
</template>
<q-list separator v-if="users.length !== 0">
<q-slide-item
v-for="item in displayUsers"
:key="item.id"
@right="handleSlide($event, item.id)"
right-color="red"
>
<template #right>
<q-icon size="lg" name="mdi-account-remove-outline"/>
</template>
<q-item
:key="item.id"
v-ripple
clickable
class="w100"
@click="goUserInfo(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 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-slide-item>
</q-list>
<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"
color="grey"
flat no-caps
@click="showBlockedUsers=!showBlockedUsers"
dropdown-icon="arrow_drop_up"
>
<template #label>
<span class="text-caption">
{{ !showBlockedUsers
? $t('users__show_archive') + ' (' + blockedUsers.length +')'
: $t('user__hide_archive')
}}
</span>
</template>
</q-btn-dropdown>
<div class="w100" style="overflow: hidden">
<transition
appear
enter-active-class="animated slideInDown"
leave-active-class="animated slideOutUp"
>
<q-list separator v-if="showBlockedUsers" class="w100">
<q-item
v-for = "item in blockedUsers"
:key="item.id"
clickable
v-ripple
@click="handleUnblockUser(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>
<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')"
noBtn
/>
<div
class="flex column justify-center items-center w100"
style="position: absolute; bottom: 0;"
v-if="!usersInit"
>
<q-linear-progress indeterminate />
</div>
</pn-scroll-list>
</div>
<pn-small-dialog
v-model="showDialogDeleteUser"
icon="mdi-account-remove-outline"
color="negative"
title="user__dialog_delete_title"
message1="user__dialog_delete_message"
mainBtnLabel="user__dialog_delete_ok"
@clickMainBtn="onConfirmDeleteUser()"
@close="onCancel()"
@before-hide="onDialogBeforeHide()"
/>
<pn-small-dialog
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"
@clickMainBtn="onConfirmRestoreUser()"
/>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUsersStore } from 'stores/users'
import { useCompaniesStore } from 'stores/companies'
import type { User } from 'types/Users'
defineOptions({ inheritAttrs: false })
const router = useRouter()
const route = useRoute()
const search = ref('')
const usersStore = useUsersStore()
const companiesStore = useCompaniesStore()
const users = usersStore.getUsers
const usersInit = computed(() => usersStore.isInit)
const deleteUserId = ref<number | undefined>(undefined)
const showDialogDeleteUser = ref<boolean>(false)
const currentSlideEvent = ref<SlideEvent | null>(null)
const closedByUserAction = ref(false)
const mapUsers = computed(() => users.map(el => ({...el, ...userSection(el)})))
interface SlideEvent {
reset: () => void
}
const displayUsersAll = computed(() => {
if (!search.value || !(search.value && search.value.trim())) return mapUsers.value
const searchValue = search.value.trim().toLowerCase()
const arrOut = mapUsers.value
.filter(el =>
el.section1.toLowerCase().includes(searchValue) ||
el.section2_1.toLowerCase().includes(searchValue) ||
el.section2_2.toLowerCase().includes(searchValue) ||
el.section3.toLowerCase().includes(searchValue)
)
return arrOut
})
const displayUsers = computed(() => displayUsersAll.value.filter(el => !el.is_block))
function userSection (user: User) {
const tname = () => {
return user.firstname
? user.lastname
? user.firstname + ' ' + user.lastname
: user.firstname
: user.lastname ?? ''
}
const section1 = user.fullname ?? tname()
const section2_1 = user.fullname ? tname() : ''
const section2_2 = user.username ?? ''
const section3 = (
user.company_id && companiesStore.companyById(user.company_id)
? companiesStore.companyById(user.company_id)?.name + ((user.role || user.department ) ? ' / ' :'')
: '') +
(user.department ? user.department + (user.role ? ' / ' : '') : '') +
(user.role ?? '')
return {
section1,
section2_1, section2_2,
section3
}
}
async function goUserInfo (id: number) {
await router.push({ name: 'user_info', params: { id: route.params.id, userId: id }})
}
function handleSlide (event: SlideEvent, id: number) {
currentSlideEvent.value = event
showDialogDeleteUser.value = true
deleteUserId.value = id
}
function onDialogBeforeHide () {
if (!closedByUserAction.value) {
onCancel()
}
closedByUserAction.value = false
}
function onCancel() {
closedByUserAction.value = true
if (currentSlideEvent.value) {
currentSlideEvent.value.reset()
currentSlideEvent.value = null
}
}
async function onConfirmDeleteUser() {
closedByUserAction.value = true
if (deleteUserId.value) {
await usersStore.blockUser(deleteUserId.value)
}
currentSlideEvent.value = null
}
const showBlockedUsers = ref(false)
const blockedUsers = computed(() => displayUsersAll.value.filter(el => el.is_block))
const unblockUserId = ref<number | undefined> (undefined)
const showDialogRestoreUser = ref(false)
function handleUnblockUser (id: number) {
showDialogRestoreUser.value = true
unblockUserId.value = id
}
async function onConfirmRestoreUser () {
if (unblockUserId.value) await usersStore.restore(unblockUserId.value)
}
watch(showDialogRestoreUser, (newD :boolean) => {
if (!newD) unblockUserId.value = undefined
})
</script>
<style>
</style>