before delete 3software
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
no-error-icon
|
||||
@focus="($refs.emailInput as typeof QInput)?.resetValidation()"
|
||||
ref="emailInput"
|
||||
:disable="type === 'changePwd'"
|
||||
/>
|
||||
<q-stepper-navigation>
|
||||
<q-btn
|
||||
@@ -54,6 +55,7 @@
|
||||
@click="handleSubmit"
|
||||
color="primary"
|
||||
:label="$t('continue')"
|
||||
:disable="code.length === 0"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
@@ -73,6 +75,7 @@
|
||||
v-model="password"
|
||||
dense
|
||||
filled
|
||||
autofocus
|
||||
:label = "$t('account_helper__password')"
|
||||
:type="isPwd ? 'password' : 'text'"
|
||||
hide-hint
|
||||
@@ -114,15 +117,14 @@
|
||||
<pn-magic-overlay
|
||||
v-if="showSuccessOverlay"
|
||||
icon="mdi-check-circle-outline"
|
||||
message1="account_helper__ok_message1"
|
||||
message2="account_helper__ok_message2"
|
||||
:message1="getHelperMessage1()"
|
||||
:message2="getHelperMessage2()"
|
||||
route-name="projects"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { AxiosError } from 'axios'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { useI18n } from "vue-i18n"
|
||||
@@ -134,7 +136,9 @@
|
||||
? 'register'
|
||||
: props.type === 'forgotPwd'
|
||||
? 'forgot'
|
||||
: 'change'
|
||||
: props.type === 'changePwd'
|
||||
? 'changePwd'
|
||||
: 'changeMethod'
|
||||
})
|
||||
|
||||
const $q = useQuasar()
|
||||
@@ -142,7 +146,7 @@
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'register' | 'forgotPwd' | 'changePwd'
|
||||
type: 'register' | 'forgotPwd' | 'changePwd' | 'changeMethod'
|
||||
email?: string
|
||||
}>()
|
||||
|
||||
@@ -182,7 +186,7 @@
|
||||
},
|
||||
3: async () => {
|
||||
await authStore.setPassword(flowType.value, login.value, code.value, password.value)
|
||||
if (flowType.value === 'register') {
|
||||
if (flowType.value === 'register' || flowType.value === 'changeMethod') {
|
||||
await authStore.loginWithCredentials(login.value, password.value)
|
||||
}
|
||||
}
|
||||
@@ -217,6 +221,28 @@
|
||||
handleError(error as AxiosError)
|
||||
}
|
||||
}
|
||||
|
||||
const getHelperMessage1 = () => {
|
||||
switch (flowType.value) {
|
||||
case 'register': return 'account_helper__register_message1'
|
||||
case 'forgot': return 'account_helper__forgot_password_message1'
|
||||
case 'changePwd': return 'account_helper__change_password_message1'
|
||||
case 'changeMethod': return 'account_helper__change_method_message1'
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
const getHelperMessage2 = () => {
|
||||
switch (flowType.value) {
|
||||
case 'register': return 'slogan'
|
||||
case 'forgot':
|
||||
case 'changePwd':
|
||||
case 'changeMethod':
|
||||
return 'account_helper__go_projects'
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
128
src/components/companyBlock.vue
Normal file
128
src/components/companyBlock.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<template #title>
|
||||
{{ $t(title) }}
|
||||
</template>
|
||||
<template #footer>
|
||||
<q-btn
|
||||
rounded color="primary"
|
||||
class="w100 q-mt-md q-mb-xs"
|
||||
:disable="!(isFormValid && (isDirty(initialCompany, modelValue)))"
|
||||
@click = "emit('update')"
|
||||
>
|
||||
{{ $t(btnText) }}
|
||||
</q-btn>
|
||||
</template>
|
||||
|
||||
<pn-scroll-list>
|
||||
<div class="flex column items-center q-pa-md q-pb-sm">
|
||||
<slot name="myCompany"/>
|
||||
<pn-image-selector
|
||||
v-model="modelValue.logo"
|
||||
:size="100"
|
||||
:iconsize="80"
|
||||
class="q-pb-lg"
|
||||
/>
|
||||
<div class="q-gutter-y-lg w100">
|
||||
<q-input
|
||||
v-for="input in textInputs"
|
||||
:key="input.id"
|
||||
v-model.trim="modelValue[input.val]"
|
||||
dense
|
||||
filled
|
||||
class="w100"
|
||||
:autogrow="input.val === 'description' || input.val === 'address'"
|
||||
:class="input.val === 'name'
|
||||
? 'fix-bottom-padding'
|
||||
: input.val === 'address'
|
||||
? 'input-fix q-pt-sm'
|
||||
: 'q-pt-sm'"
|
||||
:label="input.label ? $t(input.label) : void 0"
|
||||
:rules="input.val === 'name' ? [rules[input.val]] : []"
|
||||
no-error-icon
|
||||
:label-slot="Boolean(input.label)"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon v-if="input.icon" :name="input.icon"/>
|
||||
</template>
|
||||
<template #label v-if="input.label">
|
||||
{{$t(input.label) }}
|
||||
<span v-if="input.val === 'name'" class="text-red">*</span>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {onMounted, computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { isDirty } from 'helpers/helpers'
|
||||
import type { CompanyParams } from 'types/Company'
|
||||
|
||||
const { t }= useI18n()
|
||||
|
||||
const modelValue = defineModel<CompanyParams>({
|
||||
required: true
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
title: string,
|
||||
btnText: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update'])
|
||||
|
||||
interface TextInput {
|
||||
id: number
|
||||
label?: string
|
||||
icon?: string
|
||||
val: keyof CompanyParams
|
||||
rules: ((value: string) => boolean | string)[]
|
||||
}
|
||||
|
||||
const textInputs: TextInput[] = [
|
||||
{ id: 1, val: 'name', label: 'company_block__name', rules: [] },
|
||||
{ id: 2, val: 'description', label: 'company_block__description', rules: [] },
|
||||
{ id: 3, val: 'site', icon: 'mdi-web', rules: [] },
|
||||
{ id: 4, val: 'address', icon: 'mdi-map-marker-outline', rules: [] },
|
||||
{ id: 5, val: 'phone', icon: 'mdi-phone-outline', rules: [] },
|
||||
{ id: 6, val: 'email', icon: 'mdi-email-outline', rules: [] }
|
||||
]
|
||||
|
||||
const rulesErrorMessage = {
|
||||
name: t('company_block__error_name')
|
||||
}
|
||||
|
||||
const rules = {
|
||||
name: (val: CompanyParams['name']) => !!val?.trim() || rulesErrorMessage['name']
|
||||
}
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
const validations = {
|
||||
name: rules.name(modelValue.value.name) === true
|
||||
}
|
||||
|
||||
return Object.values(validations).every(Boolean)
|
||||
})
|
||||
|
||||
const initialCompany = ref({} as CompanyParams)
|
||||
|
||||
onMounted(() => {
|
||||
console.log(111, modelValue.value)
|
||||
initialCompany.value = { ...modelValue.value }
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.q-field--with-bottom.fix-bottom-padding {
|
||||
padding-bottom: 0 !important
|
||||
}
|
||||
|
||||
.input-fix :deep(.q-field__prepend.q-field__marginal) {
|
||||
height: auto !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,80 +0,0 @@
|
||||
<template>
|
||||
<div class="flex column items-center col-grow q-px-lg q-pt-sm">
|
||||
<pn-image-selector :size="100" :iconsize="80" class="q-pb-xs" v-model="modelValue.logo"/>
|
||||
|
||||
<q-input
|
||||
v-for="input in textInputs"
|
||||
:key="input.id"
|
||||
v-model.trim="modelValue[input.val]"
|
||||
dense
|
||||
filled
|
||||
class = "q-mt-md w100"
|
||||
:label = "input.label ? $t(input.label) : void 0"
|
||||
:rules="input.val === 'name' ? [rules[input.val]] : []"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon v-if="input.icon" :name="input.icon"/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, computed } from 'vue'
|
||||
import type { CompanyParams } from 'src/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t }= useI18n()
|
||||
|
||||
const modelValue = defineModel<CompanyParams>({
|
||||
required: false,
|
||||
default: () => ({
|
||||
name: '',
|
||||
logo: '',
|
||||
description: '',
|
||||
site: '',
|
||||
address: '',
|
||||
phone: '',
|
||||
email: ''
|
||||
})
|
||||
})
|
||||
|
||||
const emit = defineEmits(['valid'])
|
||||
const rulesErrorMessage = {
|
||||
name: t('company_card__error_name')
|
||||
}
|
||||
|
||||
const rules = {
|
||||
name: (val :CompanyParams['name']) => !!val?.trim() || rulesErrorMessage['name']
|
||||
}
|
||||
|
||||
const isValid = computed(() => {
|
||||
const checkName = rules.name(modelValue.value.name)
|
||||
return { name: checkName && (checkName !== rulesErrorMessage['name']) }
|
||||
})
|
||||
|
||||
watch(isValid, (newVal) => {
|
||||
const allValid = Object.values(newVal).every(v => v)
|
||||
emit('valid', allValid)
|
||||
}, { immediate: true})
|
||||
|
||||
interface TextInput {
|
||||
id: number
|
||||
label?: string
|
||||
icon?: string
|
||||
val: keyof CompanyParams
|
||||
rules: ((value: string) => boolean | string)[]
|
||||
}
|
||||
const textInputs: TextInput[] = [
|
||||
{ id: 1, val: 'name', label: 'company_info__name', rules: [] },
|
||||
{ id: 2, val: 'description', label: 'company_info__description', rules: [] },
|
||||
{ id: 3, val: 'site', icon: 'mdi-web', rules: [] },
|
||||
{ id: 4, val: 'address', icon: 'mdi-map-marker-outline', rules: [] },
|
||||
{ id: 5, val: 'phone', icon: 'mdi-phone-outline', rules: [] },
|
||||
{ id: 6, val: 'email', icon: 'mdi-email-outline', rules: [] },
|
||||
]
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,33 +1,33 @@
|
||||
<template>
|
||||
<div class="q-pt-md">
|
||||
<div class="q-pt-md" v-if="mapUsers.length !==0 ">
|
||||
<span class="q-pl-md text-h6">
|
||||
{{ $t('company_info__persons') }}
|
||||
{{ $t('company_info__users') }}
|
||||
</span>
|
||||
|
||||
<q-list separator>
|
||||
<q-item
|
||||
v-for="item in persons"
|
||||
v-for="item in mapUsers"
|
||||
:key="item.id"
|
||||
v-ripple
|
||||
clickable
|
||||
@click="goPersonInfo()"
|
||||
@click="goPersonInfo(item.id)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<img v-if="item.logo" :src="item.logo"/>
|
||||
<pn-auto-avatar v-else :name="item.name"/>
|
||||
</q-avatar>
|
||||
<pn-auto-avatar
|
||||
:img="item.photo"
|
||||
:name="item.section1"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label lines="1" class="text-bold">
|
||||
{{item.name}}
|
||||
<q-item-label lines="1" class="text-bold" v-if="item.section1">
|
||||
{{item.section1}}
|
||||
</q-item-label>
|
||||
<q-item-label caption lines="2">
|
||||
<span>{{item.tname}}</span>
|
||||
<span class="text-blue q-ml-sm">{{item.tusername}}</span>
|
||||
<span v-if="item.section2_1" class="q-mr-sm">{{item.section2_1}}</span>
|
||||
<span class="text-blue" v-if="item.section2_2">{{'@' + item.section2_2}}</span>
|
||||
</q-item-label>
|
||||
<q-item-label lines="1">
|
||||
{{item.role}}
|
||||
<q-item-label lines="1" v-if="item.section3">
|
||||
{{item.section3}}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
@@ -36,20 +36,59 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useUsersStore } from 'stores/users'
|
||||
import type { User } from 'types/Users'
|
||||
const usersStore = useUsersStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const router = useRouter()
|
||||
const currentCompanyId = Number(route.params.companyId)
|
||||
|
||||
const users = usersStore.users
|
||||
|
||||
const persons = [
|
||||
{id: "p1", name: 'Кирюшкин Андрей', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', tname: 'Kir_AA', tusername: '@kiruha90', role: 'DevOps' },
|
||||
{id: "p2", name: 'Пупкин Василий Александрович', logo: '', tname: 'Pupkin', tusername: '@super_pupkin', role: 'Руководитель проекта' },
|
||||
{id: "p3", name: 'Макарова Полина', logo: 'https://cdn.quasar.dev/img/avatar6.jpg', tname: 'Unikorn', tusername: '@unicorn_stars', role: 'Администратор' },
|
||||
{id: "p4", name: 'Жабов Максим', logo: '', tname: 'Zhaba', tusername: '@Zhabchenko', role: 'Аналитик' },
|
||||
]
|
||||
const mapUsers = users
|
||||
.filter(el => el.company_id === currentCompanyId)
|
||||
.map(el => ({...el, ...userSection(el)}))
|
||||
|
||||
|
||||
async function goPersonInfo () {
|
||||
console.log('update')
|
||||
await router.push({ name: 'person_info' })
|
||||
async function goPersonInfo (userId: number) {
|
||||
await router.push({ name: 'user_info', params: { id: route.params.id, userId }})
|
||||
}
|
||||
|
||||
// copy from 'pages/project-page/ProjectPageUsers.vue' кроме company.name
|
||||
function userSection (user: User) {
|
||||
const tname = () => {
|
||||
return user.firstname
|
||||
? user.lastname
|
||||
? user.firstname + ' ' + user.lastname
|
||||
: user.firstname
|
||||
: user.lastname ?? ''
|
||||
}
|
||||
|
||||
const section1 = user.name
|
||||
? user.name
|
||||
: tname()
|
||||
|
||||
const section2_1 = user.name
|
||||
? tname()
|
||||
: ''
|
||||
|
||||
const section2_2 = user.username ?? ''
|
||||
|
||||
const section3 = (
|
||||
user.department
|
||||
? user.department + ' '
|
||||
: ''
|
||||
) + (
|
||||
user.role ?? ''
|
||||
)
|
||||
|
||||
return {
|
||||
section1,
|
||||
section2_1, section2_2,
|
||||
section3
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
75
src/components/docBlock.vue
Normal file
75
src/components/docBlock.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<template #title>
|
||||
{{ $t(type + '__title') }}
|
||||
</template>
|
||||
<pn-scroll-list>
|
||||
<div class="q-px-md">
|
||||
<div
|
||||
v-if="fileText"
|
||||
style="white-space: pre-wrap;"
|
||||
>
|
||||
{{ fileText }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
align="center"
|
||||
class="text-negative"
|
||||
>
|
||||
{{ $t(type + '__not_ready') }}
|
||||
</div>
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
<div
|
||||
class="flex column justify-center items-center w100"
|
||||
style="position: absolute; bottom: 0;"
|
||||
v-if="isLoading"
|
||||
>
|
||||
<q-linear-progress indeterminate />
|
||||
</div>
|
||||
</pn-page-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useSettingsStore } from 'stores/settings'
|
||||
|
||||
const fileText = ref<string>('')
|
||||
const isLoading = ref<boolean>(true)
|
||||
const error = ref<boolean>(false)
|
||||
const settingsStore = useSettingsStore()
|
||||
const lang = ref<string>('EN')
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'terms_of_use' | 'privacy'
|
||||
}>()
|
||||
|
||||
function parseLocale(locale: string): string {
|
||||
return locale.split(/[-_]/)[0] ?? ''
|
||||
}
|
||||
|
||||
const baseDocName =
|
||||
props.type ==='terms_of_use' ? 'Terms_of_use' : 'Privacy'
|
||||
|
||||
onMounted(async () => {
|
||||
const locale = settingsStore.settings.locale
|
||||
lang.value = parseLocale(locale)
|
||||
try {
|
||||
const response = await fetch('/admin/doc/' + baseDocName + '_' + lang.value +'.txt')
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`)
|
||||
}
|
||||
|
||||
fileText.value = await response.text()
|
||||
} catch (err) {
|
||||
console.error('File load error:', err)
|
||||
error.value = true
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="flex items-center no-wrap overflow-hidden w100">
|
||||
<q-icon v-if="user?.email" size="md" class="q-mr-sm" name="mdi-account-circle-outline"/>
|
||||
<q-icon v-if="customer?.email" size="md" class="q-mr-sm" name="mdi-account-circle-outline"/>
|
||||
<q-avatar v-else size="32px" class="q-mr-sm">
|
||||
<q-img v-if="tgUser?.photo_url" :src="tgUser.photo_url"/>
|
||||
<q-icon v-else size="md" class="q-mr-sm" name="mdi-account-circle-outline"/>
|
||||
</q-avatar>
|
||||
<span v-if="user?.email" class="ellipsis">
|
||||
{{ user.email }}
|
||||
<span v-if="customer?.email" class="ellipsis">
|
||||
{{ customer.email }}
|
||||
</span>
|
||||
<span v-else class="ellipsis">
|
||||
{{
|
||||
@@ -25,7 +25,7 @@
|
||||
import type { WebApp } from '@twa-dev/types'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const user = authStore.user
|
||||
const customer = authStore.customer
|
||||
const tg = inject('tg') as WebApp
|
||||
const tgUser = tg.initDataUnsafe.user
|
||||
|
||||
|
||||
@@ -1,19 +1,42 @@
|
||||
<template>
|
||||
<div
|
||||
:style="{ backgroundColor: stringToColour(props.name) } "
|
||||
class="fit flex items-center justify-center text-white"
|
||||
>
|
||||
{{ props.name.substring(0, 1) }}
|
||||
</div>
|
||||
<q-avatar
|
||||
:square="type==='square'"
|
||||
:rounded="type==='rounded'"
|
||||
:size="size"
|
||||
>
|
||||
<img
|
||||
v-if="img"
|
||||
:src="img"
|
||||
style=" object-fit: cover;"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
:style="{ backgroundColor: stringToColour(name) } "
|
||||
class="fit flex items-center justify-center text-white"
|
||||
>
|
||||
{{ name ? name.substring(0, 1) : '-' }}
|
||||
</div>
|
||||
</q-avatar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
}>()
|
||||
interface Props {
|
||||
img?: string | null
|
||||
name: string,
|
||||
size?: string,
|
||||
type?: 'rounded' | 'square' | ''
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
img: null,
|
||||
name: '-',
|
||||
size: 'md',
|
||||
type: ''
|
||||
})
|
||||
|
||||
const stringToColour = (str: string) => {
|
||||
if (!str) return '#eee'
|
||||
let hash = 0
|
||||
str.split('').forEach(char => {
|
||||
hash = char.charCodeAt(0) + ((hash << 5) - hash)
|
||||
@@ -30,3 +53,40 @@
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- <template>
|
||||
<div
|
||||
:style="{ backgroundColor: stringToColour(props.name) } "
|
||||
class="fit flex items-center justify-center text-white"
|
||||
>
|
||||
{{ props.name ? props.name.substring(0, 1) : '' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
}>()
|
||||
|
||||
const stringToColour = (str: string) => {
|
||||
if (!str) return '#eee'
|
||||
let hash = 0
|
||||
str.split('').forEach(char => {
|
||||
hash = char.charCodeAt(0) + ((hash << 5) - hash)
|
||||
})
|
||||
let colour = '#'
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const value = (hash >> (i * 8)) & 0xff
|
||||
colour += value.toString(16).padStart(2, '0')
|
||||
}
|
||||
return colour
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style> -->
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
accept="image/*"
|
||||
/>
|
||||
<q-icon
|
||||
v-if="modelValue === '' || modelValue === undefined"
|
||||
v-if="modelValue === '' || modelValue === undefined || modelValue === null"
|
||||
name="mdi-camera-plus-outline"
|
||||
class="absolute-full fit text-grey-4"
|
||||
:style="{ fontSize: String(iconsize) + 'px'}"
|
||||
|
||||
@@ -4,18 +4,36 @@
|
||||
maximized
|
||||
persistent
|
||||
transition-show="slide-up"
|
||||
transition-hide="slide-down">
|
||||
transition-hide="slide-down"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center fullscrean-card column"
|
||||
>
|
||||
<q-icon :name = "icon" color="brand" size="160px"/>
|
||||
<div class="text-h5 q-mb-lg">
|
||||
<div
|
||||
id="icon-wrapper"
|
||||
:style="{ position: 'relative', height: size + 'px', width: size + 'px'}"
|
||||
>
|
||||
<div class="animation icon-position"></div>
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated zoomIn slow"
|
||||
>
|
||||
<q-icon
|
||||
v-if="showIcon"
|
||||
:name = "icon"
|
||||
color="brand"
|
||||
size="160px"
|
||||
class="icon-position"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="text-h5 q-mb-lg q-mx-md" align="center">
|
||||
{{ $t(message1) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="message2"
|
||||
class="absolute-bottom q-py-lg flex justify-center row"
|
||||
class="absolute-bottom q-py-lg q-mx-md" align="center"
|
||||
>
|
||||
{{ $t(message2) }}
|
||||
</div>
|
||||
@@ -24,7 +42,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -35,30 +53,35 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const showIcon = ref(false)
|
||||
const router = useRouter()
|
||||
const timers = ref<number[]>([])
|
||||
const size = 160
|
||||
|
||||
const setupTimers = () => {
|
||||
visible.value = true
|
||||
|
||||
const timer1 = window.setTimeout(() => {
|
||||
visible.value = false
|
||||
|
||||
const timer2 = window.setTimeout(() => {
|
||||
router.push({ name: props.routeName })
|
||||
}, 300)
|
||||
|
||||
timers.value.push(timer2)
|
||||
}, 2000)
|
||||
|
||||
showIcon.value = true
|
||||
}, 300)
|
||||
timers.value.push(timer1)
|
||||
};
|
||||
const timer2 = window.setTimeout(() => {
|
||||
visible.value = false
|
||||
showIcon.value = false
|
||||
}, 2000)
|
||||
timers.value.push(timer2)
|
||||
}
|
||||
|
||||
const clearTimers = () => {
|
||||
timers.value.forEach(timer => clearTimeout(timer))
|
||||
timers.value = []
|
||||
visible.value = false
|
||||
showIcon.value = false
|
||||
}
|
||||
|
||||
watch(visible, async (newVal) => {
|
||||
if (newVal === false) await router.push({ name: props.routeName })
|
||||
})
|
||||
|
||||
onMounted(setupTimers)
|
||||
onUnmounted(clearTimers)
|
||||
</script>
|
||||
@@ -68,6 +91,32 @@ onUnmounted(clearTimers)
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@property --percentage {
|
||||
initial-value: 0%;
|
||||
inherits: false;
|
||||
syntax: "<percentage>";
|
||||
}
|
||||
|
||||
.animation {
|
||||
background: conic-gradient(transparent var(--percentage), white 0);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: timer 1s linear;
|
||||
animation-fill-mode:forwards;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@keyframes timer {
|
||||
to {
|
||||
--percentage: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-position {
|
||||
position: absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
49
src/components/pnOnboardBtn.vue
Normal file
49
src/components/pnOnboardBtn.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div 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="handleClick"
|
||||
v-if="!noBtn"
|
||||
>
|
||||
<div class="flex column justify-center col-grow items-center">
|
||||
<q-icon :name="icon" size="160px" class="q-pb-md"/>
|
||||
<div class="text-h6 text-brand">
|
||||
{{message1}}
|
||||
</div>
|
||||
</div>
|
||||
</q-btn>
|
||||
<div v-else>
|
||||
<div class="flex column justify-center col-grow items-center">
|
||||
<q-icon :name="icon" size="160px" class="q-pb-md"/>
|
||||
<div class="text-h6 text-brand">
|
||||
{{message1}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="message2" class="text-caption" align="center">
|
||||
{{message2}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
icon: string
|
||||
message1: string
|
||||
message2?: string
|
||||
noBtn?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['btn-click'])
|
||||
|
||||
const handleClick = () => {
|
||||
emit('btn-click')
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,23 +1,18 @@
|
||||
<template>
|
||||
<q-page class="column items-center no-scroll">
|
||||
|
||||
<div
|
||||
class="text-white flex items-center w100 q-pl-md q-pr-sm q-ma-none text-h6 no-scroll"
|
||||
style="min-height: 48px"
|
||||
>
|
||||
<slot name="title"/>
|
||||
</div>
|
||||
<slot/>
|
||||
<div
|
||||
class="flex items-center justify-between q-ma-none q-py-none q-px-md text-white text-h6 no-scroll no-wrap w100"
|
||||
style="min-height: 48px"
|
||||
>
|
||||
<slot name="title"/>
|
||||
</div>
|
||||
<slot/>
|
||||
<div class="bg-white w100 q-ma-none q-px-md">
|
||||
<slot name="footer"/>
|
||||
</q-page>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.glass-card {
|
||||
opacity: 1 !important;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,132 +1,94 @@
|
||||
<template>
|
||||
<div id="card-body" class="w100 col-grow flex column" style="position: relative">
|
||||
<div
|
||||
class="glass-card fit top-rounded-card flex column"
|
||||
style="position: absolute; top: 0; left: 0"
|
||||
/>
|
||||
<div
|
||||
id="page-card"
|
||||
class="w100 flex column glass-card top-rounded-card no-scroll no-wrap"
|
||||
>
|
||||
<div
|
||||
id="card-body-header"
|
||||
style="min-height: var(--top-raduis);"
|
||||
style="flex-shrink: 0; min-height: var(--top-raduis);"
|
||||
>
|
||||
<q-resize-observer @resize="onHeaderResize"/>
|
||||
<slot name="card-body-header"/>
|
||||
</div>
|
||||
<div class="fit flex column col-grow">
|
||||
<q-resize-observer @resize="onResize" />
|
||||
<div id="card-scroll-area" class="noscroll">
|
||||
|
||||
<q-scroll-area
|
||||
ref="scrollArea"
|
||||
:style="{height: heightCard+'px'}"
|
||||
class="w100 q-pa-none q-ma-none"
|
||||
id="scroll-area"
|
||||
@scroll="onScroll"
|
||||
:class="{
|
||||
'shadow-top': hasScrolled,
|
||||
'shadow-bottom': hasScrolledBottom
|
||||
}"
|
||||
>
|
||||
<slot/>
|
||||
<div class="q-pa-sm"/>
|
||||
</q-scroll-area>
|
||||
</div>
|
||||
<div id="card-body" >
|
||||
<q-resize-observer @resize="onBodyResize"/>
|
||||
<pn-shadow-scroll :hideShadows="isResizing" :height="scrollAreaHeight">
|
||||
<slot/>
|
||||
</pn-shadow-scroll>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { QScrollArea } from 'quasar'
|
||||
const heightCard = ref(100)
|
||||
const hasScrolled = ref(false)
|
||||
const hasScrolledBottom = ref(false)
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
|
||||
interface sizeParams {
|
||||
height: number,
|
||||
width: number
|
||||
}
|
||||
|
||||
interface ScrollInfo {
|
||||
verticalPosition: number;
|
||||
verticalPercentage: number;
|
||||
verticalSize: number;
|
||||
verticalContainerSize: number;
|
||||
horizontalPosition: number;
|
||||
horizontalPercentage: number;
|
||||
}
|
||||
|
||||
const scrollArea = ref<InstanceType<typeof QScrollArea> | null>(null)
|
||||
|
||||
function onResize (size :sizeParams) {
|
||||
heightCard.value = size.height
|
||||
}
|
||||
|
||||
function onScroll (info: ScrollInfo) {
|
||||
hasScrolled.value = info.verticalPosition > 0
|
||||
const scrollEnd = info.verticalPosition + info.verticalContainerSize >= info.verticalSize - 1
|
||||
hasScrolledBottom.value = !scrollEnd
|
||||
const heightCard = ref(100)
|
||||
const scrollAreaHeight = ref(100)
|
||||
const headerHeight = ref(0)
|
||||
|
||||
interface sizeParams {
|
||||
height: number,
|
||||
width: number
|
||||
}
|
||||
|
||||
async function onHeaderResize(size: sizeParams) {
|
||||
headerHeight.value = size.height
|
||||
await updateScrollAreaHeight()
|
||||
}
|
||||
|
||||
async function onBodyResize(size: sizeParams) {
|
||||
heightCard.value = size.height
|
||||
await updateScrollAreaHeight()
|
||||
}
|
||||
|
||||
async function updateScrollAreaHeight() {
|
||||
await nextTick(() => {
|
||||
scrollAreaHeight.value = Math.max(0, heightCard.value)
|
||||
})
|
||||
}
|
||||
|
||||
watch(heightCard, updateScrollAreaHeight)
|
||||
watch(headerHeight, updateScrollAreaHeight)
|
||||
|
||||
const isResizing = ref(false)
|
||||
let resizeTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
watch(heightCard, () => {
|
||||
isResizing.value = true
|
||||
|
||||
if (resizeTimer) {
|
||||
clearTimeout(resizeTimer)
|
||||
resizeTimer = null
|
||||
}
|
||||
|
||||
resizeTimer = setTimeout(() => {
|
||||
isResizing.value = false
|
||||
resizeTimer = null
|
||||
}, 150)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#scroll-area div > .q-scrollarea__content {
|
||||
<style scoped>
|
||||
.glass-card {
|
||||
opacity: 1 !important;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#page-card {
|
||||
flex: 1 0 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#card-body {
|
||||
overflow: hidden;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#card-body :deep(.q-scrollarea__content) {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.q-scrollarea {
|
||||
position: relative;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.q-scrollarea::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 8px;
|
||||
background: linear-gradient(to bottom,
|
||||
rgba(0,0,0,0.12) 0%,
|
||||
rgba(0,0,0,0.08) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
will-change: opacity, transform;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.q-scrollarea::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 8px;
|
||||
background: linear-gradient(to top,
|
||||
rgba(0,0,0,0.12) 0%,
|
||||
rgba(0,0,0,0.08) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
will-change: opacity, transform;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.q-scrollarea.shadow-top::before {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.q-scrollarea.shadow-bottom::after {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
||||
107
src/components/pnShadowScroll.vue
Normal file
107
src/components/pnShadowScroll.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div :class="{'fix-scroll-area-content': hideShadows }">
|
||||
<q-scroll-area
|
||||
:style="{ height: height + 'px' }"
|
||||
class="w100 q-pa-none q-ma-none"
|
||||
@scroll="onScroll"
|
||||
:class=" {
|
||||
'shadow-top': hasScrolled,
|
||||
'shadow-bottom': hasScrolledBottom
|
||||
}"
|
||||
>
|
||||
<slot/>
|
||||
<div class="q-pa-sm"/>
|
||||
</q-scroll-area>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
height: number
|
||||
hideShadows: boolean
|
||||
}>()
|
||||
|
||||
const hasScrolled = ref(false)
|
||||
const hasScrolledBottom = ref(false)
|
||||
|
||||
interface ScrollInfo {
|
||||
verticalPosition: number;
|
||||
verticalPercentage: number;
|
||||
verticalSize: number;
|
||||
verticalContainerSize: number;
|
||||
}
|
||||
|
||||
function onScroll(info: ScrollInfo) {
|
||||
hasScrolled.value = info.verticalPosition > 0
|
||||
const scrollEnd = info.verticalPosition + info.verticalContainerSize >= info.verticalSize - 1
|
||||
hasScrolledBottom.value = !scrollEnd
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.q-scrollarea {
|
||||
position: relative;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.q-scrollarea::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(to bottom,
|
||||
rgba(0,0,0,0.12) 0%,
|
||||
rgba(0,0,0,0.08) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
will-change: opacity, transform;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.q-scrollarea::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(to top,
|
||||
rgba(0,0,0,0.12) 0%,
|
||||
rgba(0,0,0,0.08) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
will-change: opacity, transform;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.fix-scroll-area-content:deep(.q-scrollarea::before) {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.fix-scroll-area-content:deep(.q-scrollarea::after) {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.q-scrollarea.shadow-top::before {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.q-scrollarea.shadow-bottom::after {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
||||
87
src/components/pnSmallDialog.vue
Normal file
87
src/components/pnSmallDialog.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<q-dialog
|
||||
v-model="modelValue"
|
||||
>
|
||||
<q-card class="q-pa-none q-ma-none w100 no-scroll" align="center">
|
||||
<q-card-section>
|
||||
<q-avatar :color :icon size="60px" font-size="45px" text-color="white"/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section
|
||||
class="wrap no-scroll q-gutter-y-lg q-pt-none "
|
||||
style="overflow-wrap: break-word"
|
||||
>
|
||||
<div class="text-h6 text-bold ">
|
||||
{{ $t(title)}}
|
||||
</div>
|
||||
<div v-if="message1">
|
||||
{{ $t(message1)}}
|
||||
</div>
|
||||
<div v-if="message2">
|
||||
{{ $t(message2)}}
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="center" vertical>
|
||||
<div class="flex q-mt-lg no-wrap w100 justify-center q-gutter-x-md">
|
||||
<q-btn
|
||||
v-if="auxBtnLabel"
|
||||
:label="$t(auxBtnLabel)"
|
||||
outline
|
||||
color="grey"
|
||||
v-close-popup
|
||||
rounded
|
||||
class="w50"
|
||||
@click="emit('clickAuxBtn')"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
:label="$t(mainBtnLabel)"
|
||||
:color="color"
|
||||
v-close-popup
|
||||
rounded
|
||||
:class="auxBtnLabel ? 'w50' : 'w80'"
|
||||
@click="emit('clickMainBtn')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
class="w80 q-mt-md q-mb-sm" flat
|
||||
v-close-popup rounded
|
||||
@click="emit('close')"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<q-icon name="close"/>
|
||||
{{$t('cancel')}}
|
||||
</div>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
icon?: string
|
||||
color?: string
|
||||
title: string
|
||||
message1?: string
|
||||
message2?: string
|
||||
mainBtnLabel: string
|
||||
auxBtnLabel?: string
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<boolean>({
|
||||
required: true
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'clickMainBtn',
|
||||
'clickAuxBtn',
|
||||
'close'
|
||||
])
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
110
src/components/projectBlock.vue
Normal file
110
src/components/projectBlock.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<template #title>
|
||||
{{ $t(title) }}
|
||||
</template>
|
||||
<template #footer>
|
||||
<q-btn
|
||||
rounded color="primary"
|
||||
class="w100 q-mt-md q-mb-xs"
|
||||
:disable="!(isFormValid && (isDirty(initialProject, modelValue)))"
|
||||
@click = "emit('update')"
|
||||
>
|
||||
{{ $t(btnText) }}
|
||||
</q-btn>
|
||||
</template>
|
||||
|
||||
<pn-scroll-list>
|
||||
<div class="flex column items-center q-pa-md q-pb-sm">
|
||||
<pn-image-selector
|
||||
v-model="modelValue.logo"
|
||||
:size="100"
|
||||
:iconsize="80"
|
||||
class="q-pb-lg"
|
||||
/>
|
||||
<div class="q-gutter-y-lg w100">
|
||||
<q-input
|
||||
v-model="modelValue.name"
|
||||
no-error-icon
|
||||
dense
|
||||
filled
|
||||
label-slot
|
||||
class = "w100 fix-bottom-padding"
|
||||
:rules="[rules.name]"
|
||||
>
|
||||
<template #label>
|
||||
{{ $t('project_block__project_name') }} <span class="text-red">*</span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
v-model="modelValue.description"
|
||||
dense
|
||||
filled
|
||||
autogrow
|
||||
class="w100 q-pt-sm"
|
||||
:label="$t('project_block__project_description')"
|
||||
/>
|
||||
|
||||
<!-- <q-checkbox
|
||||
v-if="modelValue.logo"
|
||||
v-model="modelValue.is_logo_bg"
|
||||
class="w100"
|
||||
dense
|
||||
>
|
||||
{{ $t('project_block__image_use_as_background_chats') }}
|
||||
</q-checkbox> -->
|
||||
</div>
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { isDirty } from 'helpers/helpers'
|
||||
import type { ProjectParams } from 'types/Project'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const modelValue = defineModel<ProjectParams>({
|
||||
required: true
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
title: string,
|
||||
btnText: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update'])
|
||||
|
||||
const rulesErrorMessage = {
|
||||
name: t('project_block__error_name')
|
||||
}
|
||||
|
||||
const rules = {
|
||||
name: (val: ProjectParams['name']) => !!val?.trim() || rulesErrorMessage['name']
|
||||
}
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
const validations = {
|
||||
name: rules.name(modelValue.value.name) === true
|
||||
}
|
||||
|
||||
return Object.values(validations).every(Boolean)
|
||||
})
|
||||
|
||||
const initialProject = ref({} as ProjectParams)
|
||||
|
||||
onMounted(() => {
|
||||
initialProject.value = { ...modelValue.value }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fix-bottom-padding.q-field--with-bottom {
|
||||
padding-bottom: 0 !important
|
||||
}
|
||||
</style>
|
||||
@@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<div class="flex column items-center q-pa-lg">
|
||||
<pn-image-selector
|
||||
v-model="modelValue.logo"
|
||||
:size="100"
|
||||
:iconsize="80"
|
||||
class="q-pb-lg"
|
||||
/>
|
||||
<div class="q-gutter-y-lg w100">
|
||||
<q-input
|
||||
v-model="modelValue.name"
|
||||
no-error-icon
|
||||
dense
|
||||
filled
|
||||
label-slot
|
||||
class = "w100 fix-bottom-padding"
|
||||
:rules="[rules.name]"
|
||||
>
|
||||
<template #label>
|
||||
{{ $t('project_card__project_name') }} <span class="text-red">*</span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
v-model="modelValue.description"
|
||||
dense
|
||||
filled
|
||||
autogrow
|
||||
class="w100 q-pt-sm"
|
||||
:label="$t('project_card__project_description')"
|
||||
/>
|
||||
|
||||
<q-checkbox
|
||||
v-if="modelValue.logo"
|
||||
v-model="modelValue.is_logo_bg"
|
||||
class="w100"
|
||||
dense
|
||||
>
|
||||
{{ $t('project_card__image_use_as_background_chats') }}
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, computed } from 'vue'
|
||||
import type { ProjectParams } from 'types/Project'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t }= useI18n()
|
||||
|
||||
const modelValue = defineModel<ProjectParams>({ required: true })
|
||||
const emit = defineEmits(['valid'])
|
||||
const rulesErrorMessage = {
|
||||
name: t('project_card__error_name')
|
||||
}
|
||||
|
||||
const rules = {
|
||||
name: (val :ProjectParams['name']) => !!val?.trim() || rulesErrorMessage['name']
|
||||
}
|
||||
|
||||
const isValid = computed(() => {
|
||||
const checkName = rules.name(modelValue.value.name)
|
||||
return { name: checkName && (checkName !== rulesErrorMessage['name']) }
|
||||
})
|
||||
|
||||
watch(isValid, (newVal) => {
|
||||
const allValid = Object.values(newVal).every(v => v)
|
||||
emit('valid', allValid)
|
||||
}, { immediate: true })
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.q-field--with-bottom.fix-bottom-padding {
|
||||
padding-bottom: 0 !important
|
||||
}
|
||||
</style>
|
||||
180
src/components/userBlock.vue
Normal file
180
src/components/userBlock.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<template #title>
|
||||
{{ $t(title) }}
|
||||
</template>
|
||||
<template #footer>
|
||||
<q-btn
|
||||
rounded color="primary"
|
||||
class="w100 q-mt-md q-mb-xs"
|
||||
:disable="!(isDirty(initialUser, modelValue))"
|
||||
@click = "emit('update')"
|
||||
>
|
||||
{{ $t(btnText) }}
|
||||
</q-btn>
|
||||
</template>
|
||||
|
||||
<pn-scroll-list>
|
||||
<div class="flex column items-center q-pa-md q-pb-sm">
|
||||
<div class="relative-position">
|
||||
<pn-auto-avatar
|
||||
:img="modelValue.photo"
|
||||
:name="tname"
|
||||
size="100px"
|
||||
class="q-pb-lg"
|
||||
:style="!userStatus ? {} : { filter: 'grayscale(100%)'}"
|
||||
/>
|
||||
<div
|
||||
v-if="userStatus"
|
||||
class="absolute-center text-h4 text-bold q-pa-sm"
|
||||
:class ="'status-' + userStatus.status"
|
||||
>
|
||||
{{ $t(userStatus.text) }}
|
||||
</div>
|
||||
</div>
|
||||
<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" align="center">{{ tname }}</div>
|
||||
<div caption class="text-blue text-caption" align="center" v-if="modelValue.username">{{ modelValue.username }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-gutter-y-lg w100">
|
||||
<q-input
|
||||
v-model.trim="modelValue.fullname"
|
||||
dense
|
||||
filled
|
||||
class = "w100"
|
||||
:label = "$t('user_block__name')"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-model="modelValue.company_id"
|
||||
:options="displayCompanies"
|
||||
dense
|
||||
filled
|
||||
class="w100 q-pt-sm"
|
||||
:label = "$t('user_block__company')"
|
||||
option-value="id"
|
||||
emit-value
|
||||
map-options
|
||||
>
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section avatar>
|
||||
<pn-auto-avatar
|
||||
v-if="scope.opt.id"
|
||||
:img="scope.opt.logo"
|
||||
:name="scope.opt.label"
|
||||
size="md"
|
||||
type="rounded"
|
||||
/>
|
||||
<q-avatar
|
||||
v-else
|
||||
rounded
|
||||
size="md"
|
||||
>
|
||||
<q-icon size="32px" color="grey" name="mdi-cancel"/>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ scope.opt.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-input
|
||||
v-model.trim="modelValue.department"
|
||||
dense
|
||||
filled
|
||||
class = "w100 q-pt-sm"
|
||||
:label = "$t('user_block__department')"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-model.trim="modelValue.role"
|
||||
dense
|
||||
filled
|
||||
class = "w100 q-pt-sm"
|
||||
:label = "$t('user_block__role')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, computed, ref } from 'vue'
|
||||
import { useCompaniesStore } from 'stores/companies'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { isDirty } from 'helpers/helpers'
|
||||
import type { User } from 'types/Users'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const modelValue = defineModel<User>({
|
||||
required: true
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
title: string,
|
||||
btnText: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update'])
|
||||
|
||||
const initialUser = ref({} as User)
|
||||
|
||||
onMounted(() => {
|
||||
initialUser.value = { ...modelValue.value }
|
||||
})
|
||||
|
||||
const companiesStore = useCompaniesStore()
|
||||
const companies = computed(() => companiesStore.companies)
|
||||
|
||||
const displayCompanies = computed(() => [
|
||||
...companies.value.map(el => ({
|
||||
id: el.id,
|
||||
label: el.name,
|
||||
logo: el.logo
|
||||
})),
|
||||
{
|
||||
id: null,
|
||||
label: t('user_block__no_company'),
|
||||
logo: ''
|
||||
}
|
||||
])
|
||||
|
||||
const tname = computed(() =>
|
||||
[modelValue.value?.firstname, modelValue.value?.lastname]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
)
|
||||
|
||||
const userStatus = computed(() => {
|
||||
if (modelValue.value.is_blocked) return { status: 'blocked', text: 'user_block__user_blocked'}
|
||||
if (modelValue.value.is_leave) return { status: 'leave', text: 'user_block__user_leave'}
|
||||
return null
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fix-bottom-padding.q-field--with-bottom {
|
||||
padding-bottom: 0 !important
|
||||
}
|
||||
|
||||
.status-blocked {
|
||||
border: 2px solid red;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.status-leave {
|
||||
border: 2px solid var(--q-primary);
|
||||
color: var(--q-primary);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user