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",
"version": "0.0.1",
"description": "telegram miniapp",
"productName": "projectsNode",
"name": "tgcrew",
"version": "0.1.01",
"description": "telegram admin miniapp",
"productName": "tgCrew (admin)",
"author": "Alex Mart",
"type": "module",
"private": true,

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,
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': {
target: 'ws://localhost:3000', // или wss:// для HTTPS
changeOrigin: true,
@@ -126,7 +136,7 @@ export default defineConfig((ctx) => {
},
// 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
@@ -244,7 +254,7 @@ export default defineConfig((ctx) => {
builder: {
// https://www.electron.build/configuration/configuration
appId: 'projectsnode'
appId: 'tgcrew'
}
},

View File

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

View File

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

View File

@@ -36,8 +36,8 @@
<q-card
class="fix-card-width flex column no-scroll no-wrap q-px-none"
style="
border-top-left-radius: var(--top-raduis) !important;
border-top-right-radius: var(--top-raduis) !important;
border-top-left-radius: var(--top-radius) !important;
border-top-right-radius: var(--top-radius) !important;
"
>
<div
@@ -46,7 +46,12 @@
>
<div>
<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>
</div>
</div>

View File

@@ -18,20 +18,20 @@
<div class="flex column justify-center col-grow items-center">
<q-icon :name="icon" size="160px" class="q-pb-md"/>
<div class="text-h6 text-brand">
{{message1}}
{{ message1 }}
</div>
</div>
</div>
<div v-if="message2" class="text-caption" align="center">
{{message2}}
<div v-if="message2" class="text-caption text-center">
{{ message2 }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
defineProps<{
icon: string
message1: string
message2?: string

View File

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

View File

@@ -5,12 +5,16 @@
>
<div
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"/>
<slot name="card-body-header"/>
</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"/>
<pn-shadow-scroll :hideShadows="isResizing" :height="scrollAreaHeight">
<slot/>
@@ -21,6 +25,9 @@
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import { useSlots } from 'vue'
const slots = useSlots()
const heightCard = ref(100)
const scrollAreaHeight = ref(100)
@@ -37,7 +44,7 @@ async function onHeaderResize(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()
}
@@ -70,11 +77,6 @@ watch(heightCard, () => {
</script>
<style scoped>
.glass-card {
opacity: 1 !important;
background-color: white;
}
#page-card {
flex: 1 0 auto;
min-height: 0;

View File

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

View File

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

View File

@@ -33,7 +33,7 @@ body, html, #q-app {
:root {
--body-width: 600px;
--top-raduis: 12px;
--top-radius: 12px;
--logo-color-bg-white: grey;
--dynamic-font-size: 16px;
}
@@ -61,8 +61,16 @@ body {
}
.top-rounded-card {
border-top-left-radius: var(--top-raduis);
border-top-right-radius: var(--top-raduis);
border-top-left-radius: var(--top-radius) !important;
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 {

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)
async function goTo (path: string) {
if (path !== 'stop_using') {
await router.push({ name: path })

View File

@@ -4,12 +4,12 @@
{{$t('agreements__title')}}
</template>
<pn-scroll-list>
<div class="flex column w100 q-pa-md q-gutter-y-md">
<div>
<div class="flex column w100 q-py-md q-px-lg q-gutter-y-sm text-caption">
<div class="text-bold">
<div v-if="route.query.type === 'update'">{{ $t('agreements__description_update') }}</div>
<div>{{ $t('agreements__description') }}</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"/>
<span>
{{ $t('agreements__checkbox_agreement_terms') + ' ' }}
@@ -21,7 +21,7 @@
</span>
</span>
</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"/>
<span>
{{ $t('agreements__checkbox_agreement_consent') + ' ' }}
@@ -43,7 +43,7 @@
</div>
</pn-scroll-list>
<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
flat
color="negative"

View File

@@ -4,14 +4,16 @@
{{ $t('chat_card__title') }}
</template>
<template #footer>
<q-btn
rounded color="primary"
class="w100 q-mt-md q-mb-xs"
@click = "onSubmit"
v-if="chat && chat.invite_link"
>
{{ $t("chat_card__go_chat") }}
</q-btn>
<div class="w100 q-pa-md">
<q-btn
rounded color="primary"
class="w100"
@click="onSubmit"
v-if="chat"
>
{{ $t("chat_card__go_chat") }}
</q-btn>
</div>
</template>
<pn-scroll-list>
@@ -118,18 +120,18 @@
watch(() => chatsStore.isInit, initChat)
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)
}
const users = usersStore.getUsers
const users = computed(() => usersStore.users)
const companiesStore = useCompaniesStore()
const { userSection } = useUserSection()
const chatUsers = computed(() => {
if (!chat.value) return []
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 => ({
...el,
...userSection(el),

View File

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

View File

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

View File

@@ -16,7 +16,7 @@
<pn-scroll-list>
<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"/>
<span class="text-caption">
{{ $t('mask__info_block')}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,107 +8,177 @@
</template>
<pn-scroll-list >
<div class="q-px-md">
<template #card-body-header>
<div
id="subscribe-current-balance"
class="flex w100 q-px-md q-py-sm row"
style="border-radius: var(--top-raduis); border: 1px solid var(--q-primary);"
class="flex w100 q-px-md q-py-md row"
>
<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') }}
</div>
<div class="flex items-center column col-3">
<div class="flex items-center column text-right">
<span class="text-bold">
{{ currentPlanData?.name }}
</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 class="flex row w100 ow-wrap items-center text-caption q-pt-sm">
<div class="col-9">{{ $t('subscribe__plan_active_chats') }}</div>
<div class="col-3" align="center">
<span class="text-brand2 text-bold q-pr-xs">{{ currentPlanData?.active_chats }}</span>
<span class="text-grey">/{{ currentPlanData?.chatsQty }}</span>
<div
class="row w100 no-wrap items-center text-caption justify-between"
v-if="currentPlanData.name !== 'TEST'"
>
<div class="text-grey text-no-wrap ellipsis">
{{ $t('subscribe__plan_date') }}
</div>
</div>
</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 class="text-no-wrap text-right">
{{ date.formatDate(currentPlanData.exp * 1000, 'DD.MM.YYYY') }}
</span>
</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>
</pn-scroll-list>
</pn-page-card>
@@ -121,12 +191,12 @@
import { date } from 'quasar'
import { useI18n } from 'vue-i18n'
import type { WebApp } from '@twa-dev/types'
import { colors } from 'quasar';
import { colors } from 'quasar'
import { useQuasar } from 'quasar'
const router = useRouter()
const tg = inject('tg') as WebApp
tg.MainButton.show()
// @ts-expect-error: get hex text
tg.MainButton.color = colors.getPaletteColor('primary')
@@ -146,7 +216,7 @@
const selectPeriod = ref(periods[1]?.value)
const newPlan = ref(plans[1])
const newPlan = ref(plans[0])
interface CurrentPlan {
plan: string
@@ -155,7 +225,7 @@
}
const currentPlan = ref<CurrentPlan>({ // temp, this get from api
plan: plans[0].val,
plan: plans[1].val,
active_chats: 20,
exp: Date.now() / 1000 + 500000
})
@@ -186,12 +256,12 @@
)
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') + ' ' +
date.formatDate(newDateExp, 'DD.MM.YYYY') + ')'
})
function formatNumber (number: string | number) {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ")
}
@@ -201,6 +271,43 @@
watch(textBtn, () => tg.MainButton.setText(textBtn.value),
{ 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>
<style lang="scss">

View File

@@ -2,14 +2,13 @@
<div class="q-pa-none flex column col-grow no-scroll">
<pn-scroll-list>
<template #card-body-header>
<div class="flex row q-ma-md justify-between" v-if="chats.length !== 0">
<q-input
v-model="search"
clearable
clear-icon="close"
:placeholder="$t('chats__search')"
dense
dense filled
class="col-grow"
v-if="chats.length !== 0"
>
@@ -52,18 +51,15 @@
{{ item.description }}
</q-item-label>
<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-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-slide-item>
</q-list>
@@ -166,7 +162,6 @@
<script setup lang="ts">
import { ref, computed, onActivated, onDeactivated, onBeforeUnmount, inject } from 'vue'
import { useChatsStore } from 'stores/chats'
import { useUsersStore } from 'stores/users'
import type { WebApp } from '@twa-dev/types'
import { useI18n } from "vue-i18n"
import { useRouter, useRoute } from 'vue-router'
@@ -179,7 +174,6 @@
const search = ref('')
const showOverlay = ref<boolean>(false)
const chatsStore = useChatsStore()
const usersStore = useUsersStore()
const showDialogDeleteChat = ref<boolean>(false)
const deleteChatId = ref<number | undefined>(undefined)
const currentSlideEvent = ref<SlideEvent | null>(null)
@@ -192,7 +186,7 @@
reset: () => void
}
const chats = chatsStore.getChats
const chats = computed(() => chatsStore.chats)
const chatsInit = computed(() => chatsStore.isInit)
const fabMenu = [
@@ -201,9 +195,9 @@
]
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 arrOut = chats
const arrOut = chats.value
.filter(el =>
el.name.toLowerCase().includes(searchValue) ||
el.description && el.description.toLowerCase().includes(searchValue)

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
clearable
clear-icon="close"
:placeholder="$t('users__search')"
dense
dense filled
class="col-grow"
>
<template #prepend>
@@ -254,7 +254,7 @@
const usersStore = useUsersStore()
const companiesStore = useCompaniesStore()
const users = usersStore.getUsers
const users = computed(() => usersStore.users)
const usersInit = computed(() => usersStore.isInit)
const blockUserId = ref<number | undefined>(undefined)
@@ -263,7 +263,7 @@
const closedByUserAction = ref(false)
const { userSection } = useUserSection()
const mapUsers = computed(() => users.map(el => ({
const mapUsers = computed(() => users.value.map(el => ({
...el,
...userSection(el),
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 (typeof to.name === 'string' && publicPath.includes(to.name)) return true
const authStore = useAuthStore()
const projectsStore = useProjectsStore()
@@ -48,8 +49,6 @@ export default defineRouter(function (/* { store, ssrContext } */) {
if (to.name === 'login')
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 === 'create_account' && !authStore.isAuth) return true

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
interface Chat {
id: number
project_id: number
telegram_id: number
telegram_id: string
message_id: number
name: string
is_channel: boolean
bot_can_ban: boolean
@@ -10,7 +11,6 @@ interface Chat {
description: string | null
logo: string | null
owner_id?: number
invite_link: string
users: 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 {
if (typeof s !== 'string') return null
const regex = /^[+-]?\d+$/
return regex.test(s) ? Number(s) : null
}
// Функция для преобразования пустых строк в null
function convertEmptyStringsToNull<T extends Record<string, unknown>>(obj: T): T {
const result = { ...obj } as Record<string, unknown>
Object.keys(result).forEach(key => {
if (result[key] === '') {
result[key] = null
}
})
return result as T
}
const removeNullKeys = <T extends object>(obj: T): Partial<T> =>
Object.fromEntries(
Object.entries(obj).filter(([, v]) => v !== null)
) as Partial<T>
export {
isDirty,
parseIntString,
convertEmptyStringsToNull
removeNullKeys
}