This commit is contained in:
2025-12-03 17:40:50 +03:00
parent d3d8b7522a
commit 4cad91440c
34 changed files with 1120 additions and 434 deletions

View File

@@ -4,16 +4,14 @@
{{ $t(title) }}
</template>
<template #footer>
<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>
<q-btn
rounded color="primary"
class="w100 fix-disabled-btn"
:disable="!isFormValid"
@click = "onSubmit"
>
{{ $t(btnText) }}
</q-btn>
</template>
<pn-scroll-list>
@@ -62,7 +60,7 @@
import { computed, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import type { CompanyParams } from 'types/Company'
import { removeNullKeys } from 'utils/helpers'
import { removeNullKeys, getFieldValue } from 'utils/helpers'
const { t }= useI18n()
@@ -122,13 +120,14 @@
})
function onSubmit() {
// companyMod.description! - ! on end for TS, i promise: params !== null | undefined
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),
description: getFieldValue(companyMod.description!, props.company?.description),
site: getFieldValue(companyMod.site!, props.company?.site),
address: getFieldValue(companyMod.address!, props.company?.address),
phone: getFieldValue(companyMod.phone!, props.company?.phone),
email: getFieldValue(companyMod.email!, props.company?.email),
logo: companyMod.logo
}
emit('update', removeNullKeys(outData))

View File

@@ -0,0 +1,89 @@
<template>
<div
class="flex row q-mx-sm justify-between items-center no-wrap q-py-sm"
>
<q-btn
v-if="showCalendarBtn"
flat round
:color="calendarActive ? 'primary' : 'grey'"
@click="emit('toggle-calendar')"
>
<div>
<q-icon name="mdi-calendar-month-outline" size="sm"/>
<q-badge
color="red"
rounded
floating
transparent
style="position: relative; top: -6px; margin-left: -12px"
:style="{ opacity: calendarBadge ? 0.8 : 0 }"
/>
</div>
</q-btn>
<q-input
v-model="search"
clearable
clear-icon="close"
borderless
filled
:placeholder="$t(placeholder)"
dense
class="col-grow q-pt-xs q-mx-sm"
>
<template #prepend>
<q-icon name="mdi-magnify" color="grey"/>
</template>
</q-input>
<q-btn
v-if="showFilterBtn"
@click="emit('open-filters')"
flat round
:color="filterActive ? 'primary' : 'grey'"
>
<div>
<q-icon name="mdi-filter-outline" size="sm"/>
<q-badge
color="red"
rounded
floating
transparent
style="position: relative; top: -6px; margin-left: -12px"
:style="{ opacity: filterBadge ? 0.8 : 0 }"
/>
</div>
</q-btn>
</div>
</template>
<script setup lang="ts">
const search = defineModel<string>({
required: true,
default: ''
})
defineProps({
placeholder: {
type: String,
default: 'Search...'
},
showCalendarBtn: {
type: Boolean,
default: true
},
showFilterBtn: {
type: Boolean,
default: true
},
calendarActive: Boolean,
filterActive: Boolean,
calendarBadge: Boolean,
filterBadge: Boolean
})
const emit = defineEmits([
'toggle-calendar',
'open-filters'
])
</script>

View File

@@ -0,0 +1,97 @@
<template>
<q-dialog
v-model="modelValue"
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-radius) !important;
border-top-right-radius: var(--top-radius) !important;
"
>
<div
ref="cardHeaderRef"
class="flex items-center no-wrap justify-between w100 q-my-none q-pa-md"
>
<div class="text-h6 q-mx-xs ellipsis">{{ $t(title) }}</div>
<div class="flex items-center justify-between no-wrap">
<slot name="btnSlot">
<q-btn
icon="mdi-close"
@click="modelValue=false"
flat round
/>
</slot>
</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'
const modelValue = defineModel<boolean>({
required: true
})
defineProps<{
title: string
}>()
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
const needScroll = headerHeight.value + footerHeight.value + bodyInnerHeight.value > window.innerHeight - 48
bodyHeight.value = needScroll
? window.innerHeight - 48 - headerHeight.value - footerHeight.value
: bodyInnerHeight.value + 16
}
watch(() => slots.body?.(), updateDimensions, { flush: 'post' })
</script>
<style scoped>
.fix-card-width {
width: var(--body-width) !important;
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<q-item>
<q-item-section avatar>
<q-avatar
rounded
text-color="white"
:icon="chatType.icon"
:color="chatType.color"
size="md"
/>
</q-item-section>
<q-item-section>
<q-item-label>
<div class="row no-wrap items-center justify-between">
{{ $t(chatType.label) }}
<span class="text-caption text-grey" v-if="time">
{{ date.formatDate(time * 1000, 'DD MMM YYYY') }}
</span>
</div>
</q-item-label>
<q-item-label class="text-grey text-caption">
{{ $t(chatType.description) }}
</q-item-label>
</q-item-section>
</q-item>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { getChatType } from 'utils/chats'
import { date } from 'quasar'
import type { ChatType } from 'types/Chat'
const props = withDefaults(defineProps<{
type: ChatType
time?: number
}>(), {
type: 'full'
})
const chatType = computed(() => getChatType(props.type))
</script>
<style scoped>
</style>

View File

@@ -6,12 +6,18 @@
<slot name="title"/>
</div>
<slot/>
<div class="bg-white w100">
<div v-if="hasFooter" class="flex w100 q-pa-md bg-white">
<slot name="footer"/>
</div>
<slot v-if="hasTabs" name="tabs"/>
</template>
<script setup lang="ts">
import { useSlots, computed } from 'vue'
const slots = useSlots()
const hasFooter = computed(() => !!slots.footer)
const hasTabs = computed(() => !!slots.tabs)
</script>
<style>

View File

@@ -5,7 +5,7 @@
>
<div
id="card-body-header"
v-if="slots['card-body-header']"
v-if="!hideHeader && hasHeader"
style="flex-shrink: 0; min-height: var(--top-radius);"
>
<q-resize-observer @resize="onHeaderResize"/>
@@ -13,66 +13,84 @@
</div>
<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/>
</pn-shadow-scroll>
<div style="min-height: var(--top-radius);">
<slot name="card-body-btn"/>
<q-resize-observer @resize="onBodyBtnResize"/>
</div>
<div style="height:100%">
<q-resize-observer @resize="onBodyResize"/>
<pn-shadow-scroll :hideShadows="isResizing" :height="scrollAreaHeight">
<slot/>
</pn-shadow-scroll>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import { useSlots } from 'vue'
import { ref, watch, nextTick, useSlots, computed } from 'vue'
const slots = useSlots()
const slots = useSlots()
// work only slot no use in component
const hasHeader = computed(() => !!slots['card-body-header'])
const heightCard = ref(100)
const scrollAreaHeight = ref(100)
const headerHeight = ref(0)
interface sizeParams {
height: number,
width: number
}
async function onHeaderResize(size: sizeParams) {
headerHeight.value = size.height
await updateScrollAreaHeight()
}
async function onBodyResize(size: sizeParams) {
heightCard.value = size.height - parseInt(String(getComputedStyle(document.documentElement).getPropertyValue('--top-radius'))) // subtract padding var(top-radius)
await updateScrollAreaHeight()
}
async function updateScrollAreaHeight() {
await nextTick(() => {
scrollAreaHeight.value = Math.max(0, heightCard.value)
// need if slot draw by expression with v-if
withDefaults(defineProps<{
hideHeader?: boolean
}>(), {
hideHeader: false
})
}
watch(heightCard, updateScrollAreaHeight)
watch(headerHeight, updateScrollAreaHeight)
const heightCard = ref(100)
const scrollAreaHeight = ref(100)
const headerHeight = ref(0)
const headerBtnHeight = ref(0)
const isResizing = ref(false)
let resizeTimer: ReturnType<typeof setTimeout> | null = null
watch(heightCard, () => {
isResizing.value = true
if (resizeTimer) {
clearTimeout(resizeTimer)
resizeTimer = null
interface sizeParams {
height: number,
width: number
}
resizeTimer = setTimeout(() => {
isResizing.value = false
resizeTimer = null
}, 150)
})
async function onHeaderResize(size: sizeParams) {
headerHeight.value = size.height
await updateScrollAreaHeight()
}
function onBodyBtnResize (size: sizeParams) {
headerBtnHeight.value = size.height
}
async function onBodyResize (size: sizeParams) {
heightCard.value = size.height - headerBtnHeight.value
await updateScrollAreaHeight()
}
async function updateScrollAreaHeight() {
await nextTick(() => {
scrollAreaHeight.value = Math.max(0, heightCard.value)
})
}
watch(heightCard, updateScrollAreaHeight)
watch(headerHeight, updateScrollAreaHeight)
const isResizing = ref(false)
let resizeTimer: ReturnType<typeof setTimeout> | null = null
watch(heightCard, () => {
isResizing.value = true
if (resizeTimer) {
clearTimeout(resizeTimer)
resizeTimer = null
}
resizeTimer = setTimeout(() => {
isResizing.value = false
resizeTimer = null
}, 150)
})
</script>

View File

@@ -0,0 +1,73 @@
<template>
<q-dialog v-model="modelValue">
<q-card
class="q-pa-none q-ma-none w100 no-scroll"
>
<q-card-section class="row items-center q-pb-none">
<div class="flex no-wrap items-center w100">
<q-resize-observer @resize="onHeaderResize"/>
<span class="text-bold">
{{ $t(title)}}
</span>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</div>
</q-card-section>
<div>
<pn-shadow-scroll :height="scrollAreaHeight" :hideShadows="false">
<div>
<q-resize-observer @resize="onBodyResize"/>
<slot/>
</div>
</pn-shadow-scroll>
</div>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import pnShadowScroll from './pnShadowScroll.vue'
defineProps<{
title: string
}>()
const modelValue = defineModel<boolean>({
required: true
})
const headerHeight = ref(0)
const bodyHeight = ref(0)
const scrollAreaHeight = ref(0)
interface sizeParams {
height: number,
width: number
}
function onHeaderResize (size: sizeParams) {
headerHeight.value = size.height
}
function onBodyResize (size: sizeParams) {
bodyHeight.value = size.height
}
async function updateScrollAreaHeight () {
await nextTick(() => {
scrollAreaHeight.value = Math.min(
bodyHeight.value + 16,
window.innerHeight - headerHeight.value - 64
)
})
}
watch(bodyHeight, updateScrollAreaHeight)
watch(headerHeight, updateScrollAreaHeight)
</script>
<style scoped>
</style>

View File

@@ -4,16 +4,14 @@
{{ $t(title) }}
</template>
<template #footer>
<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>
<q-btn
rounded color="primary"
class="w100 fix-disabled-btn"
:disable="!isFormValid"
@click = "onSubmit"
>
{{ $t(btnText) }}
</q-btn>
</template>
<pn-scroll-list>
@@ -59,7 +57,7 @@
import { computed, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import type { ProjectParams } from 'types/Project'
import { removeNullKeys } from 'utils/helpers'
import { removeNullKeys, getFieldValue } from 'utils/helpers'
const { t } = useI18n()
@@ -96,7 +94,7 @@
function onSubmit() {
const outData = {
name: projectMod.name,
description: projectMod.description ?? (typeof props.project?.description === 'string' ? '' : null),
description: getFieldValue(projectMod.description, props.project?.description),
logo: projectMod.logo
}
emit('update', removeNullKeys(outData))

View File

@@ -4,20 +4,17 @@
{{ $t(title) }}
</template>
<template #footer>
<div class="w100 q-pa-md">
<q-btn
rounded color="primary"
class="w100 fix-disabled-btn"
@click = "onSubmit"
>
{{ $t(btnText) }}
</q-btn>
</div>
<q-btn
rounded color="primary"
class="w100 fix-disabled-btn"
@click = "onSubmit"
>
{{ $t(btnText) }}
</q-btn>
</template>
<pn-scroll-list>
<div class="flex column items-center q-pa-md q-pb-sm">
<div class="relative-position">
<div class="column items-center q-pa-md q-pb-sm">
<div class="relative-position flex justify-center w100 text-center">
<pn-auto-avatar
:img="userMod.photo"
:name="tname"
@@ -26,11 +23,16 @@
: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"
v-if="userStatus"
class="flex justify-center absolute-center w100 items-center"
>
{{ $t(userStatus.text) }}
<q-chip
square
:label="$t(userStatus.text)"
:color="userStatus.color"
text-color="white"
class="text-uppercase"
/>
</div>
</div>
<div class="flex row items-start justify-center no-wrap q-pb-lg">
@@ -134,7 +136,8 @@
import { computed, reactive } from 'vue'
import { useCompaniesStore } from 'stores/companies'
import { useI18n } from 'vue-i18n'
import { removeNullKeys } from 'utils/helpers'
import { removeNullKeys, getFieldValue } from 'utils/helpers'
import { getUserStatus } from 'utils/users'
import type { User } from 'types/User'
const { t } = useI18n()
@@ -178,22 +181,17 @@
.filter(Boolean)
.join(' ')
)
const userStatus = computed(() => {
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 userStatus = computed(() => props.user ? getUserStatus(props.user) : null)
function onSubmit() {
const outData = {
fullname: userMod.fullname ?? (typeof props.user?.fullname === 'string' ? '' : null),
fullname: getFieldValue(userMod.fullname, props.user?.fullname),
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),
department: getFieldValue(userMod.department, props.user?.department),
role: getFieldValue(userMod.role, props.user?.role),
phone: getFieldValue(userMod.phone, props.user?.phone),
email: getFieldValue(userMod.email, props.user?.email),
photo: userMod.photo
}
emit('update', removeNullKeys(outData))
@@ -205,19 +203,4 @@
.fix-bottom-padding.q-field--with-bottom {
padding-bottom: 0 !important
}
.status-blocked {
border: 2px solid var(--q-negative);
color: var(--q-negative);
}
.status-leave {
border: 2px solid var(--q-primary);
color: var(--q-primary);
}
.status-pending{
border: 2px solid var(--q-secondary);
color: var(--q-secondary);
}
</style>