before delete 3software
This commit is contained in:
20
src/pages/AccountChangeAuthMethodPage.vue
Normal file
20
src/pages/AccountChangeAuthMethodPage.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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') || '')
|
||||
|
||||
@@ -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') || '')
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
35
src/pages/CompanyAddPage.vue
Normal file
35
src/pages/CompanyAddPage.vue
Normal 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>
|
||||
@@ -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>
|
||||
58
src/pages/CompanyEditPage.vue
Normal file
58
src/pages/CompanyEditPage.vue
Normal 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
32
src/pages/ProjectAddPage.vue
Normal file
32
src/pages/ProjectAddPage.vue
Normal 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>
|
||||
@@ -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>
|
||||
43
src/pages/ProjectEditPage.vue
Normal file
43
src/pages/ProjectEditPage.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
45
src/pages/UserEditPage.vue
Normal file
45
src/pages/UserEditPage.vue
Normal 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>
|
||||
234
src/pages/account/3Software.vue
Normal file
234
src/pages/account/3Software.vue
Normal 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>
|
||||
10
src/pages/account/PrivacyPage.vue
Normal file
10
src/pages/account/PrivacyPage.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<doc-block type="privacy"/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import docBlock from 'components/docBlock.vue'
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
64
src/pages/account/SupportPage.vue
Normal file
64
src/pages/account/SupportPage.vue
Normal 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>
|
||||
10
src/pages/account/TermsPage.vue
Normal file
10
src/pages/account/TermsPage.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
297
src/pages/project-page/ProjectPageUsers.vue
Normal file
297
src/pages/project-page/ProjectPageUsers.vue
Normal 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>
|
||||
Reference in New Issue
Block a user