before liquid glass
This commit is contained in:
BIN
i18n-2.xlsm
BIN
i18n-2.xlsm
Binary file not shown.
@@ -13,7 +13,6 @@
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { CompanyParams } from 'types/Company'
|
import type { CompanyParams } from 'types/Company'
|
||||||
|
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
const sseStore = useSSEStore()
|
const sseStore = useSSEStore()
|
||||||
@@ -24,7 +23,7 @@
|
|||||||
const tg = inject('tg') as WebApp
|
const tg = inject('tg') as WebApp
|
||||||
|
|
||||||
tg.onEvent('settingsButtonClicked', async () => {
|
tg.onEvent('settingsButtonClicked', async () => {
|
||||||
await router.push({ name: 'account' })
|
await router.push(authStore.isAuth ? { name: 'account' } : { name: 'settings' } )
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(isAuth, async (newVal: boolean) => {
|
watch(isAuth, async (newVal: boolean) => {
|
||||||
|
|||||||
41
src/assets/logo.svg
Normal file
41
src/assets/logo.svg
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.x { fill: white; transform-box: fill-box; animation: expand-r 6s ease-in-out infinite;}
|
||||||
|
@keyframes expand-r { 50% { r: 8px; } }
|
||||||
|
|
||||||
|
.c { fill: white; transform-box: fill-box; animation: o 6s ease-in-out infinite; }
|
||||||
|
@keyframes o { 50% { transform: var(--t); } }
|
||||||
|
|
||||||
|
.l {
|
||||||
|
stroke: white;
|
||||||
|
stroke-dasharray: 1;
|
||||||
|
stroke-dashoffset: 1;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
animation: draw 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes draw {
|
||||||
|
0%, 100% { stroke-dashoffset: 0; }
|
||||||
|
50% { stroke-dashoffset: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<defs>
|
||||||
|
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
|
||||||
|
<stop stop-color="#2AABEE" offset="0%"></stop>
|
||||||
|
<stop stop-color="#229ED9" offset="100%"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
|
||||||
|
<rect width="32" height="32" rx="4" ry="4" fill="url(#linearGradient-1)" />
|
||||||
|
<circle class="x" cx="16" cy="16" r="6" style="--t:scale(1.2)" />
|
||||||
|
<circle class="c" cx="7" cy="7" r="3" style="--t:translate(9px,9px)"/>
|
||||||
|
<circle class="c" cx="28" cy="4" r="2" style="--t:translate(-12px,12px)"/>
|
||||||
|
<circle class="c" cx="6" cy="26" r="4" style="--t:translate(10px,-10px)"/>
|
||||||
|
<circle class="c" cx="26" cy="26" r="5" style="--t:translate(-10px,-10px)"/>
|
||||||
|
<line class="l" x1="16" y1="16" x2="7" y2="7" pathLength="1"/>
|
||||||
|
<line class="l" x1="16" y1="16" x2="28" y2="4" pathLength="1"/>
|
||||||
|
<line class="l" x1="16" y1="16" x2="6" y2="26" pathLength="1"/>
|
||||||
|
<line class="l" x1="16" y1="16" x2="26" y2="26" pathLength="1"/>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -2,86 +2,19 @@
|
|||||||
<div
|
<div
|
||||||
class="flex row items-center no-wrap logo-component"
|
class="flex row items-center no-wrap logo-component"
|
||||||
>
|
>
|
||||||
<svg
|
<img
|
||||||
class="iconcolor"
|
src="src/assets/logo.svg"
|
||||||
viewBox="0 0 8.4666662 8.4666662"
|
style="height: 24px; max-width: 24px; padding-right: 2px;"
|
||||||
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>
|
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="text-brand"
|
class="text-brand"
|
||||||
style="margin-right: 0.075em;"
|
style="margin-right: 0.05em;"
|
||||||
>
|
>
|
||||||
tg
|
tg
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="text-brand2 text-bold">
|
<span class="text-brand text-bold">
|
||||||
Crew
|
Crew
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,24 +2,33 @@
|
|||||||
<svg
|
<svg
|
||||||
class="q-ma-none q-pa-none"
|
class="q-ma-none q-pa-none"
|
||||||
:style="{
|
:style="{
|
||||||
fill: color ?? 'red',
|
fill: color,
|
||||||
height: size ?? '42px',
|
height: size ?? '42px',
|
||||||
width: 'auto'
|
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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps({
|
withDefaults(defineProps<{
|
||||||
color: String,
|
color: string
|
||||||
size: String,
|
size: string // for example '24px'
|
||||||
})
|
}>(),
|
||||||
|
{
|
||||||
|
color: 'gold'
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.telegram-star-wrapper :deep(.telegram-star svg) {
|
|
||||||
fill: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -72,7 +72,33 @@
|
|||||||
</q-step>
|
</q-step>
|
||||||
|
|
||||||
<q-step
|
<q-step
|
||||||
|
v-if="type === 'register'"
|
||||||
:name="3"
|
: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')"
|
:title="$t('account_helper__set_password')"
|
||||||
>
|
>
|
||||||
<q-input
|
<q-input
|
||||||
@@ -111,7 +137,7 @@
|
|||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
@click="step=2"
|
@click="step= type === 'register' ? 3 : 2"
|
||||||
color="primary"
|
color="primary"
|
||||||
:label="$t('back')"
|
:label="$t('back')"
|
||||||
class="q-ml-sm"
|
class="q-ml-sm"
|
||||||
@@ -123,18 +149,21 @@
|
|||||||
<pn-magic-overlay
|
<pn-magic-overlay
|
||||||
v-if="showSuccessOverlay"
|
v-if="showSuccessOverlay"
|
||||||
icon="mdi-check-circle-outline"
|
icon="mdi-check-circle-outline"
|
||||||
:message1="getHelperMessage1()"
|
:message1="getHelperMessage.m1"
|
||||||
:message2="getHelperMessage2()"
|
:message2="getHelperMessage.m2"
|
||||||
route-name="projects"
|
:route-name="getRoute"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import type { AxiosError } from 'axios'
|
import type { AxiosError } from 'axios'
|
||||||
import { useI18n } from "vue-i18n"
|
|
||||||
import { QInput } from 'quasar'
|
import { QInput } from 'quasar'
|
||||||
import { useAuthStore, type AuthFlowType } from 'stores/auth'
|
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>(() => {
|
const flowType = computed<AuthFlowType>(() => {
|
||||||
return props.type === 'register'
|
return props.type === 'register'
|
||||||
@@ -146,7 +175,6 @@
|
|||||||
: 'changeMethod'
|
: 'changeMethod'
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -154,8 +182,7 @@
|
|||||||
email?: string
|
email?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
type ValidationRule = (val: string) => boolean | string
|
type Step = 1 | 2 | 3 | 4
|
||||||
type Step = 1 | 2 | 3
|
|
||||||
|
|
||||||
const step = ref<Step>(1)
|
const step = ref<Step>(1)
|
||||||
const login = ref<string>(props.email || '')
|
const login = ref<string>(props.email || '')
|
||||||
@@ -164,10 +191,16 @@
|
|||||||
const showSuccessOverlay = ref(false)
|
const showSuccessOverlay = ref(false)
|
||||||
const isPwd = ref<boolean>(true)
|
const isPwd = ref<boolean>(true)
|
||||||
const validationRules = {
|
const validationRules = {
|
||||||
email: [(val: string) => /.+@.+\..+/.test(val) || t('login__incorrect_email')] as [ValidationRule],
|
email: [inputRules.email],
|
||||||
password: [(val: string) => val.length >= 8 || t('login__password_require')] as [ValidationRule]
|
password: [inputRules.password]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRoute = computed(() => {
|
||||||
|
return (props.type === 'changeMethod' || props.type === 'changePwd')
|
||||||
|
? 'account'
|
||||||
|
: 'projects'
|
||||||
|
})
|
||||||
|
|
||||||
const isEmailValid = computed(() =>
|
const isEmailValid = computed(() =>
|
||||||
validationRules.email.every(f => f(login.value) === true)
|
validationRules.email.every(f => f(login.value) === true)
|
||||||
)
|
)
|
||||||
@@ -177,30 +210,36 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
const passwordHint = computed(() => {
|
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 : ''
|
return typeof result === 'string' ? result : ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const stepActions: Record<Step, () => Promise<void>> = {
|
const stepActions: Record<Step, () => Promise<void> | void> = {
|
||||||
1: async () => {
|
1: async () => {
|
||||||
await authStore.initRegistration(flowType.value, login.value)
|
await authStore.initRegistration(flowType.value, login.value)
|
||||||
},
|
},
|
||||||
2: async () => {
|
2: async () => {
|
||||||
await authStore.confirmCode(flowType.value, login.value, code.value)
|
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)
|
await authStore.setPassword(flowType.value, login.value, code.value, password.value)
|
||||||
if (flowType.value === 'register' || flowType.value === 'changeMethod') {
|
if (flowType.value === 'register' || flowType.value === 'changeMethod') {
|
||||||
await authStore.loginWithCredentials(login.value, password.value)
|
await authStore.loginWithCredentials(login.value, password.value)
|
||||||
}
|
}
|
||||||
|
if (flowType.value === 'register') {
|
||||||
|
acceptSubmit.value = true
|
||||||
|
sessionStorage.removeItem('account-helper')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
await stepActions[step.value]()
|
await stepActions[step.value]()
|
||||||
if (step.value < 3) {
|
if (step.value < 4) {
|
||||||
step.value++
|
step.value++
|
||||||
|
if (step.value === 3 && props.type !== 'register') step.value++
|
||||||
} else {
|
} else {
|
||||||
showSuccessOverlay.value = true
|
showSuccessOverlay.value = true
|
||||||
}
|
}
|
||||||
@@ -209,26 +248,44 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getHelperMessage1 = () => {
|
const getHelperMessage = computed(() => {
|
||||||
switch (flowType.value) {
|
switch (flowType.value) {
|
||||||
case 'register': return 'account_helper__register_message1'
|
case 'register': return { m1: 'account_helper__register_message1', m2: 'slogan' }
|
||||||
case 'forgot': return 'account_helper__forgot_password_message1'
|
case 'forgot': return { m1: 'account_helper__forgot_password_message1', m2: 'account_helper__go_projects' }
|
||||||
case 'changePwd': return 'account_helper__change_password_message1'
|
case 'changePwd': return { m1: 'account_helper__change_password_message1', m2: '' }
|
||||||
case 'changeMethod': return 'account_helper__change_method_message1'
|
case 'changeMethod': return { m1:'account_helper__change_method_message1', m2: '' }
|
||||||
default: return ''
|
default: return { m1: '', m2: '' }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const getHelperMessage2 = () => {
|
const agreement = ref<('terms'| 'consent')[]>([])
|
||||||
switch (flowType.value) {
|
const acceptSubmit = ref(false)
|
||||||
case 'register': return 'slogan'
|
onMounted(() => {
|
||||||
case 'forgot':
|
if (props.type !== 'register') return
|
||||||
case 'changePwd':
|
|
||||||
case 'changeMethod':
|
const ah = sessionStorage.getItem('account-helper')
|
||||||
return 'account_helper__go_projects'
|
if (!ah) return
|
||||||
default: 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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
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) }}
|
{{ $t(btnText) }}
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<pn-scroll-list>
|
<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"/>
|
<slot name="myCompany"/>
|
||||||
|
|
||||||
<pn-image-selector
|
<pn-image-selector
|
||||||
v-model="companyMod.logo"
|
v-model="companyMod.logo"
|
||||||
:size="100"
|
:size="100"
|
||||||
:iconsize="80"
|
:iconsize="80"
|
||||||
class="q-pb-lg"
|
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
|
<q-input
|
||||||
v-for="input in textInputs"
|
v-for="input in textInputs"
|
||||||
:key="input.id"
|
:key="input.id"
|
||||||
v-model.trim="companyMod[input.val]"
|
:model-value="companyMod[input.val]"
|
||||||
|
@update:model-value="handleInput(input.val, $event, input.useTrim)"
|
||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
class="w100"
|
class="w100 fix-bottom-padding"
|
||||||
|
:class="input.val === 'name' ? '' : 'q-pt-sm'"
|
||||||
:autogrow="input.val === 'description' || input.val === 'address'"
|
: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"
|
:label="input.label ? $t(input.label) : void 0"
|
||||||
:rules="input.rules"
|
:rules="input.rules"
|
||||||
no-error-icon
|
no-error-icon
|
||||||
:label-slot="Boolean(input.label)"
|
:label-slot="Boolean(input.label)"
|
||||||
|
debounce="300"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<q-icon v-if="input.icon" :name="input.icon"/>
|
<q-icon v-if="input.icon" :name="input.icon"/>
|
||||||
@@ -50,19 +55,19 @@
|
|||||||
<span v-if="input.val === 'name'" class="text-red">*</span>
|
<span v-if="input.val === 'name'" class="text-red">*</span>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</q-form>
|
||||||
</div>
|
</div>
|
||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
</pn-page-card>
|
</pn-page-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive } from 'vue'
|
import { ref, reactive, onMounted, } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import type { CompanyParams } from 'types/Company'
|
|
||||||
import { removeNullKeys, getFieldValue } from 'utils/helpers'
|
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<{
|
const props = defineProps<{
|
||||||
title: string,
|
title: string,
|
||||||
@@ -92,32 +97,25 @@
|
|||||||
icon?: string
|
icon?: string
|
||||||
val: keyof CompanyParams
|
val: keyof CompanyParams
|
||||||
rules: ((value: string) => boolean | string)[]
|
rules: ((value: string) => boolean | string)[]
|
||||||
}
|
useTrim: boolean
|
||||||
|
|
||||||
const rules = {
|
|
||||||
name: (val: string) => !!val?.trim() || rulesErrorMessage['name']
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const textInputs: TextInput[] = [
|
const textInputs: TextInput[] = [
|
||||||
{ id: 1, val: 'name', label: 'company_block__name', rules: [rules.name] },
|
{ id: 1, val: 'name', label: 'company_block__name', rules: [inputRules.require, inputRules.middleText], useTrim: true },
|
||||||
{ id: 2, val: 'description', label: 'company_block__description', rules: [] },
|
{ id: 2, val: 'description', label: 'company_block__description', rules: [inputRules.longText], useTrim: false },
|
||||||
{ id: 3, val: 'site', icon: 'mdi-web', rules: [] },
|
{ id: 3, val: 'site', icon: 'mdi-web', rules: [inputRules.middleText], useTrim: true },
|
||||||
{ id: 4, val: 'address', icon: 'mdi-map-marker-outline', rules: [] },
|
{ id: 4, val: 'address', icon: 'mdi-map-marker-outline', rules: [inputRules.longText], useTrim: false },
|
||||||
{ id: 5, val: 'phone', icon: 'mdi-phone-outline', rules: [] },
|
{ id: 5, val: 'phone', icon: 'mdi-phone-outline', rules: [inputRules.middleText], useTrim: true },
|
||||||
{ id: 6, val: 'email', icon: 'mdi-email-outline', rules: [] }
|
{ id: 6, val: 'email', icon: 'mdi-email-outline', rules: [inputRules.middleText], useTrim: true }
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
const rulesErrorMessage = {
|
const handleInput = (field: keyof CompType, value: string | null | number, useTrim: boolean) => {
|
||||||
name: t('company_block__error_name')
|
companyMod[field] = typeof value === 'string' ? (useTrim ? value.trim() : value) : value
|
||||||
|
if (formRef.value) formRef.value.validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
const formRef = ref()
|
||||||
const validations = {
|
const isFormValid = ref(false)
|
||||||
name: companyMod.name && rules.name(companyMod.name) === true
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.values(validations).every(Boolean)
|
|
||||||
})
|
|
||||||
|
|
||||||
function onSubmit() {
|
function onSubmit() {
|
||||||
// companyMod.description! - ! on end for TS, i promise: params !== null | undefined
|
// companyMod.description! - ! on end for TS, i promise: params !== null | undefined
|
||||||
@@ -128,11 +126,16 @@
|
|||||||
address: getFieldValue(companyMod.address!, props.company?.address),
|
address: getFieldValue(companyMod.address!, props.company?.address),
|
||||||
phone: getFieldValue(companyMod.phone!, props.company?.phone),
|
phone: getFieldValue(companyMod.phone!, props.company?.phone),
|
||||||
email: getFieldValue(companyMod.email!, props.company?.email),
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="background-canvas-wrapper" class="flex fit column">
|
<div id="background-canvas-wrapper" class="flex fit column bg-base">
|
||||||
<canvas id="canvas"/>
|
<canvas id="canvas"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -8,8 +8,6 @@
|
|||||||
|
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const canvasBody = document.getElementById("canvas")
|
const canvasBody = document.getElementById("canvas")
|
||||||
const drawArea = canvasBody.getContext("2d")
|
const drawArea = canvasBody.getContext("2d")
|
||||||
@@ -39,9 +37,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
function deBouncer () {
|
function deBouncer () {
|
||||||
clearTimeout(tid)
|
clearTimeout(tid)
|
||||||
tid = setTimeout(function() {
|
tid = setTimeout(function () { resizeReset() }, delay)
|
||||||
resizeReset()
|
|
||||||
}, delay)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkDistance (x1, y1, x2, y2) {
|
function checkDistance (x1, y1, x2, y2) {
|
||||||
@@ -49,28 +45,26 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setup () {
|
function setup () {
|
||||||
|
|
||||||
resizeReset()
|
resizeReset()
|
||||||
for (let i = 0; i < opts.particleAmount; i++){
|
for (let i = 0; i < opts.particleAmount; i++) {
|
||||||
particles.push(new Particle())
|
particles.push(new Particle())
|
||||||
}
|
}
|
||||||
window.requestAnimationFrame(loop)
|
window.requestAnimationFrame(loop)
|
||||||
}
|
}
|
||||||
|
|
||||||
function loop() {
|
function loop() {
|
||||||
window.requestAnimationFrame(loop)
|
window.requestAnimationFrame(loop)
|
||||||
drawArea.clearRect(0, 0, w, h)
|
drawArea.clearRect(0, 0, w, h)
|
||||||
for (let i = 0; i < particles.length; i++){
|
for (let i = 0; i < particles.length; i++) {
|
||||||
particles[i].update()
|
particles[i].update()
|
||||||
particles[i].draw()
|
particles[i].draw()
|
||||||
}
|
}
|
||||||
for (let i = 0; i < particles.length; i++){
|
for (let i = 0; i < particles.length; i++) {
|
||||||
linkPoints(particles[i], particles)
|
linkPoints(particles[i], particles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function linkPoints (point1, hubs) {
|
||||||
function linkPoints (point1, hubs){
|
|
||||||
for (let i = 0; i < hubs.length; i++) {
|
for (let i = 0; i < hubs.length; i++) {
|
||||||
const distance = checkDistance(point1.x, point1.y, hubs[i].x, hubs[i].y)
|
const distance = checkDistance(point1.x, point1.y, hubs[i].x, hubs[i].y)
|
||||||
const opacity = 1 - distance / opts.linkRadius
|
const opacity = 1 - distance / opts.linkRadius
|
||||||
@@ -84,9 +78,9 @@ function linkPoints (point1, hubs){
|
|||||||
drawArea.stroke()
|
drawArea.stroke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Particle () {
|
function Particle () {
|
||||||
this.x = Math.random() * w
|
this.x = Math.random() * w
|
||||||
this.y = Math.random() * h
|
this.y = Math.random() * h
|
||||||
this.speed = opts.defaultSpeed + Math.random() * opts.variantSpeed
|
this.speed = opts.defaultSpeed + Math.random() * opts.variantSpeed
|
||||||
@@ -96,36 +90,36 @@ function Particle () {
|
|||||||
this.vector = {
|
this.vector = {
|
||||||
x: Math.cos(this.directionAngle) * this.speed,
|
x: Math.cos(this.directionAngle) * this.speed,
|
||||||
y: Math.sin(this.directionAngle) * this.speed
|
y: Math.sin(this.directionAngle) * this.speed
|
||||||
};
|
}
|
||||||
this.update = function(){
|
this.update = function () {
|
||||||
this.border();
|
this.border()
|
||||||
this.x += this.vector.x
|
this.x += this.vector.x
|
||||||
this.y += this.vector.y
|
this.y += this.vector.y
|
||||||
};
|
}
|
||||||
this.border = function(){
|
this.border = function () {
|
||||||
if (this.x >= w || this.x <= 0) {
|
if (this.x >= w || this.x <= 0) {
|
||||||
this.vector.x *= -1;
|
this.vector.x *= -1
|
||||||
}
|
}
|
||||||
if (this.y >= h || this.y <= 0) {
|
if (this.y >= h || this.y <= 0) {
|
||||||
this.vector.y *= -1;
|
this.vector.y *= -1
|
||||||
}
|
}
|
||||||
if (this.x > w) this.x = w
|
if (this.x > w) this.x = w
|
||||||
if (this.y > h) this.y = h
|
if (this.y > h) this.y = h
|
||||||
if (this.x < 0) this.x = 0
|
if (this.x < 0) this.x = 0
|
||||||
if (this.y < 0) this.y = 0
|
if (this.y < 0) this.y = 0
|
||||||
}
|
}
|
||||||
this.draw = function(){
|
this.draw = function () {
|
||||||
drawArea.beginPath()
|
drawArea.beginPath()
|
||||||
drawArea.arc(this.x, this.y, this.radius, 0, Math.PI*2)
|
drawArea.arc(this.x, this.y, this.radius, 0, Math.PI*2)
|
||||||
drawArea.closePath()
|
drawArea.closePath()
|
||||||
drawArea.fillStyle = this.color
|
drawArea.fillStyle = this.color
|
||||||
drawArea.fill()
|
drawArea.fill()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("resize", function(){ deBouncer() })
|
window.addEventListener("resize", function () { deBouncer() })
|
||||||
resizeReset()
|
resizeReset()
|
||||||
setup()
|
setup()
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -137,7 +131,6 @@ setup()
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
background: var(--q-primary);
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<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
|
<q-btn
|
||||||
v-if="showCalendarBtn"
|
v-if="showCalendarBtn"
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
filled
|
filled
|
||||||
:placeholder="$t(placeholder)"
|
:placeholder="$t(placeholder)"
|
||||||
dense
|
dense
|
||||||
class="col-grow q-pt-xs q-mx-sm"
|
class="col-grow q-mx-sm"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<q-icon name="mdi-magnify" color="grey"/>
|
<q-icon name="mdi-magnify" color="grey"/>
|
||||||
|
|||||||
@@ -53,40 +53,3 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
</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="
|
style="
|
||||||
border-top-left-radius: var(--top-radius) !important;
|
border-top-left-radius: var(--top-radius) !important;
|
||||||
border-top-right-radius: var(--top-radius) !important;
|
border-top-right-radius: var(--top-radius) !important;
|
||||||
|
max-height: calc(100vh - 48px);
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -22,7 +23,7 @@
|
|||||||
<slot name="btnSlot">
|
<slot name="btnSlot">
|
||||||
<q-btn
|
<q-btn
|
||||||
icon="mdi-close"
|
icon="mdi-close"
|
||||||
@click="modelValue=false"
|
@click="modelValue = false"
|
||||||
flat round
|
flat round
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
@@ -36,25 +37,28 @@
|
|||||||
:hideShadows="false"
|
:hideShadows="false"
|
||||||
:height="bodyHeight"
|
: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" />
|
<q-resize-observer @resize="updateDimensions" />
|
||||||
<slot/>
|
<slot/>
|
||||||
</div>
|
</div>
|
||||||
</pn-shadow-scroll>
|
</pn-shadow-scroll>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref="cardFooterRef"
|
style="overflow: hidden"
|
||||||
class="q-pa-md"
|
:style="{ height: footerHeight + 'px'}"
|
||||||
>
|
>
|
||||||
|
<div ref="cardFooterInnerRef" class="q-pa-md">
|
||||||
|
<q-resize-observer @resize="updateDimensions" />
|
||||||
<slot name="footer"/>
|
<slot name="footer"/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch, nextTick } from 'vue'
|
||||||
import { useSlots } from 'vue'
|
import { useSlots } from 'vue'
|
||||||
|
|
||||||
const modelValue = defineModel<boolean>({
|
const modelValue = defineModel<boolean>({
|
||||||
@@ -67,8 +71,7 @@
|
|||||||
|
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
const cardHeaderRef = ref<HTMLElement | null>(null)
|
const cardHeaderRef = ref<HTMLElement | null>(null)
|
||||||
const cardFooterRef = ref<HTMLElement | null>(null)
|
const cardFooterInnerRef = ref<HTMLElement | null>(null)
|
||||||
const cardBodyRef = ref<HTMLElement | null>(null)
|
|
||||||
const cardBodyInnerRef = ref<HTMLElement | null>(null)
|
const cardBodyInnerRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
const headerHeight = ref(0)
|
const headerHeight = ref(0)
|
||||||
@@ -76,17 +79,20 @@
|
|||||||
const bodyInnerHeight = ref(0)
|
const bodyInnerHeight = ref(0)
|
||||||
const bodyHeight = ref(0)
|
const bodyHeight = ref(0)
|
||||||
|
|
||||||
const updateDimensions = () => {
|
const updateDimensions = async () => {
|
||||||
|
await nextTick(() => {
|
||||||
headerHeight.value = cardHeaderRef.value?.offsetHeight || 0
|
headerHeight.value = cardHeaderRef.value?.offsetHeight || 0
|
||||||
footerHeight.value = cardFooterRef.value?.offsetHeight || 0
|
footerHeight.value = cardFooterInnerRef.value?.offsetHeight || 0
|
||||||
bodyInnerHeight.value = cardBodyInnerRef.value?.offsetHeight || 0
|
bodyInnerHeight.value = cardBodyInnerRef.value?.offsetHeight || 0
|
||||||
const needScroll = headerHeight.value + footerHeight.value + bodyInnerHeight.value > window.innerHeight - 48
|
const needScroll = headerHeight.value + footerHeight.value + bodyInnerHeight.value > window.innerHeight - 48
|
||||||
bodyHeight.value = needScroll
|
bodyHeight.value = needScroll
|
||||||
? window.innerHeight - 48 - headerHeight.value - footerHeight.value
|
? window.innerHeight - 48 - headerHeight.value - footerHeight.value
|
||||||
: bodyInnerHeight.value + 16
|
: bodyInnerHeight.value + 16
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => slots.body?.(), updateDimensions, { flush: 'post' })
|
watch(() => slots.body?.(), updateDimensions, { flush: 'post' })
|
||||||
|
watch(modelValue, updateDimensions)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar
|
<q-icon :name="chat.icon" :color="chat.color" size="md"/>
|
||||||
rounded
|
|
||||||
text-color="white"
|
|
||||||
:icon="chatType.icon"
|
|
||||||
:color="chatType.color"
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>
|
<q-item-label>
|
||||||
<div class="row no-wrap items-center justify-between">
|
<div class="row no-wrap items-center justify-between">
|
||||||
{{ $t(chatType.label) }}
|
{{ $t(chat.label) }}
|
||||||
<span class="text-caption text-grey" v-if="time">
|
<span class="text-caption text-grey text-no-wrap" v-if="time">
|
||||||
{{ date.formatDate(time * 1000, 'DD MMM YYYY') }}
|
{{ getDate(time) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label class="text-grey text-caption">
|
<q-item-label class="text-grey text-caption">
|
||||||
{{ $t(chatType.description) }}
|
{{ $t(chat.description) }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { chatAccess, chatState } from 'utils/chats'
|
||||||
import { getChatType } from 'utils/chats'
|
|
||||||
import { date } from 'quasar'
|
import { date } from 'quasar'
|
||||||
import type { ChatType } from 'types/Chat'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = defineProps<{
|
||||||
type: ChatType
|
access?: 0 | 1 | 2
|
||||||
|
state?: 1 | 2 | 3 | 4
|
||||||
time?: number
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
:style="{ display: 'none' }"
|
:style="{ display: 'none' }"
|
||||||
@update:model-value="handleUpload()"
|
@update:model-value="handleUpload()"
|
||||||
:filter="checkImgType"
|
:filter="checkImgType"
|
||||||
accept="image/*"
|
:accept="allowedMimeTypes.join(', ')"
|
||||||
/>
|
/>
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="modelValue === '' || modelValue === undefined || modelValue === null"
|
v-if="modelValue === '' || modelValue === undefined || modelValue === null"
|
||||||
@@ -37,21 +37,34 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-dialog v-model="showDialog">
|
<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-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
|
<div
|
||||||
class="flex row items-center jutsify-center q-pb-sm"
|
class="flex no-wrap items-center justify-center q-pb-sm absolute-bottom w100"
|
||||||
style="bottom: 0; position: absolute; left: 50%; transform: translate(-50%, 0%);">
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-for="btn in menuBtns"
|
v-for="btn in menuBtns"
|
||||||
:key="btn.name"
|
:key="btn.name"
|
||||||
:icon="btn.icon"
|
:color="btn.color"
|
||||||
@click="btn.f"
|
@click="btn.f"
|
||||||
class="q-mx-xs bg-white"
|
class="q-mx-sm"
|
||||||
round flat
|
rounded flat
|
||||||
style="opacity: 0.8"
|
style="background-color: rgba(255, 255, 255, 0.65) !important;"
|
||||||
color="primary"
|
>
|
||||||
/>
|
{{ $t(btn.label) }}
|
||||||
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
@@ -72,10 +85,10 @@
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const compressionOptions = {
|
const compressionOptions = {
|
||||||
maxSizeMB: 0.5, // Максимальный размер ~500 КБ
|
maxSizeMB: 0.5, // Max size ~500 KB
|
||||||
maxWidthOrHeight: 1200, // Максимальное разрешение
|
maxWidthOrHeight: 1200, // Max resolution
|
||||||
useWebWorker: true, // Для производительности
|
useWebWorker: true, // Performance
|
||||||
fileType: 'image/jpeg' // Формат вывода
|
fileType: 'image/jpeg' // Output format
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageFile = ref(null) // file-from selector
|
const imageFile = ref(null) // file-from selector
|
||||||
@@ -83,10 +96,10 @@
|
|||||||
const size = ref<number>(props.size ? props.size : 100)
|
const size = ref<number>(props.size ? props.size : 100)
|
||||||
const iconsize = ref<number>(props.iconsize ? props.iconsize : 75)
|
const iconsize = ref<number>(props.iconsize ? props.iconsize : 75)
|
||||||
const showDialog = ref<boolean>(false)
|
const showDialog = ref<boolean>(false)
|
||||||
|
|
||||||
const menuBtns = [
|
const menuBtns = [
|
||||||
{ name: 'change', icon: 'mdi-swap-horizontal', f: imgFileSelectorClick },
|
{ name: 'delete', color: 'negative', label: 'image_selector__delete', f: deleteImage },
|
||||||
{ name: 'delete', icon: 'mdi-delete-outline', f: deleteImage },
|
{ name: 'change', color: 'primary', label: 'image_selector__change', f: imgFileSelectorClick }
|
||||||
{ name: 'close', icon: 'mdi-close', f: () => showDialog.value = false }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const sizePx = computed(() => {
|
const sizePx = computed(() => {
|
||||||
@@ -99,14 +112,14 @@
|
|||||||
const compressedFile = await imageCompression(
|
const compressedFile = await imageCompression(
|
||||||
imageFile.value,
|
imageFile.value,
|
||||||
compressionOptions
|
compressionOptions
|
||||||
);
|
)
|
||||||
|
|
||||||
const img = await imgToBase64(compressedFile);
|
const img = await imgToBase64(compressedFile);
|
||||||
modelValue.value = typeof img === 'string' ? img : '';
|
modelValue.value = typeof img === 'string' ? img : ''
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Image error compression:', error);
|
console.error('Image error compression:', error)
|
||||||
const img = await imgToBase64(imageFile.value);
|
const img = await imgToBase64(imageFile.value)
|
||||||
modelValue.value = typeof img === 'string' ? img : '';
|
modelValue.value = typeof img === 'string' ? img : ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,21 +144,20 @@
|
|||||||
reject(new Error('Something went wrong'))
|
reject(new Error('Something went wrong'))
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.onload = () => {
|
reader.onload = () => resolve(reader.result)
|
||||||
resolve(reader.result)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkImgType(files: File[]): File[] {
|
const allowedMimeTypes = [
|
||||||
const allowedTypes = [
|
|
||||||
'image/jpeg',
|
'image/jpeg',
|
||||||
'image/png',
|
'image/png',
|
||||||
|
'image/bmp',
|
||||||
'image/webp',
|
'image/webp',
|
||||||
'image/gif',
|
'image/gif'
|
||||||
'image/bmp'
|
|
||||||
]
|
]
|
||||||
return files.filter((file: File) => allowedTypes.includes(file.type))
|
|
||||||
|
function checkImgType (files: File[]): File[] {
|
||||||
|
return files.filter((file: File) => allowedMimeTypes.includes(file.type))
|
||||||
}
|
}
|
||||||
</script>
|
</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,23 +42,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
icon: string
|
icon: string
|
||||||
message1: string
|
message1: string
|
||||||
message2?: string
|
message2?: string
|
||||||
routeName: string
|
routeName: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
const showIcon = ref(false)
|
const showIcon = ref(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const timers = ref<number[]>([])
|
const timers = ref<number[]>([])
|
||||||
const size = 160
|
const size = 160
|
||||||
|
|
||||||
const setupTimers = () => {
|
const setupTimers = () => {
|
||||||
visible.value = true
|
visible.value = true
|
||||||
const timer1 = window.setTimeout(() => {
|
const timer1 = window.setTimeout(() => {
|
||||||
showIcon.value = true
|
showIcon.value = true
|
||||||
@@ -69,21 +69,21 @@ const setupTimers = () => {
|
|||||||
showIcon.value = false
|
showIcon.value = false
|
||||||
}, 2000)
|
}, 2000)
|
||||||
timers.value.push(timer2)
|
timers.value.push(timer2)
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearTimers = () => {
|
const clearTimers = () => {
|
||||||
timers.value.forEach(timer => clearTimeout(timer))
|
timers.value.forEach(timer => clearTimeout(timer))
|
||||||
timers.value = []
|
timers.value = []
|
||||||
visible.value = false
|
visible.value = false
|
||||||
showIcon.value = false
|
showIcon.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(visible, async (newVal) => {
|
watch(visible, async (newVal) => {
|
||||||
if (newVal === false) await router.push({ name: props.routeName })
|
if (newVal === false) await router.push({ name: props.routeName })
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(setupTimers)
|
onMounted(setupTimers)
|
||||||
onUnmounted(clearTimers)
|
onUnmounted(clearTimers)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -2,22 +2,21 @@
|
|||||||
<div class="flex w100 column q-pt-xl q-pa-md">
|
<div class="flex w100 column q-pt-xl q-pa-md">
|
||||||
<div class="flex column justify-center col-grow items-center text-grey">
|
<div class="flex column justify-center col-grow items-center text-grey">
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat no-caps
|
||||||
no-caps
|
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
v-if="!noBtn"
|
v-if="!noBtn"
|
||||||
>
|
>
|
||||||
<div class="flex column justify-center col-grow items-center">
|
<div class="flex column justify-center col-grow items-center">
|
||||||
<q-icon :name="icon" size="160px" class="q-pb-md"/>
|
<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}}
|
{{ message1 }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="flex column justify-center col-grow items-center">
|
<div class="flex column justify-center col-grow items-center">
|
||||||
<q-icon :name="icon" size="160px" class="q-pb-md"/>
|
<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 }}
|
{{ message1 }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,17 +11,28 @@
|
|||||||
<q-resize-observer @resize="onHeaderResize"/>
|
<q-resize-observer @resize="onHeaderResize"/>
|
||||||
<slot name="card-body-header"/>
|
<slot name="card-body-header"/>
|
||||||
</div>
|
</div>
|
||||||
<div id="card-body"
|
<div
|
||||||
class="top-rounded-card bg-white"
|
id="card-body"
|
||||||
|
class="top-rounded-card bg-white column no-wrap"
|
||||||
>
|
>
|
||||||
<div style="min-height: var(--top-radius);">
|
<div style="min-height: var(--top-radius);">
|
||||||
<slot name="card-body-btn"/>
|
<slot name="card-body-btn"/>
|
||||||
<q-resize-observer @resize="onBodyBtnResize"/>
|
<q-resize-observer @resize="onBodyBtnResize"/>
|
||||||
</div>
|
</div>
|
||||||
<div style="height:100%">
|
<div class="column col col-grow">
|
||||||
<q-resize-observer @resize="onBodyResize"/>
|
<q-resize-observer @resize="onBodyResize"/>
|
||||||
<pn-shadow-scroll :hideShadows="isResizing" :height="scrollAreaHeight">
|
<pn-shadow-scroll
|
||||||
|
:hideShadows="isResizing"
|
||||||
|
:height="scrollAreaHeight"
|
||||||
|
@updateScrollPosition="onScrollUpdate"
|
||||||
|
:scrollPosition
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w100 column"
|
||||||
|
:style="{ minHeight: scrollAreaHeight + 'px' }"
|
||||||
|
>
|
||||||
<slot/>
|
<slot/>
|
||||||
|
</div>
|
||||||
</pn-shadow-scroll>
|
</pn-shadow-scroll>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,6 +42,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, nextTick, useSlots, computed } from 'vue'
|
import { ref, watch, nextTick, useSlots, computed } from 'vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['updateScrollPosition', 'updateHeight'])
|
||||||
|
const onScrollUpdate = (pos: number) => emit('updateScrollPosition', pos)
|
||||||
|
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
// work only slot no use in component
|
// work only slot no use in component
|
||||||
const hasHeader = computed(() => !!slots['card-body-header'])
|
const hasHeader = computed(() => !!slots['card-body-header'])
|
||||||
@@ -38,8 +52,10 @@
|
|||||||
// need if slot draw by expression with v-if
|
// need if slot draw by expression with v-if
|
||||||
withDefaults(defineProps<{
|
withDefaults(defineProps<{
|
||||||
hideHeader?: boolean
|
hideHeader?: boolean
|
||||||
|
scrollPosition?: number
|
||||||
}>(), {
|
}>(), {
|
||||||
hideHeader: false
|
hideHeader: false,
|
||||||
|
scrollPosition: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const heightCard = ref(100)
|
const heightCard = ref(100)
|
||||||
@@ -69,6 +85,7 @@
|
|||||||
async function updateScrollAreaHeight() {
|
async function updateScrollAreaHeight() {
|
||||||
await nextTick(() => {
|
await nextTick(() => {
|
||||||
scrollAreaHeight.value = Math.max(0, heightCard.value)
|
scrollAreaHeight.value = Math.max(0, heightCard.value)
|
||||||
|
emit('updateHeight', scrollAreaHeight.value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<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
|
<q-scroll-area
|
||||||
|
ref="scrollAreaRef"
|
||||||
:style="{ height: height + 'px' }"
|
: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"
|
@scroll="onScroll"
|
||||||
:class=" {
|
:class=" {
|
||||||
'shadow-top': hasScrolled,
|
'shadow-top': hasScrolled,
|
||||||
@@ -10,19 +11,21 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<slot/>
|
<slot/>
|
||||||
<div class="q-pa-sm"/>
|
|
||||||
</q-scroll-area>
|
</q-scroll-area>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, onActivated, watch } from 'vue'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
height: number
|
height: number
|
||||||
hideShadows: boolean
|
hideShadows: boolean
|
||||||
|
scrollPosition?: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['updateScrollPosition'])
|
||||||
|
|
||||||
const hasScrolled = ref(false)
|
const hasScrolled = ref(false)
|
||||||
const hasScrolledBottom = ref(false)
|
const hasScrolledBottom = ref(false)
|
||||||
|
|
||||||
@@ -33,12 +36,23 @@
|
|||||||
verticalContainerSize: number;
|
verticalContainerSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onScroll(info: ScrollInfo) {
|
function onScroll (info: ScrollInfo) {
|
||||||
hasScrolled.value = info.verticalPosition > 0
|
hasScrolled.value = info.verticalPosition > 0
|
||||||
const scrollEnd = info.verticalPosition + info.verticalContainerSize >= info.verticalSize - 1
|
const scrollEnd = info.verticalPosition + info.verticalContainerSize >= info.verticalSize - 1
|
||||||
hasScrolledBottom.value = !scrollEnd
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -87,6 +101,10 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fix-scroll-area-content-width:deep(.q-scrollarea__content) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.fix-scroll-area-content:deep(.q-scrollarea::before) {
|
.fix-scroll-area-content:deep(.q-scrollarea::before) {
|
||||||
content: none;
|
content: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog v-model="modelValue">
|
<q-dialog v-model="modelValue" class="q-ma-none">
|
||||||
<q-card
|
<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">
|
<q-card-section class="row items-center q-pb-none">
|
||||||
<div class="flex no-wrap items-center w100">
|
<div class="flex no-wrap items-center w100">
|
||||||
@@ -62,10 +63,10 @@
|
|||||||
window.innerHeight - headerHeight.value - 64
|
window.innerHeight - headerHeight.value - 64
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(bodyHeight, updateScrollAreaHeight)
|
watch(bodyHeight, updateScrollAreaHeight)
|
||||||
watch(headerHeight, updateScrollAreaHeight)
|
watch(headerHeight, updateScrollAreaHeight)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog v-model="modelValue">
|
<q-dialog v-model="modelValue">
|
||||||
<q-card
|
<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"
|
align="center"
|
||||||
>
|
>
|
||||||
<q-card-section>
|
<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>
|
</template>
|
||||||
|
|
||||||
<pn-scroll-list>
|
<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
|
<pn-image-selector
|
||||||
v-model="projectMod.logo"
|
v-model="projectMod.logo"
|
||||||
:size="100"
|
:size="100"
|
||||||
@@ -23,15 +23,23 @@
|
|||||||
type="rounded"
|
type="rounded"
|
||||||
class="q-pb-lg"
|
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
|
<q-input
|
||||||
v-model="projectMod.name"
|
v-model.trim="projectMod.name"
|
||||||
no-error-icon
|
no-error-icon
|
||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
label-slot
|
label-slot
|
||||||
class = "w100 fix-bottom-padding"
|
class = "w100 fix-bottom-padding"
|
||||||
:rules="[rules.name]"
|
:rules="[inputRules.require, inputRules.middleText]"
|
||||||
|
@update:model-value="formRef.validate()"
|
||||||
|
debounce="300"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
{{ $t('project_block__project_name') }} <span class="text-red">*</span>
|
{{ $t('project_block__project_name') }} <span class="text-red">*</span>
|
||||||
@@ -43,10 +51,14 @@
|
|||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
autogrow
|
autogrow
|
||||||
|
no-error-icon
|
||||||
class="w100 q-pt-sm"
|
class="w100 q-pt-sm"
|
||||||
:label="$t('project_block__project_description')"
|
:label="$t('project_block__project_description')"
|
||||||
|
:rules="[inputRules.longText]"
|
||||||
|
@update:model-value="formRef.validate()"
|
||||||
|
debounce="300"
|
||||||
/>
|
/>
|
||||||
</div>
|
</q-form>
|
||||||
</div>
|
</div>
|
||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
</pn-page-card>
|
</pn-page-card>
|
||||||
@@ -54,12 +66,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useInputRules } from 'composables/useInputRules'
|
||||||
import type { ProjectParams } from 'types/Project'
|
import type { ProjectParams } from 'types/Project'
|
||||||
import { removeNullKeys, getFieldValue } from 'utils/helpers'
|
import { removeNullKeys, getFieldValue } from 'utils/helpers'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { inputRules } = useInputRules()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
title: string,
|
title: string,
|
||||||
@@ -75,31 +87,22 @@
|
|||||||
logo: props.project?.logo ?? null
|
logo: props.project?.logo ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
const rulesErrorMessage = {
|
const formRef = ref()
|
||||||
name: t('project_block__error_name')
|
const isFormValid = ref(false)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
|
|
||||||
function onSubmit() {
|
function onSubmit() {
|
||||||
const outData = {
|
const outData = {
|
||||||
name: projectMod.name,
|
name: projectMod.name,
|
||||||
description: getFieldValue(projectMod.description, props.project?.description),
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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
|
<q-btn
|
||||||
rounded color="primary"
|
rounded color="primary"
|
||||||
class="w100 fix-disabled-btn"
|
class="w100 fix-disabled-btn"
|
||||||
|
:disable="!isFormValid"
|
||||||
@click = "onSubmit"
|
@click = "onSubmit"
|
||||||
>
|
>
|
||||||
{{ $t(btnText) }}
|
{{ $t(btnText) }}
|
||||||
@@ -38,17 +39,25 @@
|
|||||||
<div class="flex row items-start justify-center no-wrap q-pb-lg">
|
<div class="flex row items-start justify-center no-wrap q-pb-lg">
|
||||||
<div class="flex column justify-center">
|
<div class="flex column justify-center">
|
||||||
<div class="text-bold q-pr-xs text-center" align="center">{{ tname }}</div>
|
<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>
|
</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
|
<q-input
|
||||||
v-model.trim="userMod.fullname"
|
v-model.trim="userMod.fullname"
|
||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
class = "w100"
|
class = "w100 fix-bottom-padding"
|
||||||
:label = "$t('user_block__name')"
|
:label = "$t('user_block__name')"
|
||||||
|
:rules="[inputRules.middleText]"
|
||||||
|
@update:model-value="formRef.validate()"
|
||||||
|
debounce="300"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-select
|
<q-select
|
||||||
@@ -61,6 +70,7 @@
|
|||||||
option-value="id"
|
option-value="id"
|
||||||
emit-value
|
emit-value
|
||||||
map-options
|
map-options
|
||||||
|
@update:model-value="formRef.validate()"
|
||||||
>
|
>
|
||||||
<template #option="scope">
|
<template #option="scope">
|
||||||
<q-item v-bind="scope.itemProps">
|
<q-item v-bind="scope.itemProps">
|
||||||
@@ -91,23 +101,32 @@
|
|||||||
v-model.trim="userMod.department"
|
v-model.trim="userMod.department"
|
||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
class = "w100 q-pt-sm"
|
class = "w100 fix-bottom-padding q-pt-sm"
|
||||||
:label = "$t('user_block__department')"
|
:label = "$t('user_block__department')"
|
||||||
|
:rules="[inputRules.middleText]"
|
||||||
|
@update:model-value="formRef.validate()"
|
||||||
|
debounce="300"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model.trim="userMod.role"
|
v-model.trim="userMod.role"
|
||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
class = "w100 q-pt-sm"
|
class = "w100 fix-bottom-padding q-pt-sm"
|
||||||
:label = "$t('user_block__role')"
|
:label = "$t('user_block__role')"
|
||||||
|
:rules="[inputRules.middleText]"
|
||||||
|
@update:model-value="formRef.validate()"
|
||||||
|
debounce="300"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model.trim="userMod.phone"
|
v-model.trim="userMod.phone"
|
||||||
dense
|
dense
|
||||||
filled
|
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>
|
<template #prepend>
|
||||||
<q-icon name="mdi-phone-outline"/>
|
<q-icon name="mdi-phone-outline"/>
|
||||||
@@ -118,14 +137,16 @@
|
|||||||
v-model.trim="userMod.email"
|
v-model.trim="userMod.email"
|
||||||
dense
|
dense
|
||||||
filled
|
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>
|
<template #prepend>
|
||||||
<q-icon name="mdi-email-outline"/>
|
<q-icon name="mdi-email-outline"/>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</q-form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
</pn-page-card>
|
</pn-page-card>
|
||||||
@@ -133,14 +154,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive } from 'vue'
|
import { ref, computed, reactive, onMounted } from 'vue'
|
||||||
import { useCompaniesStore } from 'stores/companies'
|
import { useCompaniesStore } from 'stores/companies'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useInputRules } from 'composables/useInputRules'
|
||||||
import { removeNullKeys, getFieldValue } from 'utils/helpers'
|
import { removeNullKeys, getFieldValue } from 'utils/helpers'
|
||||||
import { getUserStatus } from 'utils/users'
|
import { getUserStatus } from 'utils/users'
|
||||||
import type { User } from 'types/User'
|
import type { User } from 'types/User'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { inputRules } = useInputRules()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
title: string,
|
title: string,
|
||||||
@@ -184,6 +207,9 @@
|
|||||||
|
|
||||||
const userStatus = computed(() => props.user ? getUserStatus(props.user) : null)
|
const userStatus = computed(() => props.user ? getUserStatus(props.user) : null)
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const isFormValid = ref(false)
|
||||||
|
|
||||||
function onSubmit() {
|
function onSubmit() {
|
||||||
const outData = {
|
const outData = {
|
||||||
fullname: getFieldValue(userMod.fullname, props.user?.fullname),
|
fullname: getFieldValue(userMod.fullname, props.user?.fullname),
|
||||||
@@ -192,11 +218,15 @@
|
|||||||
role: getFieldValue(userMod.role, props.user?.role),
|
role: getFieldValue(userMod.role, props.user?.role),
|
||||||
phone: getFieldValue(userMod.phone, props.user?.phone),
|
phone: getFieldValue(userMod.phone, props.user?.phone),
|
||||||
email: getFieldValue(userMod.email, props.user?.email),
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
18
src/composables/useInputRules.ts
Normal file
18
src/composables/useInputRules.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
|
||||||
|
export const useInputRules = () => {
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputRules: {
|
||||||
|
require: (val: string) => !!val?.trim() || t('input_rules__require'),
|
||||||
|
email: (val: string) => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(val) || t('input_rules__incorrect_email'),
|
||||||
|
password: (val: string) => val.length >= 8 || t('input_rules__password_require'),
|
||||||
|
middleText: (val: string) => val.length <= 60 || t('input_rules__middle_text'),
|
||||||
|
longText: (val: string) => val.length <= 250 || t('input_rules__long_text')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/composables/useSubscription.ts
Normal file
49
src/composables/useSubscription.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { api } from 'boot/axios'
|
||||||
|
|
||||||
|
async function getTariffs () {
|
||||||
|
return (await api.get('/payment/plan'))?.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTariffDiscount () {
|
||||||
|
return (await api.get('payment/plan/discount'))?.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function calcUpgradeTariff (plan_id: number, period: number) {
|
||||||
|
return (await api.get('/payment/plan/upgrade/calc', { params: { plan_id, period } }))?.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function urlUpgradeTariff (plan_id: number, period: number) {
|
||||||
|
return (await api.post('/payment/plan/upgrade', { plan_id, period }))?.data.data.url
|
||||||
|
}
|
||||||
|
|
||||||
|
async function calcChangeTariff (plan_id: number) {
|
||||||
|
return (await api.get('/payment/plan/change/calc', { params: { plan_id } }))?.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeTariff (plan_id: number) {
|
||||||
|
return (await api.post('/payment/plan/change', { plan_id }))?.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function calcRenewTariff (plan_id: number, period: number) {
|
||||||
|
return (await api.get('/payment/plan/renew/calc', { params: { plan_id, period } }))?.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function urlRenewTariff (plan_id: number, period: number) {
|
||||||
|
return (await api.post('/payment/plan/renew', { plan_id, period }))?.data.data.url
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendPromocode (promocode: string) {
|
||||||
|
return (await api.post('/payment/plan/promocode', { promocode }))?.data.success
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
getTariffs,
|
||||||
|
getTariffDiscount,
|
||||||
|
calcUpgradeTariff,
|
||||||
|
urlUpgradeTariff,
|
||||||
|
calcChangeTariff,
|
||||||
|
changeTariff,
|
||||||
|
calcRenewTariff,
|
||||||
|
urlRenewTariff,
|
||||||
|
sendPromocode
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCompaniesStore } from 'stores/companies'
|
import { useCompaniesStore } from 'stores/companies'
|
||||||
import type { User } from 'types/User'
|
import type { User } from 'types/User'
|
||||||
|
|
||||||
export function useUserSection() {
|
export function useUserSection () {
|
||||||
const companiesStore = useCompaniesStore()
|
const companiesStore = useCompaniesStore()
|
||||||
|
|
||||||
const userSection = (user: User) => {
|
const userSection = (user: User) => {
|
||||||
@@ -19,7 +19,7 @@ export function useUserSection() {
|
|||||||
|
|
||||||
const section3 = (
|
const section3 = (
|
||||||
user.company_id && companiesStore.companyById(user.company_id)
|
user.company_id && companiesStore.companyById(user.company_id)
|
||||||
? companiesStore.companyById(user.company_id)?.name + ((user.role || user.department) ? ' - ' : '')
|
? companiesStore.companyById(user.company_id)?.name + ((user.role || user.department) ? ', ' : '')
|
||||||
: ''
|
: ''
|
||||||
) +
|
) +
|
||||||
(user.department ? user.department + (user.role ? ' - ' : '') : '') +
|
(user.department ? user.department + (user.role ? ' - ' : '') : '') +
|
||||||
|
|||||||
@@ -15,6 +15,18 @@
|
|||||||
background: $brand2 !important;
|
background: $brand2 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-base {
|
||||||
|
background: $base-bg !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-tgcolor {
|
||||||
|
color: $tgcolor !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-tgcolor {
|
||||||
|
background: $tgcolor !important;
|
||||||
|
}
|
||||||
|
|
||||||
$base-width: 100;
|
$base-width: 100;
|
||||||
@while $base-width > 0 {
|
@while $base-width > 0 {
|
||||||
.w#{$base-width} { width: #{$base-width}+'%'; }
|
.w#{$base-width} { width: #{$base-width}+'%'; }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
$primary : #3390ec;
|
$primary : #419FD9;
|
||||||
$secondary : #26A69A;
|
$secondary : #26A69A;
|
||||||
$accent : #9C27B0;
|
$accent : #9C27B0;
|
||||||
|
|
||||||
@@ -12,8 +12,11 @@ $warning : #F2C037;
|
|||||||
|
|
||||||
$lightgrey : #DCDCDC;
|
$lightgrey : #DCDCDC;
|
||||||
|
|
||||||
$brand: #3390ec;
|
$brand: #419FD9;
|
||||||
$brand2: #F36D3A;
|
$brand2: #F36D3A;
|
||||||
|
$base-bg: #517DA2;
|
||||||
|
|
||||||
|
$tgcolor: #419FD9;
|
||||||
|
|
||||||
$body-font-size: var(--dynamic-font-size);
|
$body-font-size: var(--dynamic-font-size);
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
// This is just an example,
|
|
||||||
// so you can safely delete all default props below
|
|
||||||
|
|
||||||
export default {
|
|
||||||
login__forgot_password: 'Forgot password?',
|
|
||||||
login__login: 'Login',
|
|
||||||
login__password: 'Password',
|
|
||||||
login__sign_in: 'Sign in',
|
|
||||||
login__sign_up: 'Register',
|
|
||||||
login__welcome: 'Welcome!',
|
|
||||||
project__companies: 'Companies',
|
|
||||||
project__persons: 'Persons',
|
|
||||||
project__chats: 'Chats',
|
|
||||||
projects__projects: 'Projects',
|
|
||||||
projects__account: 'Account',
|
|
||||||
failed: 'Action failed',
|
|
||||||
success: 'Action was successful'
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,9 +11,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import accountHelper from 'components/accountHelper.vue'
|
import accountHelper from 'components/accountHelper.vue'
|
||||||
import { useAuthStore } from 'stores/auth'
|
|
||||||
|
|
||||||
const authStore= useAuthStore()
|
|
||||||
|
|
||||||
const type = 'changeMethod'
|
const type = 'changeMethod'
|
||||||
|
|
||||||
|
|||||||
@@ -192,14 +192,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import type { AxiosError } from 'axios'
|
import type { AxiosError } from 'axios'
|
||||||
import { useI18n } from "vue-i18n"
|
|
||||||
import { QInput } from 'quasar'
|
import { QInput } from 'quasar'
|
||||||
import { useAuthStore } from 'stores/auth'
|
import { useAuthStore } from 'stores/auth'
|
||||||
|
import { useInputRules } from 'composables/useInputRules'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { inputRules } = useInputRules()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
type ValidationRule = (val: string) => boolean | string
|
|
||||||
type Step = 1 | 2 | 3 | 4 | 5
|
type Step = 1 | 2 | 3 | 4 | 5
|
||||||
|
|
||||||
const step = ref<Step>(1)
|
const step = ref<Step>(1)
|
||||||
@@ -211,8 +210,8 @@
|
|||||||
const showSuccessOverlay = ref(false)
|
const showSuccessOverlay = ref(false)
|
||||||
const isPwd = ref<boolean>(true)
|
const isPwd = ref<boolean>(true)
|
||||||
const validationRules = {
|
const validationRules = {
|
||||||
email: [(val: string) => /.+@.+\..+/.test(val) || t('login__incorrect_email')] as [ValidationRule],
|
email: [inputRules.email],
|
||||||
password: [(val: string) => val.length >= 8 || t('login__password_require')] as [ValidationRule]
|
password: [inputRules.password]
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEmailValid = computed(() =>
|
const isEmailValid = computed(() =>
|
||||||
@@ -224,7 +223,7 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
const passwordHint = computed(() => {
|
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 : ''
|
return typeof result === 'string' ? result : ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,10 @@
|
|||||||
import accountHelper from 'components/accountHelper.vue'
|
import accountHelper from 'components/accountHelper.vue'
|
||||||
|
|
||||||
const type = 'register'
|
const type = 'register'
|
||||||
const email = ref(sessionStorage.getItem('pendingLogin') || '')
|
const email = ref('')
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
email.value = sessionStorage.getItem('pendingLogin') ?? ''
|
||||||
sessionStorage.removeItem('pendingLogin')
|
sessionStorage.removeItem('pendingLogin')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -9,37 +9,9 @@
|
|||||||
<div v-if="route.query.type === 'update'">{{ $t('agreements__description_update') }}</div>
|
<div v-if="route.query.type === 'update'">{{ $t('agreements__description_update') }}</div>
|
||||||
<div>{{ $t('agreements__description') }}</div>
|
<div>{{ $t('agreements__description') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center no-wrap">
|
<agreement-block
|
||||||
<q-checkbox v-model="agreement" val="1" class="q-pr-sm"/>
|
v-model="agreement"
|
||||||
<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="agreement" val="2" 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>
|
</div>
|
||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -74,9 +46,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, inject } from 'vue'
|
import { ref, inject, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useAuthStore } from 'stores/auth'
|
import { useAuthStore } from 'stores/auth'
|
||||||
|
import agreementBlock from 'components/agreementBlock.vue'
|
||||||
import type { WebApp } from '@twa-dev/types'
|
import type { WebApp } from '@twa-dev/types'
|
||||||
const tg = inject('tg') as WebApp
|
const tg = inject('tg') as WebApp
|
||||||
|
|
||||||
@@ -88,32 +61,40 @@
|
|||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const submitAccept = ref(false)
|
||||||
|
|
||||||
async function onSubmit () {
|
async function onSubmit () {
|
||||||
|
|
||||||
if (!route.query.method || !route.query.type) {
|
sessionStorage.removeItem('agreement')
|
||||||
|
submitAccept.value = true
|
||||||
|
|
||||||
|
if (!route.query.type) {
|
||||||
await authStore.logout()
|
await authStore.logout()
|
||||||
await router.push({ name: 'login' })
|
await router.push({ name: 'login' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.query.method === 'telegram' && route.query.type === 'register') {
|
if (route.query.type === 'register') { // if register with email - agreement inside agreement-block
|
||||||
await authStore.registerWithTelegram(tg.initData)
|
await authStore.registerWithTelegram(tg.initData)
|
||||||
await authStore.loginWithTelegram(tg.initData)
|
await authStore.loginWithTelegram(tg.initData)
|
||||||
await router.push({ name: 'projects' })
|
await router.push({ name: 'projects' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.query.method === 'email' && route.query.type === 'register') {
|
|
||||||
await router.push({ name: 'create_account' })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route.query.type === 'update') {
|
if (route.query.type === 'update') {
|
||||||
await authStore.termsAccepted()
|
await authStore.termsAccepted()
|
||||||
await router.push({ name: 'projects' })
|
await router.push({ name: 'projects' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const agreement = ref([])
|
const agreement = ref<('terms'| 'consent')[]>([])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const a = sessionStorage.getItem('agreement')
|
||||||
|
if (a) agreement.value = JSON.parse(a)
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (!submitAccept.value) sessionStorage.setItem('agreement', JSON.stringify(agreement.value))
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="q-pb-md w100" v-if="chat">
|
<div class="q-pb-md w100" v-if="chat">
|
||||||
<pn-chat-type-item :type="chat.bot_access || 0" :time="chat.bot_access_date"/>
|
<pn-chat-type-item
|
||||||
<pn-chat-type-item v-if="chat.restore_date" :type="10" :time="chat.restore_date"/>
|
:access="chat.bot_access"
|
||||||
|
:time="chat.bot_access_date"
|
||||||
|
/>
|
||||||
|
<pn-chat-type-item
|
||||||
|
v-if="chat.state !== 1"
|
||||||
|
:state="chat.state"
|
||||||
|
:time="chat.state_date ?? 0"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-list separator v-if="chatUsers.length !== 0">
|
<q-list separator v-if="chatUsers.length !== 0">
|
||||||
@@ -54,38 +61,14 @@
|
|||||||
class="w100"
|
class="w100"
|
||||||
@click="goUserInfo(item.id)"
|
@click="goUserInfo(item.id)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<pn-user-list-item-body :item showStatus>
|
||||||
<pn-auto-avatar
|
|
||||||
:img="item.photo"
|
|
||||||
:name="item.section1"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label class="text-caption text-amber-10" lines="1" v-if="chat?.owner_id === item.id">
|
<q-item-label class="text-caption text-amber-10" lines="1" v-if="chat?.owner_id === item.id">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<q-icon name="star" class="q-pr-xs"/>
|
<q-icon name="star" class="q-pr-xs"/>
|
||||||
{{ $t('chat_card__owner') }}
|
{{ $t('chat_card__owner') }}
|
||||||
</div>
|
</div>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label lines="1" v-if="getUserStatus(item) && getUserStatus(item)?.status">
|
</pn-user-list-item-body>
|
||||||
<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 lines="2">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<q-icon name="telegram" v-if="item.section2_1 || item.section2_2" class="q-pr-xs" style="color: #27a7e7"/>
|
|
||||||
<div v-if="item.section2_1" class="q-mr-sm text-bold">{{ item.section2_1 }}</div>
|
|
||||||
<div class="text-blue" v-if="item.section2_2">{{ '@' + item.section2_2 }}</div>
|
|
||||||
</div>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
@@ -102,7 +85,7 @@
|
|||||||
import { parseIntString } from 'src/utils/helpers'
|
import { parseIntString } from 'src/utils/helpers'
|
||||||
import { useUserSection } from 'composables/useUserSection'
|
import { useUserSection } from 'composables/useUserSection'
|
||||||
import pnChatTypeItem from 'components/pnChatTypeItem.vue'
|
import pnChatTypeItem from 'components/pnChatTypeItem.vue'
|
||||||
import { getUserStatus } from 'utils/users'
|
import pnUserListItemBody from 'components/pnUserListItemBody.vue'
|
||||||
import type { Chat } from 'types/Chat'
|
import type { Chat } from 'types/Chat'
|
||||||
import type { WebApp } from '@twa-dev/types'
|
import type { WebApp } from '@twa-dev/types'
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex column">
|
<div class="flex column">
|
||||||
<div class="text-caption" align="center" style="white-space: pre-wrap;">
|
<div class="text-caption text-center" style="white-space: pre-wrap;">
|
||||||
{{ $t('company_edit__my_company_hint') }}
|
{{ $t('company_edit__my_company_hint') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-caption text-primary cursor-pointer" align="center" @click="goMyCompany">
|
<div class="text-caption text-primary cursor-pointer text-center" @click="goMyCompany">
|
||||||
{{ $t('company_edit__my_company_href') }}
|
{{ $t('company_edit__my_company_href') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
const company = companyId.value && companiesStore.companyById(companyId.value) || {} as CompanyParams
|
const company = companyId.value && companiesStore.companyById(companyId.value) || {} as CompanyParams
|
||||||
|
|
||||||
async function goMyCompany () {
|
async function goMyCompany () {
|
||||||
await router.push({ name: 'your_company' })
|
await router.replace({ name: 'your_company' })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateCompany (company: CompanyParams) {
|
async function updateCompany (company: CompanyParams) {
|
||||||
|
|||||||
@@ -141,7 +141,7 @@
|
|||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
|
|
||||||
<q-dialog v-model="showDialogHelp">
|
<q-dialog v-model="showDialogHelp">
|
||||||
<q-card class="q-ma-sm w100">
|
<q-card class="q-ma-sm w100 rounded-card">
|
||||||
<q-card-section class="row items-center q-pb-none">
|
<q-card-section class="row items-center q-pb-none">
|
||||||
<span class="text-h6">
|
<span class="text-h6">
|
||||||
<q-icon name="mdi-domino-mask"/>
|
<q-icon name="mdi-domino-mask"/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<company-block
|
<company-block
|
||||||
v-if="companyMod"
|
v-if="company"
|
||||||
v-model="companyMod"
|
:company
|
||||||
title="account_company__title_card"
|
title="account_company__title_card"
|
||||||
btnText="account_company__btn"
|
btnText="account_company__btn"
|
||||||
@update="updateMyCompany"
|
@update="updateMyCompany"
|
||||||
@@ -18,20 +18,20 @@
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const companyMod = ref<CompanyParams | null>(null)
|
const company = ref<CompanyParams | null>(null)
|
||||||
|
|
||||||
async function updateMyCompany () {
|
async function updateMyCompany (company: CompanyParams) {
|
||||||
if (companyMod.value) {
|
if (company) {
|
||||||
await authStore.updateMyCompany(companyMod.value)
|
await authStore.updateMyCompany(company)
|
||||||
router.go(-1)
|
router.go(-1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (authStore.customer?.company) {
|
if (authStore.customer?.company) {
|
||||||
companyMod.value = authStore.customer?.company as CompanyParams
|
company.value = authStore.customer?.company as CompanyParams
|
||||||
} else {
|
} else {
|
||||||
companyMod.value = {
|
company.value = {
|
||||||
name: '',
|
name: '',
|
||||||
logo: '',
|
logo: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
|||||||
@@ -18,8 +18,7 @@
|
|||||||
|
|
||||||
<div class="flex column w100">
|
<div class="flex column w100">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="login"
|
v-model.trim="login"
|
||||||
autofocus
|
|
||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
class="q-mb-sm"
|
class="q-mb-sm"
|
||||||
@@ -33,7 +32,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="password"
|
v-model.trim="password"
|
||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
:label="$t('login__password')"
|
:label="$t('login__password')"
|
||||||
@@ -62,34 +61,35 @@
|
|||||||
<div class="self-end">
|
<div class="self-end">
|
||||||
<q-btn
|
<q-btn
|
||||||
@click.prevent="forgotPwd"
|
@click.prevent="forgotPwd"
|
||||||
flat
|
flat no-caps
|
||||||
no-caps
|
dense rounded
|
||||||
dense
|
|
||||||
class="text-grey"
|
class="text-grey"
|
||||||
>
|
>
|
||||||
{{$t('login__forgot_password')}}
|
<span class="q-px-sm">
|
||||||
|
{{ $t('login__forgot_password') }}
|
||||||
|
</span>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="sendAuth()"
|
@click="sendAuth()"
|
||||||
color="primary"
|
color="primary"
|
||||||
class="w100 q-my-md fix-disabled-btn"
|
class="w100 q-my-md fix-disabled-btn q-pt-sm"
|
||||||
:disabled="!isEmailValid || !isPasswordValid"
|
:disabled="!isEmailValid || !isPasswordValid"
|
||||||
:unelevated="!isEmailValid || !isPasswordValid"
|
:unelevated="!isEmailValid || !isPasswordValid"
|
||||||
|
rounded
|
||||||
>
|
>
|
||||||
{{$t('login__sign_in')}}
|
{{ $t('login__sign_in') }}
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat rounded
|
||||||
sm
|
sm no-caps
|
||||||
no-caps
|
class="q-my-md w100"
|
||||||
class="q-my-md"
|
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="createAccount"
|
@click="createAccount"
|
||||||
>
|
>
|
||||||
{{$t('login__register')}}
|
{{ $t('login__register') }}
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -97,20 +97,19 @@
|
|||||||
id="alt_login"
|
id="alt_login"
|
||||||
class="flex w100 column items-center "
|
class="flex w100 column items-center "
|
||||||
>
|
>
|
||||||
<div
|
<div class="orline w100 text-grey">
|
||||||
class="orline w100 text-grey"
|
<span class="q-mx-sm text-caption">
|
||||||
>
|
{{ $t('login__or_continue_as') }}
|
||||||
<span class="q-mx-sm text-caption">{{$t('login__or_continue_as')}}</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat rounded
|
||||||
sm
|
sm no-caps
|
||||||
no-caps
|
|
||||||
color="primary"
|
color="primary"
|
||||||
class="q-my-md"
|
class="q-my-md w100"
|
||||||
@click="handleTelegramLogin"
|
@click="handleTelegramLogin"
|
||||||
>
|
>
|
||||||
<div class="flex items-center text-blue">
|
<div class="flex items-center text-primary">
|
||||||
<q-avatar size="md" class="q-mr-sm">
|
<q-avatar size="md" class="q-mr-sm">
|
||||||
<q-img v-if="tgUser?.photo_url" :src="tgUser.photo_url"/>
|
<q-img v-if="tgUser?.photo_url" :src="tgUser.photo_url"/>
|
||||||
<q-icon v-else size="md" class="q-mr-none" name="telegram"/>
|
<q-icon v-else size="md" class="q-mr-none" name="telegram"/>
|
||||||
@@ -137,19 +136,17 @@
|
|||||||
import { ref, computed, inject, onUnmounted } from 'vue'
|
import { ref, computed, inject, onUnmounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import baseLogo from 'components/BaseLogo.vue'
|
import baseLogo from 'components/BaseLogo.vue'
|
||||||
import { useI18n } from "vue-i18n"
|
import { useInputRules } from 'composables/useInputRules'
|
||||||
import { useAuthStore } from 'stores/auth'
|
import { useAuthStore } from 'stores/auth'
|
||||||
import type { WebApp } from '@twa-dev/types'
|
import type { WebApp } from '@twa-dev/types'
|
||||||
import { QInput } from 'quasar'
|
import { QInput } from 'quasar'
|
||||||
|
|
||||||
type ValidationRule = (val: string) => boolean | string
|
|
||||||
|
|
||||||
const tg = inject('tg') as WebApp
|
const tg = inject('tg') as WebApp
|
||||||
const tgUser = tg.initDataUnsafe.user
|
const tgUser = tg.initDataUnsafe.user
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useI18n()
|
const { inputRules } = useInputRules()
|
||||||
|
|
||||||
const login = ref<string>('')
|
const login = ref<string>('')
|
||||||
const password = ref<string>('')
|
const password = ref<string>('')
|
||||||
@@ -159,8 +156,8 @@
|
|||||||
const passwordInput = ref<InstanceType<typeof QInput>>()
|
const passwordInput = ref<InstanceType<typeof QInput>>()
|
||||||
|
|
||||||
const validationRules = {
|
const validationRules = {
|
||||||
email: [(val: string) => /.+@.+\..+/.test(val) || t('login__incorrect_email')] as [ValidationRule],
|
email: [inputRules.email],
|
||||||
password: [(val: string) => val.length >= 8 || t('login__password_require')] as [ValidationRule]
|
password: [inputRules.password]
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEmailValid = computed(() =>
|
const isEmailValid = computed(() =>
|
||||||
@@ -172,7 +169,7 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
const passwordHint = computed(() => {
|
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 : ''
|
return typeof result === 'string' ? result : ''
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -213,7 +210,7 @@
|
|||||||
|
|
||||||
async function createAccount() {
|
async function createAccount() {
|
||||||
sessionStorage.setItem('pendingLogin', login.value)
|
sessionStorage.setItem('pendingLogin', login.value)
|
||||||
await router.push({ name: 'agreements', query: { method: 'email', type: 'register' }})
|
await router.push({ name: 'create_account' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTelegramApp = computed(() => {
|
const isTelegramApp = computed(() => {
|
||||||
@@ -226,7 +223,7 @@
|
|||||||
await authStore.loginWithTelegram(tg.initData)
|
await authStore.loginWithTelegram(tg.initData)
|
||||||
await router.push({ name: 'projects' })
|
await router.push({ name: 'projects' })
|
||||||
} catch {
|
} catch {
|
||||||
await router.push({ name: 'agreements', query: { method: 'telegram', type: 'register' }})
|
await router.push({ name: 'agreements', query: { type: 'register' }})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,7 @@
|
|||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pn-onboard-btn
|
<pn-onboard-btn
|
||||||
v-if="projects.length===0 && projectsInit"
|
v-if="projects.length===0 && projectsInit"
|
||||||
icon="mdi-briefcase-plus-outline"
|
icon="mdi-briefcase-plus-outline"
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<doc-block title="subscription_guide__title" document-name="Subscription_guide"/>
|
<doc-block
|
||||||
|
title="subscription_guide__title"
|
||||||
|
document-name="Subscription_guide"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -38,7 +38,13 @@
|
|||||||
/>
|
/>
|
||||||
</pn-item-btm-dialog>
|
</pn-item-btm-dialog>
|
||||||
|
|
||||||
<q-item-label class="q-mt-md" header>{{ $t('settings__bot_title') }}</q-item-label>
|
<template v-if="authStore.isAuth">
|
||||||
|
<q-item-label
|
||||||
|
class="q-mt-md"
|
||||||
|
header
|
||||||
|
>
|
||||||
|
{{ $t('settings__bot_title') }}
|
||||||
|
</q-item-label>
|
||||||
|
|
||||||
<pn-item-btm-dialog
|
<pn-item-btm-dialog
|
||||||
title="settings__language"
|
title="settings__language"
|
||||||
@@ -54,6 +60,7 @@
|
|||||||
:options="localeOptions"
|
:options="localeOptions"
|
||||||
/>
|
/>
|
||||||
</pn-item-btm-dialog>
|
</pn-item-btm-dialog>
|
||||||
|
|
||||||
<pn-item-btm-dialog
|
<pn-item-btm-dialog
|
||||||
title="settings__timezone"
|
title="settings__timezone"
|
||||||
caption="settings__bot_title"
|
caption="settings__bot_title"
|
||||||
@@ -71,6 +78,7 @@
|
|||||||
:locale
|
:locale
|
||||||
/>
|
/>
|
||||||
</pn-item-btm-dialog>
|
</pn-item-btm-dialog>
|
||||||
|
</template>
|
||||||
</q-list>
|
</q-list>
|
||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
</pn-page-card>
|
</pn-page-card>
|
||||||
@@ -79,11 +87,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, computed, onMounted } from 'vue'
|
import { ref, watch, computed, onMounted } from 'vue'
|
||||||
import { useSettingsStore } from 'stores/settings'
|
import { useSettingsStore } from 'stores/settings'
|
||||||
|
import { useAuthStore } from 'stores/auth'
|
||||||
import pnItemBtmDialog from 'components/pnItemBtmDialog.vue'
|
import pnItemBtmDialog from 'components/pnItemBtmDialog.vue'
|
||||||
import pnListSelector from 'components/pnListSelector.vue'
|
import pnListSelector from 'components/pnListSelector.vue'
|
||||||
import pnTimeZoneSelector from 'components/pnTimeZoneSelector.vue'
|
import pnTimeZoneSelector from 'components/pnTimeZoneSelector.vue'
|
||||||
|
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const localeOptions = settingsStore.supportLocale
|
const localeOptions = settingsStore.supportLocale
|
||||||
|
|
||||||
|
|||||||
@@ -1,313 +1,162 @@
|
|||||||
<!-- eslint-disable @typescript-eslint/ban-ts-comment -->
|
|
||||||
<!-- eslint-disable @typescript-eslint/ban-ts-comment -->
|
|
||||||
<!-- eslint-disable @typescript-eslint/ban-ts-comment -->
|
|
||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-card>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{$t('subscribe__title')}}
|
{{$t('subscribe__title')}}
|
||||||
</template>
|
</template>
|
||||||
|
<pn-scroll-list>
|
||||||
<pn-scroll-list >
|
|
||||||
<template #card-body-header>
|
<template #card-body-header>
|
||||||
<div
|
<div
|
||||||
|
v-if="tariffs && currentTariff"
|
||||||
id="subscribe-current-balance"
|
id="subscribe-current-balance"
|
||||||
class="flex w100 q-px-md q-py-md row"
|
class="flex w100 q-px-md q-py-md row"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="flex w100 justify-between items-center no-wrap">
|
<div class="flex w100 justify-between items-center no-wrap">
|
||||||
<div class="flex items-center text-grey">
|
<div class="flex items-center text-grey">
|
||||||
{{ $t('subscribe__current_plan') }}
|
{{ $t('subscribe__current_tariff_title') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center column text-right">
|
<div class="flex items-center column text-right">
|
||||||
<span class="text-bold">
|
<span class="text-bold">
|
||||||
{{ currentPlanData?.name }}
|
{{ currentTariff?.tariffInfo.name }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="row w100 no-wrap items-center text-caption justify-between"
|
class="row w100 no-wrap items-center text-caption justify-between"
|
||||||
v-if="currentPlanData.name !== 'TEST'"
|
v-if="!isCurrentTariffTest"
|
||||||
>
|
>
|
||||||
<div class="text-grey text-no-wrap ellipsis">
|
<div class="text-grey text-no-wrap ellipsis">
|
||||||
{{ $t('subscribe__plan_date') }}
|
{{ $t('subscribe__current_tariff_date') }}
|
||||||
</div>
|
</div>
|
||||||
<span class="text-no-wrap text-right">
|
<span class="text-no-wrap text-right" v-if="currentTariff.end_date">
|
||||||
{{ date.formatDate(currentPlanData.exp * 1000, 'DD.MM.YYYY') }}
|
{{ date.formatDate(currentTariff.end_date * 1000, 'DD.MM.YYYY') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row w100 no-wrap items-center text-caption text-grey justify-between">
|
<div class="row w100 no-wrap items-center text-caption text-grey justify-between">
|
||||||
<div>
|
<div>
|
||||||
{{ $t('subscribe__plan_active_chats') }}
|
{{ $t('subscribe__current_tariff_chat_limit') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<span class="text-brand2 text-bold q-pr-xs">{{ currentPlanData?.active_chats }}</span>
|
<span
|
||||||
<span>/{{ currentPlanData?.chatsQty }}</span>
|
class="text-bold q-pr-xs"
|
||||||
|
:class="currentTariff.tariffInfo.chat_limit && (currentTariff.tariffInfo.chat_limit > currentTariff?.used_chats) ? 'negative' : ''"
|
||||||
|
>
|
||||||
|
{{ currentTariff.used_chats }}
|
||||||
|
</span>
|
||||||
|
<span v-if="currentTariff.tariffInfo.chat_limit">
|
||||||
|
/{{ currentTariff.tariffInfo.chat_limit }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<div v-if="tariffs && currentTariff">
|
||||||
<div class="w100 column items-center text-center text-grey q-px-md">
|
<subscribe-upgrade
|
||||||
{{ $t('subscribe__plans')}}
|
v-if="isCurrentTariffTest"
|
||||||
<div class="text-caption">
|
:tariffs
|
||||||
{{ $t('subscribe__plans_description') }}
|
:periods
|
||||||
</div>
|
@tariff-upgrade="prepareTgBtn"
|
||||||
</div>
|
|
||||||
<q-list separator>
|
|
||||||
<q-item
|
|
||||||
v-for="item in plans"
|
|
||||||
:key="item.id"
|
|
||||||
clickable
|
|
||||||
v-ripple
|
|
||||||
tag="label"
|
|
||||||
:disable="item.name === 'TEST' && currentPlanData.name !== 'TEST'"
|
|
||||||
>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-radio
|
|
||||||
v-model="newPlan"
|
|
||||||
:val="item"
|
|
||||||
:disable="item.name === 'TEST' && currentPlanData.name != 'TEST'"
|
|
||||||
:keep-color="item.name === 'TEST'&& currentPlanData.name !== 'TEST'"
|
|
||||||
:color="item.name === 'TEST' && currentPlanData.name !== 'TEST' ? '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.chatsQty">
|
|
||||||
{{ $t('subscribe__chats_max') + ' ' + item.chatsQty }}
|
|
||||||
</span>
|
|
||||||
<q-icon v-else name="mdi-all-inclusive" size="sm"/>
|
|
||||||
<span class="q-pl-xs">
|
|
||||||
{{ $t('subscribe__chats')}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
v-if="item.name === 'TEST' && currentPlanData.name !== 'TEST'"
|
|
||||||
class="text-caption text-grey-7"
|
|
||||||
>
|
|
||||||
{{ $t('subscribe__TEST_via_support')}}
|
|
||||||
</span>
|
|
||||||
</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__per_month') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-bold self-center">
|
|
||||||
{{ $t('subscribe__free_tax') }}
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
|
|
||||||
<div class="q-px-md">
|
<subscribe-change-renew
|
||||||
<div class="w100 text-grey text-center q-pt-sm">
|
v-else
|
||||||
{{ $t('subscribe__plans_interval') }}
|
:tariffs
|
||||||
</div>
|
:periods
|
||||||
|
:currentTariff
|
||||||
<q-btn-group spread flat class="w100 q-py-sm">
|
@update="prepareTgBtn"
|
||||||
<q-btn
|
|
||||||
v-for="period in periods"
|
|
||||||
:key="period.id"
|
|
||||||
:outline="selectPeriod === period.value"
|
|
||||||
@click="selectPeriod = period.value"
|
|
||||||
:color="selectPeriod === period.value ? 'primary' : ''"
|
|
||||||
no-caps dense
|
|
||||||
>
|
|
||||||
<div class="column items-center w100 self-end">
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="text-h6"
|
|
||||||
:class="selectPeriod === period.value ? 'text-primary' : 'text-black'"
|
|
||||||
>
|
|
||||||
{{ $t(period.name) }}
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
class="row no-wrap w100 q-px-xs"
|
|
||||||
:class="period.sale ? 'justify-between' : 'justify-center'"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="text-caption text-grey"
|
|
||||||
>
|
|
||||||
{{ $t(period.name_in_days) }}
|
|
||||||
</span>
|
|
||||||
<q-badge v-show="period.sale" color="red">-{{ period.sale }}%</q-badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-btn>
|
|
||||||
</q-btn-group>
|
|
||||||
|
|
||||||
<div class="text-caption text-grey">
|
|
||||||
<span>{{ $t('subscribe__plans_period_notes') + ' ' }}</span>
|
|
||||||
<span
|
|
||||||
@click="router.push({ name: 'change_plan_rules' })"
|
|
||||||
class="text-info cursor-pointer"
|
|
||||||
>
|
|
||||||
{{ $t('subscribe__plans_period_notes_more_info') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-input
|
|
||||||
v-model="promocode"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
:label="$t('subscribe__promocode')"
|
|
||||||
class="q-pt-md"
|
|
||||||
>
|
|
||||||
<template #append v-if="promocode !== ''">
|
|
||||||
<q-btn
|
|
||||||
round dense flat
|
|
||||||
color="primary"
|
|
||||||
icon="mdi-arrow-right-circle-outline"
|
|
||||||
@click="sendPromocode"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
</div>
|
||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
</pn-page-card>
|
</pn-page-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, inject, onUnmounted, watch } from 'vue'
|
import { ref, computed, inject, onMounted, onUnmounted } from 'vue'
|
||||||
import telegramStar from 'components/TelegramStar.vue'
|
import { date, colors } from 'quasar'
|
||||||
import { useRouter } from 'vue-router'
|
import subscribeUpgrade from 'components/subscribe/subscribeUpgrade.vue'
|
||||||
import { date } from 'quasar'
|
import subscribeChangeRenew from 'components/subscribe/subscribeChangeRenew.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import {
|
||||||
|
getTariffs,
|
||||||
|
getTariffDiscount
|
||||||
|
} from 'composables/useSubscription'
|
||||||
|
import { useAuthStore } from 'stores/auth'
|
||||||
|
import type { Tariff } from 'types/Tariff'
|
||||||
import type { WebApp } from '@twa-dev/types'
|
import type { WebApp } from '@twa-dev/types'
|
||||||
import { colors } from 'quasar'
|
|
||||||
import { useQuasar } from 'quasar'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
const tg = inject('tg') as WebApp
|
const tg = inject('tg') as WebApp
|
||||||
|
|
||||||
// @ts-expect-error: get hex text
|
// @ts-expect-error: get hex text
|
||||||
tg.MainButton.color = colors.getPaletteColor('primary')
|
tg.MainButton.color = colors.getPaletteColor('primary')
|
||||||
|
|
||||||
const plans = [
|
const tariffs = ref<Tariff[] | null>(null)
|
||||||
{ id: 1, name: 'TEST', val: 'test', price: null, chatsQty: 5 },
|
const discountTariff = ref(null)
|
||||||
{ id: 2, name: 'START', val: 'start', price: 1000, chatsQty: 15 },
|
|
||||||
{ id: 3, name: 'PRO', val: 'pro', price: 5000, chatsQty: 40 },
|
const testTariffId = computed(() => (tariffs.value && tariffs.value.find(el => el.price === 0))?.id ?? 1)
|
||||||
{ id: 4, name: 'VIP', val: 'vip', price: 12000, chatsQty: null }
|
const isCurrentTariffTest = computed(() => currentTariff.value?.id === testTariffId.value)
|
||||||
|
|
||||||
|
const storeTariff = authStore.customer?.plan
|
||||||
|
const currentTariff = computed(() => {
|
||||||
|
if (tariffs.value && storeTariff) {
|
||||||
|
const tariffInfo = tariffs.value.find((el: Tariff) => el.id === storeTariff.id)
|
||||||
|
if (tariffInfo) return { ...storeTariff, tariffInfo }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
tariffs.value = await getTariffs()
|
||||||
|
discountTariff.value = await getTariffDiscount()
|
||||||
|
})
|
||||||
|
|
||||||
|
const basePeriods = [
|
||||||
|
{ id: 30, name: 'subscribe__1month', name_in_days: 'subscribe__30days' },
|
||||||
|
{ id: 91, name: 'subscribe__3months', name_in_days: 'subscribe__91days' },
|
||||||
|
{ id: 365, name: 'subscribe__1year', name_in_days: 'subscribe__365days' }
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
const periods = [
|
const periods = computed(() => basePeriods.map(el => {
|
||||||
{ id: 1, name: 'subscribe__1month', name_in_days: 'subscribe__30days', value: 30, sale: 0 },
|
const discount = discountTariff.value ? discountTariff.value[el.id] : 0
|
||||||
{ id: 2, name: 'subscribe__3months', name_in_days: 'subscribe__91days', value: 91, sale: 5 },
|
return {
|
||||||
{ id: 3, name: 'subscribe__1year', name_in_days: 'subscribe__365days', value: 365, sale: 15 }
|
...el,
|
||||||
]
|
discount
|
||||||
|
|
||||||
const selectPeriod = ref(periods[1]?.value)
|
|
||||||
|
|
||||||
const newPlan = ref(plans[0])
|
|
||||||
|
|
||||||
interface CurrentPlan {
|
|
||||||
plan: string
|
|
||||||
exp: number | null
|
|
||||||
active_chats: number | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPlan = ref<CurrentPlan>({ // temp, this get from api
|
|
||||||
plan: plans[1].val,
|
|
||||||
active_chats: 20,
|
|
||||||
exp: Date.now() / 1000 + 500000
|
|
||||||
})
|
|
||||||
|
|
||||||
interface CurrentPlanData extends CurrentPlan {
|
|
||||||
price: number | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPlanData = computed((): CurrentPlanData => ({
|
|
||||||
active_chats: currentPlan.value.active_chats ?? null,
|
|
||||||
exp: currentPlan.value.exp ?? null,
|
|
||||||
...plans.find(el=> el?.val === currentPlan.value.plan)
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const { t } = useI18n()
|
interface tgBtnData {
|
||||||
const textBtn = computed(() => {
|
status: 'hide' | 'show'
|
||||||
const prorata = currentPlanData.value.price
|
url?: string
|
||||||
? Math.ceil(
|
textBtn?: string
|
||||||
currentPlanData.value?.price / 30 *
|
|
||||||
date.getDateDiff(new Date(currentPlan.value.exp * 1000), Date.now())
|
|
||||||
)
|
|
||||||
: 0
|
|
||||||
const k = 1 - Number(periods.find(el => el.value === selectPeriod.value)?.sale) / 100
|
|
||||||
const stars = formatNumber(
|
|
||||||
Number(selectPeriod.value) *
|
|
||||||
Number(newPlan.value?.price) *
|
|
||||||
k - prorata
|
|
||||||
)
|
|
||||||
const newDateExp = date.addToDate(Date.now(), { months: Number(selectPeriod.value) })
|
|
||||||
|
|
||||||
return t(newPlan.value.id !== currentPlanData.value.id ? 'subscribe__pay' : 'subscribe__pay_renew') +
|
|
||||||
' ⭐' + stars +
|
|
||||||
' (' + t('subscribe__plan_exp') + ' ' +
|
|
||||||
date.formatDate(newDateExp, 'DD.MM.YYYY') + ')'
|
|
||||||
})
|
|
||||||
|
|
||||||
function formatNumber (number: string | number) {
|
|
||||||
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnmounted(() => tg.MainButton.hide())
|
const currentButtonHandler = ref<(() => void) | null>(null)
|
||||||
|
|
||||||
watch(textBtn, () => tg.MainButton.setText(textBtn.value),
|
function defaultTgBtn () {
|
||||||
{ immediate: true })
|
tg.MainButton.setText('-')
|
||||||
|
tg.MainButton.hide()
|
||||||
|
|
||||||
watch(newPlan, () =>
|
if (currentButtonHandler.value) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
tg.MainButton.offClick(currentButtonHandler.value)
|
||||||
{ newPlan.value.name === 'TEST' ? tg.MainButton.hide() : tg.MainButton.show() },
|
currentButtonHandler.value = null
|
||||||
{ immediate: true })
|
|
||||||
|
|
||||||
const maxWidthStarsSection = ref(0)
|
|
||||||
|
|
||||||
interface sizeParams {
|
|
||||||
height: number,
|
|
||||||
width: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateMaxWidthStarsSection (size: sizeParams) {
|
|
||||||
if (size.width > maxWidthStarsSection.value) {
|
|
||||||
maxWidthStarsSection.value = size.width
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const promocode = ref('')
|
function prepareTgBtn (btn: tgBtnData) {
|
||||||
|
defaultTgBtn()
|
||||||
|
|
||||||
function sendPromocode () {
|
if (btn.status === 'show' && btn.textBtn && btn.url) {
|
||||||
promocode.value = ''
|
|
||||||
showNotify('fail')
|
tg.MainButton.setText(btn.textBtn)
|
||||||
}
|
tg.MainButton.show()
|
||||||
|
|
||||||
const $q = useQuasar()
|
const handler = () => tg.openInvoice(btn.url!)
|
||||||
|
currentButtonHandler.value = handler
|
||||||
const showNotify = (message: 'success' | 'fail') => {
|
tg.MainButton.onClick(handler)
|
||||||
$q.notify({
|
}
|
||||||
message: t(message === 'success' ? 'subscribe__promocode_success' : 'subscribe__promocode_fail'),
|
|
||||||
type: message === 'success' ? 'positive' : 'negative',
|
|
||||||
position: 'bottom',
|
|
||||||
timeout: 1000,
|
|
||||||
multiLine: true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => defaultTgBtn())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -42,8 +42,8 @@
|
|||||||
import type { WebApp } from '@twa-dev/types'
|
import type { WebApp } from '@twa-dev/types'
|
||||||
|
|
||||||
const tg = inject('tg') as WebApp
|
const tg = inject('tg') as WebApp
|
||||||
const supportEmailAddress = 'a-mart@ya.ru'
|
const supportEmailAddress = 'support@tgcrew.ru'
|
||||||
const supportTelegramUser = 'alexmart80'
|
const supportTelegramUser = 'tgCrewSupport'
|
||||||
|
|
||||||
const telegramUrl = `https://t.me/${supportTelegramUser}`
|
const telegramUrl = `https://t.me/${supportTelegramUser}`
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,15 @@
|
|||||||
v-model="search"
|
v-model="search"
|
||||||
placeholder="chats__search"
|
placeholder="chats__search"
|
||||||
:show-calendar-btn="false"
|
:show-calendar-btn="false"
|
||||||
:filter-active="showFiltersDialog"
|
:show-filter-btn="false"
|
||||||
:filter-badge="!checkFiltersSelect"
|
|
||||||
@open-filters="showFiltersDialog = true"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #card-body-btn>
|
<template #card-body-btn>
|
||||||
<div class="w100 flex justify-end q-px-md">
|
<div
|
||||||
|
v-if="chats.length !== 0 || obsoleteChats.length !== 0"
|
||||||
|
class="w100 flex justify-end q-px-md"
|
||||||
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="showDialogHelp = !showDialogHelp"
|
@click="showDialogHelp = !showDialogHelp"
|
||||||
flat dense no-caps
|
flat dense no-caps
|
||||||
@@ -66,34 +67,106 @@
|
|||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<div class="row no-wrap items-center">
|
<div class="row no-wrap items-center">
|
||||||
<span class="text-caption q-pr-xs">
|
<span class="text-caption q-pr-xs">
|
||||||
{{ showDate(item.bot_access_date) }}
|
{{ getDate(item.bot_access_date) }}
|
||||||
</span>
|
</span>
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="getChatType(item.bot_access).icon"
|
:name="chatAccess(item.bot_access).icon"
|
||||||
:color="getChatType(item.bot_access).color"
|
:color="chatAccess(item.bot_access).color"
|
||||||
size="xs"
|
size="xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center no-wrap text-caption">
|
<div class="flex items-center no-wrap text-caption">
|
||||||
|
<!-- if chat reconnect -->
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="item.restore_date"
|
v-if="item.state === 2"
|
||||||
:name="getChatType('reconnect').icon"
|
:name="chatState(2).icon"
|
||||||
:color="getChatType('reconnect').color"
|
:color="chatState(2).color"
|
||||||
size="xs"
|
size="xs"
|
||||||
class="q-pr-sm"
|
class="q-pr-sm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="flex no-wrap items-center">
|
||||||
<q-icon name="mdi-account-outline"/>
|
<q-icon name="mdi-account-outline"/>
|
||||||
<span>{{ item.user_count }}</span>
|
<span>{{ item.user_count }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-slide-item>
|
</q-slide-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="obsoleteChats.length !== 0"
|
||||||
|
class="flex column items-center w100"
|
||||||
|
:class="showObsoleteChats ? 'bg-grey-12' : ''"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
class="w100 rotate-icon-btn"
|
||||||
|
color="grey"
|
||||||
|
flat
|
||||||
|
no-caps
|
||||||
|
@click="showObsoleteChats=!showObsoleteChats"
|
||||||
|
icon-right="arrow_drop_down"
|
||||||
|
:class="{ 'rotate-icon': showObsoleteChats }"
|
||||||
|
>
|
||||||
|
<span class="text-caption">
|
||||||
|
{{ !showObsoleteChats
|
||||||
|
? $t('chats__show_obsolete_chats') + ' (' + obsoleteChats.length +')'
|
||||||
|
: $t('chats__hide_obsolete_chats')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<div class="w100" style="overflow: hidden">
|
||||||
|
<transition
|
||||||
|
appear
|
||||||
|
enter-active-class="animated slideInDown"
|
||||||
|
leave-active-class="animated slideOutUp"
|
||||||
|
>
|
||||||
|
<q-list separator v-if="showObsoleteChats" class="w100">
|
||||||
|
<q-item
|
||||||
|
v-for = "item in obsoleteChats"
|
||||||
|
:key="item.id"
|
||||||
|
class="w100 text-grey"
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<pn-auto-avatar
|
||||||
|
:img="item.logo"
|
||||||
|
:name="item.name"
|
||||||
|
type="rounded"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label lines="1" class="text-bold">
|
||||||
|
{{ item.name }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<div
|
||||||
|
class="row no-wrap items-center"
|
||||||
|
v-if="item.state_date"
|
||||||
|
>
|
||||||
|
<span class="text-caption q-pr-xs">
|
||||||
|
{{ getDate(item.state_date) }}
|
||||||
|
</span>
|
||||||
|
<q-icon
|
||||||
|
:name="chatState(item.state).icon"
|
||||||
|
:color="chatState(item.state).color"
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<pn-onboard-btn
|
<pn-onboard-btn
|
||||||
v-if="chats.length === 0 && chatsInit"
|
v-if="chats.length === 0 && obsoleteChats.length === 0 && chatsInit"
|
||||||
icon="mdi-chat-plus-outline"
|
icon="mdi-chat-plus-outline"
|
||||||
:message1="$t('chats__onboard_msg1')"
|
:message1="$t('chats__onboard_msg1')"
|
||||||
:message2="$t('chats__onboard_msg2')"
|
:message2="$t('chats__onboard_msg2')"
|
||||||
@@ -123,6 +196,7 @@
|
|||||||
v-if="showFab"
|
v-if="showFab"
|
||||||
icon="add"
|
icon="add"
|
||||||
color="brand"
|
color="brand"
|
||||||
|
:class="isEnableChatAdd ? '' : 'fab-disabled'"
|
||||||
direction="up"
|
direction="up"
|
||||||
vertical-actions-align="right"
|
vertical-actions-align="right"
|
||||||
@click="showOverlay = !showOverlay"
|
@click="showOverlay = !showOverlay"
|
||||||
@@ -138,7 +212,7 @@
|
|||||||
>
|
>
|
||||||
|
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<q-list class="q-py-sm">
|
<q-list class="q-py-sm" v-if="isEnableChatAdd">
|
||||||
<q-item
|
<q-item
|
||||||
v-for="item in fabMenu"
|
v-for="item in fabMenu"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@@ -176,6 +250,14 @@
|
|||||||
</template>
|
</template>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
<div v-else class="w100 bg-white column q-py-md q-px-md rounded-card fab-action-item">
|
||||||
|
<div class="text-negative">
|
||||||
|
{{ $t('chats__disable_add_chat') }}
|
||||||
|
</div>
|
||||||
|
<span class="text-caption text-grey">
|
||||||
|
{{ $t('chats__disable_add_chat_description') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</q-fab-action>
|
</q-fab-action>
|
||||||
</q-fab>
|
</q-fab>
|
||||||
@@ -204,12 +286,11 @@
|
|||||||
>
|
>
|
||||||
<div class="q-py-none q-px-md q-ma-none">
|
<div class="q-py-none q-px-md q-ma-none">
|
||||||
<q-list class="q-pt-sm">
|
<q-list class="q-pt-sm">
|
||||||
<pn-chat-type-item
|
<pn-chat-type-item :access="2"/>
|
||||||
v-for="item in [0, 1, 2, 9, 10] as const"
|
<pn-chat-type-item :access="1"/>
|
||||||
:key="item"
|
<pn-chat-type-item :state="2"/>
|
||||||
:type="item"
|
<pn-chat-type-item :state="3"/>
|
||||||
class="q-px-none"
|
<pn-chat-type-item :state="4"/>
|
||||||
/>
|
|
||||||
</q-list>
|
</q-list>
|
||||||
<q-separator class="q-mt-md"/>
|
<q-separator class="q-mt-md"/>
|
||||||
<div class="text-caption q-pt-md">
|
<div class="text-caption q-pt-md">
|
||||||
@@ -226,70 +307,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</pn-simple-scroll-dialog>
|
</pn-simple-scroll-dialog>
|
||||||
|
|
||||||
<!-- filter dialog -->
|
|
||||||
<pn-bottom-sheet-dialog
|
|
||||||
title="chats__filters"
|
|
||||||
v-model="showFiltersDialog"
|
|
||||||
>
|
|
||||||
<template #btnSlot>
|
|
||||||
<div>
|
|
||||||
<q-btn
|
|
||||||
v-if="!checkFiltersSelect"
|
|
||||||
@click="resetFilters"
|
|
||||||
flat
|
|
||||||
no-caps
|
|
||||||
dense
|
|
||||||
color="grey-6"
|
|
||||||
>
|
|
||||||
{{ $t('chats__filters_reset')}}
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<q-btn
|
|
||||||
rounded
|
|
||||||
class="w100"
|
|
||||||
color="primary"
|
|
||||||
@click="showFiltersDialog = false"
|
|
||||||
>
|
|
||||||
{{$t('chats__filters_continue')}}
|
|
||||||
</q-btn>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="q-pl-sm text-bold text-caption">
|
|
||||||
{{ $t('chats__filters_type') }}
|
|
||||||
</div>
|
|
||||||
<div class="flex column">
|
|
||||||
<div
|
|
||||||
v-for="(item,idx) in chatTypeOptions"
|
|
||||||
:key="idx"
|
|
||||||
>
|
|
||||||
<q-checkbox
|
|
||||||
v-model="filters.chatType"
|
|
||||||
:val="item.value"
|
|
||||||
>
|
|
||||||
{{ $t(item.label) }}
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</pn-bottom-sheet-dialog>
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onActivated, onDeactivated, onBeforeUnmount, inject } from 'vue'
|
import { ref, computed, onActivated, onDeactivated, onBeforeUnmount, inject } from 'vue'
|
||||||
|
import { useAuthStore } from 'stores/auth'
|
||||||
import { useChatsStore } from 'stores/chats'
|
import { useChatsStore } from 'stores/chats'
|
||||||
import type { WebApp } from '@twa-dev/types'
|
import type { WebApp } from '@twa-dev/types'
|
||||||
import type { Chat, ChatType } from 'types/Chat'
|
import type { Chat } from 'types/Chat'
|
||||||
import { useI18n } from "vue-i18n"
|
import { useI18n } from "vue-i18n"
|
||||||
import { date } from 'quasar'
|
import { date } from 'quasar'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import pnChatTypeItem from 'components/pnChatTypeItem.vue'
|
import pnChatTypeItem from 'components/pnChatTypeItem.vue'
|
||||||
import pnSimpleScrollDialog from 'components/pnSimpleScrollDialog.vue'
|
import pnSimpleScrollDialog from 'components/pnSimpleScrollDialog.vue'
|
||||||
import pnActionBar from 'components/pnActionBar.vue'
|
import pnActionBar from 'components/pnActionBar.vue'
|
||||||
import pnBottomSheetDialog from 'components/pnBottomSheetDialog.vue'
|
import { chatAccess, chatState } from 'utils/chats'
|
||||||
import { getChatType } from 'utils/chats'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -312,6 +344,15 @@
|
|||||||
reset: () => void
|
reset: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const isEnableChatAdd = computed(() => {
|
||||||
|
if (!authStore.customer?.plan) return false
|
||||||
|
const a = authStore.customer?.plan?.chat_limit
|
||||||
|
if (a === null) return true
|
||||||
|
const b = authStore.customer?.plan?.used_chats
|
||||||
|
return a >= b
|
||||||
|
})
|
||||||
|
|
||||||
const chats = computed(() => chatsStore.chats)
|
const chats = computed(() => chatsStore.chats)
|
||||||
const chatsInit = computed(() => chatsStore.isInit)
|
const chatsInit = computed(() => chatsStore.isInit)
|
||||||
|
|
||||||
@@ -327,7 +368,6 @@
|
|||||||
const displayChats = computed(() => {
|
const displayChats = computed(() => {
|
||||||
return chats.value
|
return chats.value
|
||||||
.filter(searchChats)
|
.filter(searchChats)
|
||||||
.filter(chatsType)
|
|
||||||
|
|
||||||
function searchChats (el: Chat) {
|
function searchChats (el: Chat) {
|
||||||
if (!search.value || !(search.value && search.value.trim())) return true
|
if (!search.value || !(search.value && search.value.trim())) return true
|
||||||
@@ -335,11 +375,6 @@
|
|||||||
return el.name.toLowerCase().includes(searchValue) ||
|
return el.name.toLowerCase().includes(searchValue) ||
|
||||||
el.description && el.description.toLowerCase().includes(searchValue)
|
el.description && el.description.toLowerCase().includes(searchValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
function chatsType (el: Chat) {
|
|
||||||
if (filters.value.chatType.length === 0) return true
|
|
||||||
return filters.value.chatType.includes(el.bot_access)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleSlide (event: SlideEvent, id: number) {
|
function handleSlide (event: SlideEvent, id: number) {
|
||||||
@@ -431,40 +466,15 @@
|
|||||||
if (timerId.value) clearTimeout(timerId.value)
|
if (timerId.value) clearTimeout(timerId.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// filter chat
|
function getDate (d: number) {
|
||||||
const showFiltersDialog = ref(false)
|
|
||||||
interface Filters {
|
|
||||||
chatType: number[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultFilters = {
|
|
||||||
chatType: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const filters = ref<Filters>({ ...defaultFilters })
|
|
||||||
|
|
||||||
const checkFiltersSelect = computed(() => (
|
|
||||||
Object.values(filters.value).every(el => el.length === 0)
|
|
||||||
))
|
|
||||||
|
|
||||||
function resetFilters() {
|
|
||||||
(Object.keys(filters.value) as (keyof Filters)[]).forEach(key => filters.value[key] = [])
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatTypeOptions = computed(() => {
|
|
||||||
return [2, 1, 0, 9].map(el => ({
|
|
||||||
id: el,
|
|
||||||
value: el,
|
|
||||||
label: getChatType(el as Partial<ChatType>).label
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
function showDate (d: number) {
|
|
||||||
return new Date(d * 1000).getFullYear() === new Date(Date.now()).getFullYear()
|
return new Date(d * 1000).getFullYear() === new Date(Date.now()).getFullYear()
|
||||||
? date.formatDate(d * 1000, 'DD MMM')
|
? date.formatDate(d * 1000, 'DD MMM')
|
||||||
: date.formatDate(d * 1000, 'MMM YY')
|
: date.formatDate(d * 1000, 'MMM YY')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const obsoleteChats = computed(() => chatsStore.obsoleteChats)
|
||||||
|
const showObsoleteChats = ref(false)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -496,4 +506,24 @@
|
|||||||
align-self: center;
|
align-self: center;
|
||||||
height: 98%;
|
height: 98%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* icon for obsolete btn chats */
|
||||||
|
.rotate-icon-btn {
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate-icon-btn.rotate-icon :deep(.q-icon) {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate-icon-btn :deep(.q-icon) {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab-disabled :deep(.q-btn--fab) {
|
||||||
|
opacity: 0.65
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,20 +4,12 @@
|
|||||||
:hideHeader="users.length === 0"
|
:hideHeader="users.length === 0"
|
||||||
>
|
>
|
||||||
<template #card-body-header>
|
<template #card-body-header>
|
||||||
<div class="flex row q-ma-md justify-between">
|
<pn-action-bar
|
||||||
<q-input
|
|
||||||
v-model="search"
|
v-model="search"
|
||||||
clearable
|
placeholder="users__search"
|
||||||
clear-icon="close"
|
:show-calendar-btn="false"
|
||||||
:placeholder="$t('users__search')"
|
:show-filter-btn="false"
|
||||||
dense filled
|
/>
|
||||||
class="col-grow"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-magnify" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<q-list separator v-if="users.length !== 0">
|
<q-list separator v-if="users.length !== 0">
|
||||||
@@ -37,32 +29,7 @@
|
|||||||
class="w100"
|
class="w100"
|
||||||
@click="goUserInfo(item.id)"
|
@click="goUserInfo(item.id)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<pn-user-list-item-body :item showStatus/>
|
||||||
<pn-auto-avatar
|
|
||||||
:img="item.photo"
|
|
||||||
:name="item.section1"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label lines="1" v-if="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 lines="2">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<q-icon name="telegram" v-if="item.section2_1 || item.section2_2" class="q-pr-xs" style="color: #27a7e7"/>
|
|
||||||
<div v-if="item.section2_1" class="q-mr-sm text-bold">{{item.section2_1}}</div>
|
|
||||||
<div class="text-blue" v-if="item.section2_2">{{'@' + item.section2_2}}</div>
|
|
||||||
</div>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-slide-item>
|
</q-slide-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
@@ -102,27 +69,7 @@
|
|||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="w100 text-grey"
|
class="w100 text-grey"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<pn-user-list-item-body :item/>
|
||||||
<pn-auto-avatar
|
|
||||||
:img="item.photo"
|
|
||||||
:name="item.section1"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label lines="1" class="text-bold" v-if="item.section1">
|
|
||||||
{{item.section1}}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label lines="1" caption v-if="item.section3">
|
|
||||||
{{item.section3}}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label caption lines="2">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<q-icon name="telegram" v-if="item.section2_1 || item.section2_2" class="q-pr-xs" style="color: #27a7e7"/>
|
|
||||||
<div v-if="item.section2_1" class="q-mr-sm text-bold">{{item.section2_1}}</div>
|
|
||||||
<div class="text-blue" v-if="item.section2_2">{{'@' + item.section2_2}}</div>
|
|
||||||
</div>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</transition>
|
</transition>
|
||||||
@@ -130,7 +77,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- END LEAVE USERS SECTION -->
|
<!-- END LEAVE USERS SECTION -->
|
||||||
|
|
||||||
|
|
||||||
<!-- BLOCKED USERS SECTION -->
|
<!-- BLOCKED USERS SECTION -->
|
||||||
<div
|
<div
|
||||||
v-if="blockedUsers.length!==0"
|
v-if="blockedUsers.length!==0"
|
||||||
@@ -169,27 +115,7 @@
|
|||||||
@click="handleUnblockUser(item.id)"
|
@click="handleUnblockUser(item.id)"
|
||||||
class="w100 text-grey"
|
class="w100 text-grey"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<pn-user-list-item-body :item/>
|
||||||
<pn-auto-avatar
|
|
||||||
:img="item.photo"
|
|
||||||
:name="item.section1"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label lines="1" class="text-bold" v-if="item.section1">
|
|
||||||
{{item.section1}}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label lines="2" caption v-if="item.section3">
|
|
||||||
{{item.section3}}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label caption lines="2">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<q-icon name="telegram" v-if="item.section2_1 || item.section2_2" class="q-pr-xs" style="color: #27a7e7"/>
|
|
||||||
<div v-if="item.section2_1" class="q-mr-sm text-bold">{{item.section2_1}}</div>
|
|
||||||
<div class="text-blue" v-if="item.section2_2">{{'@' + item.section2_2}}</div>
|
|
||||||
</div>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</transition>
|
</transition>
|
||||||
@@ -246,7 +172,8 @@
|
|||||||
import { useUsersStore } from 'stores/users'
|
import { useUsersStore } from 'stores/users'
|
||||||
import { useCompaniesStore } from 'stores/companies'
|
import { useCompaniesStore } from 'stores/companies'
|
||||||
import { useUserSection } from 'composables/useUserSection'
|
import { useUserSection } from 'composables/useUserSection'
|
||||||
import { getUserStatus } from 'utils/users'
|
import pnUserListItemBody from 'components/pnUserListItemBody.vue'
|
||||||
|
import pnActionBar from 'components/pnActionBar.vue'
|
||||||
|
|
||||||
defineOptions({ inheritAttrs: false })
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default defineRouter(function (/* { store, ssrContext } */) {
|
|||||||
history: createHistory(process.env.VUE_ROUTER_BASE)
|
history: createHistory(process.env.VUE_ROUTER_BASE)
|
||||||
})
|
})
|
||||||
|
|
||||||
const publicPath = ['404', 'terms', 'privacy', 'consent']
|
const publicPath = ['404', 'terms', 'privacy', 'consent', 'settings']
|
||||||
|
|
||||||
Router.beforeEach(async (to) => {
|
Router.beforeEach(async (to) => {
|
||||||
if (to.name !== 'telegram_only' && sessionStorage.getItem('isTelegram') === 'false') {
|
if (to.name !== 'telegram_only' && sessionStorage.getItem('isTelegram') === 'false') {
|
||||||
@@ -50,8 +50,9 @@ export default defineRouter(function (/* { store, ssrContext } */) {
|
|||||||
return authStore.isAuth ? { name: 'projects', replace: true } : true
|
return authStore.isAuth ? { name: 'projects', replace: true } : true
|
||||||
|
|
||||||
if (to.name === 'agreements') return true
|
if (to.name === 'agreements') return true
|
||||||
|
|
||||||
if (to.name === 'create_account' && !authStore.isAuth) return true
|
if (to.name === 'create_account' && !authStore.isAuth) return true
|
||||||
|
if (to.name === 'recovery_password' && !authStore.isAuth) return true
|
||||||
|
|
||||||
|
|
||||||
if (!authStore.isAuth) return { name: 'login', replace: true }
|
if (!authStore.isAuth) return { name: 'login', replace: true }
|
||||||
|
|
||||||
|
|||||||
@@ -138,9 +138,9 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('pages/account/ConsentPage.vue')
|
component: () => import('pages/account/ConsentPage.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'change_plan_rules',
|
name: 'change_tariff_rules',
|
||||||
path: '/change-plan-rules',
|
path: '/change-tariff-rules',
|
||||||
component: () => import('pages/account/ChangePlanRules.vue')
|
component: () => import('src/pages/account/ChangeTariffRules.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'support',
|
name: 'support',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { api } from 'boot/axios'
|
import { api } from 'boot/axios'
|
||||||
import { useProjectsStore } from 'stores/projects'
|
import { useProjectsStore } from 'stores/projects'
|
||||||
|
import { useAuthStore } from 'stores/auth'
|
||||||
import { useChatsStore } from 'stores/chats'
|
import { useChatsStore } from 'stores/chats'
|
||||||
import { useUsersStore } from 'stores/users'
|
import { useUsersStore } from 'stores/users'
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ export const useSSEStore = defineStore('sse', () => {
|
|||||||
const maxReconnectAttempts = 5
|
const maxReconnectAttempts = 5
|
||||||
|
|
||||||
const projectsStore = useProjectsStore()
|
const projectsStore = useProjectsStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
const chatsStore = useChatsStore()
|
const chatsStore = useChatsStore()
|
||||||
const usersStore = useUsersStore()
|
const usersStore = useUsersStore()
|
||||||
|
|
||||||
@@ -61,33 +63,30 @@ export const useSSEStore = defineStore('sse', () => {
|
|||||||
const setupEventListeners = () => {
|
const setupEventListeners = () => {
|
||||||
if (!eventSource.value) return
|
if (!eventSource.value) return
|
||||||
|
|
||||||
eventSource.value.addEventListener('chat-attach', (event) => {
|
eventSource.value.addEventListener('chat-update', (event) => {
|
||||||
const data = JSON.parse(event.data)
|
const data = JSON.parse(event.data)
|
||||||
|
projectsStore.init().catch(() => {}) // for update count of chats on projects page
|
||||||
|
authStore.updateProfile().catch(() => {}) // for update count of active chats in subscribe
|
||||||
if (data.project_id === currentProjectId.value) {
|
if (data.project_id === currentProjectId.value) {
|
||||||
chatsStore.init().catch(() => {})
|
chatsStore.init().catch(() => {})
|
||||||
|
usersStore.init().catch(() => {}) // for update users from chats
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
eventSource.value.addEventListener('chat-delete', (event) => {
|
eventSource.value.addEventListener('chat-delete', (event) => {
|
||||||
const data = JSON.parse(event.data)
|
const data = JSON.parse(event.data)
|
||||||
|
projectsStore.init().catch(() => {}) // for update count of chats on projects page
|
||||||
|
authStore.updateProfile().catch(() => {}) // for update count of active chats in subscribe
|
||||||
if (data.project_id === currentProjectId.value) {
|
if (data.project_id === currentProjectId.value) {
|
||||||
chatsStore.init().catch(() => {})
|
chatsStore.init().catch(() => {})
|
||||||
usersStore.init().catch(() => {})
|
usersStore.init().catch(() => {}) // for update users from chats
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
eventSource.value.addEventListener('chat-detach', (event) => {
|
eventSource.value.addEventListener('customer-update', (event) => {
|
||||||
const data = JSON.parse(event.data)
|
const data = JSON.parse(event.data)
|
||||||
if (data.project_id === currentProjectId.value) {
|
if (data.project_id === currentProjectId.value) {
|
||||||
chatsStore.detach(data.id)
|
authStore.updateProfile().catch(() => {})
|
||||||
usersStore.init().catch(() => {})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
eventSource.value.addEventListener('chat-update', (event) => {
|
|
||||||
const data = JSON.parse(event.data)
|
|
||||||
if (data.project_id === currentProjectId.value) {
|
|
||||||
chatsStore.init().catch(() => {})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { api } from 'boot/axios'
|
import { api } from 'boot/axios'
|
||||||
import type { CompanyParams } from 'types/Company'
|
import type { CompanyParams } from 'types/Company'
|
||||||
|
import type { CurrentTariff } from 'types/Tariff'
|
||||||
import { useProjectsStore } from 'stores/projects'
|
import { useProjectsStore } from 'stores/projects'
|
||||||
|
|
||||||
interface Customer {
|
interface Customer {
|
||||||
@@ -12,6 +13,7 @@ interface Customer {
|
|||||||
last_name?: string
|
last_name?: string
|
||||||
avatar?: string
|
avatar?: string
|
||||||
company?: CompanyParams
|
company?: CompanyParams
|
||||||
|
plan?: CurrentTariff
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WSMessage {
|
interface WSMessage {
|
||||||
@@ -24,7 +26,7 @@ interface WSMessage {
|
|||||||
|
|
||||||
const ENDPOINT_MAP = {
|
const ENDPOINT_MAP = {
|
||||||
register: '/auth/email/register',
|
register: '/auth/email/register',
|
||||||
forgot: '/auth/forgot',
|
forgot: '/auth/email/recovery',
|
||||||
changePwd: '/auth/email/change-password',
|
changePwd: '/auth/email/change-password',
|
||||||
changeMethod: '/auth/email/upgrade'
|
changeMethod: '/auth/email/upgrade'
|
||||||
} as const
|
} as const
|
||||||
@@ -104,6 +106,12 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
const setNewEmailPassword = async (code: string, code2: string, email: string, password: string) =>
|
const setNewEmailPassword = async (code: string, code2: string, email: string, password: string) =>
|
||||||
await api.post('/auth/email/change-email', { code, code2, email, password })
|
await api.post('/auth/email/change-email', { code, code2, email, password })
|
||||||
|
|
||||||
|
// for payment
|
||||||
|
async function updateProfile () {
|
||||||
|
const { data } = await api.get('/customer/profile')
|
||||||
|
customer.value = data.data
|
||||||
|
}
|
||||||
|
|
||||||
// user data company
|
// user data company
|
||||||
const updateMyCompany = async (companyData: CompanyParams) => {
|
const updateMyCompany = async (companyData: CompanyParams) => {
|
||||||
const response = await api.put('/customer/profile', { company: companyData })
|
const response = await api.put('/customer/profile', { company: companyData })
|
||||||
@@ -131,6 +139,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
initRegistration,
|
initRegistration,
|
||||||
confirmCode,
|
confirmCode,
|
||||||
setPassword,
|
setPassword,
|
||||||
|
updateProfile,
|
||||||
updateMyCompany,
|
updateMyCompany,
|
||||||
getCodeCurrentEmail,
|
getCodeCurrentEmail,
|
||||||
confirmCurrentEmailCode,
|
confirmCurrentEmailCode,
|
||||||
|
|||||||
@@ -1,22 +1,35 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { api } from 'boot/axios'
|
import { api } from 'boot/axios'
|
||||||
import type { Chat } from 'types/Chat'
|
import type { Chat, ObsoleteChat } from 'types/Chat'
|
||||||
import { useProjectsStore } from 'stores/projects'
|
import { useProjectsStore } from 'stores/projects'
|
||||||
import { useUsersStore } from 'stores/users'
|
import { useUsersStore } from 'stores/users'
|
||||||
|
|
||||||
export const useChatsStore = defineStore('chats', () => {
|
export const useChatsStore = defineStore('chats', () => {
|
||||||
const projectsStore = useProjectsStore()
|
const projectsStore = useProjectsStore()
|
||||||
const chats = ref<Chat[]>([])
|
const chats = ref<Chat[]>([])
|
||||||
const isInit = ref<boolean>(false)
|
const obsoleteChats = ref<ObsoleteChat[]>([])
|
||||||
|
const isInit = ref(false)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
||||||
|
|
||||||
async function init () {
|
async function init () {
|
||||||
|
// guard for double request
|
||||||
|
if (isLoading.value) return
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
reset()
|
reset()
|
||||||
const { data } = await api.get('/project/' + currentProjectId.value + '/chat')
|
const chatsResponse = await api.get(`/project/${currentProjectId.value}/chat`)
|
||||||
chats.value.push(...data.data)
|
chats.value = chatsResponse.data.data
|
||||||
|
const obsoleteChatsResponse = await api.get(`/project/${currentProjectId.value}/chat/obsolete`)
|
||||||
|
obsoleteChats.value = obsoleteChatsResponse.data.data
|
||||||
isInit.value = true
|
isInit.value = true
|
||||||
|
} catch {
|
||||||
|
reset()
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset () {
|
function reset () {
|
||||||
@@ -28,6 +41,8 @@ export const useChatsStore = defineStore('chats', () => {
|
|||||||
const { data } = await api.delete('/project/' + currentProjectId.value + '/chat/' + chatId)
|
const { data } = await api.delete('/project/' + currentProjectId.value + '/chat/' + chatId)
|
||||||
chats.value = chats.value.filter(item => item.id !== data.data.id)
|
chats.value = chats.value.filter(item => item.id !== data.data.id)
|
||||||
const usersStore = useUsersStore()
|
const usersStore = useUsersStore()
|
||||||
|
reset()
|
||||||
|
await init()
|
||||||
await usersStore.init()
|
await usersStore.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,30 +55,14 @@ export const useChatsStore = defineStore('chats', () => {
|
|||||||
return chats.value.find(el => el.id === id)
|
return chats.value.find(el => el.id === id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// attach, update and detach for use in SSE-store
|
|
||||||
function attach (chat: Chat) {
|
|
||||||
chats.value.push(chat)
|
|
||||||
}
|
|
||||||
|
|
||||||
function update (chat: Chat) {
|
|
||||||
const idx = chats.value.findIndex(item => item.id === chat.id)
|
|
||||||
if (chats.value[idx]) Object.assign(chats.value[idx], chat)
|
|
||||||
}
|
|
||||||
|
|
||||||
function detach (chatId: number) {
|
|
||||||
chats.value = chats.value.filter(item => item.id !== chatId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chats,
|
chats,
|
||||||
|
obsoleteChats,
|
||||||
isInit,
|
isInit,
|
||||||
init,
|
init,
|
||||||
reset,
|
reset,
|
||||||
unlink,
|
unlink,
|
||||||
getKey,
|
getKey,
|
||||||
chatById,
|
chatById
|
||||||
attach,
|
|
||||||
update,
|
|
||||||
detach
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,13 +15,23 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||||||
const chatsStore = useChatsStore()
|
const chatsStore = useChatsStore()
|
||||||
const usersStore = useUsersStore()
|
const usersStore = useUsersStore()
|
||||||
const companiesStore = useCompaniesStore()
|
const companiesStore = useCompaniesStore()
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
async function init () {
|
async function init () {
|
||||||
|
// guard for double request
|
||||||
|
if (isLoading.value) return
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
reset()
|
||||||
const { data } = await api.get('/project')
|
const { data } = await api.get('/project')
|
||||||
const projectsAPI = data.data
|
const projectsAPI = data.data
|
||||||
projects.value.push(...projectsAPI)
|
projects.value.push(...projectsAPI)
|
||||||
isInit.value = true
|
isInit.value = true
|
||||||
|
} catch {
|
||||||
|
reset()
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset () {
|
function reset () {
|
||||||
@@ -31,7 +41,7 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function add (projectData: ProjectParams) {
|
async function add (projectData: ProjectParams) {
|
||||||
const { data }= await api.post('/project', projectData)
|
const { data } = await api.post('/project', projectData)
|
||||||
const newProjectAPI = data.data
|
const newProjectAPI = data.data
|
||||||
projects.value.push(newProjectAPI)
|
projects.value.push(newProjectAPI)
|
||||||
return newProjectAPI
|
return newProjectAPI
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ref, computed, inject } from 'vue'
|
|||||||
import { api } from 'boot/axios'
|
import { api } from 'boot/axios'
|
||||||
import { useAuthStore } from 'stores/auth'
|
import { useAuthStore } from 'stores/auth'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { Lang } from 'quasar'
|
||||||
import type { WebApp } from '@twa-dev/types'
|
import type { WebApp } from '@twa-dev/types'
|
||||||
|
|
||||||
interface AppSettings {
|
interface AppSettings {
|
||||||
@@ -42,6 +43,24 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
{ value: 20, label: 'settings__fontsize_large' }
|
{ value: 20, label: 'settings__fontsize_large' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const quasarLangMap: Record<string, string> = {
|
||||||
|
'en-US': 'en-US',
|
||||||
|
'ru-RU': 'ru'
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateQuasarLang = async (locale: string) => {
|
||||||
|
const quasarLang = quasarLangMap[locale] || 'en-US'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const langModule = await import(
|
||||||
|
`../../node_modules/quasar/lang/${quasarLang}.js`
|
||||||
|
)
|
||||||
|
Lang.set(langModule.default)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Quasar Error load locale:', quasarLang, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const detectLocale = (): string => {
|
const detectLocale = (): string => {
|
||||||
const localeMap = {
|
const localeMap = {
|
||||||
ru: 'ru-RU',
|
ru: 'ru-RU',
|
||||||
@@ -69,9 +88,10 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyLocale = () => {
|
const applyLocale = async () => {
|
||||||
if (settings.value.locale && i18nLocale) {
|
if (settings.value.locale && i18nLocale) {
|
||||||
i18nLocale.value = settings.value.locale
|
i18nLocale.value = settings.value.locale
|
||||||
|
await updateQuasarLang(settings.value.locale)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,19 +124,15 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateCssVariable()
|
updateCssVariable()
|
||||||
applyLocale()
|
await applyLocale()
|
||||||
isInit.value = true
|
isInit.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveSettings = async () => {
|
|
||||||
await api.put('/customer/settings', settings.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSettings = async (newSettings: Partial<AppSettings>) => {
|
const updateSettings = async (newSettings: Partial<AppSettings>) => {
|
||||||
settings.value = { ...settings.value, ...newSettings }
|
settings.value = { ...settings.value, ...newSettings }
|
||||||
updateCssVariable()
|
updateCssVariable()
|
||||||
applyLocale()
|
await applyLocale()
|
||||||
await saveSettings()
|
if (authStore.isAuth) await api.put('/customer/settings', settings.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -8,15 +8,24 @@ export const useUsersStore = defineStore('users', () => {
|
|||||||
const projectsStore = useProjectsStore()
|
const projectsStore = useProjectsStore()
|
||||||
const users = ref<User[]>([])
|
const users = ref<User[]>([])
|
||||||
const isInit = ref<boolean>(false)
|
const isInit = ref<boolean>(false)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
||||||
|
|
||||||
async function init () {
|
async function init () {
|
||||||
|
// guard for double request
|
||||||
|
if (isLoading.value) return
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
reset()
|
reset()
|
||||||
const { data } = await api.get(`/project/${currentProjectId.value}/user`)
|
const { data } = await api.get(`/project/${currentProjectId.value}/user`)
|
||||||
const usersAPI = data.data
|
users.value = data.data
|
||||||
users.value.push(...usersAPI)
|
|
||||||
isInit.value = true
|
isInit.value = true
|
||||||
|
} catch {
|
||||||
|
reset()
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset () {
|
function reset () {
|
||||||
@@ -28,7 +37,7 @@ export const useUsersStore = defineStore('users', () => {
|
|||||||
const { data } = await api.put(`/project/${currentProjectId.value}/user/${userId}`, userData)
|
const { data } = await api.put(`/project/${currentProjectId.value}/user/${userId}`, userData)
|
||||||
const userAPI = data.data
|
const userAPI = data.data
|
||||||
const idx = users.value.findIndex(item => item.id === userAPI.id)
|
const idx = users.value.findIndex(item => item.id === userAPI.id)
|
||||||
if (users.value[idx]) Object.assign(users.value[idx], userAPI)
|
if (idx !== -1) users.value[idx] = { ...users.value[idx], ...userAPI }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function blockUser (userId: number) {
|
async function blockUser (userId: number) {
|
||||||
@@ -48,7 +57,7 @@ export const useUsersStore = defineStore('users', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function userById (id: number) {
|
function userById (id: number) {
|
||||||
return users.value.find(el =>el.id === id)
|
return users.value.find(el => el.id === id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function userNameById (id: number) {
|
function userNameById (id: number) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ interface Chat {
|
|||||||
message_id: number
|
message_id: number
|
||||||
name: string
|
name: string
|
||||||
is_channel: boolean
|
is_channel: boolean
|
||||||
bot_can_ban: boolean
|
|
||||||
user_count: number
|
user_count: number
|
||||||
last_update_time: number
|
last_update_time: number
|
||||||
description: string | null
|
description: string | null
|
||||||
@@ -14,20 +13,24 @@ interface Chat {
|
|||||||
bot_access: 0 | 1 | 2
|
bot_access: 0 | 1 | 2
|
||||||
bot_access_date: number
|
bot_access_date: number
|
||||||
bot_rights: 1 | 2 | 3
|
bot_rights: 1 | 2 | 3
|
||||||
restore_date: null | number
|
state: 1 | 2 | 3 | 4
|
||||||
|
state_date: null | number
|
||||||
users: number []
|
users: number []
|
||||||
last_pts?: number
|
last_pts?: number
|
||||||
[key: string]: number | string | boolean | null | number[]
|
[key: string]: number | string | boolean | null | number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatType =
|
interface ObsoleteChat {
|
||||||
0 | 'detach' |
|
id: number
|
||||||
1 | 'notify' |
|
name: string
|
||||||
2 | 'full' |
|
state: 1 | 2 | 3 | 4
|
||||||
9 | 'delete' |
|
state_date: null | number
|
||||||
10 | 'reconnect'
|
telegram_id: string
|
||||||
|
logo: string | null
|
||||||
|
[key: string]: number | string | null
|
||||||
|
}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
Chat,
|
Chat,
|
||||||
ChatType
|
ObsoleteChat
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/types/Tariff.ts
Normal file
32
src/types/Tariff.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
interface Tariff {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
chat_limit: number | null
|
||||||
|
price: number
|
||||||
|
is_archived: boolean
|
||||||
|
[key: string]: string | boolean | number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CurrentTariff {
|
||||||
|
id: number
|
||||||
|
can_change: boolean
|
||||||
|
debt: number
|
||||||
|
used_chats: number
|
||||||
|
start_date: number
|
||||||
|
end_date: number | null
|
||||||
|
chat_limit: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TariffPeriod {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
name_in_days: string
|
||||||
|
discount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type {
|
||||||
|
Tariff,
|
||||||
|
CurrentTariff,
|
||||||
|
TariffPeriod
|
||||||
|
}
|
||||||
@@ -1,50 +1,84 @@
|
|||||||
import type { ChatType } from "types/Chat"
|
function chatAccess (i: 0 | 1 | 2 ) {
|
||||||
|
switch (i) {
|
||||||
function getChatType (type: ChatType) {
|
case 0: return typeDescription('no_access')
|
||||||
switch (type) {
|
case 1: return typeDescription('notify')
|
||||||
default:
|
case 2: return typeDescription('full')
|
||||||
case 0:
|
}
|
||||||
case 'detach': return {
|
|
||||||
icon: 'mdi-link-off',
|
}
|
||||||
color: 'negative',
|
|
||||||
label: 'chat_type__detach_label',
|
function chatState (i: 1 | 2 | 3 | 4) {
|
||||||
description: 'chat_type__detach_description'
|
switch (i) {
|
||||||
|
case 1: return typeDescription('new')
|
||||||
|
case 2: return typeDescription('reconnect')
|
||||||
|
case 3: return typeDescription('detach')
|
||||||
|
case 4: return typeDescription('delete')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatType = 'no_access' | 'notify' | 'full' | 'new' | 'reconnect' | 'detach' | 'delete'
|
||||||
|
|
||||||
|
function typeDescription (type: ChatType) {
|
||||||
|
switch (type) {
|
||||||
|
case 'no_access': return {
|
||||||
|
info: 'no_access',
|
||||||
|
icon: 'mdi-close',
|
||||||
|
color: 'negative',
|
||||||
|
label: 'chat_type__label_no_access',
|
||||||
|
description: 'chat_type__description_no_access'
|
||||||
}
|
}
|
||||||
|
|
||||||
case 1:
|
|
||||||
case 'notify': return {
|
case 'notify': return {
|
||||||
|
info: type,
|
||||||
icon: 'mdi-bell-outline',
|
icon: 'mdi-bell-outline',
|
||||||
color: 'info',
|
color: 'info',
|
||||||
label: 'chat_type__notify_label',
|
label: 'chat_type__label_notify',
|
||||||
description: 'chat_type__notify_description'
|
description: 'chat_type__description_notify'
|
||||||
}
|
}
|
||||||
|
|
||||||
case 2:
|
|
||||||
case 'full': return {
|
case 'full': return {
|
||||||
icon: 'mdi-star-shooting-outline',
|
info: type,
|
||||||
|
icon: 'mdi-crown-outline',
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
label: 'chat_type__full_label',
|
label: 'chat_type__label_full',
|
||||||
description: 'chat_type__full_description'
|
description: 'chat_type__description_full'
|
||||||
}
|
}
|
||||||
|
|
||||||
case 9:
|
case 'new': return {
|
||||||
case 'delete': return {
|
info: type,
|
||||||
icon: 'mdi-forum-remove-outline',
|
icon: '',
|
||||||
color: 'negative',
|
color: 'primary',
|
||||||
label: 'chat_type__delete_label',
|
label: 'chat_type__label_new',
|
||||||
description: 'chat_type__delete_description'
|
description: 'chat_type__description_new'
|
||||||
}
|
}
|
||||||
|
|
||||||
case 10:
|
|
||||||
case 'reconnect': return {
|
case 'reconnect': return {
|
||||||
|
info: type,
|
||||||
icon: 'mdi-repeat-variant',
|
icon: 'mdi-repeat-variant',
|
||||||
color: 'negative',
|
color: 'negative',
|
||||||
label: 'chat_type__reconnect_label',
|
label: 'chat_type__label_reconnect',
|
||||||
description: 'chat_type__reconnect_description'
|
description: 'chat_type__description_reconnect'
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'detach': return {
|
||||||
|
info: type,
|
||||||
|
icon: 'mdi-link-off',
|
||||||
|
color: 'negative',
|
||||||
|
label: 'chat_type__label_detach',
|
||||||
|
description: 'chat_type__description_detach'
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'delete': return {
|
||||||
|
info: type,
|
||||||
|
icon: 'mdi-chat-remove-outline',
|
||||||
|
color: 'negative',
|
||||||
|
label: 'chat_type__label_delete',
|
||||||
|
description: 'chat_type__description_delete'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getChatType
|
chatAccess,
|
||||||
|
chatState
|
||||||
}
|
}
|
||||||
@@ -4,17 +4,35 @@ function parseIntString (s: string | string[] | undefined) :number | null {
|
|||||||
return regex.test(s) ? Number(s) : null
|
return regex.test(s) ? Number(s) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeNullKeys = <T extends object>(obj: T): Partial<T> =>
|
type RemoveNullKeysReturn<T, K extends keyof T = never> =
|
||||||
Object.fromEntries(
|
Partial<Pick<T, Exclude<keyof T, K>>> & Pick<T, K>
|
||||||
Object.entries(obj).filter(([, v]) => v !== null)
|
|
||||||
) as Partial<T>
|
const removeNullKeys = <T extends object, K extends keyof T = never>(
|
||||||
|
obj: T,
|
||||||
|
exceptions?: readonly K[]
|
||||||
|
): RemoveNullKeysReturn<T, K> => {
|
||||||
|
const result: Partial<T> = {}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(obj) as [keyof T, T[keyof T]][]) {
|
||||||
|
if (exceptions?.includes(key as K) || value !== null) {
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result as RemoveNullKeysReturn<T, K>
|
||||||
|
}
|
||||||
|
|
||||||
const getFieldValue = (mod: string, props: undefined | null | string ) => {
|
const getFieldValue = (mod: string, props: undefined | null | string ) => {
|
||||||
return mod !== '' ? mod : props === null || props === undefined ? null : ''
|
return mod !== '' ? mod : props === null || props === undefined ? null : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatNumber = (number: string | number) => {
|
||||||
|
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ")
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
parseIntString,
|
parseIntString,
|
||||||
removeNullKeys,
|
removeNullKeys,
|
||||||
getFieldValue
|
getFieldValue,
|
||||||
|
formatNumber
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user