This commit is contained in:
Matej Glasnák committed 2025-12-11 01:11:41 +01:00
1 parent b12e815c57
commit 438d31e254
6 files changed
+321 -155

No files matched your search

@@ -32,7 +32,4 @@ export default {
&:hover &:hover
opacity: 1 opacity: 1
@media (max-width: 768px)
display: none !important
</style> </style>
@@ -0,0 +1,74 @@
<template>
<div class="actions">
<button class="add-btn" @click="$emit('addTimeblock')">+</button>
<button
:disabled="!props.deletable"
class="delete-btn"
@click="$emit('removeTimeblock')"
>
×
</button>
</div>
</template>
<script setup lang="ts">
interface Props {
deletable: boolean;
}
const props = withDefaults(defineProps<Props>(), {
deletable: false,
});
defineEmits<{
(e: 'addTimeblock'): void;
(e: 'removeTimeblock'): void;
}>();
</script>
<style lang="sass" scoped>
.actions
display: flex
align-items: center
gap: 8px
.add-btn
background: #7f39fb
color: white
border: none
border-radius: 4px
width: 44px
height: 44px
font-size: 28px
cursor: pointer
display: flex
align-items: center
justify-content: center
transition: all 0.2s
line-height: 1
&:hover
background: #6825e0
.delete-btn
background: #ff5252
color: white
border: none
border-radius: 4px
width: 44px
height: 44px
font-size: 28px
cursor: pointer
display: flex
align-items: center
justify-content: center
transition: all 0.2s
line-height: 1
&:disabled
opacity: 0.5
cursor: not-allowed
&:hover
background: #ff1744
</style>
@@ -1,115 +1,150 @@
<template> <template>
<div class="timeblock__header"> <div class="timeblock__header">
<div class="timeblock__date"> <div class="timeblock__header__top">
<button <p>
class="timeblock__date-button" Časový blok {{ index + 1 }}
:disabled="isCurrentDay || !isEditable" -
@click="$emit('previousDay')" {{ employee._fullName }}
> </p>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
</svg>
</button>
<Z-datepicker
:model-value="modelValue.date"
format="dd.MM.yyyy"
locale="sk"
select-text="Vybrať"
cancel-text="Zrušiť"
placeholder="Dátum"
:auto-apply="true"
:enable-time-picker="false"
:min-date="minDate"
:disabled="!isEditable"
class="timeblock__date-picker"
:class="{ 'timeblock__date-picker--disabled': !isEditable }"
@update:model-value="$emit('updateDate', $event)"
/>
<button
class="timeblock__date-button"
:disabled="!isEditable"
@click="$emit('nextDay')"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" />
</svg>
</button>
</div> </div>
<div class="timeblock__controls"> <div class="timeblock__header__content">
<v-text-field <div class="timeblock__date-wrapper">
:value="modelValue.starts_at_time" <div class="timeblock__date">
:class="{ <button
'error-input': class="timeblock__date-button"
collision.startCollides || :disabled="isCurrentDay || !isEditable"
!isTimeRangeValid || @click="$emit('previousDay')"
(hasValidationErrors && hasStartTimeError), >
}" <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
:disabled="!isEditable" <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
type="time" </svg>
variant="solo" </button>
bg-color="white"
label="Začiatok"
density="compact"
hide-details
@input="$emit('updateStartTime', $event)"
/>
<v-text-field
:value="modelValue.ends_at_time"
:class="{
'error-input':
collision.endCollides ||
!isTimeRangeValid ||
(hasValidationErrors && hasEndTimeError),
}"
:disabled="!isEditable"
type="time"
variant="solo"
bg-color="white"
label="Koniec"
density="compact"
hide-details
@input="$emit('updateEndTime', $event)"
/>
<div class="timeblock__time-overview"> <Z-datepicker
<p> :key="dateFormat"
Naplánovaný čas: :model-value="modelValue.date"
<span>{{ plannedTime }}h</span> :format="dateFormat"
</p> locale="sk"
<p> select-text="Vybrať"
Volný čas: cancel-text="Zrušiť"
<span>{{ freeTime }}h</span> placeholder="Dátum"
</p> :auto-apply="true"
:enable-time-picker="false"
:min-date="minDate"
:disabled="!isEditable"
class="timeblock__date-picker"
:class="{ 'timeblock__date-picker--disabled': !isEditable }"
@update:model-value="$emit('updateDate', $event)"
/>
<button
class="timeblock__date-button"
:disabled="!isEditable"
@click="$emit('nextDay')"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" />
</svg>
</button>
</div>
<div class="actions actions--mobile">
<a-employee-timeblock-header-actions
:deletable="deletable"
@add-timeblock="$emit('addTimeblock')"
@remove-timeblock="$emit('removeTimeblock')"
/>
</div>
</div> </div>
</div>
<div
v-if="!isTimeRangeValid || endTimeErrorMessage"
class="validation-error"
>
{{ endTimeErrorMessage || 'Koniec musí byť po začiatku' }}
</div>
<div class="actions"> <div class="timeblock__controls">
<button class="add-btn" @click="$emit('addTimeblock')">+</button> <div class="timeblock__controls__time">
<button <v-text-field
:disabled="!deletable" :value="modelValue.starts_at_time"
class="delete-btn" :class="{
@click="$emit('removeTimeblock')" 'error-input':
collision.startCollides ||
!isTimeRangeValid ||
(hasValidationErrors && hasStartTimeError),
}"
:disabled="!isEditable"
type="time"
variant="solo"
bg-color="white"
label="Začiatok"
density="compact"
hide-details
@input="$emit('updateStartTime', $event)"
/>
<v-text-field
:value="modelValue.ends_at_time"
:class="{
'error-input':
collision.endCollides ||
!isTimeRangeValid ||
(hasValidationErrors && hasEndTimeError),
}"
:disabled="!isEditable"
type="time"
variant="solo"
bg-color="white"
label="Koniec"
density="compact"
hide-details
@input="$emit('updateEndTime', $event)"
/>
</div>
<div class="timeblock__time-overview">
<p>
<span
class="timeblock__time-overview__label timeblock__time-overview__label--desktop"
>Naplánovaný čas:</span
>
<span
class="timeblock__time-overview__label timeblock__time-overview__label--mobile"
>Nap. čas:</span
>
<span>{{ plannedTime }}h</span>
</p>
<p>
<span
class="timeblock__time-overview__label timeblock__time-overview__label--desktop"
>Volný čas:</span
>
<span
class="timeblock__time-overview__label timeblock__time-overview__label--mobile"
>Vol. čas:</span
>
<span>{{ freeTime }}h</span>
</p>
</div>
</div>
<div
v-if="!isTimeRangeValid || endTimeErrorMessage"
class="validation-error"
> >
× {{ endTimeErrorMessage || 'Koniec musí byť po začiatku' }}
</button> </div>
<div class="actions actions--desktop">
<a-employee-timeblock-header-actions
:deletable="deletable"
@add-timeblock="$emit('addTimeblock')"
@remove-timeblock="$emit('removeTimeblock')"
/>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed, onMounted, onUnmounted, ref } from 'vue';
import moment from 'moment'; import moment from 'moment';
import ZDatepicker from '@/plugins/tm@_components/z-datepicker.vue'; import ZDatepicker from '@/plugins/tm@_components/z-datepicker.vue';
import type { TimeblockData } from './types'; import type { EmployeeFullData, TimeblockData } from './types';
import AEmployeeTimeblockHeaderActions from './a-employee-timeblock-header-actions.vue';
interface Props { interface Props {
modelValue: TimeblockData; modelValue: TimeblockData;
@@ -117,6 +152,8 @@ interface Props {
deletable: boolean; deletable: boolean;
isEditable: boolean; isEditable: boolean;
isTimeRangeValid: boolean; isTimeRangeValid: boolean;
employee: EmployeeFullData;
index: number;
collision: { collision: {
hasCollision: boolean; hasCollision: boolean;
startCollides: boolean; startCollides: boolean;
@@ -151,10 +188,34 @@ const isCurrentDay = computed(() => {
if (!props.modelValue.date) return false; if (!props.modelValue.date) return false;
return moment(props.modelValue.date).isSame(moment(), 'day'); return moment(props.modelValue.date).isSame(moment(), 'day');
}); });
const isMobile = ref(false);
const handleResize = () => {
isMobile.value = window.innerWidth <= 768;
};
onMounted(() => {
handleResize();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
const dateFormat = computed(() => {
return isMobile.value ? 'dd.MM' : 'dd.MM.yyyy';
});
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>
.timeblock__header .timeblock__header__top
padding: 8px 16px 0 16px
font-weight: 400
font-size: 14px
.timeblock__header__content
display: flex display: flex
align-items: center align-items: center
gap: 15px gap: 15px
@@ -166,12 +227,30 @@ const isCurrentDay = computed(() => {
flex-direction: column flex-direction: column
align-items: stretch align-items: stretch
.timeblock__date-wrapper
display: flex
align-items: center
justify-content: space-between
@media (max-width: 768px)
width: 100%
.timeblock__date .timeblock__date
display: flex display: flex
align-items: center align-items: center
flex: 1 flex: 1
max-width: 220px max-width: 220px
@media (max-width: 768px)
max-width: 170px
::v-deep
.dp__input
padding: 0 0 0 29px !important
.dp__input_icon
padding: 6px 9px !important
left: 0 !important
.timeblock__date-button .timeblock__date-button
background: #f5f5f5 background: #f5f5f5
border: 1px solid #e0e0e0 border: 1px solid #e0e0e0
@@ -239,26 +318,48 @@ const isCurrentDay = computed(() => {
gap: 10px gap: 10px
flex: 1 flex: 1
&__time
display: flex
gap: 10px
@media (max-width: 768px) @media (max-width: 768px)
width: 100% width: 100%
flex: 1
::v-deep .v-input // ::v-deep .v-input
width: 140px !important // width: 140px !important
flex-grow: 0 // flex-grow: 0
// @media (max-width: 768px)
// ::v-deep
// .v-input
// width: 58px !important
// .v-label
// margin-inline-start: 5px !important
// .v-field__input
// padding-inline-start: 5px !important
// padding-inline-end: 5px !important
.timeblock__time-overview .timeblock__time-overview
height: 44px height: 44px
display: flex display: flex
flex-direction: column flex-direction: column
justify-content: space-between justify-content: space-between
flex-shrink: 0
min-width: max-content
p p
font-size: 14px font-size: 14px
font-weight: 400 font-weight: 400
white-space: nowrap
display: flex
gap: 4px
span span:not(.timeblock__time-overview__label)
font-weight: 500 font-weight: 500
color: #7f39fb color: #7f39fb
white-space: nowrap
flex-shrink: 0
.error-input .error-input
::v-deep input ::v-deep input
@@ -270,48 +371,27 @@ const isCurrentDay = computed(() => {
margin-top: 5px margin-top: 5px
padding-left: 10px padding-left: 10px
.actions .actions--mobile
display: flex display: none
align-items: center @media (max-width: 768px)
gap: 8px
.add-btn
background: #7f39fb
color: white
border: none
border-radius: 4px
width: 44px
height: 44px
font-size: 28px
cursor: pointer
display: flex display: flex
align-items: center
justify-content: center
transition: all 0.2s
line-height: 1
&:hover .actions--desktop
background: #6825e0 display: block
.delete-btn @media (max-width: 768px)
background: #ff5252 display: none
color: white
border: none
border-radius: 4px
width: 44px
height: 44px
font-size: 28px
cursor: pointer
display: flex
align-items: center
justify-content: center
transition: all 0.2s
line-height: 1
&:disabled .timeblock__time-overview__label--desktop
opacity: 0.5 display: block
cursor: not-allowed
&:hover @media (max-width: 768px)
background: #ff1744 display: none
.timeblock__time-overview__label--mobile
display: none
@media (max-width: 768px)
display: block
white-space: nowrap
</style> </style>
@@ -197,12 +197,11 @@ defineExpose({
<style lang="sass" scoped> <style lang="sass" scoped>
.timeline .timeline
height: 98px // height: 98px
&__content &__content
position: relative position: relative
width: 100% width: 100%
margin-bottom: 10px
&-loading &-loading
height: 100% height: 100%
@@ -224,10 +223,9 @@ defineExpose({
.timeline-bar .timeline-bar
position: relative position: relative
height: 60px height: 30px
width: calc(24 * 60px) width: calc(24 * 60px)
background: #fafafa background: #fafafa
border: 1px solid #e0e0e0
border-radius: 4px border-radius: 4px
@media (max-width: 768px) @media (max-width: 768px)
@@ -250,7 +248,7 @@ defineExpose({
.scheduled-block .scheduled-block
position: absolute position: absolute
top: 5px top: 5px
height: 50px height: 22px
background: #7f39fb background: #7f39fb
border: 1px solid #6a1bf7 border: 1px solid #6a1bf7
opacity: 0.6 opacity: 0.6
@@ -260,7 +258,7 @@ defineExpose({
.unsaved-timeblock .unsaved-timeblock
position: absolute position: absolute
top: 5px top: 5px
height: 50px height: 22px
background: #FFA726 background: #FFA726
opacity: 0.5 opacity: 0.5
border: 2px dashed #FF9800 border: 2px dashed #FF9800
@@ -270,7 +268,7 @@ defineExpose({
.current-timeblock .current-timeblock
position: absolute position: absolute
top: 5px top: 5px
height: 50px height: 22px
background: #2196F3 background: #2196F3
opacity: 0.7 opacity: 0.7
border: 2px solid #1976D2 border: 2px solid #1976D2
@@ -283,7 +281,7 @@ defineExpose({
.collision-segment .collision-segment
position: absolute position: absolute
top: 5px top: 5px
height: 50px height: 22px
background: #ff5252 background: #ff5252
opacity: 0.6 opacity: 0.6
border-radius: 2px border-radius: 2px
@@ -13,6 +13,8 @@
:end-time-error-message="endTimeErrorMessage" :end-time-error-message="endTimeErrorMessage"
:planned-time="_scheduledTime" :planned-time="_scheduledTime"
:free-time="_freeTime" :free-time="_freeTime"
:employee="employee"
:index="index"
@previous-day="previousDay" @previous-day="previousDay"
@next-day="nextDay" @next-day="nextDay"
@update-date="updateDate" @update-date="updateDate"
@@ -309,12 +311,25 @@ watch(timelineRef, (newRef) => {
} }
}); });
// Watch for loading state - when data finishes loading, center the timeline
watch(isLoading, (newIsLoading, oldIsLoading) => {
// When loading changes from true to false, timeline is re-rendered
if (oldIsLoading && !newIsLoading) {
// Wait for DOM to update, then center
nextTick(() => {
setTimeout(() => {
centerTimeline();
}, 100); // Small delay to ensure DOM is fully rendered
});
}
});
// Date navigation // Date navigation
const previousDay = () => { const previousDay = () => {
if (!props.modelValue.date) return; if (!props.modelValue.date) return;
const currentDate = moment(props.modelValue.date); const currentDate = moment(props.modelValue.date);
const newDate = currentDate.subtract(1, 'day').toDate(); const newDate = currentDate.clone().subtract(1, 'day').toDate();
// Don't allow going to past dates // Don't allow going to past dates
if (moment(newDate).isBefore(moment(), 'day')) return; if (moment(newDate).isBefore(moment(), 'day')) return;
@@ -326,7 +341,7 @@ const nextDay = () => {
if (!props.modelValue.date) return; if (!props.modelValue.date) return;
const currentDate = moment(props.modelValue.date); const currentDate = moment(props.modelValue.date);
const newDate = currentDate.add(1, 'day').toDate(); const newDate = currentDate.clone().add(1, 'day').toDate();
updateDate(newDate); updateDate(newDate);
}; };
@@ -370,4 +385,5 @@ defineExpose({
border: 1px solid #e5e5e5 border: 1px solid #e5e5e5
border-radius: 8px border-radius: 8px
margin-bottom: 8px margin-bottom: 8px
overflow: hidden
</style> </style>
@@ -164,8 +164,9 @@ defineExpose({
&--has-background &--has-background
border: 1px solid #e5e5e5 border: 1px solid #e5e5e5
border-bottom: 3px solid #7f39fb
padding: 16px padding: 16px
border-radius: 8px border-radius: 8px 8px 0 0
&__timeblocks &__timeblocks
display: flex display: flex