before liquid glass
This commit is contained in:
@@ -2,86 +2,19 @@
|
||||
<div
|
||||
class="flex row items-center no-wrap logo-component"
|
||||
>
|
||||
<svg
|
||||
class="iconcolor"
|
||||
viewBox="0 0 8.4666662 8.4666662"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs id="defs1" />
|
||||
<g id="layer1">
|
||||
<rect
|
||||
class="fill-brand"
|
||||
style="stroke-width:0.233149"
|
||||
id="rect5"
|
||||
width="6.9885192"
|
||||
height="0.35581663"
|
||||
x="3.114475"
|
||||
y="0.86827624"
|
||||
transform="matrix(0.77578367,0.63099897,-0.77578367,0.63099897,0,0)"
|
||||
/>
|
||||
<rect
|
||||
class="fill-brand"
|
||||
style="stroke-width:0.24961"
|
||||
id="rect5-7"
|
||||
width="7.4819207"
|
||||
height="0.3809379"
|
||||
x="-3.9267058"
|
||||
y="5.7988153"
|
||||
transform="matrix(-0.70756824,0.70664502,0.70756824,0.70664502,0,0)"
|
||||
/>
|
||||
<circle
|
||||
class="fill-brand"
|
||||
style="stroke-width:0.134869"
|
||||
id="path5-8"
|
||||
cx="1.5875"
|
||||
cy="6.8791666"
|
||||
r="1.0583333"
|
||||
/>
|
||||
<circle
|
||||
class="fill-brand"
|
||||
style="stroke-width:0.168586"
|
||||
id="path5-8-5"
|
||||
cx="7.1437502"
|
||||
cy="7.1437502"
|
||||
r="1.3229166"
|
||||
/>
|
||||
<circle
|
||||
class="fill-brand"
|
||||
style="stroke-width:0.118011"
|
||||
id="path5-8-5-1"
|
||||
cx="1.4552083"
|
||||
cy="2.5135417"
|
||||
r="0.92604166"
|
||||
/>
|
||||
<circle
|
||||
class="fill-brand"
|
||||
style="stroke-width:0.101152"
|
||||
id="path5-8-5-1-7"
|
||||
cx="7.1437502"
|
||||
cy="1.3229166"
|
||||
r="0.79374999"
|
||||
/>
|
||||
<circle
|
||||
style="stroke-width:0.23602"
|
||||
id="path5"
|
||||
cx="3.96875"
|
||||
cy="4.4979167"
|
||||
r="1.8520833"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<img
|
||||
src="src/assets/logo.svg"
|
||||
style="height: 24px; max-width: 24px; padding-right: 2px;"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="text-brand"
|
||||
style="margin-right: 0.075em;"
|
||||
style="margin-right: 0.05em;"
|
||||
>
|
||||
tg
|
||||
</span>
|
||||
|
||||
<span class="text-brand2 text-bold">
|
||||
<span class="text-brand text-bold">
|
||||
Crew
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
<template>
|
||||
<svg
|
||||
class="q-ma-none q-pa-none"
|
||||
:style="{
|
||||
fill: color ?? 'red',
|
||||
<svg
|
||||
class="q-ma-none q-pa-none"
|
||||
:style="{
|
||||
fill: color,
|
||||
height: size ?? '42px',
|
||||
width: 'auto'
|
||||
}"
|
||||
width="14" height="15" viewBox="0 0 14 15" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M6.63869 12.1902L3.50621 14.1092C3.18049 14.3087 2.75468 14.2064 2.55515 13.8807C2.45769 13.7216 2.42864 13.5299 2.47457 13.3491L2.95948 11.4405C3.13452 10.7515 3.60599 10.1756 4.24682 9.86791L7.6642 8.22716C7.82352 8.15067 7.89067 7.95951 7.81418 7.80019C7.75223 7.67116 7.61214 7.59896 7.47111 7.62338L3.66713 8.28194C2.89387 8.41581 2.1009 8.20228 1.49941 7.69823L0.297703 6.69116C0.00493565 6.44581 -0.0335059 6.00958 0.211842 5.71682C0.33117 5.57442 0.502766 5.48602 0.687982 5.47153L4.35956 5.18419C4.61895 5.16389 4.845 4.99974 4.94458 4.75937L6.36101 1.3402C6.5072 0.987302 6.91179 0.819734 7.26469 0.965925C7.43413 1.03612 7.56876 1.17075 7.63896 1.3402L9.05539 4.75937C9.15496 4.99974 9.38101 5.16389 9.6404 5.18419L13.3322 5.47311C13.713 5.50291 13.9975 5.83578 13.9677 6.2166C13.9534 6.39979 13.8667 6.56975 13.7269 6.68896L10.9114 9.08928C10.7131 9.25826 10.6267 9.52425 10.6876 9.77748L11.5532 13.3733C11.6426 13.7447 11.414 14.1182 11.0427 14.2076C10.8642 14.2506 10.676 14.2208 10.5195 14.1249L7.36128 12.1902C7.13956 12.0544 6.8604 12.0544 6.63869 12.1902Z"></path></svg>
|
||||
}"
|
||||
width="14"
|
||||
height="15"
|
||||
viewBox="0 0 14 15"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.63869 12.1902L3.50621 14.1092C3.18049 14.3087 2.75468 14.2064 2.55515 13.8807C2.45769 13.7216 2.42864 13.5299 2.47457 13.3491L2.95948 11.4405C3.13452 10.7515 3.60599 10.1756 4.24682 9.86791L7.6642 8.22716C7.82352 8.15067 7.89067 7.95951 7.81418 7.80019C7.75223 7.67116 7.61214 7.59896 7.47111 7.62338L3.66713 8.28194C2.89387 8.41581 2.1009 8.20228 1.49941 7.69823L0.297703 6.69116C0.00493565 6.44581 -0.0335059 6.00958 0.211842 5.71682C0.33117 5.57442 0.502766 5.48602 0.687982 5.47153L4.35956 5.18419C4.61895 5.16389 4.845 4.99974 4.94458 4.75937L6.36101 1.3402C6.5072 0.987302 6.91179 0.819734 7.26469 0.965925C7.43413 1.03612 7.56876 1.17075 7.63896 1.3402L9.05539 4.75937C9.15496 4.99974 9.38101 5.16389 9.6404 5.18419L13.3322 5.47311C13.713 5.50291 13.9975 5.83578 13.9677 6.2166C13.9534 6.39979 13.8667 6.56975 13.7269 6.68896L10.9114 9.08928C10.7131 9.25826 10.6267 9.52425 10.6876 9.77748L11.5532 13.3733C11.6426 13.7447 11.414 14.1182 11.0427 14.2076C10.8642 14.2506 10.676 14.2208 10.5195 14.1249L7.36128 12.1902C7.13956 12.0544 6.8604 12.0544 6.63869 12.1902Z">
|
||||
</path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
color: String,
|
||||
size: String,
|
||||
})
|
||||
|
||||
withDefaults(defineProps<{
|
||||
color: string
|
||||
size: string // for example '24px'
|
||||
}>(),
|
||||
{
|
||||
color: 'gold'
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.telegram-star-wrapper :deep(.telegram-star svg) {
|
||||
fill: red;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -72,10 +72,36 @@
|
||||
</q-step>
|
||||
|
||||
<q-step
|
||||
v-if="type === 'register'"
|
||||
:name="3"
|
||||
:title="$t('account_helper__agreement')"
|
||||
:done="step>3"
|
||||
>
|
||||
<agreement-block v-model="agreement"/>
|
||||
<q-stepper-navigation>
|
||||
<q-btn
|
||||
@click="handleSubmit"
|
||||
color="primary"
|
||||
:label="$t('account_helper__agreement_btn')"
|
||||
:disabled="agreement.length !== 2"
|
||||
:unelevated="agreement.length !== 2"
|
||||
class="fix-disabled-btn"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
@click="step=2"
|
||||
color="primary"
|
||||
:label="$t('back')"
|
||||
class="q-ml-sm"
|
||||
/>
|
||||
</q-stepper-navigation>
|
||||
</q-step>
|
||||
|
||||
<q-step
|
||||
:name="4"
|
||||
:title="$t('account_helper__set_password')"
|
||||
>
|
||||
<q-input
|
||||
<q-input
|
||||
v-model="password"
|
||||
dense
|
||||
filled
|
||||
@@ -90,15 +116,15 @@
|
||||
@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>
|
||||
<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
|
||||
@@ -111,7 +137,7 @@
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
@click="step=2"
|
||||
@click="step= type === 'register' ? 3 : 2"
|
||||
color="primary"
|
||||
:label="$t('back')"
|
||||
class="q-ml-sm"
|
||||
@@ -123,18 +149,21 @@
|
||||
<pn-magic-overlay
|
||||
v-if="showSuccessOverlay"
|
||||
icon="mdi-check-circle-outline"
|
||||
:message1="getHelperMessage1()"
|
||||
:message2="getHelperMessage2()"
|
||||
route-name="projects"
|
||||
:message1="getHelperMessage.m1"
|
||||
:message2="getHelperMessage.m2"
|
||||
:route-name="getRoute"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import type { AxiosError } from 'axios'
|
||||
import { useI18n } from "vue-i18n"
|
||||
import { QInput } from 'quasar'
|
||||
import { useAuthStore, type AuthFlowType } from 'stores/auth'
|
||||
import agreementBlock from 'components/agreementBlock.vue'
|
||||
import { useInputRules } from 'composables/useInputRules'
|
||||
|
||||
const { inputRules } = useInputRules()
|
||||
|
||||
const flowType = computed<AuthFlowType>(() => {
|
||||
return props.type === 'register'
|
||||
@@ -146,7 +175,6 @@
|
||||
: 'changeMethod'
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -154,8 +182,7 @@
|
||||
email?: string
|
||||
}>()
|
||||
|
||||
type ValidationRule = (val: string) => boolean | string
|
||||
type Step = 1 | 2 | 3
|
||||
type Step = 1 | 2 | 3 | 4
|
||||
|
||||
const step = ref<Step>(1)
|
||||
const login = ref<string>(props.email || '')
|
||||
@@ -164,10 +191,16 @@
|
||||
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]
|
||||
email: [inputRules.email],
|
||||
password: [inputRules.password]
|
||||
}
|
||||
|
||||
const getRoute = computed(() => {
|
||||
return (props.type === 'changeMethod' || props.type === 'changePwd')
|
||||
? 'account'
|
||||
: 'projects'
|
||||
})
|
||||
|
||||
const isEmailValid = computed(() =>
|
||||
validationRules.email.every(f => f(login.value) === true)
|
||||
)
|
||||
@@ -177,30 +210,36 @@
|
||||
)
|
||||
|
||||
const passwordHint = computed(() => {
|
||||
const result = validationRules.password[0](password.value)
|
||||
const result = validationRules.password[0] ? validationRules.password[0](password.value) : null
|
||||
return typeof result === 'string' ? result : ''
|
||||
})
|
||||
|
||||
const stepActions: Record<Step, () => Promise<void>> = {
|
||||
const stepActions: Record<Step, () => Promise<void> | void> = {
|
||||
1: async () => {
|
||||
await authStore.initRegistration(flowType.value, login.value)
|
||||
},
|
||||
2: async () => {
|
||||
await authStore.confirmCode(flowType.value, login.value, code.value)
|
||||
},
|
||||
3: async () => {
|
||||
3: () => {},
|
||||
4: async () => {
|
||||
await authStore.setPassword(flowType.value, login.value, code.value, password.value)
|
||||
if (flowType.value === 'register' || flowType.value === 'changeMethod') {
|
||||
await authStore.loginWithCredentials(login.value, password.value)
|
||||
}
|
||||
if (flowType.value === 'register') {
|
||||
acceptSubmit.value = true
|
||||
sessionStorage.removeItem('account-helper')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await stepActions[step.value]()
|
||||
if (step.value < 3) {
|
||||
if (step.value < 4) {
|
||||
step.value++
|
||||
if (step.value === 3 && props.type !== 'register') step.value++
|
||||
} else {
|
||||
showSuccessOverlay.value = true
|
||||
}
|
||||
@@ -209,28 +248,46 @@
|
||||
}
|
||||
}
|
||||
|
||||
const getHelperMessage1 = () => {
|
||||
const getHelperMessage = computed(() => {
|
||||
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 ''
|
||||
case 'register': return { m1: 'account_helper__register_message1', m2: 'slogan' }
|
||||
case 'forgot': return { m1: 'account_helper__forgot_password_message1', m2: 'account_helper__go_projects' }
|
||||
case 'changePwd': return { m1: 'account_helper__change_password_message1', m2: '' }
|
||||
case 'changeMethod': return { m1:'account_helper__change_method_message1', m2: '' }
|
||||
default: return { m1: '', m2: '' }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const getHelperMessage2 = () => {
|
||||
switch (flowType.value) {
|
||||
case 'register': return 'slogan'
|
||||
case 'forgot':
|
||||
case 'changePwd':
|
||||
case 'changeMethod':
|
||||
return 'account_helper__go_projects'
|
||||
default: return ''
|
||||
const agreement = ref<('terms'| 'consent')[]>([])
|
||||
const acceptSubmit = ref(false)
|
||||
onMounted(() => {
|
||||
if (props.type !== 'register') return
|
||||
|
||||
const ah = sessionStorage.getItem('account-helper')
|
||||
if (!ah) return
|
||||
|
||||
const ahVal = JSON.parse(ah)
|
||||
login.value = ahVal.login
|
||||
code.value = ahVal.code
|
||||
agreement.value = ahVal.agreement
|
||||
step.value = ahVal.step
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (props.type === 'register' && step.value >= 3 && !acceptSubmit.value) {
|
||||
sessionStorage.setItem(
|
||||
'account-helper',
|
||||
JSON.stringify({
|
||||
login: login.value,
|
||||
code: code.value,
|
||||
agreement: agreement.value,
|
||||
step: step.value
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
</style>
|
||||
|
||||
45
src/components/agreementBlock.vue
Normal file
45
src/components/agreementBlock.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="column w100 text-caption">
|
||||
<div class="flex items-center no-wrap">
|
||||
<q-checkbox v-model="modelValue" val="terms" class="q-pr-sm"/>
|
||||
<span>
|
||||
{{ $t('agreements__checkbox_agreement_terms') + ' ' }}
|
||||
<span
|
||||
@click="router.push({ name: 'terms' })"
|
||||
class="cursor-pointer text-primary"
|
||||
>
|
||||
{{ $t('agreements__checkbox_agreement_terms_doc') }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center no-wrap">
|
||||
<q-checkbox v-model="modelValue" val="consent" class="q-pr-sm"/>
|
||||
<span>
|
||||
{{ $t('agreements__checkbox_agreement_consent') + ' ' }}
|
||||
<span
|
||||
@click="router.push({ name: 'consent' })"
|
||||
class="cursor-pointer text-primary"
|
||||
>
|
||||
{{ $t('agreements__checkbox_agreement_consent_doc') }}
|
||||
</span>
|
||||
{{ ' ' + $t('agreements__checkbox_agreement_privacy') + ' ' }}
|
||||
<span
|
||||
@click="router.push({ name: 'privacy' })"
|
||||
class="cursor-pointer text-primary"
|
||||
>
|
||||
{{ $t('agreements__checkbox_agreement_privacy_doc') }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const modelValue = defineModel<('terms'| 'consent')[]>({ default: () => [] })
|
||||
|
||||
const emit = defineEmits(['agree'])
|
||||
const router = useRouter()
|
||||
|
||||
</script>
|
||||
@@ -13,34 +13,39 @@
|
||||
{{ $t(btnText) }}
|
||||
</q-btn>
|
||||
</template>
|
||||
|
||||
<pn-scroll-list>
|
||||
<div class="flex column items-center q-pa-md q-pb-sm">
|
||||
<div class="flex column items-center q-pa-md q-pb-sm w100">
|
||||
<slot name="myCompany"/>
|
||||
|
||||
<pn-image-selector
|
||||
v-model="companyMod.logo"
|
||||
:size="100"
|
||||
:iconsize="80"
|
||||
class="q-pb-lg"
|
||||
/>
|
||||
<div class="q-gutter-y-lg w100">
|
||||
|
||||
<q-form
|
||||
ref="formRef"
|
||||
@validation-success="isFormValid = true"
|
||||
@validation-error="isFormValid = false"
|
||||
class="q-gutter-y-lg w100"
|
||||
>
|
||||
|
||||
<q-input
|
||||
v-for="input in textInputs"
|
||||
:key="input.id"
|
||||
v-model.trim="companyMod[input.val]"
|
||||
:model-value="companyMod[input.val]"
|
||||
@update:model-value="handleInput(input.val, $event, input.useTrim)"
|
||||
dense
|
||||
filled
|
||||
class="w100"
|
||||
class="w100 fix-bottom-padding"
|
||||
:class="input.val === 'name' ? '' : 'q-pt-sm'"
|
||||
: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.rules"
|
||||
no-error-icon
|
||||
:label-slot="Boolean(input.label)"
|
||||
debounce="300"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon v-if="input.icon" :name="input.icon"/>
|
||||
@@ -50,19 +55,19 @@
|
||||
<span v-if="input.val === 'name'" class="text-red">*</span>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { CompanyParams } from 'types/Company'
|
||||
import { ref, reactive, onMounted, } from 'vue'
|
||||
import { removeNullKeys, getFieldValue } from 'utils/helpers'
|
||||
import { useInputRules } from 'composables/useInputRules'
|
||||
import type { CompanyParams } from 'types/Company'
|
||||
|
||||
const { t }= useI18n()
|
||||
const { inputRules } = useInputRules()
|
||||
|
||||
const props = defineProps<{
|
||||
title: string,
|
||||
@@ -92,32 +97,25 @@
|
||||
icon?: string
|
||||
val: keyof CompanyParams
|
||||
rules: ((value: string) => boolean | string)[]
|
||||
}
|
||||
|
||||
const rules = {
|
||||
name: (val: string) => !!val?.trim() || rulesErrorMessage['name']
|
||||
useTrim: boolean
|
||||
}
|
||||
|
||||
const textInputs: TextInput[] = [
|
||||
{ id: 1, val: 'name', label: 'company_block__name', rules: [rules.name] },
|
||||
{ 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: [] }
|
||||
{ id: 1, val: 'name', label: 'company_block__name', rules: [inputRules.require, inputRules.middleText], useTrim: true },
|
||||
{ id: 2, val: 'description', label: 'company_block__description', rules: [inputRules.longText], useTrim: false },
|
||||
{ id: 3, val: 'site', icon: 'mdi-web', rules: [inputRules.middleText], useTrim: true },
|
||||
{ id: 4, val: 'address', icon: 'mdi-map-marker-outline', rules: [inputRules.longText], useTrim: false },
|
||||
{ id: 5, val: 'phone', icon: 'mdi-phone-outline', rules: [inputRules.middleText], useTrim: true },
|
||||
{ id: 6, val: 'email', icon: 'mdi-email-outline', rules: [inputRules.middleText], useTrim: true }
|
||||
] as const
|
||||
|
||||
const rulesErrorMessage = {
|
||||
name: t('company_block__error_name')
|
||||
const handleInput = (field: keyof CompType, value: string | null | number, useTrim: boolean) => {
|
||||
companyMod[field] = typeof value === 'string' ? (useTrim ? value.trim() : value) : value
|
||||
if (formRef.value) formRef.value.validate()
|
||||
}
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
const validations = {
|
||||
name: companyMod.name && rules.name(companyMod.name) === true
|
||||
}
|
||||
|
||||
return Object.values(validations).every(Boolean)
|
||||
})
|
||||
const formRef = ref()
|
||||
const isFormValid = ref(false)
|
||||
|
||||
function onSubmit() {
|
||||
// companyMod.description! - ! on end for TS, i promise: params !== null | undefined
|
||||
@@ -128,11 +126,16 @@
|
||||
address: getFieldValue(companyMod.address!, props.company?.address),
|
||||
phone: getFieldValue(companyMod.phone!, props.company?.phone),
|
||||
email: getFieldValue(companyMod.email!, props.company?.email),
|
||||
logo: companyMod.logo
|
||||
logo: companyMod.logo !== '' ? companyMod.logo : null
|
||||
}
|
||||
emit('update', removeNullKeys(outData))
|
||||
emit('update', removeNullKeys(outData, ['logo']))
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await formRef.value.validate()
|
||||
if (!isFormValid.value) formRef.value.resetValidation()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div id="background-canvas-wrapper" class="flex fit column">
|
||||
<div id="background-canvas-wrapper" class="flex fit column bg-base">
|
||||
<canvas id="canvas"/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -8,8 +8,6 @@
|
||||
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
const canvasBody = document.getElementById("canvas")
|
||||
const drawArea = canvasBody.getContext("2d")
|
||||
@@ -39,9 +37,7 @@ onMounted(() => {
|
||||
|
||||
function deBouncer () {
|
||||
clearTimeout(tid)
|
||||
tid = setTimeout(function() {
|
||||
resizeReset()
|
||||
}, delay)
|
||||
tid = setTimeout(function () { resizeReset() }, delay)
|
||||
}
|
||||
|
||||
function checkDistance (x1, y1, x2, y2) {
|
||||
@@ -49,83 +45,81 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
function setup () {
|
||||
|
||||
resizeReset()
|
||||
for (let i = 0; i < opts.particleAmount; i++){
|
||||
for (let i = 0; i < opts.particleAmount; i++) {
|
||||
particles.push(new Particle())
|
||||
}
|
||||
window.requestAnimationFrame(loop)
|
||||
}
|
||||
|
||||
function loop() {
|
||||
window.requestAnimationFrame(loop)
|
||||
drawArea.clearRect(0, 0, w, h)
|
||||
for (let i = 0; i < particles.length; i++){
|
||||
particles[i].update()
|
||||
particles[i].draw()
|
||||
function loop() {
|
||||
window.requestAnimationFrame(loop)
|
||||
drawArea.clearRect(0, 0, w, h)
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
particles[i].update()
|
||||
particles[i].draw()
|
||||
}
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
linkPoints(particles[i], particles)
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < particles.length; i++){
|
||||
linkPoints(particles[i], particles)
|
||||
|
||||
function linkPoints (point1, hubs) {
|
||||
for (let i = 0; i < hubs.length; i++) {
|
||||
const distance = checkDistance(point1.x, point1.y, hubs[i].x, hubs[i].y)
|
||||
const opacity = 1 - distance / opts.linkRadius
|
||||
if (opacity > 0) {
|
||||
drawArea.lineWidth = 0.5
|
||||
drawArea.strokeStyle = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${opacity})`
|
||||
drawArea.beginPath()
|
||||
drawArea.moveTo(point1.x, point1.y)
|
||||
drawArea.lineTo(hubs[i].x, hubs[i].y)
|
||||
drawArea.closePath()
|
||||
drawArea.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function linkPoints (point1, hubs){
|
||||
for (let i = 0; i < hubs.length; i++) {
|
||||
const distance = checkDistance(point1.x, point1.y, hubs[i].x, hubs[i].y)
|
||||
const opacity = 1 - distance / opts.linkRadius
|
||||
if (opacity > 0) {
|
||||
drawArea.lineWidth = 0.5
|
||||
drawArea.strokeStyle = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${opacity})`
|
||||
function Particle () {
|
||||
this.x = Math.random() * w
|
||||
this.y = Math.random() * h
|
||||
this.speed = opts.defaultSpeed + Math.random() * opts.variantSpeed
|
||||
this.directionAngle = Math.floor(Math.random() * 360)
|
||||
this.color = opts.particleColor
|
||||
this.radius = opts.defaultRadius + Math.random() * opts. variantRadius
|
||||
this.vector = {
|
||||
x: Math.cos(this.directionAngle) * this.speed,
|
||||
y: Math.sin(this.directionAngle) * this.speed
|
||||
}
|
||||
this.update = function () {
|
||||
this.border()
|
||||
this.x += this.vector.x
|
||||
this.y += this.vector.y
|
||||
}
|
||||
this.border = function () {
|
||||
if (this.x >= w || this.x <= 0) {
|
||||
this.vector.x *= -1
|
||||
}
|
||||
if (this.y >= h || this.y <= 0) {
|
||||
this.vector.y *= -1
|
||||
}
|
||||
if (this.x > w) this.x = w
|
||||
if (this.y > h) this.y = h
|
||||
if (this.x < 0) this.x = 0
|
||||
if (this.y < 0) this.y = 0
|
||||
}
|
||||
this.draw = function () {
|
||||
drawArea.beginPath()
|
||||
drawArea.moveTo(point1.x, point1.y)
|
||||
drawArea.lineTo(hubs[i].x, hubs[i].y)
|
||||
drawArea.arc(this.x, this.y, this.radius, 0, Math.PI*2)
|
||||
drawArea.closePath()
|
||||
drawArea.stroke()
|
||||
drawArea.fillStyle = this.color
|
||||
drawArea.fill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Particle () {
|
||||
this.x = Math.random() * w
|
||||
this.y = Math.random() * h
|
||||
this.speed = opts.defaultSpeed + Math.random() * opts.variantSpeed
|
||||
this.directionAngle = Math.floor(Math.random() * 360)
|
||||
this.color = opts.particleColor
|
||||
this.radius = opts.defaultRadius + Math.random() * opts. variantRadius
|
||||
this.vector = {
|
||||
x: Math.cos(this.directionAngle) * this.speed,
|
||||
y: Math.sin(this.directionAngle) * this.speed
|
||||
};
|
||||
this.update = function(){
|
||||
this.border();
|
||||
this.x += this.vector.x
|
||||
this.y += this.vector.y
|
||||
};
|
||||
this.border = function(){
|
||||
if (this.x >= w || this.x <= 0) {
|
||||
this.vector.x *= -1;
|
||||
}
|
||||
if (this.y >= h || this.y <= 0) {
|
||||
this.vector.y *= -1;
|
||||
}
|
||||
if (this.x > w) this.x = w
|
||||
if (this.y > h) this.y = h
|
||||
if (this.x < 0) this.x = 0
|
||||
if (this.y < 0) this.y = 0
|
||||
}
|
||||
this.draw = function(){
|
||||
drawArea.beginPath()
|
||||
drawArea.arc(this.x, this.y, this.radius, 0, Math.PI*2)
|
||||
drawArea.closePath()
|
||||
drawArea.fillStyle = this.color
|
||||
drawArea.fill()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", function(){ deBouncer() })
|
||||
resizeReset()
|
||||
setup()
|
||||
window.addEventListener("resize", function () { deBouncer() })
|
||||
resizeReset()
|
||||
setup()
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -137,7 +131,6 @@ setup()
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
background: var(--q-primary);
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex row q-mx-sm justify-between items-center no-wrap q-py-sm"
|
||||
class="flex row q-mx-sm q-my-md justify-between items-center no-wrap"
|
||||
>
|
||||
<q-btn
|
||||
v-if="showCalendarBtn"
|
||||
@@ -29,7 +29,7 @@
|
||||
filled
|
||||
:placeholder="$t(placeholder)"
|
||||
dense
|
||||
class="col-grow q-pt-xs q-mx-sm"
|
||||
class="col-grow q-mx-sm"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" color="grey"/>
|
||||
|
||||
@@ -53,40 +53,3 @@
|
||||
|
||||
<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> -->
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
style="
|
||||
border-top-left-radius: var(--top-radius) !important;
|
||||
border-top-right-radius: var(--top-radius) !important;
|
||||
max-height: calc(100vh - 48px);
|
||||
"
|
||||
>
|
||||
<div
|
||||
@@ -22,7 +23,7 @@
|
||||
<slot name="btnSlot">
|
||||
<q-btn
|
||||
icon="mdi-close"
|
||||
@click="modelValue=false"
|
||||
@click="modelValue = false"
|
||||
flat round
|
||||
/>
|
||||
</slot>
|
||||
@@ -36,17 +37,20 @@
|
||||
:hideShadows="false"
|
||||
:height="bodyHeight"
|
||||
>
|
||||
<div ref="cardBodyInnerRef" class="q-px-md q-ma-none">
|
||||
<div ref="cardBodyInnerRef" class="q-pa-none q-ma-none">
|
||||
<q-resize-observer @resize="updateDimensions" />
|
||||
<slot/>
|
||||
</div>
|
||||
</pn-shadow-scroll>
|
||||
</div>
|
||||
<div
|
||||
ref="cardFooterRef"
|
||||
class="q-pa-md"
|
||||
style="overflow: hidden"
|
||||
:style="{ height: footerHeight + 'px'}"
|
||||
>
|
||||
<slot name="footer"/>
|
||||
<div ref="cardFooterInnerRef" class="q-pa-md">
|
||||
<q-resize-observer @resize="updateDimensions" />
|
||||
<slot name="footer"/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
@@ -54,7 +58,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { useSlots } from 'vue'
|
||||
|
||||
const modelValue = defineModel<boolean>({
|
||||
@@ -67,8 +71,7 @@
|
||||
|
||||
const slots = useSlots()
|
||||
const cardHeaderRef = ref<HTMLElement | null>(null)
|
||||
const cardFooterRef = ref<HTMLElement | null>(null)
|
||||
const cardBodyRef = ref<HTMLElement | null>(null)
|
||||
const cardFooterInnerRef = ref<HTMLElement | null>(null)
|
||||
const cardBodyInnerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const headerHeight = ref(0)
|
||||
@@ -76,17 +79,20 @@
|
||||
const bodyInnerHeight = ref(0)
|
||||
const bodyHeight = ref(0)
|
||||
|
||||
const updateDimensions = () => {
|
||||
const updateDimensions = async () => {
|
||||
await nextTick(() => {
|
||||
headerHeight.value = cardHeaderRef.value?.offsetHeight || 0
|
||||
footerHeight.value = cardFooterRef.value?.offsetHeight || 0
|
||||
footerHeight.value = cardFooterInnerRef.value?.offsetHeight || 0
|
||||
bodyInnerHeight.value = cardBodyInnerRef.value?.offsetHeight || 0
|
||||
const needScroll = headerHeight.value + footerHeight.value + bodyInnerHeight.value > window.innerHeight - 48
|
||||
bodyHeight.value = needScroll
|
||||
? window.innerHeight - 48 - headerHeight.value - footerHeight.value
|
||||
: bodyInnerHeight.value + 16
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => slots.body?.(), updateDimensions, { flush: 'post' })
|
||||
watch(modelValue, updateDimensions)
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,45 +1,47 @@
|
||||
<template>
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-avatar
|
||||
rounded
|
||||
text-color="white"
|
||||
:icon="chatType.icon"
|
||||
:color="chatType.color"
|
||||
size="md"
|
||||
/>
|
||||
<q-icon :name="chat.icon" :color="chat.color" size="md"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<div class="row no-wrap items-center justify-between">
|
||||
{{ $t(chatType.label) }}
|
||||
<span class="text-caption text-grey" v-if="time">
|
||||
{{ date.formatDate(time * 1000, 'DD MMM YYYY') }}
|
||||
{{ $t(chat.label) }}
|
||||
<span class="text-caption text-grey text-no-wrap" v-if="time">
|
||||
{{ getDate(time) }}
|
||||
</span>
|
||||
</div>
|
||||
</q-item-label>
|
||||
<q-item-label class="text-grey text-caption">
|
||||
{{ $t(chatType.description) }}
|
||||
{{ $t(chat.description) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { getChatType } from 'utils/chats'
|
||||
import { chatAccess, chatState } from 'utils/chats'
|
||||
import { date } from 'quasar'
|
||||
import type { ChatType } from 'types/Chat'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
type: ChatType
|
||||
const props = defineProps<{
|
||||
access?: 0 | 1 | 2
|
||||
state?: 1 | 2 | 3 | 4
|
||||
time?: number
|
||||
}>(), {
|
||||
type: 'full'
|
||||
})
|
||||
}>()
|
||||
|
||||
const chat = props.access
|
||||
? chatAccess(props.access)
|
||||
: props.state
|
||||
? chatState(props.state)
|
||||
: { info: '', icon: '', color: '', label: '', description: '' }
|
||||
|
||||
function getDate (d: number) {
|
||||
return new Date(d * 1000).getFullYear() === new Date(Date.now()).getFullYear()
|
||||
? date.formatDate(d * 1000, 'DD MMM')
|
||||
: date.formatDate(d * 1000, 'DD MMM YY')
|
||||
}
|
||||
|
||||
const chatType = computed(() => getChatType(props.type))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
:style="{ display: 'none' }"
|
||||
@update:model-value="handleUpload()"
|
||||
:filter="checkImgType"
|
||||
accept="image/*"
|
||||
:accept="allowedMimeTypes.join(', ')"
|
||||
/>
|
||||
<q-icon
|
||||
v-if="modelValue === '' || modelValue === undefined || modelValue === null"
|
||||
@@ -37,21 +37,34 @@
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="showDialog">
|
||||
<q-card class="w100 relative-position" style="height: auto;">
|
||||
<q-card
|
||||
class="w100 relative-position overflow-hidden"
|
||||
style="height: auto;"
|
||||
>
|
||||
<q-img :src="modelValue"/>
|
||||
<q-btn
|
||||
icon="mdi-close"
|
||||
@click="showDialog = false"
|
||||
class="absolute-top-right q-ma-sm"
|
||||
color="grey-9"
|
||||
round flat
|
||||
style="background-color: rgba(255, 255, 255, 0.65) !important;"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex row items-center jutsify-center q-pb-sm"
|
||||
style="bottom: 0; position: absolute; left: 50%; transform: translate(-50%, 0%);">
|
||||
class="flex no-wrap items-center justify-center q-pb-sm absolute-bottom w100"
|
||||
>
|
||||
<q-btn
|
||||
v-for="btn in menuBtns"
|
||||
:key="btn.name"
|
||||
:icon="btn.icon"
|
||||
:color="btn.color"
|
||||
@click="btn.f"
|
||||
class="q-mx-xs bg-white"
|
||||
round flat
|
||||
style="opacity: 0.8"
|
||||
color="primary"
|
||||
/>
|
||||
class="q-mx-sm"
|
||||
rounded flat
|
||||
style="background-color: rgba(255, 255, 255, 0.65) !important;"
|
||||
>
|
||||
{{ $t(btn.label) }}
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
@@ -72,10 +85,10 @@
|
||||
}>()
|
||||
|
||||
const compressionOptions = {
|
||||
maxSizeMB: 0.5, // Максимальный размер ~500 КБ
|
||||
maxWidthOrHeight: 1200, // Максимальное разрешение
|
||||
useWebWorker: true, // Для производительности
|
||||
fileType: 'image/jpeg' // Формат вывода
|
||||
maxSizeMB: 0.5, // Max size ~500 KB
|
||||
maxWidthOrHeight: 1200, // Max resolution
|
||||
useWebWorker: true, // Performance
|
||||
fileType: 'image/jpeg' // Output format
|
||||
}
|
||||
|
||||
const imageFile = ref(null) // file-from selector
|
||||
@@ -83,10 +96,10 @@
|
||||
const size = ref<number>(props.size ? props.size : 100)
|
||||
const iconsize = ref<number>(props.iconsize ? props.iconsize : 75)
|
||||
const showDialog = ref<boolean>(false)
|
||||
|
||||
const menuBtns = [
|
||||
{ name: 'change', icon: 'mdi-swap-horizontal', f: imgFileSelectorClick },
|
||||
{ name: 'delete', icon: 'mdi-delete-outline', f: deleteImage },
|
||||
{ name: 'close', icon: 'mdi-close', f: () => showDialog.value = false }
|
||||
{ name: 'delete', color: 'negative', label: 'image_selector__delete', f: deleteImage },
|
||||
{ name: 'change', color: 'primary', label: 'image_selector__change', f: imgFileSelectorClick }
|
||||
]
|
||||
|
||||
const sizePx = computed(() => {
|
||||
@@ -99,14 +112,14 @@
|
||||
const compressedFile = await imageCompression(
|
||||
imageFile.value,
|
||||
compressionOptions
|
||||
);
|
||||
)
|
||||
|
||||
const img = await imgToBase64(compressedFile);
|
||||
modelValue.value = typeof img === 'string' ? img : '';
|
||||
modelValue.value = typeof img === 'string' ? img : ''
|
||||
} catch (error) {
|
||||
console.error('Image error compression:', error);
|
||||
const img = await imgToBase64(imageFile.value);
|
||||
modelValue.value = typeof img === 'string' ? img : '';
|
||||
console.error('Image error compression:', error)
|
||||
const img = await imgToBase64(imageFile.value)
|
||||
modelValue.value = typeof img === 'string' ? img : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,21 +144,20 @@
|
||||
reject(new Error('Something went wrong'))
|
||||
}
|
||||
|
||||
reader.onload = () => {
|
||||
resolve(reader.result)
|
||||
}
|
||||
reader.onload = () => resolve(reader.result)
|
||||
})
|
||||
}
|
||||
|
||||
function checkImgType(files: File[]): File[] {
|
||||
const allowedTypes = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
'image/gif',
|
||||
'image/bmp'
|
||||
]
|
||||
return files.filter((file: File) => allowedTypes.includes(file.type))
|
||||
const allowedMimeTypes = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/bmp',
|
||||
'image/webp',
|
||||
'image/gif'
|
||||
]
|
||||
|
||||
function checkImgType (files: File[]): File[] {
|
||||
return files.filter((file: File) => allowedMimeTypes.includes(file.type))
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
64
src/components/pnInlinePair.vue
Normal file
64
src/components/pnInlinePair.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="row no-wrap items-center w100">
|
||||
<div
|
||||
v-if="item1"
|
||||
class="second-line-item flex no-wrap items-center q-mr-sm"
|
||||
>
|
||||
<q-icon
|
||||
v-if="item1Icon"
|
||||
:name="item1Icon"
|
||||
:color="item1IconColor"
|
||||
class="q-mr-none"
|
||||
/>
|
||||
<div
|
||||
class="ellipsis"
|
||||
:class="item1Class ?? '' "
|
||||
>
|
||||
{{ item1 }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="item2"
|
||||
class="second-line-item flex no-wrap items-center"
|
||||
>
|
||||
<q-icon
|
||||
v-if="item2Icon"
|
||||
:name="item2Icon"
|
||||
:color="item2IconColor"
|
||||
class="q-mr-none"
|
||||
/>
|
||||
<div
|
||||
class="ellipsis"
|
||||
:class="item2Class ?? '' "
|
||||
>
|
||||
{{ item2 }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
item1: string | null
|
||||
item1Class?: string
|
||||
item1Icon?: string
|
||||
item1IconColor?: string
|
||||
item2: string | null
|
||||
item2Class?: string
|
||||
item2Icon?: string
|
||||
item2IconColor?: string
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.second-line-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 60%;
|
||||
}
|
||||
</style>
|
||||
@@ -42,48 +42,48 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps<{
|
||||
icon: string
|
||||
message1: string
|
||||
message2?: string
|
||||
routeName: string
|
||||
}>()
|
||||
const props = defineProps<{
|
||||
icon: string
|
||||
message1: string
|
||||
message2?: string
|
||||
routeName: string
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const showIcon = ref(false)
|
||||
const router = useRouter()
|
||||
const timers = ref<number[]>([])
|
||||
const size = 160
|
||||
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(() => {
|
||||
showIcon.value = true
|
||||
}, 300)
|
||||
timers.value.push(timer1)
|
||||
const timer2 = window.setTimeout(() => {
|
||||
const setupTimers = () => {
|
||||
visible.value = true
|
||||
const timer1 = window.setTimeout(() => {
|
||||
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
|
||||
}, 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 })
|
||||
})
|
||||
|
||||
watch(visible, async (newVal) => {
|
||||
if (newVal === false) await router.push({ name: props.routeName })
|
||||
})
|
||||
|
||||
onMounted(setupTimers)
|
||||
onUnmounted(clearTimers)
|
||||
onMounted(setupTimers)
|
||||
onUnmounted(clearTimers)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -2,22 +2,21 @@
|
||||
<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
|
||||
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 class="text-h6 text-brand text-center">
|
||||
{{ 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">
|
||||
<div class="text-h6 text-brand text-center">
|
||||
{{ message1 }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,4 +45,4 @@
|
||||
|
||||
</script>
|
||||
<style>
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -11,17 +11,28 @@
|
||||
<q-resize-observer @resize="onHeaderResize"/>
|
||||
<slot name="card-body-header"/>
|
||||
</div>
|
||||
<div id="card-body"
|
||||
class="top-rounded-card bg-white"
|
||||
<div
|
||||
id="card-body"
|
||||
class="top-rounded-card bg-white column no-wrap"
|
||||
>
|
||||
<div style="min-height: var(--top-radius);">
|
||||
<slot name="card-body-btn"/>
|
||||
<q-resize-observer @resize="onBodyBtnResize"/>
|
||||
</div>
|
||||
<div style="height:100%">
|
||||
<div class="column col col-grow">
|
||||
<q-resize-observer @resize="onBodyResize"/>
|
||||
<pn-shadow-scroll :hideShadows="isResizing" :height="scrollAreaHeight">
|
||||
<slot/>
|
||||
<pn-shadow-scroll
|
||||
:hideShadows="isResizing"
|
||||
:height="scrollAreaHeight"
|
||||
@updateScrollPosition="onScrollUpdate"
|
||||
:scrollPosition
|
||||
>
|
||||
<div
|
||||
class="w100 column"
|
||||
:style="{ minHeight: scrollAreaHeight + 'px' }"
|
||||
>
|
||||
<slot/>
|
||||
</div>
|
||||
</pn-shadow-scroll>
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,6 +41,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick, useSlots, computed } from 'vue'
|
||||
|
||||
const emit = defineEmits(['updateScrollPosition', 'updateHeight'])
|
||||
const onScrollUpdate = (pos: number) => emit('updateScrollPosition', pos)
|
||||
|
||||
const slots = useSlots()
|
||||
// work only slot no use in component
|
||||
@@ -37,9 +51,11 @@
|
||||
|
||||
// need if slot draw by expression with v-if
|
||||
withDefaults(defineProps<{
|
||||
hideHeader?: boolean
|
||||
hideHeader?: boolean
|
||||
scrollPosition?: number
|
||||
}>(), {
|
||||
hideHeader: false
|
||||
hideHeader: false,
|
||||
scrollPosition: 0
|
||||
})
|
||||
|
||||
const heightCard = ref(100)
|
||||
@@ -69,6 +85,7 @@
|
||||
async function updateScrollAreaHeight() {
|
||||
await nextTick(() => {
|
||||
scrollAreaHeight.value = Math.max(0, heightCard.value)
|
||||
emit('updateHeight', scrollAreaHeight.value)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<div :class="{'fix-scroll-area-content': hideShadows }">
|
||||
<div :class="{'fix-scroll-area-content': hideShadows }" class="fix-scroll-area-content-width column col col-grow">
|
||||
<q-scroll-area
|
||||
ref="scrollAreaRef"
|
||||
:style="{ height: height + 'px' }"
|
||||
class="w100 q-pa-none q-ma-none"
|
||||
class="w100 q-pa-none q-ma-none column col col-grow"
|
||||
@scroll="onScroll"
|
||||
:class=" {
|
||||
'shadow-top': hasScrolled,
|
||||
@@ -10,19 +11,21 @@
|
||||
}"
|
||||
>
|
||||
<slot/>
|
||||
<div class="q-pa-sm"/>
|
||||
</q-scroll-area>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onActivated, watch } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
height: number
|
||||
hideShadows: boolean
|
||||
scrollPosition?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['updateScrollPosition'])
|
||||
|
||||
const hasScrolled = ref(false)
|
||||
const hasScrolledBottom = ref(false)
|
||||
|
||||
@@ -33,12 +36,23 @@
|
||||
verticalContainerSize: number;
|
||||
}
|
||||
|
||||
function onScroll(info: ScrollInfo) {
|
||||
function onScroll (info: ScrollInfo) {
|
||||
hasScrolled.value = info.verticalPosition > 0
|
||||
const scrollEnd = info.verticalPosition + info.verticalContainerSize >= info.verticalSize - 1
|
||||
hasScrolledBottom.value = !scrollEnd
|
||||
emit('updateScrollPosition', scrollAreaRef.value?.getScrollPosition().top || 0)
|
||||
}
|
||||
|
||||
const scrollAreaRef = ref()
|
||||
|
||||
onActivated(() => {
|
||||
scrollAreaRef.value?.setScrollPosition('vertical', props?.scrollPosition || 0)
|
||||
})
|
||||
|
||||
watch(() => props.scrollPosition, (newPosition) => {
|
||||
scrollAreaRef.value?.setScrollPosition('vertical', newPosition)
|
||||
}, { immediate: true })
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -87,6 +101,10 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.fix-scroll-area-content-width:deep(.q-scrollarea__content) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fix-scroll-area-content:deep(.q-scrollarea::before) {
|
||||
content: none;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<q-dialog v-model="modelValue">
|
||||
<q-dialog v-model="modelValue" class="q-ma-none">
|
||||
<q-card
|
||||
class="q-pa-none q-ma-none w100 no-scroll"
|
||||
class="q-pa-none q-ma-none w100 no-scroll rounded-card"
|
||||
style="max-height: calc(100vh - 48px);"
|
||||
>
|
||||
<q-card-section class="row items-center q-pb-none">
|
||||
<div class="flex no-wrap items-center w100">
|
||||
@@ -56,16 +57,16 @@
|
||||
}
|
||||
|
||||
async function updateScrollAreaHeight () {
|
||||
await nextTick(() => {
|
||||
scrollAreaHeight.value = Math.min(
|
||||
bodyHeight.value + 16,
|
||||
window.innerHeight - headerHeight.value - 64
|
||||
)
|
||||
})
|
||||
}
|
||||
await nextTick(() => {
|
||||
scrollAreaHeight.value = Math.min(
|
||||
bodyHeight.value + 16,
|
||||
window.innerHeight - headerHeight.value - 64
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
watch(bodyHeight, updateScrollAreaHeight)
|
||||
watch(headerHeight, updateScrollAreaHeight)
|
||||
watch(bodyHeight, updateScrollAreaHeight)
|
||||
watch(headerHeight, updateScrollAreaHeight)
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<q-dialog v-model="modelValue">
|
||||
<q-card
|
||||
class="q-pa-none q-ma-none w100 no-scroll"
|
||||
class="q-pa-none q-ma-none w100 no-scroll rounded-card"
|
||||
align="center"
|
||||
>
|
||||
<q-card-section>
|
||||
|
||||
57
src/components/pnUserListItemBody.vue
Normal file
57
src/components/pnUserListItemBody.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<q-item-section avatar>
|
||||
<pn-auto-avatar
|
||||
:img="item.photo"
|
||||
:name="item.section1"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<slot/>
|
||||
<q-item-label lines="1" v-if="showStatus && getUserStatus(item) && getUserStatus(item)?.status">
|
||||
<q-badge :color="getUserStatus(item)?.color">
|
||||
{{ $t(getUserStatus(item)?.text ?? '') }}
|
||||
</q-badge>
|
||||
</q-item-label>
|
||||
<q-item-label lines="1" class="text-bold" v-if="item.section1">
|
||||
{{item.section1}}
|
||||
</q-item-label>
|
||||
<q-item-label lines="2" caption v-if="item.section3">
|
||||
{{item.section3}}
|
||||
</q-item-label>
|
||||
<q-item-label caption v-if="item.section2_1 || item.section2_2">
|
||||
<pn-inline-pair
|
||||
:item1="item.section2_1 ? item.section2_1 :null"
|
||||
item1Class="text-bold"
|
||||
item1Icon="telegram"
|
||||
item1IconColor="tgcolor"
|
||||
:item2="item.section2_2 ? '@' + item.section2_2 : null"
|
||||
item2Class="text-blue"
|
||||
/>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getUserStatus } from 'utils/users'
|
||||
import pnInlinePair from 'components/pnInlinePair.vue'
|
||||
import type { User } from 'types/User'
|
||||
|
||||
type UserListItem = User & {
|
||||
section1: string
|
||||
section2_1: string
|
||||
section2_2: string
|
||||
section3: string
|
||||
is_blocked: boolean
|
||||
is_leave: boolean
|
||||
companyName?: string | null
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
item: UserListItem
|
||||
showStatus?: boolean
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -15,7 +15,7 @@
|
||||
</template>
|
||||
|
||||
<pn-scroll-list>
|
||||
<div class="flex column items-center q-pa-md q-pb-sm">
|
||||
<div class="flex column items-center q-pa-md q-pb-sm w100">
|
||||
<pn-image-selector
|
||||
v-model="projectMod.logo"
|
||||
:size="100"
|
||||
@@ -23,15 +23,23 @@
|
||||
type="rounded"
|
||||
class="q-pb-lg"
|
||||
/>
|
||||
<div class="q-gutter-y-lg w100">
|
||||
|
||||
<q-form
|
||||
ref="formRef"
|
||||
@validation-success="isFormValid = true"
|
||||
@validation-error="isFormValid = false"
|
||||
class="q-gutter-y-lg w100"
|
||||
>
|
||||
<q-input
|
||||
v-model="projectMod.name"
|
||||
v-model.trim="projectMod.name"
|
||||
no-error-icon
|
||||
dense
|
||||
filled
|
||||
label-slot
|
||||
class = "w100 fix-bottom-padding"
|
||||
:rules="[rules.name]"
|
||||
:rules="[inputRules.require, inputRules.middleText]"
|
||||
@update:model-value="formRef.validate()"
|
||||
debounce="300"
|
||||
>
|
||||
<template #label>
|
||||
{{ $t('project_block__project_name') }} <span class="text-red">*</span>
|
||||
@@ -43,10 +51,14 @@
|
||||
dense
|
||||
filled
|
||||
autogrow
|
||||
no-error-icon
|
||||
class="w100 q-pt-sm"
|
||||
:label="$t('project_block__project_description')"
|
||||
:rules="[inputRules.longText]"
|
||||
@update:model-value="formRef.validate()"
|
||||
debounce="300"
|
||||
/>
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
@@ -54,12 +66,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useInputRules } from 'composables/useInputRules'
|
||||
import type { ProjectParams } from 'types/Project'
|
||||
import { removeNullKeys, getFieldValue } from 'utils/helpers'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { inputRules } = useInputRules()
|
||||
|
||||
const props = defineProps<{
|
||||
title: string,
|
||||
@@ -75,31 +87,22 @@
|
||||
logo: props.project?.logo ?? null
|
||||
})
|
||||
|
||||
const rulesErrorMessage = {
|
||||
name: t('project_block__error_name')
|
||||
}
|
||||
|
||||
const rules = {
|
||||
name: (val: string) => !!val?.trim() || rulesErrorMessage['name']
|
||||
}
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
const validations = {
|
||||
name: projectMod.name && rules.name(projectMod.name) === true
|
||||
}
|
||||
|
||||
return Object.values(validations).every(Boolean)
|
||||
})
|
||||
const formRef = ref()
|
||||
const isFormValid = ref(false)
|
||||
|
||||
function onSubmit() {
|
||||
const outData = {
|
||||
name: projectMod.name,
|
||||
description: getFieldValue(projectMod.description, props.project?.description),
|
||||
logo: projectMod.logo
|
||||
logo: projectMod.logo !== '' ? projectMod.logo : null
|
||||
}
|
||||
emit('update', removeNullKeys(outData))
|
||||
emit('update', removeNullKeys(outData, ['logo']))
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await formRef.value.validate()
|
||||
if (!isFormValid.value) formRef.value.resetValidation()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
81
src/components/subscribe/pnSubscribePeriods.vue
Normal file
81
src/components/subscribe/pnSubscribePeriods.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="w100">
|
||||
<q-btn-group spread flat class="w100 q-py-xs">
|
||||
<q-btn
|
||||
v-for="period in periods"
|
||||
:key="period.id"
|
||||
:outline="selectedPeriod === period.id"
|
||||
@click="selectedPeriod = period.id"
|
||||
:color="selectedPeriod === period.id ? 'primary' : ''"
|
||||
no-caps dense
|
||||
>
|
||||
<div class="column items-center w100 self-start">
|
||||
<span
|
||||
class="text-caption text-grey text-no-wrap"
|
||||
>
|
||||
{{ $t(period.name_in_days) }}
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="text-h6"
|
||||
:class="selectedPeriod === period.id ? 'text-primary' : 'text-black'"
|
||||
style="line-height: 1em !important;"
|
||||
>
|
||||
{{ $t(period.name) }}
|
||||
</div>
|
||||
<div
|
||||
class="row no-wrap w100 q-px-sm q-pt-xs justify-center"
|
||||
>
|
||||
<div
|
||||
v-if="getPeriodPrice(period)"
|
||||
class="flex no-wrap text-caption text-grey items-center q-pa-none q-ma-none q-pr-xs"
|
||||
>
|
||||
<telegram-star color="gold" size="12px" class="q-mr-xs"/>
|
||||
{{ formatNumber(getPeriodPrice(period)) }}
|
||||
</div>
|
||||
|
||||
<q-badge color="red" v-if="period.discount">
|
||||
{{ period.discount * 100 }}%
|
||||
</q-badge>
|
||||
</div>
|
||||
</div>
|
||||
</q-btn>
|
||||
</q-btn-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import telegramStar from 'components/TelegramStar.vue'
|
||||
import { formatNumber } from 'utils/helpers'
|
||||
import type { TariffPeriod } from 'types/Tariff'
|
||||
|
||||
const props = defineProps<{
|
||||
periods: TariffPeriod[],
|
||||
tariffPrice: number
|
||||
}>()
|
||||
|
||||
|
||||
|
||||
const emit = defineEmits(['update:selected'])
|
||||
|
||||
const selectedPeriod = ref<number>()
|
||||
|
||||
watch(() => props.periods, () => {
|
||||
if (props.periods.length !== 0) {
|
||||
selectedPeriod.value = props.periods[0]?.id
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(selectedPeriod, (newVal) => {
|
||||
if (newVal !== undefined) emit('update:selected', newVal)
|
||||
}, { immediate: true })
|
||||
|
||||
function getPeriodPrice (period: TariffPeriod) {
|
||||
return Math.floor(props.tariffPrice * (1 - period.discount) * period.id / 30)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
119
src/components/subscribe/pnSubscribeTariffs.vue
Normal file
119
src/components/subscribe/pnSubscribeTariffs.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="w100">
|
||||
<div class="w100 column items-center text-center text-grey q-px-md">
|
||||
<div class="text-caption">
|
||||
{{ $t('subscribe_tariffs__description') }}
|
||||
</div>
|
||||
</div>
|
||||
<q-list separator>
|
||||
<q-item
|
||||
v-for="item in tariffs"
|
||||
:key="item.id"
|
||||
clickable
|
||||
v-ripple
|
||||
tag="label"
|
||||
:disable="disableTestTariff(item.id)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-radio
|
||||
v-model="selectedTariff"
|
||||
:val="item.id"
|
||||
:disable="disableTestTariff(item.id)"
|
||||
:keep-color="disableTestTariff(item.id)"
|
||||
:color="disableTestTariff(item.id) ? 'grey-5' : ''"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<div class="column">
|
||||
<span class="text-bold">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<div class="flex no-wrap items-center text-caption" style="line-height: 1em;">
|
||||
<span v-if="item.chat_limit">
|
||||
{{ $t('subscribe_tariffs__chats_max') + ' ' + item.chat_limit }}
|
||||
</span>
|
||||
<q-icon v-else name="mdi-all-inclusive" size="sm"/>
|
||||
<span class="q-pl-xs">
|
||||
{{ $t('subscribe_tariffs__chats')}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-item-section>
|
||||
<q-item-section
|
||||
class="text-left"
|
||||
:style="{ width: `${maxWidthStarsSection}px` }"
|
||||
>
|
||||
<q-resize-observer @resize="updateMaxWidthStarsSection" />
|
||||
<div v-if="item.price" class="flex column justify-start">
|
||||
<div class="flex no-wrap items-center">
|
||||
<telegram-star color="gold" size="18px" class="q-mr-xs"/>
|
||||
<span class="text-h6">{{ formatNumber(item.price) }}</span>
|
||||
<span class="text-caption q-pl-xs">{{ $t('subscribe_tariffs__per_month') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-bold self-center">
|
||||
{{ $t('subscribe_tariffs__free_tax') }}
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import telegramStar from 'components/TelegramStar.vue'
|
||||
import { formatNumber } from 'utils/helpers'
|
||||
import type { Tariff } from 'types/Tariff'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
tariffs: Tariff[]
|
||||
currentTariffId?: number
|
||||
disable?: boolean
|
||||
}>(),
|
||||
{
|
||||
disable: false
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits(['update:selected'])
|
||||
|
||||
const selectedTariff = ref<number>()
|
||||
|
||||
const testTariffId = computed(() => props.tariffs.find(el => el.price === 0)?.id)
|
||||
|
||||
watch(() => props.tariffs, () => {
|
||||
if (props.currentTariffId !== undefined) {
|
||||
selectedTariff.value = props.currentTariffId
|
||||
} else if (testTariffId.value) {
|
||||
selectedTariff.value = testTariffId.value
|
||||
} else if (props.tariffs.length > 0) {
|
||||
selectedTariff.value = props.tariffs[0]?.id
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => props.currentTariffId, (newId) => {
|
||||
if (newId !== undefined) selectedTariff.value = newId
|
||||
})
|
||||
|
||||
watch(selectedTariff, (newVal) => {
|
||||
if (newVal !== undefined) emit('update:selected', newVal)
|
||||
}, { immediate: true })
|
||||
|
||||
function disableTestTariff (tariffId: number) {
|
||||
if (props.disable) return true
|
||||
if (!testTariffId.value) return false
|
||||
return props.currentTariffId === testTariffId.value || tariffId === testTariffId.value
|
||||
}
|
||||
|
||||
const maxWidthStarsSection = ref(0)
|
||||
|
||||
function updateMaxWidthStarsSection(size: { width: number, height: number }) {
|
||||
if (size.width > maxWidthStarsSection.value) {
|
||||
maxWidthStarsSection.value = size.width
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
253
src/components/subscribe/subscribeChangeRenew.vue
Normal file
253
src/components/subscribe/subscribeChangeRenew.vue
Normal file
@@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<div class="column w100 q-px-md">
|
||||
<div class="flex justify-end w100">
|
||||
<q-btn
|
||||
flat no-caps color="grey" rounded dense
|
||||
@click="showTarfiffSelectDialog = true"
|
||||
>
|
||||
<span class="q-px-xs text-caption">
|
||||
{{$t('subscribe__dialog_select_tariff_show')}}
|
||||
</span>
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<div class="w100 text-grey text-center q-pt-sm">
|
||||
{{ t('subscribe__tariffs_interval_renewal') }}
|
||||
</div>
|
||||
|
||||
<pn-subscribe-periods
|
||||
@update:selected="selectedPeriod = $event"
|
||||
:periods
|
||||
:tariff-price="selectedTariffPrice"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="text-caption column rounded-card q-px-md q-py-sm q-mt-md"
|
||||
style="border: 1px solid var(--q-negative);"
|
||||
v-if="currentTariff?.debt !== 0"
|
||||
>
|
||||
<div class="row justify-between item-center text-negative">
|
||||
<span>
|
||||
{{ $t('suscribe__credit_from') + ' ' +
|
||||
(currentTariff?.end_date
|
||||
? date.formatDate(currentTariff?.end_date * 1000, 'DD.MM.YYYY')
|
||||
: '')
|
||||
+ ': ' }}
|
||||
</span>
|
||||
<div class="flex no-wrap items-center q-pa-none q-ma-none">
|
||||
<telegram-star color="gold" size="12px" class="q-mr-xs"/>
|
||||
{{ formatNumber(currentTariff?.debt ?? '') }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-grey">
|
||||
{{ $t('suscribe__credit_description') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-grey w100 text-caption q-pt-lg">
|
||||
{{ $t('subscribe__promocode_description') }}
|
||||
</div>
|
||||
<q-input
|
||||
v-model="promocode"
|
||||
dense
|
||||
filled
|
||||
:label="$t('subscribe__promocode')"
|
||||
class="q-pt-sm"
|
||||
>
|
||||
<template #append v-if="promocode !== ''">
|
||||
<q-btn
|
||||
round dense flat
|
||||
color="primary"
|
||||
icon="mdi-arrow-right-circle-outline"
|
||||
@click="clickSendPromocode"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<pn-bottom-sheet-dialog
|
||||
v-if="tariffs && currentTariff"
|
||||
title="subscribe__dialog_select_tariff_title"
|
||||
v-model="showTarfiffSelectDialog"
|
||||
>
|
||||
<div class="w100 column">
|
||||
<q-banner
|
||||
v-if="!currentTariff.can_change"
|
||||
class="bg-negative text-white q-mb-md"
|
||||
>
|
||||
<div>
|
||||
{{ $t(messageBlockChange?.body ?? '') }}
|
||||
</div>
|
||||
<div class="text-caption" style="opacity: 0.8">
|
||||
{{ $t(messageBlockChange?.description ?? '') }}
|
||||
</div>
|
||||
</q-banner>
|
||||
<pn-subscribe-tariffs
|
||||
@update:selected="selectedTariff = $event"
|
||||
:tariffs
|
||||
:currentTariffId="currentTariff.id"
|
||||
:disable="!currentTariff.can_change"
|
||||
/>
|
||||
<div class="text-caption text-grey q-px-md q-py-md">
|
||||
<div class="w100">
|
||||
{{ $t('subscribe__dialog_select_tariff_notes_title') }}
|
||||
</div>
|
||||
<div class="w100">
|
||||
{{ $t('subscribe__dialog_select_tariff_notes_1') }}
|
||||
</div>
|
||||
<div class="w100">
|
||||
{{ $t('subscribe__dialog_select_tariff_notes_2') }}
|
||||
</div>
|
||||
<div class="w100">
|
||||
{{ $t('subscribe__dialog_select_tariff_notes_3') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w100 text-info cursor-pointer"
|
||||
@click="router.push({ name: 'change_tariff_rules'})"
|
||||
>
|
||||
{{ $t('subscribe__dialog_select_tariff_notes_more') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="column">
|
||||
<q-btn
|
||||
rounded
|
||||
class="w100"
|
||||
color="primary"
|
||||
v-close-popup
|
||||
:outline="!currentTariff.can_change || currentTariff?.id === selectedTariff"
|
||||
@click="clickChangeTariff"
|
||||
>
|
||||
{{ $t(currentTariff.can_change && currentTariff?.id !== selectedTariff
|
||||
? 'subscribe__dialog_select_tariff_btn'
|
||||
: 'subscribe__dialog_select_tariff_btn_cancel'
|
||||
) }}
|
||||
</q-btn>
|
||||
<div
|
||||
v-if="props.currentTariff?.id !== selectedTariff"
|
||||
class="q-px-md text-caption text-negative text-center"
|
||||
>
|
||||
{{ $t('subscribe__dialog_select_tariff_recalc_msg') + ': ' }}
|
||||
{{
|
||||
typeof activePeriodMessage === 'number'
|
||||
? date.formatDate(activePeriodMessage * 1000, 'DD.MM.YYYY')
|
||||
: 'Error'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</pn-bottom-sheet-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { date, useQuasar } from 'quasar'
|
||||
import { formatNumber } from 'utils/helpers'
|
||||
import telegramStar from 'components/TelegramStar.vue'
|
||||
import pnSubscribeTariffs from 'components/subscribe/pnSubscribeTariffs.vue'
|
||||
import pnSubscribePeriods from 'components/subscribe/pnSubscribePeriods.vue'
|
||||
import pnBottomSheetDialog from 'components/pnBottomSheetDialog.vue'
|
||||
import {
|
||||
calcChangeTariff,
|
||||
changeTariff,
|
||||
calcRenewTariff,
|
||||
urlRenewTariff,
|
||||
sendPromocode
|
||||
} from 'composables/useSubscription'
|
||||
import type { Tariff, CurrentTariff, TariffPeriod } from 'types/Tariff'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps<{
|
||||
tariffs: Tariff[]
|
||||
currentTariff?: CurrentTariff
|
||||
periods: TariffPeriod[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update'])
|
||||
|
||||
const selectedTariff = ref<number>()
|
||||
const selectedPeriod = ref<number>()
|
||||
|
||||
const selectedTariffPrice = computed(() => props.tariffs.find((el: Tariff) => el.id === props.currentTariff?.id)?.price ?? 0)
|
||||
|
||||
const showTarfiffSelectDialog = ref(false)
|
||||
async function clickChangeTariff () {
|
||||
if (props.currentTariff &&
|
||||
selectedTariff.value &&
|
||||
props.currentTariff.can_change &&
|
||||
props.currentTariff?.id !== selectedTariff.value)
|
||||
await changeTariff(selectedTariff.value)
|
||||
}
|
||||
|
||||
const messageBlockChange = computed(() => {
|
||||
if (props.currentTariff && !props.currentTariff.can_change) {
|
||||
return props.currentTariff.debt === 0
|
||||
? {
|
||||
body: 'subscribe__dialog_select_tariff_warn_debt_msg_1',
|
||||
description: 'subscribe__dialog_select_tariff_warn_debt_msg_2'
|
||||
}
|
||||
: {
|
||||
body: 'subscribe__dialog_select_tariff_warn_once_day_msg_1',
|
||||
description: 'subscribe__dialog_select_tariff_warn_once_day_msg_2'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const activePeriodMessage = ref('')
|
||||
watch(selectedTariff, async (newVal) => {
|
||||
activePeriodMessage.value = (newVal && newVal !== props.currentTariff?.id )
|
||||
? (await calcChangeTariff(newVal)).plan_end_date
|
||||
: ''
|
||||
})
|
||||
|
||||
watch(showTarfiffSelectDialog, async (newVal) => {
|
||||
if (newVal) emit('update', { status: 'hide' })
|
||||
else await updatePayment()
|
||||
})
|
||||
|
||||
watch(selectedPeriod, async () => await updatePayment())
|
||||
|
||||
const updatePayment = async () => {
|
||||
if (!props.currentTariff?.id || !selectedPeriod.value) return
|
||||
|
||||
const renew = await calcRenewTariff(props.currentTariff.id, selectedPeriod.value)
|
||||
const textBtn = t('subscribe__pay_renew') + ' ⭐' + formatNumber(renew.amount) +
|
||||
' (' + t('subscribe__tariff_exp') + ' ' + date.formatDate(renew.end_date * 1000, 'DD.MM.YYYY') + ')'
|
||||
const url = await urlRenewTariff(props.currentTariff.id, selectedPeriod.value)
|
||||
|
||||
emit('update', { status: 'show', textBtn, url })
|
||||
}
|
||||
|
||||
// promocode
|
||||
const promocode = ref('')
|
||||
const $q = useQuasar()
|
||||
|
||||
async function clickSendPromocode () {
|
||||
const result = await sendPromocode(promocode.value)
|
||||
if (result) {
|
||||
showNotify('success')
|
||||
} else
|
||||
showNotify('fail')
|
||||
|
||||
promocode.value = ''
|
||||
}
|
||||
|
||||
const showNotify = (message: 'success' | 'fail') => {
|
||||
$q.notify({
|
||||
message: t(message === 'success' ? 'subscribe__promocode_success' : 'subscribe__promocode_fail'),
|
||||
type: message === 'success' ? 'positive' : 'negative',
|
||||
position: 'bottom',
|
||||
timeout: 1000,
|
||||
multiLine: true
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
76
src/components/subscribe/subscribeUpgrade.vue
Normal file
76
src/components/subscribe/subscribeUpgrade.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<!-- only for test tariff -->
|
||||
<div
|
||||
class="column w100"
|
||||
v-if="testTariffId && tariffs.length > 0 && periods.length > 0"
|
||||
>
|
||||
<div class="w100 text-grey text-center">
|
||||
{{ $t('subscribe__tariffs') }}
|
||||
</div>
|
||||
<pn-subscribe-tariffs
|
||||
@update:selected="selectedTariff = $event"
|
||||
:tariffs
|
||||
/>
|
||||
<q-slide-transition>
|
||||
<div v-if="selectedTariff !== testTariffId">
|
||||
<div class="w100 text-grey text-center q-pt-md">
|
||||
{{ $t('subscribe__tariffs_interval') }}
|
||||
</div>
|
||||
<pn-subscribe-periods
|
||||
@update:selected="selectedPeriod = $event"
|
||||
:periods
|
||||
:tariff-price="selectedTariffPrice"
|
||||
class="q-px-md"
|
||||
/>
|
||||
</div>
|
||||
</q-slide-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { date } from 'quasar'
|
||||
import { formatNumber } from 'utils/helpers'
|
||||
import pnSubscribeTariffs from 'components/subscribe/pnSubscribeTariffs.vue'
|
||||
import pnSubscribePeriods from 'components/subscribe/pnSubscribePeriods.vue'
|
||||
import {
|
||||
calcUpgradeTariff,
|
||||
urlUpgradeTariff
|
||||
} from 'composables/useSubscription'
|
||||
import type { Tariff, TariffPeriod } from 'types/Tariff'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
tariffs: Tariff[]
|
||||
periods: TariffPeriod[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['tariff-upgrade'])
|
||||
|
||||
const selectedTariff = ref<number>()
|
||||
const selectedPeriod = ref<number>()
|
||||
|
||||
const testTariffId = computed(() => props.tariffs.find(el => el.price === 0)?.id ?? null)
|
||||
const selectedTariffPrice = computed(() => props.tariffs.find((el: Tariff) => el.id === selectedTariff.value)?.price ?? 0)
|
||||
|
||||
watch([selectedTariff, selectedPeriod], async () => {
|
||||
if (selectedTariff.value === testTariffId.value) {
|
||||
emit('tariff-upgrade', { status: 'hide' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedTariff.value || !selectedPeriod.value) return
|
||||
|
||||
const upgrade = await calcUpgradeTariff(selectedTariff.value, selectedPeriod.value)
|
||||
const textBtn = t('subscribe__pay') + ' ⭐' + formatNumber(upgrade.amount) +
|
||||
' (' + t('subscribe__tariff_exp') + ' ' + date.formatDate(upgrade.end_date * 1000, 'DD.MM.YYYY') + ')'
|
||||
const url = await urlUpgradeTariff(selectedTariff.value, selectedPeriod.value)
|
||||
|
||||
emit('tariff-upgrade', { status: 'show', textBtn, url })
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
@@ -7,6 +7,7 @@
|
||||
<q-btn
|
||||
rounded color="primary"
|
||||
class="w100 fix-disabled-btn"
|
||||
:disable="!isFormValid"
|
||||
@click = "onSubmit"
|
||||
>
|
||||
{{ $t(btnText) }}
|
||||
@@ -38,17 +39,25 @@
|
||||
<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="user?.username">{{ user.username }}</div>
|
||||
<div caption class="text-primary text-caption" align="center" v-if="user?.username">{{ user.username }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-gutter-y-lg w100">
|
||||
<q-form
|
||||
ref="formRef"
|
||||
@validation-success="isFormValid = true"
|
||||
@validation-error="isFormValid = false"
|
||||
class="q-gutter-y-lg w100"
|
||||
>
|
||||
<q-input
|
||||
v-model.trim="userMod.fullname"
|
||||
dense
|
||||
filled
|
||||
class = "w100"
|
||||
class = "w100 fix-bottom-padding"
|
||||
:label = "$t('user_block__name')"
|
||||
:rules="[inputRules.middleText]"
|
||||
@update:model-value="formRef.validate()"
|
||||
debounce="300"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
@@ -61,6 +70,7 @@
|
||||
option-value="id"
|
||||
emit-value
|
||||
map-options
|
||||
@update:model-value="formRef.validate()"
|
||||
>
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
@@ -91,23 +101,32 @@
|
||||
v-model.trim="userMod.department"
|
||||
dense
|
||||
filled
|
||||
class = "w100 q-pt-sm"
|
||||
class = "w100 fix-bottom-padding q-pt-sm"
|
||||
:label = "$t('user_block__department')"
|
||||
:rules="[inputRules.middleText]"
|
||||
@update:model-value="formRef.validate()"
|
||||
debounce="300"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-model.trim="userMod.role"
|
||||
dense
|
||||
filled
|
||||
class = "w100 q-pt-sm"
|
||||
class = "w100 fix-bottom-padding q-pt-sm"
|
||||
:label = "$t('user_block__role')"
|
||||
:rules="[inputRules.middleText]"
|
||||
@update:model-value="formRef.validate()"
|
||||
debounce="300"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-model.trim="userMod.phone"
|
||||
dense
|
||||
filled
|
||||
class = "w100 q-pt-sm"
|
||||
class = "w100 fix-bottom-padding q-pt-sm"
|
||||
:rules="[inputRules.middleText]"
|
||||
@update:model-value="formRef.validate()"
|
||||
debounce="300"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-phone-outline"/>
|
||||
@@ -118,14 +137,16 @@
|
||||
v-model.trim="userMod.email"
|
||||
dense
|
||||
filled
|
||||
class = "w100 q-pt-sm"
|
||||
class = "w100 fix-bottom-padding q-pt-sm"
|
||||
:rules="[inputRules.middleText]"
|
||||
@update:model-value="formRef.validate()"
|
||||
debounce="300"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-email-outline"/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
</q-form>
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
@@ -133,14 +154,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue'
|
||||
import { ref, computed, reactive, onMounted } from 'vue'
|
||||
import { useCompaniesStore } from 'stores/companies'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useInputRules } from 'composables/useInputRules'
|
||||
import { removeNullKeys, getFieldValue } from 'utils/helpers'
|
||||
import { getUserStatus } from 'utils/users'
|
||||
import type { User } from 'types/User'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { inputRules } = useInputRules()
|
||||
|
||||
const props = defineProps<{
|
||||
title: string,
|
||||
@@ -184,6 +207,9 @@
|
||||
|
||||
const userStatus = computed(() => props.user ? getUserStatus(props.user) : null)
|
||||
|
||||
const formRef = ref()
|
||||
const isFormValid = ref(false)
|
||||
|
||||
function onSubmit() {
|
||||
const outData = {
|
||||
fullname: getFieldValue(userMod.fullname, props.user?.fullname),
|
||||
@@ -192,11 +218,15 @@
|
||||
role: getFieldValue(userMod.role, props.user?.role),
|
||||
phone: getFieldValue(userMod.phone, props.user?.phone),
|
||||
email: getFieldValue(userMod.email, props.user?.email),
|
||||
photo: userMod.photo
|
||||
}
|
||||
emit('update', removeNullKeys(outData))
|
||||
emit('update', removeNullKeys(outData, ['company_id']))
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await formRef.value.validate()
|
||||
if (!isFormValid.value) formRef.value.resetValidation()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user