This commit is contained in:
2025-11-02 10:08:57 +03:00
parent 83d2666f22
commit d3d8b7522a
41 changed files with 518 additions and 496 deletions

Binary file not shown.

View File

@@ -1,8 +1,8 @@
{ {
"name": "projectsnode", "name": "tgcrew",
"version": "0.0.1", "version": "0.1.01",
"description": "telegram miniapp", "description": "telegram admin miniapp",
"productName": "projectsNode", "productName": "tgCrew (admin)",
"author": "Alex Mart", "author": "Alex Mart",
"type": "module", "type": "module",
"private": true, "private": true,
@@ -49,4 +49,4 @@
"npm": ">= 6.13.4", "npm": ">= 6.13.4",
"yarn": ">= 1.21.1" "yarn": ">= 1.21.1"
} }
} }

1
public/resources Symbolic link
View File

@@ -0,0 +1 @@
F:/project/tgCrew/backend/public/resources

View File

@@ -117,6 +117,16 @@ export default defineConfig((ctx) => {
changeOrigin: true, changeOrigin: true,
ws: true ws: true
}, },
// необходимо создать simlink
// mklink /D "F:\project\tgCrew\admin\public\resources" "F:\project\tgCrew\backend\public\resources"
'/resources': {
target: 'http://localhost:3000',
changeOrigin: true,
secure: false,
pathRewrite: {
'^/resources': '/resources'
}
}
/* '/ws': { /* '/ws': {
target: 'ws://localhost:3000', // или wss:// для HTTPS target: 'ws://localhost:3000', // или wss:// для HTTPS
changeOrigin: true, changeOrigin: true,
@@ -126,7 +136,7 @@ export default defineConfig((ctx) => {
}, },
// https: true, // https: true,
// open: true // opens browser window automatically open: false // opens browser window automatically
}, },
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework // https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework
@@ -244,7 +254,7 @@ export default defineConfig((ctx) => {
builder: { builder: {
// https://www.electron.build/configuration/configuration // https://www.electron.build/configuration/configuration
appId: 'projectsnode' appId: 'tgcrew'
} }
}, },

View File

@@ -4,21 +4,23 @@
{{ $t(title) }} {{ $t(title) }}
</template> </template>
<template #footer> <template #footer>
<q-btn <div class="w100 q-pa-md">
rounded color="primary" <q-btn
class="w100 q-mt-md q-mb-xs fix-disabled-btn" rounded color="primary"
:disable="!isFormValid" class="w100 fix-disabled-btn"
@click = "onSubmit" :disable="!isFormValid"
> @click = "onSubmit"
{{ $t(btnText) }} >
</q-btn> {{ $t(btnText) }}
</q-btn>
</div>
</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">
<slot name="myCompany"/> <slot name="myCompany"/>
<pn-image-selector <pn-image-selector
v-model="modelValue.logo" v-model="companyMod.logo"
:size="100" :size="100"
:iconsize="80" :iconsize="80"
class="q-pb-lg" class="q-pb-lg"
@@ -27,7 +29,7 @@
<q-input <q-input
v-for="input in textInputs" v-for="input in textInputs"
:key="input.id" :key="input.id"
v-model.trim="modelValue[input.val]" v-model.trim="companyMod[input.val]"
dense dense
filled filled
class="w100" class="w100"
@@ -38,7 +40,7 @@
? 'input-fix q-pt-sm' ? 'input-fix q-pt-sm'
: 'q-pt-sm'" : 'q-pt-sm'"
:label="input.label ? $t(input.label) : void 0" :label="input.label ? $t(input.label) : void 0"
:rules="input.val === 'name' ? [rules[input.val]] : []" :rules="input.rules"
no-error-icon no-error-icon
:label-slot="Boolean(input.label)" :label-slot="Boolean(input.label)"
> >
@@ -57,24 +59,35 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, reactive } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { CompanyParams } from 'types/Company' import type { CompanyParams } from 'types/Company'
import { convertEmptyStringsToNull } from 'src/utils/helpers' import { removeNullKeys } from 'utils/helpers'
const { t }= useI18n() const { t }= useI18n()
const modelValue = defineModel<CompanyParams>({ const props = defineProps<{
required: true
})
defineProps<{
title: string, title: string,
btnText: string btnText: string,
company?: CompanyParams
}>() }>()
const emit = defineEmits(['update']) const emit = defineEmits(['update'])
type CompType = {
[K in keyof CompanyParams]: CompanyParams[K] extends number ? never : CompanyParams[K]
}
const companyMod = reactive<CompType>({
name: props.company?.name ?? '',
description: props.company?.description ?? '',
site: props.company?.site ?? '',
address: props.company?.address ?? '',
phone: props.company?.phone ?? '',
email: props.company?.email ?? '',
logo: props.company?.logo ?? null
})
interface TextInput { interface TextInput {
id: number id: number
label?: string label?: string
@@ -83,34 +96,42 @@
rules: ((value: string) => boolean | string)[] rules: ((value: string) => boolean | string)[]
} }
const rules = {
name: (val: string) => !!val?.trim() || rulesErrorMessage['name']
}
const textInputs: TextInput[] = [ const textInputs: TextInput[] = [
{ id: 1, val: 'name', label: 'company_block__name', rules: [] }, { id: 1, val: 'name', label: 'company_block__name', rules: [rules.name] },
{ id: 2, val: 'description', label: 'company_block__description', rules: [] }, { id: 2, val: 'description', label: 'company_block__description', rules: [] },
{ id: 3, val: 'site', icon: 'mdi-web', rules: [] }, { id: 3, val: 'site', icon: 'mdi-web', rules: [] },
{ id: 4, val: 'address', icon: 'mdi-map-marker-outline', rules: [] }, { id: 4, val: 'address', icon: 'mdi-map-marker-outline', rules: [] },
{ id: 5, val: 'phone', icon: 'mdi-phone-outline', rules: [] }, { id: 5, val: 'phone', icon: 'mdi-phone-outline', rules: [] },
{ id: 6, val: 'email', icon: 'mdi-email-outline', rules: [] } { id: 6, val: 'email', icon: 'mdi-email-outline', rules: [] }
] ] as const
const rulesErrorMessage = { const rulesErrorMessage = {
name: t('company_block__error_name') name: t('company_block__error_name')
} }
const rules = {
name: (val: CompanyParams['name']) => !!val?.trim() || rulesErrorMessage['name']
}
const isFormValid = computed(() => { const isFormValid = computed(() => {
const validations = { const validations = {
name: rules.name(modelValue.value.name) === true name: companyMod.name && rules.name(companyMod.name) === true
} }
return Object.values(validations).every(Boolean) return Object.values(validations).every(Boolean)
}) })
function onSubmit() { function onSubmit() {
const cleanedData = convertEmptyStringsToNull(modelValue.value) const outData = {
emit('update', cleanedData) name: companyMod.name,
description: companyMod.description ?? (typeof props.company?.description === 'string' ? '' : null),
site: companyMod.site ?? (typeof props.company?.site === 'string' ? '' : null),
address: companyMod.address ?? (typeof props.company?.address === 'string' ? '' : null),
phone: companyMod.phone ?? (typeof props.company?.phone === 'string' ? '' : null),
email: companyMod.email ?? (typeof props.company?.email === 'string' ? '' : null),
logo: companyMod.logo
}
emit('update', removeNullKeys(outData))
} }
</script> </script>

View File

@@ -30,7 +30,7 @@
:style="{ :style="{
height: sizePx, height: sizePx,
maxWidth: sizePx, maxWidth: sizePx,
borderRadius: avatar ? String(size/2) + 'px' : 'var(--top-raduis)', borderRadius: avatar ? String(size/2) + 'px' : 'var(--top-radius)',
}" }"
@click="showDialog = true" @click="showDialog = true"
/> />

View File

@@ -36,8 +36,8 @@
<q-card <q-card
class="fix-card-width flex column no-scroll no-wrap q-px-none" class="fix-card-width flex column no-scroll no-wrap q-px-none"
style=" style="
border-top-left-radius: var(--top-raduis) !important; border-top-left-radius: var(--top-radius) !important;
border-top-right-radius: var(--top-raduis) !important; border-top-right-radius: var(--top-radius) !important;
" "
> >
<div <div
@@ -46,7 +46,12 @@
> >
<div> <div>
<div class="flex column q-mx-xs"> <div class="flex column q-mx-xs">
<span class="text-h6 ellipsis">{{ $t(title) }}</span> <span
class="text-h6"
style="line-height: 1.5rem;"
>
{{ $t(title) }}
</span>
<span v-if="caption" class="text-grey text-caption">{{ $t(caption) }}</span> <span v-if="caption" class="text-grey text-caption">{{ $t(caption) }}</span>
</div> </div>
</div> </div>

View File

@@ -18,20 +18,20 @@
<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">
{{message1}} {{ message1 }}
</div> </div>
</div> </div>
</div> </div>
<div v-if="message2" class="text-caption" align="center"> <div v-if="message2" class="text-caption text-center">
{{message2}} {{ message2 }}
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ defineProps<{
icon: string icon: string
message1: string message1: string
message2?: string message2?: string

View File

@@ -6,7 +6,7 @@
<slot name="title"/> <slot name="title"/>
</div> </div>
<slot/> <slot/>
<div class="bg-white w100 q-ma-none q-px-md flex items-center"> <div class="bg-white w100">
<slot name="footer"/> <slot name="footer"/>
</div> </div>
</template> </template>

View File

@@ -5,12 +5,16 @@
> >
<div <div
id="card-body-header" id="card-body-header"
style="flex-shrink: 0; min-height: var(--top-raduis);" v-if="slots['card-body-header']"
style="flex-shrink: 0; min-height: var(--top-radius);"
> >
<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 id="card-body"
class="top-rounded-card bg-white"
style="padding-top: var(--top-radius);"
>
<q-resize-observer @resize="onBodyResize"/> <q-resize-observer @resize="onBodyResize"/>
<pn-shadow-scroll :hideShadows="isResizing" :height="scrollAreaHeight"> <pn-shadow-scroll :hideShadows="isResizing" :height="scrollAreaHeight">
<slot/> <slot/>
@@ -21,6 +25,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, nextTick } from 'vue' import { ref, watch, nextTick } from 'vue'
import { useSlots } from 'vue'
const slots = useSlots()
const heightCard = ref(100) const heightCard = ref(100)
const scrollAreaHeight = ref(100) const scrollAreaHeight = ref(100)
@@ -37,7 +44,7 @@ async function onHeaderResize(size: sizeParams) {
} }
async function onBodyResize(size: sizeParams) { async function onBodyResize(size: sizeParams) {
heightCard.value = size.height heightCard.value = size.height - parseInt(String(getComputedStyle(document.documentElement).getPropertyValue('--top-radius'))) // subtract padding var(top-radius)
await updateScrollAreaHeight() await updateScrollAreaHeight()
} }
@@ -70,11 +77,6 @@ watch(heightCard, () => {
</script> </script>
<style scoped> <style scoped>
.glass-card {
opacity: 1 !important;
background-color: white;
}
#page-card { #page-card {
flex: 1 0 auto; flex: 1 0 auto;
min-height: 0; min-height: 0;

View File

@@ -4,27 +4,30 @@
{{ $t(title) }} {{ $t(title) }}
</template> </template>
<template #footer> <template #footer>
<q-btn <div class="w100 q-pa-md">
rounded color="primary" <q-btn
class="w100 q-mt-md q-mb-xs fix-disabled-btn" rounded color="primary"
:disable="!isFormValid" class="w100 fix-disabled-btn"
@click = "onSubmit" :disable="!isFormValid"
> @click = "onSubmit"
{{ $t(btnText) }} >
</q-btn> {{ $t(btnText) }}
</q-btn>
</div>
</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">
<pn-image-selector <pn-image-selector
v-model="modelValue.logo" v-model="projectMod.logo"
:size="100" :size="100"
:iconsize="80" :iconsize="80"
type="rounded"
class="q-pb-lg" class="q-pb-lg"
/> />
<div class="q-gutter-y-lg w100"> <div class="q-gutter-y-lg w100">
<q-input <q-input
v-model="modelValue.name" v-model="projectMod.name"
no-error-icon no-error-icon
dense dense
filled filled
@@ -38,7 +41,7 @@
</q-input> </q-input>
<q-input <q-input
v-model="modelValue.description" v-model="projectMod.description"
dense dense
filled filled
autogrow autogrow
@@ -53,44 +56,52 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, reactive } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { ProjectParams } from 'types/Project' import type { ProjectParams } from 'types/Project'
import { convertEmptyStringsToNull } from 'src/utils/helpers' import { removeNullKeys } from 'utils/helpers'
const { t } = useI18n() const { t } = useI18n()
const modelValue = defineModel<ProjectParams>({ const props = defineProps<{
required: true
})
defineProps<{
title: string, title: string,
btnText: string btnText: string,
project?: ProjectParams
}>() }>()
const emit = defineEmits(['update']) const emit = defineEmits(['update'])
const projectMod = reactive({
name: props.project?.name ?? '',
description: props.project?.description ?? '',
logo: props.project?.logo ?? null
})
const rulesErrorMessage = { const rulesErrorMessage = {
name: t('project_block__error_name') name: t('project_block__error_name')
} }
const rules = { const rules = {
name: (val: ProjectParams['name']) => !!val?.trim() || rulesErrorMessage['name'] name: (val: string) => !!val?.trim() || rulesErrorMessage['name']
} }
const isFormValid = computed(() => { const isFormValid = computed(() => {
const validations = { const validations = {
name: rules.name(modelValue.value.name) === true name: projectMod.name && rules.name(projectMod.name) === true
} }
return Object.values(validations).every(Boolean) return Object.values(validations).every(Boolean)
}) })
function onSubmit() { function onSubmit() {
const cleanedData = convertEmptyStringsToNull(modelValue.value) const outData = {
emit('update', cleanedData) name: projectMod.name,
description: projectMod.description ?? (typeof props.project?.description === 'string' ? '' : null),
logo: projectMod.logo
}
emit('update', removeNullKeys(outData))
} }
</script> </script>
<style scoped> <style scoped>

View File

@@ -4,43 +4,45 @@
{{ $t(title) }} {{ $t(title) }}
</template> </template>
<template #footer> <template #footer>
<q-btn <div class="w100 q-pa-md">
rounded color="primary" <q-btn
class="w100 q-mt-md q-mb-xs fix-disabled-btn" rounded color="primary"
@click = "onSubmit" class="w100 fix-disabled-btn"
> @click = "onSubmit"
{{ $t(btnText) }} >
</q-btn> {{ $t(btnText) }}
</q-btn>
</div>
</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">
<div class="relative-position"> <div class="relative-position">
<pn-auto-avatar <pn-auto-avatar
:img="modelValue.photo" :img="userMod.photo"
:name="tname" :name="tname"
size="100px" size="100px"
class="q-pb-lg" class="q-pb-lg"
:style="!userStatus ? {} : { filter: 'grayscale(100%)'}" :style="!userStatus ? {} : { filter: 'grayscale(100%)'}"
/> />
<div <div
v-if="userStatus" v-if="userStatus"
class="absolute-center text-h4 text-bold q-pa-sm text-center" class="absolute-center text-h4 text-bold q-pa-sm text-center"
:class ="'status-' + userStatus.status" :class ="'status-' + userStatus.status"
> >
{{ $t(userStatus.text) }} {{ $t(userStatus.text) }}
</div> </div>
</div> </div>
<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="modelValue.username">{{ modelValue.username }}</div> <div caption class="text-blue text-caption" align="center" v-if="user?.username">{{ user.username }}</div>
</div> </div>
</div> </div>
<div class="q-gutter-y-lg w100"> <div class="q-gutter-y-lg w100">
<q-input <q-input
v-model.trim="modelValue.fullname" v-model.trim="userMod.fullname"
dense dense
filled filled
class = "w100" class = "w100"
@@ -48,7 +50,7 @@
/> />
<q-select <q-select
v-model="modelValue.company_id" v-model="userMod.company_id"
:options="displayCompanies" :options="displayCompanies"
dense dense
filled filled
@@ -84,7 +86,7 @@
</q-select> </q-select>
<q-input <q-input
v-model.trim="modelValue.department" v-model.trim="userMod.department"
dense dense
filled filled
class = "w100 q-pt-sm" class = "w100 q-pt-sm"
@@ -92,7 +94,7 @@
/> />
<q-input <q-input
v-model.trim="modelValue.role" v-model.trim="userMod.role"
dense dense
filled filled
class = "w100 q-pt-sm" class = "w100 q-pt-sm"
@@ -100,7 +102,7 @@
/> />
<q-input <q-input
v-model.trim="modelValue.phone" v-model.trim="userMod.phone"
dense dense
filled filled
class = "w100 q-pt-sm" class = "w100 q-pt-sm"
@@ -111,7 +113,7 @@
</q-input> </q-input>
<q-input <q-input
v-model.trim="modelValue.email" v-model.trim="userMod.email"
dense dense
filled filled
class = "w100 q-pt-sm" class = "w100 q-pt-sm"
@@ -129,30 +131,31 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, reactive } from 'vue'
import { useCompaniesStore } from 'stores/companies' import { useCompaniesStore } from 'stores/companies'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { convertEmptyStringsToNull } from 'src/utils/helpers' import { removeNullKeys } from 'utils/helpers'
import type { User } from 'types/User' import type { User } from 'types/User'
const { t } = useI18n() const { t } = useI18n()
const modelValue = defineModel<User>({ const props = defineProps<{
required: true
})
defineProps<{
title: string, title: string,
btnText: string btnText: string,
user?: User
}>() }>()
const emit = defineEmits(['update']) const emit = defineEmits(['update'])
function onSubmit() { const userMod = reactive({
const cleanedData = convertEmptyStringsToNull(modelValue.value) fullname: props.user?.fullname ?? '',
emit('update', cleanedData) company_id: props.user?.company_id ?? null,
} department: props.user?.department ?? '',
role: props.user?.role ?? '',
phone: props.user?.phone ?? '',
email: props.user?.email ?? '',
photo: props.user?.photo ?? null
})
const companiesStore = useCompaniesStore() const companiesStore = useCompaniesStore()
const companies = computed(() => companiesStore.companies) const companies = computed(() => companiesStore.companies)
@@ -171,18 +174,31 @@
]) ])
const tname = computed(() => const tname = computed(() =>
[modelValue.value?.firstname, modelValue.value?.lastname] [props.user?.firstname, props.user?.lastname]
.filter(Boolean) .filter(Boolean)
.join(' ') .join(' ')
) )
const userStatus = computed(() => { const userStatus = computed(() => {
if (modelValue.value.is_blocked) return { status: 'blocked', text: 'user_block__user_blocked' } if (props.user?.is_blocked) return { status: 'blocked', text: 'user_block__user_blocked' }
if (modelValue.value.is_leave) return { status: 'leave', text: 'user_block__user_leave' } if (props.user?.is_leave) return { status: 'leave', text: 'user_block__user_leave' }
if (!modelValue.value.is_terms_accepted) return { status: 'pending', text: 'user_block__user_pending' } if (!props.user?.is_terms_accepted) return { status: 'pending', text: 'user_block__user_pending' }
return null return null
}) })
function onSubmit() {
const outData = {
fullname: userMod.fullname ?? (typeof props.user?.fullname === 'string' ? '' : null),
company_id: userMod.company_id,
department: userMod.department ?? (typeof props.user?.department === 'string' ? '' : null),
role: userMod.role ?? (typeof props.user?.role === 'string' ? '' : null),
phone: userMod.phone ?? (typeof props.user?.phone === 'string' ? '' : null),
email: userMod.email ?? (typeof props.user?.email === 'string' ? '' : null),
photo: userMod.photo
}
emit('update', removeNullKeys(outData))
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -33,7 +33,7 @@ body, html, #q-app {
:root { :root {
--body-width: 600px; --body-width: 600px;
--top-raduis: 12px; --top-radius: 12px;
--logo-color-bg-white: grey; --logo-color-bg-white: grey;
--dynamic-font-size: 16px; --dynamic-font-size: 16px;
} }
@@ -61,8 +61,16 @@ body {
} }
.top-rounded-card { .top-rounded-card {
border-top-left-radius: var(--top-raduis); border-top-left-radius: var(--top-radius) !important;
border-top-right-radius: var(--top-raduis); border-top-right-radius: var(--top-radius) !important;
}
.rounded-card {
border-radius: var(--top-radius) !important;
}
.glass-card {
background-color: rgb(255, 255, 255, 0.91) !important;
} }
.orline { .orline {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -92,6 +92,7 @@
)) ))
const showStopUsingDialog = ref(false) const showStopUsingDialog = ref(false)
async function goTo (path: string) { async function goTo (path: string) {
if (path !== 'stop_using') { if (path !== 'stop_using') {
await router.push({ name: path }) await router.push({ name: path })

View File

@@ -4,12 +4,12 @@
{{$t('agreements__title')}} {{$t('agreements__title')}}
</template> </template>
<pn-scroll-list> <pn-scroll-list>
<div class="flex column w100 q-pa-md q-gutter-y-md"> <div class="flex column w100 q-py-md q-px-lg q-gutter-y-sm text-caption">
<div> <div class="text-bold">
<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 text-caption"> <div class="flex items-center no-wrap">
<q-checkbox v-model="agreement" val="1" class="q-pr-sm"/> <q-checkbox v-model="agreement" val="1" class="q-pr-sm"/>
<span> <span>
{{ $t('agreements__checkbox_agreement_terms') + ' ' }} {{ $t('agreements__checkbox_agreement_terms') + ' ' }}
@@ -21,7 +21,7 @@
</span> </span>
</span> </span>
</div> </div>
<div class="flex items-center no-wrap text-caption"> <div class="flex items-center no-wrap">
<q-checkbox v-model="agreement" val="2" class="q-pr-sm"/> <q-checkbox v-model="agreement" val="2" class="q-pr-sm"/>
<span> <span>
{{ $t('agreements__checkbox_agreement_consent') + ' ' }} {{ $t('agreements__checkbox_agreement_consent') + ' ' }}
@@ -43,7 +43,7 @@
</div> </div>
</pn-scroll-list> </pn-scroll-list>
<template #footer> <template #footer>
<div class="flex no-wrap justify-center w100 q-gutter-x-md q-pb-md"> <div class="flex no-wrap justify-center w100 q-gutter-x-md q-py-md">
<q-btn <q-btn
flat flat
color="negative" color="negative"

View File

@@ -4,14 +4,16 @@
{{ $t('chat_card__title') }} {{ $t('chat_card__title') }}
</template> </template>
<template #footer> <template #footer>
<q-btn <div class="w100 q-pa-md">
rounded color="primary" <q-btn
class="w100 q-mt-md q-mb-xs" rounded color="primary"
@click = "onSubmit" class="w100"
v-if="chat && chat.invite_link" @click="onSubmit"
> v-if="chat"
{{ $t("chat_card__go_chat") }} >
</q-btn> {{ $t("chat_card__go_chat") }}
</q-btn>
</div>
</template> </template>
<pn-scroll-list> <pn-scroll-list>
@@ -118,18 +120,18 @@
watch(() => chatsStore.isInit, initChat) watch(() => chatsStore.isInit, initChat)
async function onSubmit () { async function onSubmit () {
if (chat.value && chat.value.invite_link) tg.openTelegramLink(chat.value.invite_link) if (chat.value) tg.openTelegramLink('https://t.me/c/' + chat.value.telegram_id + '/' + chat.value.message_id)
if (chat.value) await chatsStore.getChatUsers(chat.value.id) if (chat.value) await chatsStore.getChatUsers(chat.value.id)
} }
const users = usersStore.getUsers const users = computed(() => usersStore.users)
const companiesStore = useCompaniesStore() const companiesStore = useCompaniesStore()
const { userSection } = useUserSection() const { userSection } = useUserSection()
const chatUsers = computed(() => { const chatUsers = computed(() => {
if (!chat.value) return [] if (!chat.value) return []
const idSet = new Set(chat.value.users) const idSet = new Set(chat.value.users)
const arr = users.filter(el => idSet.has(el.id)) const arr = users.value.filter(el => idSet.has(el.id))
return arr.map(el => ({ return arr.map(el => ({
...el, ...el,
...userSection(el), ...userSection(el),

View File

@@ -1,6 +1,5 @@
<template> <template>
<company-block <company-block
v-model="newCompany"
title="company_add__title_card" title="company_add__title_card"
btnText="company_add__btn" btnText="company_add__btn"
@update="addCompany" @update="addCompany"
@@ -8,7 +7,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import companyBlock from 'components/companyBlock.vue' import companyBlock from 'components/companyBlock.vue'
import { useCompaniesStore } from 'stores/companies' import { useCompaniesStore } from 'stores/companies'
@@ -17,8 +15,6 @@
const router = useRouter() const router = useRouter()
const companiesStore = useCompaniesStore() const companiesStore = useCompaniesStore()
const newCompany = ref<CompanyParams>({} as CompanyParams)
async function addCompany(companyData: CompanyParams) { async function addCompany(companyData: CompanyParams) {
await companiesStore.add(companyData) await companiesStore.add(companyData)
router.go(-1) router.go(-1)

View File

@@ -1,7 +1,6 @@
<template> <template>
<company-block <company-block
v-if="company" :company
v-model="company"
title="company_edit__title_card" title="company_edit__title_card"
btnText="company_edit__btn" btnText="company_edit__btn"
@update="updateCompany" @update="updateCompany"
@@ -12,6 +11,7 @@
<q-icon name="star" class="q-pr-xs"/> <q-icon name="star" class="q-pr-xs"/>
{{ $t('company_edit__my_company') }} {{ $t('company_edit__my_company') }}
</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" align="center" style="white-space: pre-wrap;">
{{ $t('company_edit__my_company_hint') }} {{ $t('company_edit__my_company_hint') }}
@@ -26,45 +26,29 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue' import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import companyBlock from 'components/companyBlock.vue' import companyBlock from 'components/companyBlock.vue'
import { useCompaniesStore } from 'stores/companies' import { useCompaniesStore } from 'stores/companies'
import { parseIntString } from 'src/utils/helpers' import { parseIntString } from 'utils/helpers'
import type { CompanyParams } from 'types/Company' import type { CompanyParams } from 'types/Company'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const companiesStore = useCompaniesStore() const companiesStore = useCompaniesStore()
const company = ref<CompanyParams | null>(null)
const companyId = computed(() => parseIntString(route.params.companyId)) const companyId = computed(() => parseIntString(route.params.companyId))
function initCompany() { const company = companyId.value && companiesStore.companyById(companyId.value) || {} as CompanyParams
if (companiesStore.isInit && companyId.value) {
const foundCompany = companiesStore.companyById(companyId.value)
if (foundCompany) {
company.value = foundCompany as CompanyParams
}
}
}
if (companiesStore.isInit) initCompany()
watch(() => companiesStore.isInit, (isInit) => {
if (isInit) initCompany()
})
async function goMyCompany () { async function goMyCompany () {
await router.push({ name: 'your_company' }) await router.push({ name: 'your_company' })
} }
async function updateCompany (companyData: CompanyParams) { async function updateCompany (company: CompanyParams) {
if (companyId.value) { if (!companyId .value || !company) return
await companiesStore.update(companyId.value, companyData) await companiesStore.update(companyId.value, company)
router.go(-1) router.go(-1)
}
} }
</script> </script>

View File

@@ -16,7 +16,7 @@
<pn-scroll-list> <pn-scroll-list>
<template #card-body-header> <template #card-body-header>
<div class="q-ma-sm q-pa-sm bg-grey-11 flex no-wrap items-center" style="border-radius: var(--top-raduis);"> <div class="q-ma-sm q-pa-sm bg-grey-11 flex no-wrap items-center" style="border-radius: var(--top-radius);">
<q-icon name="mdi-information q-pr-xs" size="sm" color="primary"/> <q-icon name="mdi-information q-pr-xs" size="sm" color="primary"/>
<span class="text-caption"> <span class="text-caption">
{{ $t('mask__info_block')}} {{ $t('mask__info_block')}}

View File

@@ -238,6 +238,6 @@
<style scoped> <style scoped>
.login-card { .login-card {
opacity: 0.9 !important; opacity: 0.9 !important;
border-radius: var(--top-raduis); border-radius: var(--top-radius);
} }
</style> </style>

View File

@@ -1,6 +1,5 @@
<template> <template>
<project-block <project-block
v-model="newProject"
title="project_create__title_card" title="project_create__title_card"
btnText="project_create__btn" btnText="project_create__btn"
@update="addProject" @update="addProject"
@@ -8,7 +7,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import projectBlock from 'components/projectBlock.vue' import projectBlock from 'components/projectBlock.vue'
import { useProjectsStore } from 'stores/projects' import { useProjectsStore } from 'stores/projects'
@@ -17,8 +15,6 @@
const router = useRouter() const router = useRouter()
const projectsStore = useProjectsStore() const projectsStore = useProjectsStore()
const newProject = ref<ProjectParams>({} as ProjectParams)
async function addProject (projectData: ProjectParams) { async function addProject (projectData: ProjectParams) {
const newDataProject = await projectsStore.add(projectData) const newDataProject = await projectsStore.add(projectData)
await router.replace({ name: 'chats', params: { id: newDataProject.id }}) await router.replace({ name: 'chats', params: { id: newDataProject.id }})

View File

@@ -1,7 +1,6 @@
<template> <template>
<project-block <project-block
v-if="project" :project
v-model="project"
title="project_edit__title_card" title="project_edit__title_card"
btnText="project_edit__btn" btnText="project_edit__btn"
@update="updateProject" @update="updateProject"
@@ -9,7 +8,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue' import { computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import projectBlock from 'components/projectBlock.vue' import projectBlock from 'components/projectBlock.vue'
import { useProjectsStore } from 'stores/projects' import { useProjectsStore } from 'stores/projects'
@@ -18,27 +17,14 @@
const router = useRouter() const router = useRouter()
const projectsStore = useProjectsStore() const projectsStore = useProjectsStore()
const project = ref<ProjectParams | null>(null)
const projectId = computed(() => projectsStore.currentProjectId) const projectId = computed(() => projectsStore.currentProjectId)
const project = projectId.value && projectsStore.projectById(projectId.value) || {} as ProjectParams
if (projectsStore.isInit) {
project.value = projectId.value
? { ...projectsStore.projectById(projectId.value) } as ProjectParams
: null
}
watch(() => projectsStore.isInit, (isInit) => {
if (isInit && projectId.value && !project.value) {
project.value = { ...projectsStore.projectById(projectId.value) as ProjectParams }
}
})
async function updateProject (projectData: ProjectParams) { async function updateProject (projectData: ProjectParams) {
if (projectId.value) { if (!projectId .value || !project) return
await projectsStore.update(projectId.value, projectData) await projectsStore.update(projectId.value, projectData)
router.go(-1) router.go(-1)
}
} }
</script> </script>

View File

@@ -75,8 +75,8 @@
const usersStore = useUsersStore() const usersStore = useUsersStore()
const companiesStore = useCompaniesStore() const companiesStore = useCompaniesStore()
const chatsQty = computed(() => chatsStore.getChats.length) const chatsQty = computed(() => chatsStore.chats.length)
const usersQty = computed(() => usersStore.getUsers.length) const usersQty = computed(() => usersStore.users.length)
const companiesQty = computed(() => companiesStore.getCompanies.length) const companiesQty = computed(() => companiesStore.getCompanies.length)
const tabs = ref([ const tabs = ref([

View File

@@ -21,12 +21,12 @@
v-if="projects.length!==0 || archiveProjects.length!==0" v-if="projects.length!==0 || archiveProjects.length!==0"
> >
<q-input <q-input
v-if="projects.length!== 0" v-if="projects.length!==0"
v-model="searchProject" v-model="searchProject"
clearable clearable
clear-icon="close" clear-icon="close"
:placeholder="$t('projects__search')" :placeholder="$t('projects__search')"
dense dense filled
class="col-grow q-px-md q-py-md" class="col-grow q-px-md q-py-md"
> >
<template #prepend> <template #prepend>
@@ -61,16 +61,16 @@
/> />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label lines="1" class="text-bold">{{ item.name }}</q-item-label> <q-item-label lines="1" class="text-bold">
{{ item.name }}
</q-item-label>
<q-item-label <q-item-label
caption lines="2" caption lines="2"
style="max-width: -webkit-fill-available; white-space: pre-line" style="max-width: -webkit-fill-available; white-space: pre-line"
> >
{{ item.description }} {{ item.description }}
</q-item-label> </q-item-label>
</q-item-section> <q-item-label lines="1" class="text-caption text-grey">
<q-item-section side top>
<q-item-label lines="1" class="text-caption">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex items-center"> <div class="flex items-center">
<q-icon name="mdi-chat-outline"/> <q-icon name="mdi-chat-outline"/>
@@ -82,10 +82,12 @@
</div> </div>
</div> </div>
</q-item-label> </q-item-label>
</q-item-section>
<q-item-section side>
<q-item-label> <q-item-label>
<q-btn <q-btn
color="grey" color="grey"
size="sm" size="md"
dense flat round dense flat round
icon="mdi-pencil" icon="mdi-pencil"
class="q-ml-sm" class="q-ml-sm"
@@ -225,7 +227,7 @@
const router = useRouter() const router = useRouter()
const projectsStore = useProjectsStore() const projectsStore = useProjectsStore()
const projects = projectsStore.getProjects const projects = computed(() => projectsStore.projects)
const projectsInit = computed(() => projectsStore.isInit) const projectsInit = computed(() => projectsStore.isInit)
const searchProject = ref('') const searchProject = ref('')
@@ -259,9 +261,9 @@
} }
const displayProjects = computed(() => { const displayProjects = computed(() => {
if (!searchProject.value || !(searchProject.value && searchProject.value.trim())) return projects if (!searchProject.value || !(searchProject.value && searchProject.value.trim())) return projects.value
const searchChatValue = searchProject.value.trim().toLowerCase() const searchChatValue = searchProject.value.trim().toLowerCase()
const arrOut = projects const arrOut = projects.value
.filter((el: Project) => .filter((el: Project) =>
el.name.toLowerCase().includes(searchChatValue) || el.name.toLowerCase().includes(searchChatValue) ||
el.description && el.description.toLowerCase().includes(searchProject.value) el.description && el.description.toLowerCase().includes(searchProject.value)

View File

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

View File

@@ -7,4 +7,4 @@
</script> </script>
<style> <style>
</style> </style>

View File

@@ -8,107 +8,177 @@
</template> </template>
<pn-scroll-list > <pn-scroll-list >
<div class="q-px-md"> <template #card-body-header>
<div <div
id="subscribe-current-balance" id="subscribe-current-balance"
class="flex w100 q-px-md q-py-sm row" class="flex w100 q-px-md q-py-md row"
style="border-radius: var(--top-raduis); border: 1px solid var(--q-primary);"
> >
<div class="flex w100 justify-between items-center no-wrap"> <div class="flex w100 justify-between items-center no-wrap">
<div class="flex no-wrap items-center text-h6 col-9"> <div class="flex items-center text-grey">
{{ $t('subscribe__current_plan') }} {{ $t('subscribe__current_plan') }}
</div> </div>
<div class="flex items-center column col-3"> <div class="flex items-center column text-right">
<span class="text-bold"> <span class="text-bold">
{{ currentPlanData?.name }} {{ currentPlanData?.name }}
</span> </span>
<span class="text-caption" style="line-height: 0.5em;">
{{ $t('subscribe__plan_exp') }}
{{ date.formatDate(currentPlanData.exp * 1000, 'DD.MM.YYYY') }}
</span>
</div> </div>
</div> </div>
<div class="flex row w100 ow-wrap items-center text-caption q-pt-sm"> <div
<div class="col-9">{{ $t('subscribe__plan_active_chats') }}</div> class="row w100 no-wrap items-center text-caption justify-between"
<div class="col-3" align="center"> v-if="currentPlanData.name !== 'TEST'"
<span class="text-brand2 text-bold q-pr-xs">{{ currentPlanData?.active_chats }}</span> >
<span class="text-grey">/{{ currentPlanData?.chatsQty }}</span> <div class="text-grey text-no-wrap ellipsis">
{{ $t('subscribe__plan_date') }}
</div> </div>
</div> <span class="text-no-wrap text-right">
{{ date.formatDate(currentPlanData.exp * 1000, 'DD.MM.YYYY') }}
</div>
<div class="flex w100 justify-center text-grey q-pt-md q-pb-none">{{ $t('subscribe__plans')}}</div>
<q-list separator>
<q-item
v-for="item in plans"
:key="item.id"
>
<q-item-section avatar>
<q-radio v-model="newPlan" :val="item" v-if="item.name!=='TEST'"/>
</q-item-section>
<q-item-section>
<q-item-label class="text-bold">{{ item.name }}</q-item-label>
<div class="flex no-wrap items-center text-caption">
<span v-if="item.chatsQty" class="q-pr-xs">
{{ $t('subscribe__chats_max') }}
</span>
<span v-if="item.chatsQty">
{{ item.chatsQty }}
</span>
<q-icon v-else name="mdi-all-inclusive" size="sm"/>
<span class="q-pl-xs">
{{ $t('subscribe__chats')}}
</span>
</div>
</q-item-section>
<q-item-section side>
<div v-if="item.price" class="flex column items-center">
<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">
{{ $t('subscribe__free_tax') }}
</div>
</q-item-section>
</q-item>
</q-list>
<div class="flex w100 justify-center text-caption text-grey q-pt-sm text-center">
{{ $t('subscribe__plans_description') }}
</div>
<q-btn-group spread flat class="w100 q-py-sm">
<q-btn
v-for="period in periods"
:key="period.id"
:color="selectPeriod === period.value ? 'primary' : 'white'"
:text-color="selectPeriod === period.value ? 'white' : 'primary'"
@click="selectPeriod = period.value"
no-caps
>
<div class="column items-center w100 self-end">
<q-badge v-show="period.sale" color="red">{{ period.sale }}% off</q-badge>
<span>{{ $t(period.name) }}</span>
<span class="text-caption">({{ $t(period.name_in_days) }})</span>
</div>
</q-btn>
</q-btn-group>
<div class="text-caption column 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> </span>
</div> </div>
<div class="row w100 no-wrap items-center text-caption text-grey justify-between">
<div>
{{ $t('subscribe__plan_active_chats') }}
</div>
<div class="text-right">
<span class="text-brand2 text-bold q-pr-xs">{{ currentPlanData?.active_chats }}</span>
<span>/{{ currentPlanData?.chatsQty }}</span>
</div>
</div>
</div>
</template>
<div class="w100 column items-center text-center text-grey q-px-md">
{{ $t('subscribe__plans')}}
<div class="text-caption">
{{ $t('subscribe__plans_description') }}
</div>
</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">
<div class="w100 text-grey text-center q-pt-sm">
{{ $t('subscribe__plans_interval') }}
</div>
<q-btn-group spread flat class="w100 q-py-sm">
<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>
@@ -121,12 +191,12 @@
import { date } from 'quasar' import { date } from 'quasar'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { WebApp } from '@twa-dev/types' import type { WebApp } from '@twa-dev/types'
import { colors } from 'quasar'; import { colors } from 'quasar'
import { useQuasar } from 'quasar'
const router = useRouter() const router = useRouter()
const tg = inject('tg') as WebApp const tg = inject('tg') as WebApp
tg.MainButton.show()
// @ts-expect-error: get hex text // @ts-expect-error: get hex text
tg.MainButton.color = colors.getPaletteColor('primary') tg.MainButton.color = colors.getPaletteColor('primary')
@@ -146,7 +216,7 @@
const selectPeriod = ref(periods[1]?.value) const selectPeriod = ref(periods[1]?.value)
const newPlan = ref(plans[1]) const newPlan = ref(plans[0])
interface CurrentPlan { interface CurrentPlan {
plan: string plan: string
@@ -155,7 +225,7 @@
} }
const currentPlan = ref<CurrentPlan>({ // temp, this get from api const currentPlan = ref<CurrentPlan>({ // temp, this get from api
plan: plans[0].val, plan: plans[1].val,
active_chats: 20, active_chats: 20,
exp: Date.now() / 1000 + 500000 exp: Date.now() / 1000 + 500000
}) })
@@ -186,12 +256,12 @@
) )
const newDateExp = date.addToDate(Date.now(), { months: Number(selectPeriod.value) }) const newDateExp = date.addToDate(Date.now(), { months: Number(selectPeriod.value) })
return t('subscribe__pay') + ' ⭐' + stars + return t(newPlan.value.id !== currentPlanData.value.id ? 'subscribe__pay' : 'subscribe__pay_renew') +
' ⭐' + stars +
' (' + t('subscribe__plan_exp') + ' ' + ' (' + t('subscribe__plan_exp') + ' ' +
date.formatDate(newDateExp, 'DD.MM.YYYY') + ')' date.formatDate(newDateExp, 'DD.MM.YYYY') + ')'
}) })
function formatNumber (number: string | number) { function formatNumber (number: string | number) {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ") return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ")
} }
@@ -201,6 +271,43 @@
watch(textBtn, () => tg.MainButton.setText(textBtn.value), watch(textBtn, () => tg.MainButton.setText(textBtn.value),
{ immediate: true }) { immediate: true })
watch(newPlan, () =>
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
{ newPlan.value.name === 'TEST' ? tg.MainButton.hide() : tg.MainButton.show() },
{ 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 sendPromocode () {
promocode.value = ''
showNotify('fail')
}
const $q = useQuasar()
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> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -2,14 +2,13 @@
<div class="q-pa-none flex column col-grow no-scroll"> <div class="q-pa-none flex column col-grow no-scroll">
<pn-scroll-list> <pn-scroll-list>
<template #card-body-header> <template #card-body-header>
<div class="flex row q-ma-md justify-between" v-if="chats.length !== 0"> <div class="flex row q-ma-md justify-between" v-if="chats.length !== 0">
<q-input <q-input
v-model="search" v-model="search"
clearable clearable
clear-icon="close" clear-icon="close"
:placeholder="$t('chats__search')" :placeholder="$t('chats__search')"
dense dense filled
class="col-grow" class="col-grow"
v-if="chats.length !== 0" v-if="chats.length !== 0"
> >
@@ -52,18 +51,15 @@
{{ item.description }} {{ item.description }}
</q-item-label> </q-item-label>
<q-item-label caption lines="1"> <q-item-label caption lines="1">
<div class = "flex justify-start items-center no-wrap">
<div class="q-mr-sm flex items-center no-wrap">
<q-icon name="mdi-account-multiple-outline" class="q-mr-xs"/>
<span>{{ item.user_count }}</span>
</div>
<div class="q-mx-sm flex items-center no-wrap ellipsis" v-if="item.owner_id">
<q-icon name="mdi-key" class="q-mr-xs"/>
<span class="ellipsis">{{ usersStore.userNameById(item.owner_id) }} </span>
</div>
</div>
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
<q-item-section side>
<div class="flex items-center no-wrap text-caption">
<q-icon name="mdi-account-outline"/>
<span>{{ item.user_count }}</span>
</div>
</q-item-section>
</q-item> </q-item>
</q-slide-item> </q-slide-item>
</q-list> </q-list>
@@ -166,7 +162,6 @@
<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 { useChatsStore } from 'stores/chats' import { useChatsStore } from 'stores/chats'
import { useUsersStore } from 'stores/users'
import type { WebApp } from '@twa-dev/types' import type { WebApp } from '@twa-dev/types'
import { useI18n } from "vue-i18n" import { useI18n } from "vue-i18n"
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
@@ -179,7 +174,6 @@
const search = ref('') const search = ref('')
const showOverlay = ref<boolean>(false) const showOverlay = ref<boolean>(false)
const chatsStore = useChatsStore() const chatsStore = useChatsStore()
const usersStore = useUsersStore()
const showDialogDeleteChat = ref<boolean>(false) const showDialogDeleteChat = ref<boolean>(false)
const deleteChatId = ref<number | undefined>(undefined) const deleteChatId = ref<number | undefined>(undefined)
const currentSlideEvent = ref<SlideEvent | null>(null) const currentSlideEvent = ref<SlideEvent | null>(null)
@@ -192,7 +186,7 @@
reset: () => void reset: () => void
} }
const chats = chatsStore.getChats const chats = computed(() => chatsStore.chats)
const chatsInit = computed(() => chatsStore.isInit) const chatsInit = computed(() => chatsStore.isInit)
const fabMenu = [ const fabMenu = [
@@ -201,9 +195,9 @@
] ]
const displayChats = computed(() => { const displayChats = computed(() => {
if (!search.value || !(search.value && search.value.trim())) return chats if (!search.value || !(search.value && search.value.trim())) return chats.value
const searchValue = search.value.trim().toLowerCase() const searchValue = search.value.trim().toLowerCase()
const arrOut = chats const arrOut = chats.value
.filter(el => .filter(el =>
el.name.toLowerCase().includes(searchValue) || el.name.toLowerCase().includes(searchValue) ||
el.description && el.description.toLowerCase().includes(searchValue) el.description && el.description.toLowerCase().includes(searchValue)

View File

@@ -1,29 +1,27 @@
<template> <template>
<div class="q-pa-none flex column col-grow no-scroll"> <div class="q-pa-none flex column col-grow no-scroll">
<pn-scroll-list> <pn-scroll-list>
<template #card-body-header> <div class="flex items-center justify-end q-pa-sm w100" v-if="companies.length !== 0">
<div class="flex items-center justify-end q-pa-sm w100" v-if="companies.length !== 0"> <q-btn
<q-btn :color="companies.length <= 2 ? 'grey-6' : 'primary'"
:color="companies.length <= 2 ? 'grey-6' : 'primary'" flat
flat no-caps
no-caps @click="maskCompany"
@click="maskCompany" :disable="companies.length <= 2"
:disable="companies.length <= 2" class="q-pr-md"
class="q-pr-md" rounded
rounded >
> <q-icon
<q-icon left
left size="sm"
size="sm" name="mdi-domino-mask"
name="mdi-domino-mask" class="q-mr-xs"
class="q-mr-xs" />
/> <div>
<div> {{ $t('companies__mask')}}
{{ $t('companies__mask')}} </div>
</div> </q-btn>
</q-btn> </div>
</div>
</template>
<q-list separator v-if="companies.length !== 0"> <q-list separator v-if="companies.length !== 0">
<q-slide-item <q-slide-item

View File

@@ -1,6 +1,7 @@
<template> <template>
<div <div
id="project-info" id="project-info"
v-if="project"
:style="{ height: headerHeight + 'px', minHeight: '48px' }" :style="{ height: headerHeight + 'px', minHeight: '48px' }"
class="flex row items-center justify-between no-wrap w100 q-gutter-x-sm" class="flex row items-center justify-between no-wrap w100 q-gutter-x-sm"
style="overflow: hidden; transition: height 0.3s ease-in-out; margin-left: 0" style="overflow: hidden; transition: height 0.3s ease-in-out; margin-left: 0"
@@ -34,7 +35,7 @@
size="lg" size="lg"
/> />
<div class="flex column text-white fit"> <div class="flex column text-white fit q-pl-sm">
<div <div
class="text-h6 q-pl-sm text-field" class="text-h6 q-pl-sm text-field"
> >
@@ -85,18 +86,15 @@
return currentProject return currentProject
? { ? {
name: currentProject.name, name: currentProject.name,
description: currentProject.description ?? '', description: currentProject.description,
logo: currentProject.logo ?? '' logo: currentProject.logo
}
: {
name: '',
description: '',
logo: ''
} }
: null
}) })
function toggleExpand () { function toggleExpand () {
expandProjectInfo.value = !expandProjectInfo.value if (project.value && (project.value.description || project.value.logo))
expandProjectInfo.value = !expandProjectInfo.value
} }
async function toProjects () { async function toProjects () {

View File

@@ -8,7 +8,7 @@
clearable clearable
clear-icon="close" clear-icon="close"
:placeholder="$t('users__search')" :placeholder="$t('users__search')"
dense dense filled
class="col-grow" class="col-grow"
> >
<template #prepend> <template #prepend>
@@ -254,7 +254,7 @@
const usersStore = useUsersStore() const usersStore = useUsersStore()
const companiesStore = useCompaniesStore() const companiesStore = useCompaniesStore()
const users = usersStore.getUsers const users = computed(() => usersStore.users)
const usersInit = computed(() => usersStore.isInit) const usersInit = computed(() => usersStore.isInit)
const blockUserId = ref<number | undefined>(undefined) const blockUserId = ref<number | undefined>(undefined)
@@ -263,7 +263,7 @@
const closedByUserAction = ref(false) const closedByUserAction = ref(false)
const { userSection } = useUserSection() const { userSection } = useUserSection()
const mapUsers = computed(() => users.map(el => ({ const mapUsers = computed(() => users.value.map(el => ({
...el, ...el,
...userSection(el), ...userSection(el),
companyName: el.company_id && companiesStore.companyById(el.company_id) companyName: el.company_id && companiesStore.companyById(el.company_id)

View File

@@ -41,6 +41,7 @@ export default defineRouter(function (/* { store, ssrContext } */) {
} }
if (to.name === 'telegram_only') return true if (to.name === 'telegram_only') return true
if (typeof to.name === 'string' && publicPath.includes(to.name)) return true
const authStore = useAuthStore() const authStore = useAuthStore()
const projectsStore = useProjectsStore() const projectsStore = useProjectsStore()
@@ -48,8 +49,6 @@ export default defineRouter(function (/* { store, ssrContext } */) {
if (to.name === 'login') if (to.name === 'login')
return authStore.isAuth ? { name: 'projects', replace: true } : true return authStore.isAuth ? { name: 'projects', replace: true } : true
if (typeof to.name === 'string' && publicPath.includes(to.name)) return 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

View File

@@ -120,7 +120,7 @@ const routes: RouteRecordRaw[] = [
{ {
name: 'agreements', name: 'agreements',
path: '/agreements', path: '/agreements',
component: () => import('src/pages/AgreementsPage.vue') component: () => import('pages/AgreementsPage.vue')
}, },
{ {
name: 'terms', name: 'terms',
@@ -145,7 +145,7 @@ const routes: RouteRecordRaw[] = [
{ {
name: 'support', name: 'support',
path: '/support', path: '/support',
component: () => import('src/pages/account/SupportPage.vue') component: () => import('pages/account/SupportPage.vue')
}, },
{ {
name: 'your_company', name: 'your_company',

View File

@@ -15,9 +15,9 @@ interface Customer {
} }
interface WSMessage { interface WSMessage {
id: number id?: number
action: string action?: string
entity: string entity?: string
entity_id?: number entity_id?: number
needUpdateStores: string[] needUpdateStores: string[]
} }
@@ -53,11 +53,17 @@ export const useAuthStore = defineStore('auth', () => {
} }
socket.onmessage = (event) => { socket.onmessage = (event) => {
if (wsEvent.value) { const eventData = JSON.parse(event.data)
console.log(eventData, eventData.event === 'chat-attached')
if (eventData.event === 'chat-attached') {
console.log(eventData)
wsEvent.value = { needUpdateStores: ['users', 'chats' ] }
}
/* if (wsEvent.value) {
wsEvent.value.needUpdateStores = [] wsEvent.value.needUpdateStores = []
wsEvent.value = JSON.parse(event.data) wsEvent.value = JSON.parse(event.data)
if (wsEvent.value?.entity === 'chat') wsEvent.value.needUpdateStores = ['users'] if (wsEvent.value?.entity === 'chat') wsEvent.value.needUpdateStores = ['users']
} } */
socket.onerror = (event) => console.error("Ошибка ", event) socket.onerror = (event) => console.error("Ошибка ", event)
} }

View File

@@ -41,8 +41,6 @@ export const useChatsStore = defineStore('chats', () => {
async function getChatUsers (chatId: number) { async function getChatUsers (chatId: number) {
await api.get('/project/' + currentProjectId.value + '/chat/' + chatId) await api.get('/project/' + currentProjectId.value + '/chat/' + chatId)
} }
const getChats = computed(() => chats.value)
function chatById (id: number) { function chatById (id: number) {
return chats.value.find(el =>el.id === id) return chats.value.find(el =>el.id === id)
@@ -50,7 +48,7 @@ export const useChatsStore = defineStore('chats', () => {
const authStore = useAuthStore() const authStore = useAuthStore()
watch(() => authStore.wsEvent, async (event) => { watch(() => authStore.wsEvent, async (event) => {
if (!event || event.entity !== 'chat' || !event.needUpdateStores.includes('chats')) return if (!event || !event.needUpdateStores.includes('chats')) return
await init() await init()
}, { deep: true }) }, { deep: true })
@@ -61,7 +59,6 @@ export const useChatsStore = defineStore('chats', () => {
reset, reset,
unlink, unlink,
getKey, getKey,
getChats,
chatById, chatById,
getChatUsers getChatUsers
} }

View File

@@ -1,4 +1,4 @@
import { ref, watch, computed } from 'vue' import { ref, watch } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { api } from 'boot/axios' import { api } from 'boot/axios'
@@ -81,8 +81,6 @@ export const useProjectsStore = defineStore('projects', () => {
companiesStore.reset() companiesStore.reset()
} }
const getProjects = computed(() => projects.value)
watch (currentProjectId, async (newId) => { watch (currentProjectId, async (newId) => {
if (newId) { if (newId) {
await initStores() await initStores()
@@ -104,7 +102,6 @@ export const useProjectsStore = defineStore('projects', () => {
projectById, projectById,
setCurrentProjectId, setCurrentProjectId,
initStores, initStores,
resetStores, resetStores
getProjects
} }
}) })

View File

@@ -58,11 +58,9 @@ export const useUsersStore = defineStore('users', () => {
|| '---' || '---'
} }
const getUsers = computed(() => users.value)
const authStore = useAuthStore() const authStore = useAuthStore()
watch(() => authStore.wsEvent, async (event) => { watch(() => authStore.wsEvent, async (event) => {
if (!event || event.entity !== 'user' || !event.needUpdateStores.includes('users')) return if (!event || !event.needUpdateStores.includes('users')) return
await init() await init()
}, { deep: true }) }, { deep: true })
@@ -75,7 +73,6 @@ export const useUsersStore = defineStore('users', () => {
blockUser, blockUser,
unblockUser, unblockUser,
userById, userById,
userNameById, userNameById
getUsers
} }
}) })

View File

@@ -1,7 +1,8 @@
interface Chat { interface Chat {
id: number id: number
project_id: number project_id: number
telegram_id: number telegram_id: string
message_id: number
name: string name: string
is_channel: boolean is_channel: boolean
bot_can_ban: boolean bot_can_ban: boolean
@@ -10,7 +11,6 @@ interface Chat {
description: string | null description: string | null
logo: string | null logo: string | null
owner_id?: number owner_id?: number
invite_link: string
users: number [] users: number []
[key: string]: number | string | boolean | null | number[] [key: string]: number | string | boolean | null | number[]
} }

View File

@@ -1,113 +1,15 @@
function isDirty(
obj1: Record<string, unknown> | null | undefined,
obj2: Record<string, unknown> | null | undefined
): boolean {
const actualObj1 = obj1 ?? {}
const actualObj2 = obj2 ?? {}
const filteredObj1 = filterIgnored(actualObj1)
const filteredObj2 = filterIgnored(actualObj2)
const allKeys = new Set([...Object.keys(filteredObj1), ...Object.keys(filteredObj2)])
for (const key of allKeys) {
const hasKey1 = Object.hasOwn(filteredObj1, key)
const hasKey2 = Object.hasOwn(filteredObj2, key)
// Различие в наличии ключа
if (hasKey1 !== hasKey2) return true
if (hasKey1 && hasKey2) {
const val1 = filteredObj1[key]
const val2 = filteredObj2[key]
// Сравнение массивов
if (Array.isArray(val1) && Array.isArray(val2)) {
if (val1.length !== val2.length) return true
const set2 = new Set(val2)
if (!val1.every(item => set2.has(item))) return true
}
// Один массив, другой - нет
else if (Array.isArray(val1) || Array.isArray(val2)) {
return true
}
// Сравнение строк
else if (typeof val1 === 'string' && typeof val2 === 'string') {
if (val1.trim() !== val2.trim()) return true
}
// Сравнение примитивов
else if (val1 !== val2) {
return true
}
}
}
return false
}
function filterIgnored(obj: Record<string, unknown>): Record<string, string | number | boolean | (string | number)[]> {
const filtered: Record<string, string | number | boolean | (string | number)[]> = {}
for (const key in obj) {
const value = obj[key]
// Обработка массивов
if (Array.isArray(value)) {
// Отбрасываем пустые массивы
if (value.length === 0) continue
// Фильтруем массивы с некорректными элементами
if (value.every(item =>
typeof item === 'string' ||
typeof item === 'number'
)) {
filtered[key] = value
}
continue
}
// Обработка примитивов
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
continue
}
// Обработка строк
if (typeof value === 'string') {
const trimmed = value.trim()
if (trimmed === '') continue
filtered[key] = trimmed
}
// Обработка чисел и boolean
else if (value !== 0 && value !== false) {
filtered[key] = value
}
}
return filtered
}
function parseIntString (s: string | string[] | undefined) :number | null { function parseIntString (s: string | string[] | undefined) :number | null {
if (typeof s !== 'string') return null if (typeof s !== 'string') return null
const regex = /^[+-]?\d+$/ const regex = /^[+-]?\d+$/
return regex.test(s) ? Number(s) : null return regex.test(s) ? Number(s) : null
} }
// Функция для преобразования пустых строк в null const removeNullKeys = <T extends object>(obj: T): Partial<T> =>
function convertEmptyStringsToNull<T extends Record<string, unknown>>(obj: T): T { Object.fromEntries(
const result = { ...obj } as Record<string, unknown> Object.entries(obj).filter(([, v]) => v !== null)
) as Partial<T>
Object.keys(result).forEach(key => {
if (result[key] === '') {
result[key] = null
}
})
return result as T
}
export { export {
isDirty,
parseIntString, parseIntString,
convertEmptyStringsToNull removeNullKeys
} }