12
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user