12
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { defineBoot } from '#q-app/wrappers'
|
||||
import axios, { type AxiosError } from 'axios'
|
||||
import { Notify } from 'quasar'
|
||||
|
||||
class ServerError extends Error {
|
||||
constructor(
|
||||
@@ -19,6 +20,7 @@ const api = axios.create({
|
||||
api.interceptors.response.use(
|
||||
response => response,
|
||||
async (error: AxiosError<{ error?: { code: string; message: string } }>) => {
|
||||
console.log(error)
|
||||
const errorData = error.response?.data?.error || {
|
||||
code: 'ZERO',
|
||||
message: error.message || 'Unknown error'
|
||||
@@ -29,6 +31,12 @@ api.interceptors.response.use(
|
||||
errorData.message
|
||||
)
|
||||
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: errorData.code + ': ' + errorData.message,
|
||||
icon: 'mdi-alert-outline',
|
||||
position: 'bottom'
|
||||
})
|
||||
return Promise.reject(serverError)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
<template>
|
||||
<div class="q-pt-md" v-if="mapUsers.length !==0 ">
|
||||
<span class="q-pl-md text-h6">
|
||||
{{ $t('company_info__users') }}
|
||||
</span>
|
||||
|
||||
<q-list separator>
|
||||
<q-item
|
||||
v-for="item in mapUsers"
|
||||
:key="item.id"
|
||||
v-ripple
|
||||
clickable
|
||||
@click="goPersonInfo(item.id)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<pn-auto-avatar
|
||||
:img="item.photo"
|
||||
:name="item.section1"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label lines="1" class="text-bold" v-if="item.section1">
|
||||
{{item.section1}}
|
||||
</q-item-label>
|
||||
<q-item-label caption lines="2">
|
||||
<span v-if="item.section2_1" class="q-mr-sm">{{item.section2_1}}</span>
|
||||
<span class="text-blue" v-if="item.section2_2">{{'@' + item.section2_2}}</span>
|
||||
</q-item-label>
|
||||
<q-item-label lines="1" v-if="item.section3">
|
||||
{{item.section3}}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useUsersStore } from 'stores/users'
|
||||
import type { User } from 'types/Users'
|
||||
const usersStore = useUsersStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const currentCompanyId = Number(route.params.companyId)
|
||||
|
||||
const users = usersStore.users
|
||||
|
||||
const mapUsers = users
|
||||
.filter(el => el.company_id === currentCompanyId)
|
||||
.map(el => ({...el, ...userSection(el)}))
|
||||
|
||||
|
||||
async function goPersonInfo (userId: number) {
|
||||
await router.push({ name: 'user_info', params: { id: route.params.id, userId }})
|
||||
}
|
||||
|
||||
// copy from 'pages/project-page/ProjectPageUsers.vue' кроме company.name
|
||||
function userSection (user: User) {
|
||||
const tname = () => {
|
||||
return user.firstname
|
||||
? user.lastname
|
||||
? user.firstname + ' ' + user.lastname
|
||||
: user.firstname
|
||||
: user.lastname ?? ''
|
||||
}
|
||||
|
||||
const section1 = user.name
|
||||
? user.name
|
||||
: tname()
|
||||
|
||||
const section2_1 = user.name
|
||||
? tname()
|
||||
: ''
|
||||
|
||||
const section2_2 = user.username ?? ''
|
||||
|
||||
const section3 = (
|
||||
user.department
|
||||
? user.department + ' '
|
||||
: ''
|
||||
) + (
|
||||
user.role ?? ''
|
||||
)
|
||||
|
||||
return {
|
||||
section1,
|
||||
section2_1, section2_2,
|
||||
section3
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -34,37 +34,46 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useSettingsStore } from 'stores/settings'
|
||||
|
||||
const fileText = ref<string>('')
|
||||
const isLoading = ref<boolean>(true)
|
||||
const error = ref<boolean>(false)
|
||||
const settingsStore = useSettingsStore()
|
||||
const lang = ref<string>('EN')
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'terms_of_use' | 'privacy'
|
||||
}>()
|
||||
|
||||
function parseLocale(locale: string): string {
|
||||
return locale.split(/[-_]/)[0] ?? ''
|
||||
const settingsStore = useSettingsStore()
|
||||
const fileText = ref<string | null>('')
|
||||
const isLoading = ref(true)
|
||||
const DEFAULT_LANG = 'ru'
|
||||
|
||||
const baseDocName = props.type === 'terms_of_use'
|
||||
? 'Terms_of_use'
|
||||
: 'Privacy'
|
||||
|
||||
const parseLocale = (locale: string) => locale.split(/[-_]/)[0] || DEFAULT_LANG
|
||||
|
||||
const fetchDocument = async (language: string) => {
|
||||
try {
|
||||
const response = await fetch(`/admin/doc/${baseDocName}_${language}.txt`)
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||
return await response.text()
|
||||
} catch (error) {
|
||||
console.error(`Failed to load ${language} version:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const baseDocName =
|
||||
props.type ==='terms_of_use' ? 'Terms_of_use' : 'Privacy'
|
||||
|
||||
onMounted(async () => {
|
||||
const locale = settingsStore.settings.locale
|
||||
lang.value = parseLocale(locale)
|
||||
try {
|
||||
const response = await fetch('/admin/doc/' + baseDocName + '_' + lang.value +'.txt')
|
||||
const lang = parseLocale(settingsStore.settings.locale)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`)
|
||||
fileText.value = await fetchDocument(lang)
|
||||
|
||||
if (!fileText.value && lang !== DEFAULT_LANG) {
|
||||
fileText.value = await fetchDocument(DEFAULT_LANG)
|
||||
}
|
||||
|
||||
fileText.value = await response.text()
|
||||
} catch (err) {
|
||||
console.error('File load error:', err)
|
||||
error.value = true
|
||||
if (!fileText.value) throw new Error('All loading attempts failed')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Document loading failed:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
125
src/components/pnItemBtmDialog.vue
Normal file
125
src/components/pnItemBtmDialog.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<q-item
|
||||
@click="showDialog=true"
|
||||
clickable
|
||||
v-ripple
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar
|
||||
rounded
|
||||
text-color="white"
|
||||
:icon
|
||||
:color="iconColor"
|
||||
size="lg"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
{{ $t(title) }}
|
||||
</q-item-label>
|
||||
<q-item-label caption>
|
||||
<slot name="value"/>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="mdi-chevron-right" color="grey"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-dialog
|
||||
v-model="showDialog"
|
||||
maximized
|
||||
transition-show="slide-up"
|
||||
transition-hide="slide-down"
|
||||
position="bottom"
|
||||
>
|
||||
<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;
|
||||
"
|
||||
>
|
||||
<div
|
||||
ref="cardHeaderRef"
|
||||
class="flex items-center no-wrap justify-between w100 q-my-none q-pa-md"
|
||||
>
|
||||
<div>
|
||||
<div class="flex column q-mx-xs">
|
||||
<span class="text-h6 ellipsis">{{ $t(title) }}</span>
|
||||
<span v-if="caption" class="text-grey text-caption">{{ $t(caption) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between no-wrap">
|
||||
<q-btn
|
||||
icon="mdi-close"
|
||||
@click="showDialog=false"
|
||||
flat round
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="cardBodyRef"
|
||||
class="q-px-none q-ma-none"
|
||||
>
|
||||
<pn-shadow-scroll
|
||||
:hideShadows="false"
|
||||
:height="bodyHeight"
|
||||
>
|
||||
<div ref="cardBodyInnerRef" class="q-px-md q-ma-none">
|
||||
<q-resize-observer @resize="updateDimensions" />
|
||||
<slot/>
|
||||
</div>
|
||||
</pn-shadow-scroll>
|
||||
</div>
|
||||
<div
|
||||
ref="cardFooterRef"
|
||||
class="q-pa-md"
|
||||
>
|
||||
<slot name="footer"/>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useSlots } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
caption?: string
|
||||
icon: string
|
||||
iconColor: string
|
||||
}>()
|
||||
|
||||
const showDialog=ref<boolean>(false)
|
||||
|
||||
const slots = useSlots()
|
||||
const cardHeaderRef = ref<HTMLElement | null>(null)
|
||||
const cardFooterRef = ref<HTMLElement | null>(null)
|
||||
const cardBodyRef = ref<HTMLElement | null>(null)
|
||||
const cardBodyInnerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const headerHeight = ref(0)
|
||||
const footerHeight = ref(0)
|
||||
const bodyInnerHeight = ref(0)
|
||||
const bodyHeight = ref(0)
|
||||
|
||||
const updateDimensions = () => {
|
||||
headerHeight.value = cardHeaderRef.value?.offsetHeight || 0
|
||||
footerHeight.value = cardFooterRef.value?.offsetHeight || 0
|
||||
bodyInnerHeight.value = cardBodyInnerRef.value?.offsetHeight || 0
|
||||
bodyHeight.value = window.innerHeight - headerHeight.value - footerHeight.value - 48
|
||||
}
|
||||
|
||||
watch(() => slots.body?.(), updateDimensions, { flush: 'post' })
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fix-card-width {
|
||||
width: var(--body-width) !important;
|
||||
}
|
||||
</style>
|
||||
53
src/components/pnListSelector.vue
Normal file
53
src/components/pnListSelector.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<q-list class="q-gutter-y-sm">
|
||||
<q-btn
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
flat
|
||||
no-caps dense
|
||||
class="w100"
|
||||
align="left"
|
||||
@click="selectItem(option)"
|
||||
:class="isSelected(option.value) ? 'text-primary' : ''"
|
||||
>
|
||||
<q-icon
|
||||
:name="isSelected(option.value) ? 'mdi-check' : ''"
|
||||
color="primary"
|
||||
class="q-pr-sm"
|
||||
/>
|
||||
<span
|
||||
:class="!isSelected(option.value) ? 'text-weight-regular' : ''"
|
||||
>
|
||||
{{ $te(option.label) ? $t(option.label) : option.label }}
|
||||
</span>
|
||||
</q-btn>
|
||||
</q-list>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface ListOption {
|
||||
label: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
options: ListOption[]
|
||||
}>()
|
||||
|
||||
const model = defineModel<string | number | null>({
|
||||
required: true
|
||||
})
|
||||
|
||||
const isSelected = computed(() => (value: string | number) => {
|
||||
return model.value === value
|
||||
})
|
||||
|
||||
const selectItem = (option: ListOption) => {
|
||||
model.value = option.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<q-dialog
|
||||
v-model="modelValue"
|
||||
>
|
||||
<q-card class="q-pa-none q-ma-none w100 no-scroll" align="center">
|
||||
<q-dialog v-model="modelValue">
|
||||
<q-card
|
||||
class="q-pa-none q-ma-none w100 no-scroll"
|
||||
align="center"
|
||||
>
|
||||
<q-card-section>
|
||||
<q-avatar :color :icon size="60px" font-size="45px" text-color="white"/>
|
||||
</q-card-section>
|
||||
@@ -12,50 +13,55 @@
|
||||
style="overflow-wrap: break-word"
|
||||
>
|
||||
<div class="text-h6 text-bold ">
|
||||
{{ $t(title)}}
|
||||
{{ $t(title) }}
|
||||
</div>
|
||||
<div v-if="message1">
|
||||
{{ $t(message1)}}
|
||||
{{ $t(message1) }}
|
||||
</div>
|
||||
<div v-if="message2">
|
||||
{{ $t(message2)}}
|
||||
{{ $t(message2) }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="center" vertical>
|
||||
<div class="flex q-mt-lg no-wrap w100 justify-center q-gutter-x-md">
|
||||
<q-card-section>
|
||||
<div class="flex column w100 q-mt-lg q-px-sm">
|
||||
<div class="flex q-gutter-md">
|
||||
<div class="col-grow" v-if="auxBtnLabel">
|
||||
<q-btn
|
||||
:label="$t(auxBtnLabel)"
|
||||
outline
|
||||
color="grey"
|
||||
class="w100"
|
||||
v-close-popup
|
||||
rounded
|
||||
@click="emit('clickAuxBtn')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-grow">
|
||||
<q-btn
|
||||
:label="$t(mainBtnLabel)"
|
||||
:color="color"
|
||||
class="w100"
|
||||
v-close-popup
|
||||
rounded
|
||||
@click="emit('clickMainBtn')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
v-if="auxBtnLabel"
|
||||
:label="$t(auxBtnLabel)"
|
||||
outline
|
||||
color="grey"
|
||||
v-close-popup
|
||||
rounded
|
||||
class="w50"
|
||||
@click="emit('clickAuxBtn')"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
:label="$t(mainBtnLabel)"
|
||||
:color="color"
|
||||
v-close-popup
|
||||
rounded
|
||||
:class="auxBtnLabel ? 'w50' : 'w80'"
|
||||
@click="emit('clickMainBtn')"
|
||||
/>
|
||||
class="w100 q-mt-md q-mb-sm" flat
|
||||
v-close-popup rounded
|
||||
@click="emit('close')"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<q-icon name="close"/>
|
||||
{{$t('close')}}
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
class="w80 q-mt-md q-mb-sm" flat
|
||||
v-close-popup rounded
|
||||
@click="emit('close')"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<q-icon name="close"/>
|
||||
{{$t('cancel')}}
|
||||
</div>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
@@ -83,5 +89,5 @@
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
140
src/components/pnTimeZoneSelector.vue
Normal file
140
src/components/pnTimeZoneSelector.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="w100 flex column">
|
||||
<div class="flex row w100">
|
||||
<q-input
|
||||
v-model="search"
|
||||
clearable
|
||||
clear-icon="close"
|
||||
:placeholder="$t('settings__timezone_search')"
|
||||
dense
|
||||
class="col-grow"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<q-list class="q-gutter-y-sm q-my-sm">
|
||||
<q-btn
|
||||
v-for="tz in displayTimeZones"
|
||||
:key="tz.value"
|
||||
flat
|
||||
no-caps
|
||||
dense
|
||||
class="w100"
|
||||
align="left"
|
||||
@click="selectTz(tz)"
|
||||
:class="isSelected(tz.value) ? 'text-primary' : ''"
|
||||
>
|
||||
<div class="flex w100 no-wrap">
|
||||
<q-icon
|
||||
:name="isSelected(tz.value) ? 'mdi-check' : ''"
|
||||
color="primary"
|
||||
class="q-pr-sm"
|
||||
/>
|
||||
<div class="flex row no-wrap justify-between w100">
|
||||
<div>
|
||||
<span class="text-grey text-weight-regular">{{ tz.continent + ' / ' }}</span>
|
||||
<span>{{ tz.city + ' ' }}</span>
|
||||
</div>
|
||||
<span class="text-grey text-weight-regular">{{ tz.offsetDisplay }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-btn>
|
||||
</q-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watchEffect } from 'vue'
|
||||
|
||||
interface TimeZone {
|
||||
value: string
|
||||
continent: string
|
||||
city: string
|
||||
offsetDisplay: string
|
||||
offsetHours: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
locale: string
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<{ tz: string; offset: number, offsetString: string }>({
|
||||
required: true
|
||||
})
|
||||
|
||||
const search = ref('')
|
||||
const timeZonesBase = Intl.supportedValuesOf('timeZone')
|
||||
const timeZones = ref<TimeZone[]>([])
|
||||
|
||||
watchEffect(() => {
|
||||
const date = Date.now()
|
||||
|
||||
timeZones.value = timeZonesBase.map((tz) => {
|
||||
const [continent, ...cityParts] = tz.split('/')
|
||||
const city = cityParts.join('/').replace(/_/g, ' ')
|
||||
|
||||
const formatter = new Intl.DateTimeFormat('en', {
|
||||
timeZone: tz,
|
||||
timeZoneName: 'longOffset'
|
||||
})
|
||||
|
||||
const offsetPart = formatter
|
||||
.formatToParts(date)
|
||||
.find(part => part.type === 'timeZoneName')?.value || 'UTC'
|
||||
|
||||
const cleanOffset = offsetPart.replace(/^GMT|^UTC/, '') || '+00:00'
|
||||
let sign = 1
|
||||
let offsetString = cleanOffset
|
||||
|
||||
if (cleanOffset.startsWith('-')) {
|
||||
sign = -1
|
||||
offsetString = cleanOffset.substring(1)
|
||||
} else if (cleanOffset.startsWith('+')) {
|
||||
offsetString = cleanOffset.substring(1)
|
||||
}
|
||||
|
||||
const [hours = '0', minutes = '0'] = offsetString.split(':')
|
||||
const totalHours = sign * (parseInt(hours) + parseInt(minutes) / 60)
|
||||
|
||||
const absHours = Math.abs(parseInt(hours))
|
||||
const absMinutes = Math.abs(parseInt(minutes))
|
||||
const displaySign = sign === -1 ? '-' : '+'
|
||||
const offsetDisplay = `${displaySign}${String(absHours).padStart(2, '0')}:${String(absMinutes).padStart(2, '0')}`
|
||||
|
||||
return {
|
||||
value: tz,
|
||||
continent: continent?.replace(/_/g, ' ') || '',
|
||||
city: city,
|
||||
offsetDisplay,
|
||||
offsetHours: totalHours
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const displayTimeZones = computed(() => {
|
||||
if (!search.value || !(search.value && search.value.trim())) return timeZones.value
|
||||
const searchValue = search.value.trim().toLowerCase()
|
||||
|
||||
return timeZones.value.filter(
|
||||
el =>
|
||||
el.continent.toLowerCase().includes(searchValue) ||
|
||||
el.city.toLowerCase().includes(searchValue) ||
|
||||
el.offsetDisplay.includes(searchValue) ||
|
||||
el.offsetDisplay.replace(':', '').includes(searchValue)
|
||||
)
|
||||
})
|
||||
|
||||
const isSelected = (value: string) => {
|
||||
return modelValue.value.tz === value
|
||||
}
|
||||
|
||||
const selectTz = (tz: TimeZone) => {
|
||||
modelValue.value = { tz: tz.value, offset: tz.offsetHours, offsetString: tz.offsetDisplay }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -68,7 +68,6 @@
|
||||
{ id: 5, name: 'account__company_data', icon: 'mdi-account-group-outline', description: 'account__company_data_description', pathName: 'your_company' },
|
||||
{ id: 6, name: 'account__settings', icon: 'mdi-cog-outline', description: 'account__settings_description', iconColor: 'info', pathName: 'settings' },
|
||||
{ id: 7, name: 'account__support', icon: 'mdi-lifebuoy', description: 'account__support_description', iconColor: 'info', pathName: 'support' },
|
||||
{ id: 8, name: 'account__3rd_party_software', icon: 'mdi-scale-balance', description: 'account__3rd_party_software_description', iconColor: 'grey', pathName: '3software' },
|
||||
{ id: 9, name: 'account__terms_of_use', icon: 'mdi-book-open-variant-outline', description: '', iconColor: 'grey', pathName: 'terms' },
|
||||
{ id: 10, name: 'account__privacy', icon: 'mdi-lock-outline', description: '', iconColor: 'grey', pathName: 'privacy' }
|
||||
]))
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<company-block
|
||||
v-model="newCompany"
|
||||
title="company_create__title_card"
|
||||
btnText="company_create__btn"
|
||||
title="company_add__title_card"
|
||||
btnText="company_add__btn"
|
||||
@update = "addCompany"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
v-model="searchProject"
|
||||
clearable
|
||||
clear-icon="close"
|
||||
:placeholder="$t('project_chats__search')"
|
||||
:placeholder="$t('projects__search')"
|
||||
dense
|
||||
class="col-grow q-px-md q-py-md"
|
||||
>
|
||||
@@ -91,22 +91,22 @@
|
||||
class="flex column items-center w100"
|
||||
:class="showArchive ? 'bg-grey-12' : ''"
|
||||
>
|
||||
<q-btn-dropdown
|
||||
class="w100 fix-rotate-arrow"
|
||||
<q-btn
|
||||
class="w100 rotate-icon-btn"
|
||||
color="grey"
|
||||
flat no-caps
|
||||
flat
|
||||
no-caps
|
||||
@click="showArchive=!showArchive"
|
||||
dropdown-icon="arrow_drop_up"
|
||||
icon-right="arrow_drop_down"
|
||||
:class="{ 'rotate-icon': showArchive }"
|
||||
>
|
||||
<template #label>
|
||||
<span class="text-caption">
|
||||
{{ !showArchive
|
||||
? $t('projects__show_archive') + ' (' + archiveProjects.length +')'
|
||||
: $t('projects__hide_archive')
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</q-btn-dropdown>
|
||||
<span class="text-caption">
|
||||
{{ !showArchive
|
||||
? $t('projects__show_archive') + ' (' + archiveProjects.length +')'
|
||||
: $t('projects__hide_archive')
|
||||
}}
|
||||
</span>
|
||||
</q-btn>
|
||||
|
||||
<div class="w100" style="overflow: hidden">
|
||||
<transition
|
||||
@@ -186,6 +186,7 @@
|
||||
color="negative"
|
||||
title="projects__dialog_archive_title"
|
||||
message1="projects__dialog_archive_message"
|
||||
message2="projects__dialog_archive_message2"
|
||||
mainBtnLabel="projects__dialog_archive_ok"
|
||||
@clickMainBtn="onConfirmArchiveProject()"
|
||||
@close="onCancel()"
|
||||
@@ -198,7 +199,8 @@
|
||||
color="green"
|
||||
title="projects__dialog_restore_title"
|
||||
message1="projects__dialog_restore_message"
|
||||
mainBtnLabel="projects__dialog_cancel_ok"
|
||||
message2="projects__dialog_restore_message2"
|
||||
mainBtnLabel="projects__dialog_restore_ok"
|
||||
@clickMainBtn="restoreFromArchive()"
|
||||
/>
|
||||
</template>
|
||||
@@ -319,7 +321,17 @@
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.fix-rotate-arrow :deep(.q-btn-dropdown--simple) {
|
||||
margin-left: 0;
|
||||
.rotate-icon-btn {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.rotate-icon-btn.rotate-icon :deep(.q-icon) {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||
}
|
||||
|
||||
.rotate-icon-btn :deep(.q-icon) {
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<template #title>
|
||||
{{ $t('software__title') }}
|
||||
</template>
|
||||
|
||||
<pn-scroll-list>
|
||||
<template #card-body-header>
|
||||
<div class="q-pa-md">
|
||||
{{ $t('software__description') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<q-list separator>
|
||||
<q-expansion-item
|
||||
v-for="item in software"
|
||||
group="somegroup"
|
||||
>
|
||||
<template #header>
|
||||
<q-item-section avatar>
|
||||
<q-avatar square size="sm" v-if="item.logo">
|
||||
<img :src="'3software/logo/' + item.logo">
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<div class="flex items-baseline">
|
||||
<span class="text-h6">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<span v-if = "item.ver" class="text-caption q-pl-xs">
|
||||
{{ 'v.' + item.ver }}
|
||||
</span>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</template>
|
||||
<div class="w100 flex column q-px-md q-gutter-y-md q-py-sm">
|
||||
<div class="flex row no-wrap items-center">
|
||||
<q-icon name="mdi-scale-balance" size="sm" class="q-pr-lg" color="grey"/>
|
||||
<div
|
||||
@click="downloadFile('3software/license/' + item.license_file)"
|
||||
class="flex w100 column q-pl-sm cursor-pointer"
|
||||
>
|
||||
<span> {{ item.license }} </span>
|
||||
<span
|
||||
class="text-caption"
|
||||
style="white-space: pre-line;"
|
||||
>
|
||||
{{ item.license_copyright }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex row no-wrap items-center" v-if="item.web">
|
||||
<q-icon name="mdi-web" size="sm" class="q-pr-lg" color="grey"/>
|
||||
<span
|
||||
class="q-pl-sm cursor-pointer"
|
||||
@click="tg.openLink(item.web_url)"
|
||||
>
|
||||
{{ item.web }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex row no-wrap items-center" v-if="item.git">
|
||||
<q-icon name="mdi-github" size="sm" class="q-pr-lg" color="grey"/>
|
||||
<span
|
||||
class="q-pl-sm cursor-pointer"
|
||||
@click="tg.openLink(item.git_url)"
|
||||
>
|
||||
{{ item.git }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import type { WebApp } from '@twa-dev/types'
|
||||
|
||||
const tg = inject('tg') as WebApp
|
||||
|
||||
const software = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Vue',
|
||||
ver: '3.4',
|
||||
logo: 'vue.webp',
|
||||
web: 'vuejs.org',
|
||||
web_url: 'https://vuejs.org/',
|
||||
git: 'vuejs/core',
|
||||
git_url: 'https://github.com/vuejs/core',
|
||||
license: 'MIT License',
|
||||
license_copyright: 'Copyright (c) 2018-present, Yuxi (Evan) You and Vue contributors',
|
||||
license_file: 'vue/LICENSE.txt'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Quasar',
|
||||
ver: '2.x',
|
||||
logo: 'quasar.png',
|
||||
web: 'quasar.dev',
|
||||
web_url: 'https://quasar.dev/',
|
||||
git: 'quasarframework/quasar',
|
||||
git_url: 'https://github.com/quasarframework/quasar',
|
||||
license: 'MIT License',
|
||||
license_copyright: 'Copyright (c) 2015-present Razvan Stoenescu',
|
||||
license_file: 'quasar/LICENSE.txt'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Pinia',
|
||||
ver: '2.x',
|
||||
logo: 'pinia.svg',
|
||||
web: 'pinia.vuejs.org',
|
||||
web_url: 'https://pinia.vuejs.org/',
|
||||
git: 'vuejs/pinia',
|
||||
git_url: 'https://github.com/vuejs/pinia',
|
||||
license: 'MIT License',
|
||||
license_copyright: 'Copyright (c) 2019-present Eduardo San Martin Morote',
|
||||
license_file: 'pinia/LICENSE.txt'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Vue Router',
|
||||
ver: '4.x',
|
||||
logo: 'vue.webp',
|
||||
web: 'router.vuejs.org',
|
||||
web_url: 'https://router.vuejs.org/',
|
||||
git: 'vuejs/router',
|
||||
git_url: 'https://github.com/vuejs/router',
|
||||
license: 'MIT License',
|
||||
license_copyright: 'Copyright (c) 2019-present Eduardo San Martin Morote',
|
||||
license_file: 'vue-router/LICENSE.txt'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Vue i18n',
|
||||
ver: '9.x',
|
||||
logo: 'vue-i18n.svg',
|
||||
web: 'vue-i18n.intlify.dev',
|
||||
web_url: 'https://vue-i18n.intlify.dev/',
|
||||
git: 'intlify/vue-i18n',
|
||||
git_url: 'https://github.com/intlify/vue-i18n',
|
||||
license: 'MIT License',
|
||||
license_copyright: 'Copyright (c) 2016-present kazuya kawaguchi and contributors',
|
||||
license_file: 'vue-i18n/LICENSE.txt'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Axios',
|
||||
ver: '1.x',
|
||||
logo: 'axios.svg',
|
||||
web: 'axios-http.com',
|
||||
web_url: 'https://axios-http.com/',
|
||||
git: 'axios/axios',
|
||||
git_url: 'https://github.com/axios/axios',
|
||||
license: 'MIT License',
|
||||
license_copyright: 'Copyright (c) 2014-present Matt Zabriskie & Collaborators',
|
||||
license_file: 'axios/LICENSE.txt'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'better-sqlite3',
|
||||
ver: '11.x',
|
||||
logo: '',
|
||||
web: '',
|
||||
web_url: '',
|
||||
git: 'WiseLibs/better-sqlite3',
|
||||
git_url: 'https://github.com/WiseLibs/better-sqlite3',
|
||||
license: 'MIT License',
|
||||
license_copyright: 'Copyright (c) 2017 Joshua Wise',
|
||||
license_file: 'better-sqlite3/LICENSE.txt'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Express',
|
||||
ver: '4.x',
|
||||
logo: 'express.svg',
|
||||
web: 'expressjs.com',
|
||||
web_url: 'https://expressjs.com/',
|
||||
git: 'expressjs/express',
|
||||
git_url: 'https://github.com/expressjs/express',
|
||||
license: 'MIT License',
|
||||
license_copyright: `Copyright (c) 2009-2014 TJ Holowaychuk <tj@vision-media.ca>
|
||||
Copyright (c) 2013-2014 Roman Shtylman <shtylman+expressjs@gmail.com>
|
||||
Copyright (c) 2014-2015 Douglas Christopher Wilson <doug@somethingdoug.com>`,
|
||||
license_file: 'express/LICENSE.txt'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
const downloadFile = async (url: string) => {
|
||||
try {
|
||||
const fullUrl = '/admin/' + url;
|
||||
const fileUrl = new URL(fullUrl, window.location.origin).href;
|
||||
|
||||
const response = await fetch(fileUrl)
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP error: ${response.status}`)
|
||||
|
||||
const blob = await response.blob()
|
||||
const downloadUrl = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
|
||||
const extractFileName = (url: string): string => {
|
||||
return url.substring(url.lastIndexOf('/') + 1)
|
||||
}
|
||||
|
||||
link.href = downloadUrl
|
||||
link.download = extractFileName(url)
|
||||
link.style.display = 'none'
|
||||
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(downloadUrl)
|
||||
}, 100)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Download error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scope>
|
||||
</style>
|
||||
@@ -6,75 +6,120 @@
|
||||
|
||||
<pn-scroll-list>
|
||||
<q-list separator>
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-avatar color="primary" rounded text-color="white" icon="mdi-translate" size="md" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<span>{{ $t('settings__language') }}</span>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-select
|
||||
class="fix-input-right text-body1"
|
||||
v-model="locale"
|
||||
:options="localeOptions"
|
||||
dense
|
||||
borderless
|
||||
emit-value
|
||||
map-options
|
||||
hide-bottom-space
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-avatar color="primary" rounded text-color="white" icon="mdi-format-size" size="md" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<span>{{ $t('settings__font_size') }}</span>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<div class="flex justify-end">
|
||||
<q-btn
|
||||
@click="settingsStore.decreaseFontSize()"
|
||||
color="negative" flat
|
||||
icon="mdi-format-font-size-decrease"
|
||||
class="q-pa-sm q-mx-xs"
|
||||
:disable="!settingsStore.canDecrease"
|
||||
/>
|
||||
<q-btn
|
||||
@click="settingsStore.increaseFontSize()"
|
||||
color="positive" flat
|
||||
icon="mdi-format-font-size-increase"
|
||||
class="q-pa-sm q-mx-xs"
|
||||
:disable="!settingsStore.canIncrease"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item-label header>{{ $t('settings__software_title') }}</q-item-label>
|
||||
|
||||
<pn-item-btm-dialog
|
||||
title="settings__language"
|
||||
caption="settings__software_title"
|
||||
icon="mdi-translate"
|
||||
iconColor="primary"
|
||||
>
|
||||
<template #value>
|
||||
{{ localeOptions.find(el => el.value === locale)?.label }}
|
||||
</template>
|
||||
<pn-list-selector
|
||||
v-model="locale"
|
||||
:options="localeOptions"
|
||||
/>
|
||||
</pn-item-btm-dialog>
|
||||
|
||||
<pn-item-btm-dialog
|
||||
title="settings__font_size"
|
||||
caption="settings__software_title"
|
||||
icon="mdi-format-size"
|
||||
iconColor="primary"
|
||||
>
|
||||
<template #value>
|
||||
{{ $t(fontSizeLabel) }}
|
||||
</template>
|
||||
<pn-list-selector
|
||||
v-model="fontSize"
|
||||
:options="fontSizeOptions"
|
||||
/>
|
||||
</pn-item-btm-dialog>
|
||||
|
||||
<q-item-label class="q-mt-md" header>{{ $t('settings__bot_title') }}</q-item-label>
|
||||
|
||||
<pn-item-btm-dialog
|
||||
title="settings__language"
|
||||
caption="settings__bot_title"
|
||||
icon="mdi-translate"
|
||||
iconColor="primary"
|
||||
>
|
||||
<template #value>
|
||||
{{ localeOptions.find(el => el.value === localeBot)?.label }}
|
||||
</template>
|
||||
<pn-list-selector
|
||||
v-model="localeBot"
|
||||
:options="localeOptions"
|
||||
/>
|
||||
</pn-item-btm-dialog>
|
||||
<pn-item-btm-dialog
|
||||
title="settings__timezone"
|
||||
caption="settings__bot_title"
|
||||
icon="mdi-map-clock-outline"
|
||||
iconColor="primary"
|
||||
>
|
||||
<template #value>
|
||||
{{ timeZoneBot.tz }}
|
||||
{{ timeZoneBot.offsetString }}
|
||||
</template>
|
||||
|
||||
<pn-time-zone-selector
|
||||
v-model="timeZoneBot"
|
||||
:locale
|
||||
/>
|
||||
</pn-item-btm-dialog>
|
||||
</q-list>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ref, watch, computed, onMounted } from 'vue'
|
||||
import { useSettingsStore } from 'stores/settings'
|
||||
import pnItemBtmDialog from 'components/pnItemBtmDialog.vue'
|
||||
import pnListSelector from 'components/pnListSelector.vue'
|
||||
import pnTimeZoneSelector from 'components/pnTimeZoneSelector.vue'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const locale = ref('')
|
||||
const localeBot = ref('')
|
||||
const localeOptions = settingsStore.supportLocale
|
||||
|
||||
const locale = computed({
|
||||
get: () => settingsStore.settings.locale,
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
set: (value: string) => settingsStore.updateLocale(value)
|
||||
watch(locale, async (newValue) => {
|
||||
await settingsStore.updateSettings({ locale: newValue })
|
||||
})
|
||||
|
||||
watch(localeBot, async (newValue) => {
|
||||
await settingsStore.updateSettings({ localeBot: newValue })
|
||||
})
|
||||
|
||||
const fontSize = ref(14)
|
||||
const fontSizeOptions = settingsStore.supportFontSizes
|
||||
const fontSizeLabel = computed(() =>
|
||||
fontSizeOptions.find(el => el.value === fontSize.value)?.label ?? ''
|
||||
)
|
||||
|
||||
watch(fontSize, async (newValue) => {
|
||||
await settingsStore.updateSettings({ fontSize: newValue })
|
||||
})
|
||||
|
||||
const timeZoneBot = ref<{ tz: string, offset: number }>({ tz: '', offset: 1 })
|
||||
|
||||
watch(timeZoneBot, async (newValue) => {
|
||||
if (newValue) await settingsStore.updateSettings({ timeZoneBot: newValue })
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
locale.value = settingsStore.settings.locale
|
||||
localeBot.value = settingsStore.settings.localeBot
|
||||
fontSize.value = settingsStore.settings.fontSize
|
||||
timeZoneBot.value = settingsStore.settings.timeZoneBot
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fix-input-right :deep(.q-field__native) {
|
||||
justify-content: end;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
v-model="search"
|
||||
clearable
|
||||
clear-icon="close"
|
||||
:placeholder="$t('project_chats__search')"
|
||||
:placeholder="$t('chats__search')"
|
||||
dense
|
||||
class="col-grow"
|
||||
v-if="chats.length !== 0"
|
||||
@@ -71,8 +71,8 @@
|
||||
<pn-onboard-btn
|
||||
v-if="chats.length === 0 && chatsInit"
|
||||
icon="mdi-chat-plus-outline"
|
||||
:message1="$t('project_chat__onboard_msg1')"
|
||||
:message2="$t('project_chat__onboard_msg2')"
|
||||
:message1="$t('chats__onboard_msg1')"
|
||||
:message2="$t('chats__onboard_msg2')"
|
||||
@click="showOverlay=true; fabState=true"
|
||||
/>
|
||||
<div
|
||||
@@ -110,7 +110,7 @@
|
||||
anchor="center left" self="center end"
|
||||
style="width: calc(min(100vw, var(--body-width)) - 102px) !important;"
|
||||
>
|
||||
{{ $t('project_chats_disabled_FAB')}}
|
||||
{{ $t('chats_disabled_FAB')}}
|
||||
</q-tooltip>
|
||||
</template>
|
||||
<q-fab-action
|
||||
@@ -152,9 +152,10 @@
|
||||
v-model="showDialogDeleteChat"
|
||||
icon="mdi-link-off"
|
||||
color="negative"
|
||||
title="project_chat__delete_warning"
|
||||
message1="project_chat__delete_warning_message"
|
||||
mainBtnLabel="project_chat__dialog_cancel_ok"
|
||||
title="chats__dialog_unlink_title"
|
||||
message1="chats__dialog_unlink_message"
|
||||
message2="chats__dialog_unlink_message2"
|
||||
mainBtnLabel="chats__dialog_unlink_ok"
|
||||
@clickMainBtn="onConfirm()"
|
||||
@close="onCancel()"
|
||||
@before-hide="onDialogBeforeHide()"
|
||||
@@ -190,8 +191,8 @@
|
||||
const chatsInit = computed(() => chatsStore.isInit)
|
||||
|
||||
const fabMenu = [
|
||||
{id: 1, icon: 'mdi-chat-plus-outline', name: 'project_chats__attach_chat', description: 'project_chats__attach_chat_description', func: attachChat},
|
||||
{id: 2, icon: 'mdi-share-outline', name: 'project_chats__send_chat', description: 'project_chats__send_chat_description', func: sendChat},
|
||||
{id: 1, icon: 'mdi-chat-plus-outline', name: 'chats__attach_chat', description: 'chats__attach_chat_description', func: attachChat},
|
||||
{id: 2, icon: 'mdi-share-outline', name: 'chats__send_chat', description: 'chats__send_chat_description', func: sendChat},
|
||||
]
|
||||
|
||||
const displayChats = computed(() => {
|
||||
@@ -253,7 +254,7 @@
|
||||
const key = await chatsStore.getKey()
|
||||
const message = urlAdmin + key + urlAdminPermission
|
||||
const tgShareUrl = 'https://t.me/share/url?url=' +
|
||||
encodeURIComponent( t('project_chats__send_chat_title')) +
|
||||
encodeURIComponent( t('chats__send_chat_title')) +
|
||||
'&text=' + `${encodeURIComponent(message)}`
|
||||
tg.openTelegramLink(tgShareUrl)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:color="companies.length <= 2 ? 'grey-6' : 'primary'"
|
||||
flat
|
||||
no-caps
|
||||
@click="maskCompany()"
|
||||
@click="maskCompany"
|
||||
:disable="companies.length <= 2"
|
||||
class="q-pr-md"
|
||||
rounded
|
||||
@@ -16,9 +16,10 @@
|
||||
left
|
||||
size="sm"
|
||||
name="mdi-domino-mask"
|
||||
class="q-mr-xs"
|
||||
/>
|
||||
<div>
|
||||
{{ $t('company__mask')}}
|
||||
{{ $t('companies__mask')}}
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
@@ -53,7 +54,7 @@
|
||||
<q-item-label lines="1" class="text-caption text-amber-10" v-if="item.id === myCompany?.id">
|
||||
<div class="flex items-center">
|
||||
<q-icon name="star" class="q-pr-xs"/>
|
||||
{{ $t('company__my_company') }}
|
||||
{{ $t('companies__my_company') }}
|
||||
</div>
|
||||
</q-item-label>
|
||||
<q-item-label lines="1" class="text-bold">{{ item.name }}</q-item-label>
|
||||
@@ -93,9 +94,9 @@
|
||||
<pn-onboard-btn
|
||||
v-if="companies.length <= 1 && companiesInit"
|
||||
icon="mdi-account-multiple-plus-outline"
|
||||
:message1="$t('company__onboard_msg1')"
|
||||
:message2="$t('company__onboard_msg2')"
|
||||
@btn-click="createCompany()"
|
||||
:message1="$t('companies__onboard_msg1')"
|
||||
:message2="$t('companies__onboard_msg2')"
|
||||
@btn-click="createCompany"
|
||||
/>
|
||||
<div
|
||||
class="flex column justify-center items-center w100"
|
||||
@@ -130,9 +131,10 @@
|
||||
v-model="showDialogDeleteCompany"
|
||||
icon="mdi-account-multiple-minus-outline"
|
||||
color="negative"
|
||||
title="company__dialog_delete_title"
|
||||
message1="company__dialog_delete_message"
|
||||
mainBtnLabel="company__dialog_delete_ok"
|
||||
title="companies__dialog_delete_title"
|
||||
message1="companies__dialog_delete_message"
|
||||
message2="companies__dialog_delete_message2"
|
||||
mainBtnLabel="companies__dialog_delete_ok"
|
||||
@clickMainBtn="onConfirm()"
|
||||
@close="onCancel()"
|
||||
@before-hide="onDialogBeforeHide()"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
v-model="search"
|
||||
clearable
|
||||
clear-icon="close"
|
||||
:placeholder="$t('project_users__search')"
|
||||
:placeholder="$t('users__search')"
|
||||
dense
|
||||
class="col-grow"
|
||||
>
|
||||
@@ -60,27 +60,92 @@
|
||||
</q-slide-item>
|
||||
</q-list>
|
||||
|
||||
<!-- LEAVE USERS SECTION -->
|
||||
<div
|
||||
v-if="leaveUsers.length!==0"
|
||||
class="flex column items-center w100"
|
||||
:class="showLeaveUsers ? 'bg-grey-12' : ''"
|
||||
>
|
||||
<q-btn
|
||||
class="w100 rotate-icon-btn"
|
||||
color="grey"
|
||||
flat
|
||||
no-caps
|
||||
@click="showLeaveUsers=!showLeaveUsers"
|
||||
icon-right="arrow_drop_down"
|
||||
:class="{ 'rotate-icon': showLeaveUsers }"
|
||||
>
|
||||
<span class="text-caption">
|
||||
{{ !showLeaveUsers
|
||||
? $t('users__show_left_users') + ' (' + leaveUsers.length +')'
|
||||
: $t('users__hide_left_users')
|
||||
}}
|
||||
</span>
|
||||
</q-btn>
|
||||
|
||||
<div class="w100" style="overflow: hidden">
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated slideInDown"
|
||||
leave-active-class="animated slideOutUp"
|
||||
>
|
||||
<q-list separator v-if="showLeaveUsers" class="w100">
|
||||
<q-item
|
||||
v-for = "item in leaveUsers"
|
||||
:key="item.id"
|
||||
class="w100 text-grey"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<pn-auto-avatar
|
||||
:img="item.photo"
|
||||
:name="item.section1"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label lines="1" class="text-bold" v-if="item.section1">
|
||||
{{item.section1}}
|
||||
</q-item-label>
|
||||
<q-item-label lines="1" caption v-if="item.section3">
|
||||
{{item.section3}}
|
||||
</q-item-label>
|
||||
<q-item-label caption lines="2">
|
||||
<div class="flex items-center">
|
||||
<q-icon name="telegram" v-if="item.section2_1 || item.section2_2" class="q-pr-xs" style="color: #27a7e7"/>
|
||||
<div v-if="item.section2_1" class="q-mr-sm text-bold">{{item.section2_1}}</div>
|
||||
<div class="text-blue" v-if="item.section2_2">{{'@' + item.section2_2}}</div>
|
||||
</div>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END LEAVE USERS SECTION -->
|
||||
|
||||
|
||||
<!-- BLOCKED USERS SECTION -->
|
||||
<div
|
||||
v-if="blockedUsers.length!==0"
|
||||
class="flex column items-center w100"
|
||||
:class="showBlockedUsers ? 'bg-grey-12' : ''"
|
||||
>
|
||||
<q-btn-dropdown
|
||||
class="w100 fix-rotate-arrow"
|
||||
<q-btn
|
||||
class="w100 rotate-icon-btn"
|
||||
color="grey"
|
||||
flat no-caps
|
||||
flat
|
||||
no-caps
|
||||
@click="showBlockedUsers=!showBlockedUsers"
|
||||
dropdown-icon="arrow_drop_up"
|
||||
icon-right="arrow_drop_down"
|
||||
:class="{ 'rotate-icon': showBlockedUsers }"
|
||||
>
|
||||
<template #label>
|
||||
<span class="text-caption">
|
||||
{{ !showBlockedUsers
|
||||
? $t('users__show_archive') + ' (' + blockedUsers.length +')'
|
||||
: $t('user__hide_archive')
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</q-btn-dropdown>
|
||||
<span class="text-caption">
|
||||
{{ !showBlockedUsers
|
||||
? $t('users__show_blocked_users') + ' (' + blockedUsers.length +')'
|
||||
: $t('users__hide_blocked_users')
|
||||
}}
|
||||
</span>
|
||||
</q-btn>
|
||||
|
||||
<div class="w100" style="overflow: hidden">
|
||||
<transition
|
||||
@@ -123,12 +188,13 @@
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END BLOCKED USERS SECTION -->
|
||||
|
||||
<pn-onboard-btn
|
||||
v-if="users.length === 0 && usersInit"
|
||||
icon="mdi-account-outline"
|
||||
:message1="$t('project_users__onboard_msg1')"
|
||||
:message2="$t('project_users__onboard_msg2')"
|
||||
:message1="$t('users__onboard_msg1')"
|
||||
:message2="$t('users__onboard_msg2')"
|
||||
noBtn
|
||||
/>
|
||||
<div
|
||||
@@ -142,13 +208,14 @@
|
||||
</div>
|
||||
|
||||
<pn-small-dialog
|
||||
v-model="showDialogDeleteUser"
|
||||
v-model="showDialogBlockUser"
|
||||
icon="mdi-account-remove-outline"
|
||||
color="negative"
|
||||
title="user__dialog_delete_title"
|
||||
message1="user__dialog_delete_message"
|
||||
mainBtnLabel="user__dialog_delete_ok"
|
||||
@clickMainBtn="onConfirmDeleteUser()"
|
||||
title="users__dialog_block_title"
|
||||
message1="users__dialog_block_message"
|
||||
message2="users__dialog_block_message2"
|
||||
mainBtnLabel="users__dialog_block_ok"
|
||||
@clickMainBtn="onConfirmBlockUser()"
|
||||
@close="onCancel()"
|
||||
@before-hide="onDialogBeforeHide()"
|
||||
/>
|
||||
@@ -157,9 +224,10 @@
|
||||
v-model="showDialogRestoreUser"
|
||||
icon="mdi-account-reactivate-outline"
|
||||
color="green"
|
||||
title="user__dialog_restore_title"
|
||||
message1="user__dialog_restore_message"
|
||||
mainBtnLabel="user__dialog_restore_ok"
|
||||
title="users__dialog_restore_title"
|
||||
message1="users__dialog_restore_message"
|
||||
message2="users__dialog_restore_message2"
|
||||
mainBtnLabel="users__dialog_restore_ok"
|
||||
@clickMainBtn="onConfirmRestoreUser()"
|
||||
/>
|
||||
|
||||
@@ -183,12 +251,18 @@
|
||||
const users = usersStore.getUsers
|
||||
const usersInit = computed(() => usersStore.isInit)
|
||||
|
||||
const deleteUserId = ref<number | undefined>(undefined)
|
||||
const showDialogDeleteUser = ref<boolean>(false)
|
||||
const blockUserId = ref<number | undefined>(undefined)
|
||||
const showDialogBlockUser = ref<boolean>(false)
|
||||
const currentSlideEvent = ref<SlideEvent | null>(null)
|
||||
const closedByUserAction = ref(false)
|
||||
|
||||
const mapUsers = computed(() => users.map(el => ({...el, ...userSection(el)})))
|
||||
const mapUsers = computed(() => users.map(el => ({
|
||||
...el,
|
||||
...userSection(el),
|
||||
companyName: el.company_id && companiesStore.companyById(el.company_id)
|
||||
? companiesStore.companyById(el.company_id)?.name
|
||||
: null
|
||||
})))
|
||||
|
||||
interface SlideEvent {
|
||||
reset: () => void
|
||||
@@ -202,12 +276,13 @@
|
||||
el.section1.toLowerCase().includes(searchValue) ||
|
||||
el.section2_1.toLowerCase().includes(searchValue) ||
|
||||
el.section2_2.toLowerCase().includes(searchValue) ||
|
||||
el.section3.toLowerCase().includes(searchValue)
|
||||
el.section3.toLowerCase().includes(searchValue) ||
|
||||
el.companyName && el.companyName.toLowerCase().includes(searchValue)
|
||||
)
|
||||
return arrOut
|
||||
})
|
||||
|
||||
const displayUsers = computed(() => displayUsersAll.value.filter(el => !el.is_block))
|
||||
const displayUsers = computed(() => displayUsersAll.value.filter(el => !el.is_blocked))
|
||||
|
||||
function userSection (user: User) {
|
||||
const tname = () => {
|
||||
@@ -244,8 +319,8 @@
|
||||
|
||||
function handleSlide (event: SlideEvent, id: number) {
|
||||
currentSlideEvent.value = event
|
||||
showDialogDeleteUser.value = true
|
||||
deleteUserId.value = id
|
||||
showDialogBlockUser.value = true
|
||||
blockUserId.value = id
|
||||
}
|
||||
|
||||
function onDialogBeforeHide () {
|
||||
@@ -263,26 +338,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function onConfirmDeleteUser() {
|
||||
async function onConfirmBlockUser() {
|
||||
closedByUserAction.value = true
|
||||
if (deleteUserId.value) {
|
||||
await usersStore.blockUser(deleteUserId.value)
|
||||
if (blockUserId.value) {
|
||||
await usersStore.blockUser(blockUserId.value)
|
||||
}
|
||||
currentSlideEvent.value = null
|
||||
}
|
||||
|
||||
const showBlockedUsers = ref(false)
|
||||
const blockedUsers = computed(() => displayUsersAll.value.filter(el => el.is_block))
|
||||
const blockedUsers = computed(() => displayUsersAll.value.filter(el => el.is_blocked))
|
||||
const unblockUserId = ref<number | undefined> (undefined)
|
||||
const showDialogRestoreUser = ref(false)
|
||||
|
||||
const showLeaveUsers = ref(false)
|
||||
const leaveUsers = computed(() => displayUsersAll.value.filter(el => el.is_leave))
|
||||
|
||||
function handleUnblockUser (id: number) {
|
||||
showDialogRestoreUser.value = true
|
||||
unblockUserId.value = id
|
||||
}
|
||||
|
||||
async function onConfirmRestoreUser () {
|
||||
if (unblockUserId.value) await usersStore.restore(unblockUserId.value)
|
||||
if (unblockUserId.value) await usersStore.unblockUser(unblockUserId.value)
|
||||
}
|
||||
|
||||
watch(showDialogRestoreUser, (newD :boolean) => {
|
||||
@@ -293,5 +371,18 @@
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
.rotate-icon-btn {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.rotate-icon-btn.rotate-icon :deep(.q-icon) {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||
}
|
||||
|
||||
.rotate-icon-btn :deep(.q-icon) {
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -129,11 +129,6 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('pages/account/SubscribePage.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
name: '3software',
|
||||
path: '/3software',
|
||||
component: () => import('pages/account/3Software.vue'),
|
||||
},
|
||||
{
|
||||
name: 'terms',
|
||||
path: '/terms-of-use',
|
||||
|
||||
@@ -14,6 +14,14 @@ interface Customer {
|
||||
company?: CompanyParams
|
||||
}
|
||||
|
||||
interface WSMessage {
|
||||
id: number
|
||||
action: string
|
||||
entity: string
|
||||
entity_id?: number
|
||||
needUpdateStores: string[]
|
||||
}
|
||||
|
||||
const ENDPOINT_MAP = {
|
||||
register: '/auth/email/register',
|
||||
forgot: '/auth/forgot',
|
||||
@@ -25,6 +33,7 @@ export type AuthFlowType = keyof typeof ENDPOINT_MAP
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const customer = ref<Customer | null>(null)
|
||||
const wsEvent = ref<WSMessage | null>(null)
|
||||
const projectsStore = useProjectsStore()
|
||||
|
||||
const isAuth = computed(() => !!customer.value)
|
||||
@@ -35,29 +44,26 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
customer.value = data.data
|
||||
const socket = new WebSocket("wss://946gp81j-9000.euw.devtunnels.ms/api/admin")
|
||||
console.log(socket)
|
||||
socket.onopen = function() {
|
||||
console.log("Соединение установлено.");
|
||||
};
|
||||
socket.onopen = () => console.log("Connection ws create.")
|
||||
|
||||
socket.onclose = function(event) {
|
||||
if (event.wasClean) {
|
||||
console.log('Соединение закрыто чисто');
|
||||
} else {
|
||||
console.log('Обрыв соединения'); // например, "убит" процесс сервера
|
||||
socket.onclose = (event) => {
|
||||
if (event.wasClean) console.log('Connection ws close.')
|
||||
else console.log('Connection ws failure!')
|
||||
console.log('Code: ' + event.code + ', reason : ' + event.reason)
|
||||
}
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
if (wsEvent.value) {
|
||||
wsEvent.value.needUpdateStores = []
|
||||
wsEvent.value = JSON.parse(event.data)
|
||||
if (wsEvent.value?.entity === 'chat') wsEvent.value.needUpdateStores = ['users']
|
||||
}
|
||||
console.log('Код: ' + event.code + ' причина: ' + event.reason);
|
||||
};
|
||||
|
||||
socket.onmessage = function(event) {
|
||||
console.log("Получены данные " + event.data);
|
||||
};
|
||||
|
||||
socket.onerror = function(error) {
|
||||
console.log("Ошибка " + error.message);
|
||||
};
|
||||
socket.onerror = (event) => console.error("Ошибка ", event)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (isAuth.value) console.log(error)
|
||||
if (isAuth.value) console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,9 +87,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
if (flowType !== 'changePwd')
|
||||
await api.post(ENDPOINT_MAP[flowType], { email })
|
||||
else
|
||||
{console.log(222)
|
||||
await api.post(ENDPOINT_MAP[flowType])
|
||||
}
|
||||
}
|
||||
|
||||
const confirmCode = async (flowType: AuthFlowType, email: string, code: string) => {
|
||||
@@ -100,36 +104,32 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
|
||||
//change email of account
|
||||
const getCodeCurrentEmail = async () => {
|
||||
const getCodeCurrentEmail = async () =>
|
||||
await api.post('/auth/email/change-email')
|
||||
}
|
||||
|
||||
const confirmCurrentEmailCode = async (code: string) => {
|
||||
|
||||
const confirmCurrentEmailCode = async (code: string) =>
|
||||
await api.post('/auth/email/change-email', { code })
|
||||
}
|
||||
|
||||
const getCodeNewEmail = async (code: string, email: string) => {
|
||||
const getCodeNewEmail = async (code: string, email: string) =>
|
||||
await api.post('/auth/email/change-email', { code, email })
|
||||
}
|
||||
|
||||
const confirmNewEmailCode = async (code: string, code2: string, email: string) => {
|
||||
const confirmNewEmailCode = async (code: string, code2: string, email: string) =>
|
||||
await api.post('/auth/email/change-email', { code, code2, email })
|
||||
}
|
||||
|
||||
const setNewEmailPassword = async (code: string, code2: string, email: string, password: string) => {
|
||||
const setNewEmailPassword = async (code: string, code2: string, email: string, password: string) =>
|
||||
await api.post('/auth/email/change-email', { code, code2, email, password })
|
||||
}
|
||||
|
||||
// user data company
|
||||
const updateMyCompany = async (companyData: CompanyParams) => {
|
||||
const response = await api.put('/customer/profile', { company: companyData })
|
||||
console.log(response)
|
||||
if (response.status === 200 && customer.value) customer.value.company = companyData
|
||||
}
|
||||
|
||||
return {
|
||||
customer,
|
||||
isAuth,
|
||||
wsEvent,
|
||||
initialize,
|
||||
loginWithCredentials,
|
||||
loginWithTelegram,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from 'boot/axios'
|
||||
import type { Chat } from 'types/Chat'
|
||||
import { useProjectsStore } from 'stores/projects'
|
||||
import { useAuthStore } from 'stores/auth'
|
||||
|
||||
export const useChatsStore = defineStore('chats', () => {
|
||||
const projectsStore = useProjectsStore()
|
||||
@@ -12,8 +13,9 @@ export const useChatsStore = defineStore('chats', () => {
|
||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
||||
|
||||
async function init () {
|
||||
const response = await api.get('/project/' + currentProjectId.value + '/chat')
|
||||
const chatsAPI = response.data.data
|
||||
reset()
|
||||
const { data } = await api.get('/project/' + currentProjectId.value + '/chat')
|
||||
const chatsAPI = data.data
|
||||
chats.value.push(...chatsAPI)
|
||||
isInit.value = true
|
||||
}
|
||||
@@ -41,6 +43,12 @@ export const useChatsStore = defineStore('chats', () => {
|
||||
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
|
||||
await init()
|
||||
}, { deep: true })
|
||||
|
||||
return {
|
||||
chats,
|
||||
|
||||
@@ -14,6 +14,7 @@ export const useCompaniesStore = defineStore('companies', () => {
|
||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
||||
|
||||
async function init () {
|
||||
reset()
|
||||
const { data }= await api.get('/project/' + currentProjectId.value + '/company')
|
||||
const companiesAPI = data.data
|
||||
companies.value.push(...(companiesAPI.sort((a: Company, b: Company) => (a.id - b.id))))
|
||||
|
||||
@@ -9,16 +9,17 @@ import type { WebApp } from '@twa-dev/types'
|
||||
interface AppSettings {
|
||||
fontSize: number
|
||||
locale: string
|
||||
timeZoneBot: { tz: string, offset: number, offsetString: string }
|
||||
localeBot: string
|
||||
}
|
||||
|
||||
const defaultFontSize = 16
|
||||
const minFontSize = 10
|
||||
const maxFontSize = 22
|
||||
const fontSizeStep = 2
|
||||
|
||||
const defaultSettings: AppSettings = {
|
||||
fontSize: defaultFontSize,
|
||||
locale: 'en-US'
|
||||
locale: 'en-US',
|
||||
timeZoneBot: { tz: 'Europe/Moscow', offset: 3, offsetString: '+03:00' },
|
||||
localeBot: 'en-US'
|
||||
}
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
@@ -30,13 +31,17 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
const isInit = ref(false)
|
||||
|
||||
const currentFontSize = computed(() => settings.value?.fontSize ?? defaultFontSize)
|
||||
const canIncrease = computed(() => currentFontSize.value < maxFontSize)
|
||||
const canDecrease = computed(() => currentFontSize.value > minFontSize)
|
||||
|
||||
const supportLocale = [
|
||||
{ value: 'en-US', label: 'English' },
|
||||
{ value: 'ru-RU', label: 'Русский' }
|
||||
]
|
||||
|
||||
const supportFontSizes = [
|
||||
{ value: 12, label: 'settings__fontsize_small' },
|
||||
{ value: 16, label: 'settings__fontsize_medium' },
|
||||
{ value: 20, label: 'settings__fontsize_large' }
|
||||
]
|
||||
|
||||
const detectLocale = (): string => {
|
||||
const localeMap = {
|
||||
@@ -74,10 +79,12 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
const init = async () => {
|
||||
if (authStore.isAuth) {
|
||||
try {
|
||||
const response = await api.get('/customer/settings')
|
||||
const { data } = await api.get('/customer/settings')
|
||||
settings.value = {
|
||||
fontSize: response.data.data.settings.fontSize || defaultSettings.fontSize,
|
||||
locale: response.data.data.settings.locale || detectLocale()
|
||||
fontSize: data.data.settings.fontSize || defaultSettings.fontSize,
|
||||
locale: data.data.settings.locale || detectLocale(),
|
||||
timeZoneBot: data.data.settings.timeZone || defaultSettings.timeZoneBot,
|
||||
localeBot: data.data.settings.localeBot || detectLocale()
|
||||
}
|
||||
} catch {
|
||||
settings.value.locale = detectLocale()
|
||||
@@ -93,12 +100,6 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
isInit.value = true
|
||||
}
|
||||
|
||||
const updateLocale = async (newLocale: string) => {
|
||||
settings.value.locale = newLocale
|
||||
applyLocale()
|
||||
await saveSettings()
|
||||
}
|
||||
|
||||
const saveSettings = async () => {
|
||||
await api.put('/customer/settings', { settings: settings.value })
|
||||
}
|
||||
@@ -110,19 +111,6 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
await saveSettings()
|
||||
}
|
||||
|
||||
const clampFontSize = (size: number) =>
|
||||
Math.max(minFontSize, Math.min(size, maxFontSize))
|
||||
|
||||
const increaseFontSize = async () => {
|
||||
const newSize = clampFontSize(currentFontSize.value + fontSizeStep)
|
||||
await updateSettings({ fontSize: newSize })
|
||||
}
|
||||
|
||||
const decreaseFontSize = async () => {
|
||||
const newSize = clampFontSize(currentFontSize.value - fontSizeStep)
|
||||
await updateSettings({ fontSize: newSize })
|
||||
}
|
||||
|
||||
watch(() => authStore.isAuth, (newVal) => {
|
||||
if (newVal !== undefined) void init()
|
||||
}, { immediate: true })
|
||||
@@ -130,14 +118,10 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
return {
|
||||
settings,
|
||||
supportLocale,
|
||||
supportFontSizes,
|
||||
isInit,
|
||||
currentFontSize,
|
||||
canIncrease,
|
||||
canDecrease,
|
||||
init,
|
||||
increaseFontSize,
|
||||
decreaseFontSize,
|
||||
updateSettings,
|
||||
updateLocale
|
||||
updateSettings
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from 'boot/axios'
|
||||
import type { User, UserParams } from 'types/Users'
|
||||
import { useProjectsStore } from 'stores/projects'
|
||||
import { useAuthStore } from 'stores/auth'
|
||||
|
||||
export const useUsersStore = defineStore('users', () => {
|
||||
const projectsStore = useProjectsStore()
|
||||
@@ -12,6 +13,7 @@ export const useUsersStore = defineStore('users', () => {
|
||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
||||
|
||||
async function init () {
|
||||
reset()
|
||||
const { data } = await api.get('/project/' + currentProjectId.value + '/user')
|
||||
const usersAPI = data.data
|
||||
users.value.push(...usersAPI)
|
||||
@@ -38,6 +40,13 @@ export const useUsersStore = defineStore('users', () => {
|
||||
if (users.value[idx]) Object.assign(users.value[idx], userAPI)
|
||||
}
|
||||
|
||||
async function unblockUser (userId: number) {
|
||||
const { data } = await api.put('/project/' + currentProjectId.value + '/user/' + userId, { is_blocked: false })
|
||||
const userAPI = data.data
|
||||
const idx = users.value.findIndex(item => item.id === userAPI.id)
|
||||
if (users.value[idx]) Object.assign(users.value[idx], userAPI)
|
||||
}
|
||||
|
||||
function userById (id: number) {
|
||||
return users.value.find(el =>el.id === id)
|
||||
}
|
||||
@@ -52,6 +61,12 @@ 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
|
||||
await init()
|
||||
}, { deep: true })
|
||||
|
||||
return {
|
||||
users,
|
||||
isInit,
|
||||
@@ -59,6 +74,7 @@ export const useUsersStore = defineStore('users', () => {
|
||||
reset,
|
||||
update,
|
||||
blockUser,
|
||||
unblockUser,
|
||||
userById,
|
||||
userNameById,
|
||||
getUsers
|
||||
|
||||
Reference in New Issue
Block a user