mirror of
https://bitbucket.org/studiotem/jandl-taskmanager-app.git
synced 2026-10-21 14:29:34 +00:00
stage
This commit is contained in:
1 parent
b12e815c57
commit
438d31e254
6 files changed
+321
-155
No files matched your search
@@ -32,7 +32,4 @@ export default {
|
||||
|
||||
&:hover
|
||||
opacity: 1
|
||||
|
||||
@media (max-width: 768px)
|
||||
display: none !important
|
||||
</style>
|
||||
+74
@@ -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>
|
||||
+221
-141
@@ -1,115 +1,150 @@
|
||||
<template>
|
||||
<div class="timeblock__header">
|
||||
<div class="timeblock__date">
|
||||
<button
|
||||
class="timeblock__date-button"
|
||||
:disabled="isCurrentDay || !isEditable"
|
||||
@click="$emit('previousDay')"
|
||||
>
|
||||
<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 class="timeblock__header__top">
|
||||
<p>
|
||||
Časový blok {{ index + 1 }}
|
||||
-
|
||||
{{ employee._fullName }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="timeblock__controls">
|
||||
<v-text-field
|
||||
:value="modelValue.starts_at_time"
|
||||
:class="{
|
||||
'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 class="timeblock__header__content">
|
||||
<div class="timeblock__date-wrapper">
|
||||
<div class="timeblock__date">
|
||||
<button
|
||||
class="timeblock__date-button"
|
||||
:disabled="isCurrentDay || !isEditable"
|
||||
@click="$emit('previousDay')"
|
||||
>
|
||||
<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>
|
||||
|
||||
<div class="timeblock__time-overview">
|
||||
<p>
|
||||
Naplánovaný čas:
|
||||
<span>{{ plannedTime }}h</span>
|
||||
</p>
|
||||
<p>
|
||||
Volný čas:
|
||||
<span>{{ freeTime }}h</span>
|
||||
</p>
|
||||
<Z-datepicker
|
||||
:key="dateFormat"
|
||||
:model-value="modelValue.date"
|
||||
:format="dateFormat"
|
||||
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 class="actions actions--mobile">
|
||||
<a-employee-timeblock-header-actions
|
||||
:deletable="deletable"
|
||||
@add-timeblock="$emit('addTimeblock')"
|
||||
@remove-timeblock="$emit('removeTimeblock')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isTimeRangeValid || endTimeErrorMessage"
|
||||
class="validation-error"
|
||||
>
|
||||
{{ endTimeErrorMessage || 'Koniec musí byť po začiatku' }}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="add-btn" @click="$emit('addTimeblock')">+</button>
|
||||
<button
|
||||
:disabled="!deletable"
|
||||
class="delete-btn"
|
||||
@click="$emit('removeTimeblock')"
|
||||
<div class="timeblock__controls">
|
||||
<div class="timeblock__controls__time">
|
||||
<v-text-field
|
||||
:value="modelValue.starts_at_time"
|
||||
:class="{
|
||||
'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"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{{ endTimeErrorMessage || 'Koniec musí byť po začiatku' }}
|
||||
</div>
|
||||
|
||||
<div class="actions actions--desktop">
|
||||
<a-employee-timeblock-header-actions
|
||||
:deletable="deletable"
|
||||
@add-timeblock="$emit('addTimeblock')"
|
||||
@remove-timeblock="$emit('removeTimeblock')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import moment from 'moment';
|
||||
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 {
|
||||
modelValue: TimeblockData;
|
||||
@@ -117,6 +152,8 @@ interface Props {
|
||||
deletable: boolean;
|
||||
isEditable: boolean;
|
||||
isTimeRangeValid: boolean;
|
||||
employee: EmployeeFullData;
|
||||
index: number;
|
||||
collision: {
|
||||
hasCollision: boolean;
|
||||
startCollides: boolean;
|
||||
@@ -151,10 +188,34 @@ const isCurrentDay = computed(() => {
|
||||
if (!props.modelValue.date) return false;
|
||||
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>
|
||||
|
||||
<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
|
||||
align-items: center
|
||||
gap: 15px
|
||||
@@ -166,12 +227,30 @@ const isCurrentDay = computed(() => {
|
||||
flex-direction: column
|
||||
align-items: stretch
|
||||
|
||||
.timeblock__date-wrapper
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: space-between
|
||||
|
||||
@media (max-width: 768px)
|
||||
width: 100%
|
||||
|
||||
.timeblock__date
|
||||
display: flex
|
||||
align-items: center
|
||||
flex: 1
|
||||
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
|
||||
background: #f5f5f5
|
||||
border: 1px solid #e0e0e0
|
||||
@@ -239,26 +318,48 @@ const isCurrentDay = computed(() => {
|
||||
gap: 10px
|
||||
flex: 1
|
||||
|
||||
&__time
|
||||
display: flex
|
||||
gap: 10px
|
||||
|
||||
@media (max-width: 768px)
|
||||
width: 100%
|
||||
flex: 1
|
||||
|
||||
::v-deep .v-input
|
||||
width: 140px !important
|
||||
flex-grow: 0
|
||||
// ::v-deep .v-input
|
||||
// width: 140px !important
|
||||
// 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
|
||||
height: 44px
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: space-between
|
||||
flex-shrink: 0
|
||||
min-width: max-content
|
||||
|
||||
p
|
||||
font-size: 14px
|
||||
font-weight: 400
|
||||
white-space: nowrap
|
||||
display: flex
|
||||
gap: 4px
|
||||
|
||||
span
|
||||
span:not(.timeblock__time-overview__label)
|
||||
font-weight: 500
|
||||
color: #7f39fb
|
||||
white-space: nowrap
|
||||
flex-shrink: 0
|
||||
|
||||
.error-input
|
||||
::v-deep input
|
||||
@@ -270,48 +371,27 @@ const isCurrentDay = computed(() => {
|
||||
margin-top: 5px
|
||||
padding-left: 10px
|
||||
|
||||
.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
|
||||
.actions--mobile
|
||||
display: none
|
||||
@media (max-width: 768px)
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
transition: all 0.2s
|
||||
line-height: 1
|
||||
|
||||
&:hover
|
||||
background: #6825e0
|
||||
.actions--desktop
|
||||
display: block
|
||||
|
||||
.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
|
||||
@media (max-width: 768px)
|
||||
display: none
|
||||
|
||||
&:disabled
|
||||
opacity: 0.5
|
||||
cursor: not-allowed
|
||||
.timeblock__time-overview__label--desktop
|
||||
display: block
|
||||
|
||||
&:hover
|
||||
background: #ff1744
|
||||
@media (max-width: 768px)
|
||||
display: none
|
||||
|
||||
.timeblock__time-overview__label--mobile
|
||||
display: none
|
||||
|
||||
@media (max-width: 768px)
|
||||
display: block
|
||||
white-space: nowrap
|
||||
</style>
|
||||
+6
-8
@@ -197,12 +197,11 @@ defineExpose({
|
||||
|
||||
<style lang="sass" scoped>
|
||||
.timeline
|
||||
height: 98px
|
||||
// height: 98px
|
||||
|
||||
&__content
|
||||
position: relative
|
||||
width: 100%
|
||||
margin-bottom: 10px
|
||||
|
||||
&-loading
|
||||
height: 100%
|
||||
@@ -224,10 +223,9 @@ defineExpose({
|
||||
|
||||
.timeline-bar
|
||||
position: relative
|
||||
height: 60px
|
||||
height: 30px
|
||||
width: calc(24 * 60px)
|
||||
background: #fafafa
|
||||
border: 1px solid #e0e0e0
|
||||
border-radius: 4px
|
||||
|
||||
@media (max-width: 768px)
|
||||
@@ -250,7 +248,7 @@ defineExpose({
|
||||
.scheduled-block
|
||||
position: absolute
|
||||
top: 5px
|
||||
height: 50px
|
||||
height: 22px
|
||||
background: #7f39fb
|
||||
border: 1px solid #6a1bf7
|
||||
opacity: 0.6
|
||||
@@ -260,7 +258,7 @@ defineExpose({
|
||||
.unsaved-timeblock
|
||||
position: absolute
|
||||
top: 5px
|
||||
height: 50px
|
||||
height: 22px
|
||||
background: #FFA726
|
||||
opacity: 0.5
|
||||
border: 2px dashed #FF9800
|
||||
@@ -270,7 +268,7 @@ defineExpose({
|
||||
.current-timeblock
|
||||
position: absolute
|
||||
top: 5px
|
||||
height: 50px
|
||||
height: 22px
|
||||
background: #2196F3
|
||||
opacity: 0.7
|
||||
border: 2px solid #1976D2
|
||||
@@ -283,7 +281,7 @@ defineExpose({
|
||||
.collision-segment
|
||||
position: absolute
|
||||
top: 5px
|
||||
height: 50px
|
||||
height: 22px
|
||||
background: #ff5252
|
||||
opacity: 0.6
|
||||
border-radius: 2px
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
:end-time-error-message="endTimeErrorMessage"
|
||||
:planned-time="_scheduledTime"
|
||||
:free-time="_freeTime"
|
||||
:employee="employee"
|
||||
:index="index"
|
||||
@previous-day="previousDay"
|
||||
@next-day="nextDay"
|
||||
@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
|
||||
const previousDay = () => {
|
||||
if (!props.modelValue.date) return;
|
||||
|
||||
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
|
||||
if (moment(newDate).isBefore(moment(), 'day')) return;
|
||||
@@ -326,7 +341,7 @@ const nextDay = () => {
|
||||
if (!props.modelValue.date) return;
|
||||
|
||||
const currentDate = moment(props.modelValue.date);
|
||||
const newDate = currentDate.add(1, 'day').toDate();
|
||||
const newDate = currentDate.clone().add(1, 'day').toDate();
|
||||
|
||||
updateDate(newDate);
|
||||
};
|
||||
@@ -370,4 +385,5 @@ defineExpose({
|
||||
border: 1px solid #e5e5e5
|
||||
border-radius: 8px
|
||||
margin-bottom: 8px
|
||||
overflow: hidden
|
||||
</style>
|
||||
@@ -164,8 +164,9 @@ defineExpose({
|
||||
|
||||
&--has-background
|
||||
border: 1px solid #e5e5e5
|
||||
border-bottom: 3px solid #7f39fb
|
||||
padding: 16px
|
||||
border-radius: 8px
|
||||
border-radius: 8px 8px 0 0
|
||||
|
||||
&__timeblocks
|
||||
display: flex
|
||||
|
||||
Reference in new issue
Block a user