349 lines
15 KiB
Vue
349 lines
15 KiB
Vue
<script
|
|
setup
|
|
lang="ts"
|
|
>
|
|
import TargoInput from 'src/modules/shared/components/targo-input.vue';
|
|
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import { colors, getCssVar, QSelect } from 'quasar';
|
|
import { useUiStore } from 'src/stores/ui-store';
|
|
import { getCurrentDailyMinutesWorked, getShiftOptions, getTimeStringFromMinutes, SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
|
import type { Shift, ShiftOption, ShiftType } from 'src/modules/timesheets/models/shift.models';
|
|
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
|
|
|
|
// ========== state ========================================
|
|
|
|
const SHIFT_TYPES_WITH_PREDEFINED_TIMES: ShiftType[] = ['HOLIDAY', 'SICK', 'VACATION'];
|
|
// const COMMENT_LENGTH_MAX = 280;
|
|
|
|
const shift = defineModel<Shift>('shift', { required: true });
|
|
|
|
const {
|
|
currentShifts,
|
|
hasShiftAfter = false,
|
|
isTimesheetApproved = false,
|
|
errorMessage = undefined,
|
|
expectedDailyHours = 8,
|
|
} = defineProps<{
|
|
currentShifts: Shift[];
|
|
hasShiftAfter?: boolean;
|
|
isTimesheetApproved?: boolean;
|
|
errorMessage?: string | undefined;
|
|
expectedDailyHours?: number;
|
|
isHoliday?: boolean;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
'requestDelete': [void];
|
|
'onTimeFieldBlur': [void];
|
|
}>();
|
|
|
|
const uiStore = useUiStore();
|
|
const shiftTypeSelected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
|
const selectRef = ref<QSelect | null>(null);
|
|
// const isShowingCommentPopup = ref(false);
|
|
const errorMessageRow = ref('');
|
|
const isShowingPredefinedTime = ref(shift.value.type === 'HOLIDAY');
|
|
const predefinedHoursString = ref('');
|
|
const predefinedHoursBgColor = ref(`bg-${shiftTypeSelected.value?.icon_color ?? ''}`);
|
|
|
|
// ========== computed ========================================
|
|
|
|
const commentLength = computed(() => shift.value.comment?.length ?? 0);
|
|
const isApproved = computed(() => isTimesheetApproved || shift.value.is_approved);
|
|
const hasPTO = computed(() => currentShifts.some(shift => SHIFT_TYPES_WITH_PREDEFINED_TIMES.includes(shift.type)));
|
|
|
|
// ========== methods =========================================
|
|
|
|
const onBlurShiftTypeSelect = () => {
|
|
if (shiftTypeSelected.value === undefined) {
|
|
shift.value.type = 'REGULAR';
|
|
shift.value.id = 0;
|
|
emit('requestDelete');
|
|
}
|
|
};
|
|
|
|
const onTimeFieldBlur = (time_string: string) => {
|
|
if (time_string.length < 1 || !time_string) {
|
|
shift.value.has_error = true;
|
|
errorMessageRow.value = 'timesheet.errors.SHIFT_TIME_REQUIRED';
|
|
} else {
|
|
shift.value.has_error = false;
|
|
errorMessageRow.value = '';
|
|
emit('onTimeFieldBlur');
|
|
}
|
|
}
|
|
|
|
const onShiftTypeChange = (option: ShiftOption) => {
|
|
shift.value.type = option.value;
|
|
|
|
if (SHIFT_TYPES_WITH_PREDEFINED_TIMES.includes(option.value)) {
|
|
predefinedHoursBgColor.value = `bg-${option.icon_color}`;
|
|
shift.value.start_time = '00:00';
|
|
|
|
if (option.value === 'SICK' || option.value === 'VACATION') {
|
|
const workedMinutes = getCurrentDailyMinutesWorked(currentShifts);
|
|
const expectedWorkedMinutes = expectedDailyHours * 60;
|
|
const leftOverMinutes = expectedWorkedMinutes - workedMinutes;
|
|
|
|
shift.value.end_time = getTimeStringFromMinutes(leftOverMinutes);
|
|
isShowingPredefinedTime.value = false;
|
|
} else {
|
|
isShowingPredefinedTime.value = true;
|
|
predefinedHoursString.value = getHoursMinutesStringFromHoursFloat(expectedDailyHours);
|
|
shift.value.end_time = getTimeStringFromMinutes(expectedDailyHours * 60);
|
|
}
|
|
} else
|
|
isShowingPredefinedTime.value = false;
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (uiStore.focusNextComponent) {
|
|
selectRef.value?.focus();
|
|
selectRef.value?.showPopup();
|
|
shiftTypeSelected.value = undefined;
|
|
uiStore.focusNextComponent = false;
|
|
}
|
|
|
|
if (errorMessage)
|
|
errorMessageRow.value = errorMessage;
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="column">
|
|
<div class="row q-pa-sm">
|
|
<div class="col column">
|
|
<div class="row justify-center q-pb-xs q-px-sm full-width">
|
|
<!-- shift type -->
|
|
<q-select
|
|
ref="selectRef"
|
|
v-model="shiftTypeSelected"
|
|
dense
|
|
borderless
|
|
color="accent"
|
|
label-color="white"
|
|
stack-label
|
|
label-slot
|
|
hide-dropdown-icon
|
|
:readonly="isApproved"
|
|
:options="getShiftOptions(hasPTO, currentShifts.length > 1)"
|
|
lazy-rules
|
|
no-error-icon
|
|
hide-bottom-space
|
|
options-selected-class="text-white text-bold bg-accent"
|
|
class="col q-px-md rounded-5 inset-shadow text-uppercase"
|
|
:class="isApproved ? 'bg-white' : ($q.dark.isActive ? 'bg-primary' : 'bg-secondary')"
|
|
popup-content-class="text-uppercase text-weight-medium rounded-5 shadow-12 z-top"
|
|
popup-content-style="border: 1px solid var(--q-primary)"
|
|
menu-anchor="bottom middle"
|
|
menu-self="top middle"
|
|
:menu-offset="[0, 5]"
|
|
:style="`border: 1px solid ${$q.dark.isActive ? 'var(--q-secondary)' : colors.getPaletteColor('blue-grey-4')}; background-color: ${colors.lighten(getCssVar('secondary')!, 50)}`"
|
|
@blur="onBlurShiftTypeSelect"
|
|
@update:model-value="onShiftTypeChange"
|
|
>
|
|
<template #selected-item="scope">
|
|
<div
|
|
class="row items-center text-weight-bold q-pt-sm no-wrap ellipsis"
|
|
:tabindex="scope.tabindex"
|
|
>
|
|
<q-icon
|
|
:name="scope.opt.icon"
|
|
:color="shift.is_approved ? 'accent' : scope.opt.icon_color"
|
|
size="sm"
|
|
class="col-auto"
|
|
:class="shift.is_approved ? 'q-mx-xs' : 'q-mr-xs'"
|
|
/>
|
|
|
|
<span
|
|
style="font-size: 1.3em;"
|
|
class="col ellipsis"
|
|
:class="shift.is_approved ? 'text-accent' : ''"
|
|
>
|
|
{{ $t(scope.opt.label) }}
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<template #option="scope">
|
|
<q-item
|
|
clickable
|
|
v-bind="scope.itemProps"
|
|
>
|
|
<q-item-section avatar>
|
|
<q-icon :name="scope.opt.icon" />
|
|
</q-item-section>
|
|
|
|
<q-item-section class="text-left">
|
|
{{ $t(scope.label) }}
|
|
</q-item-section>
|
|
</q-item>
|
|
</template>
|
|
|
|
<!-- work-from-home toggle -->
|
|
<template #after>
|
|
<q-icon
|
|
v-if="shift.is_approved"
|
|
:name="shift.is_remote ? 'las la-laptop' : 'las la-building'"
|
|
size="1.2em"
|
|
color="accent"
|
|
class="q-mr-sm"
|
|
>
|
|
<q-tooltip
|
|
anchor="top middle"
|
|
self="bottom middle"
|
|
:offset="[0, 10]"
|
|
class="text-uppercase text-weight-bold text-white bg-primary"
|
|
>
|
|
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
|
$t('timesheet.shift.types.OFFICE') }}
|
|
</q-tooltip>
|
|
</q-icon>
|
|
|
|
<q-toggle
|
|
v-else
|
|
v-model="shift.is_remote"
|
|
:disable="shift.is_approved"
|
|
dense
|
|
keep-color
|
|
size="3em"
|
|
:color="isHoliday ? 'purple-5' : 'accent'"
|
|
icon="las la-building"
|
|
checked-icon="las la-laptop"
|
|
>
|
|
<q-tooltip
|
|
anchor="top middle"
|
|
self="bottom middle"
|
|
:offset="[0, 10]"
|
|
class="text-uppercase text-weight-medium text-white bg-accent"
|
|
>
|
|
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
|
$t('timesheet.shift.types.OFFICE') }}
|
|
</q-tooltip>
|
|
</q-toggle>
|
|
</template>
|
|
|
|
<template #label>
|
|
<span
|
|
class="text-weight-medium text-uppercase q-px-sm no-pointer-events"
|
|
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-blue-grey-7'"
|
|
>
|
|
{{ $t('timesheet.shift.types.label') }}
|
|
</span>
|
|
</template>
|
|
</q-select>
|
|
</div>
|
|
|
|
<div
|
|
v-if="isShowingPredefinedTime"
|
|
class="col row items-start text-uppercase rounded-5 q-pa-xs relative-position"
|
|
>
|
|
<div
|
|
class="absolute-full rounded-5 q-mx-sm q-my-xs"
|
|
:class="predefinedHoursBgColor"
|
|
style="opacity: 0.3;"
|
|
></div>
|
|
|
|
<span class="col text-center text-uppercase text-h6 text-bold q-py-xs">
|
|
{{ getHoursMinutesStringFromHoursFloat(expectedDailyHours) }}
|
|
</span>
|
|
</div>
|
|
|
|
<div
|
|
v-else
|
|
class="col row items-start text-uppercase rounded-5 q-pt-sm"
|
|
>
|
|
<!-- punch in field -->
|
|
<div class="col">
|
|
<TargoInput
|
|
v-model="shift.start_time"
|
|
no-top-padding
|
|
dense
|
|
type="time"
|
|
:readonly="isApproved"
|
|
:background-color="isApproved ? 'white' : undefined"
|
|
:input-text-color="isApproved ? 'accent' : ''"
|
|
:label="$t('shared.misc.in')"
|
|
:error="shift.has_error"
|
|
@blur="onTimeFieldBlur(shift.start_time)"
|
|
/>
|
|
</div>
|
|
|
|
<!-- punch out field -->
|
|
<div class="col">
|
|
<TargoInput
|
|
v-model="shift.end_time"
|
|
no-top-padding
|
|
dense
|
|
type="time"
|
|
:readonly="isApproved"
|
|
:background-color="isApproved ? 'white' : undefined"
|
|
:input-text-color="isApproved ? 'accent' : ''"
|
|
:label="$t('shared.misc.out')"
|
|
:error="shift.has_error"
|
|
@blur="onTimeFieldBlur(shift.end_time)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-auto q-pt-md">
|
|
<TargoInput
|
|
v-model="shift.comment"
|
|
no-top-padding
|
|
dense
|
|
:readonly="isApproved"
|
|
:background-color="isApproved ? 'white' : undefined"
|
|
:input-text-color="isApproved ? 'accent' : ''"
|
|
:label="$t('timesheet.expense.employee_comment')"
|
|
:append-content="isApproved ? '' : `${commentLength ?? 0}/280`"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-auto">
|
|
<q-btn
|
|
v-if="!shift.is_approved"
|
|
outline
|
|
dense
|
|
color="negative"
|
|
icon="las la-trash"
|
|
size="lg"
|
|
class="full-height rounded-5"
|
|
@click="$emit('requestDelete')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<q-separator
|
|
v-if="hasShiftAfter"
|
|
spaced
|
|
size="2px"
|
|
:color="isApproved ? 'accent2' : 'accent'"
|
|
class="q-mx-lg"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
:deep(.q-field--error) {
|
|
background-color: var(--q-negative) !important;
|
|
}
|
|
|
|
:deep(.q-field--error .q-field__bottom) {
|
|
color: white;
|
|
font-weight: 900;
|
|
border-radius: 0 0 5px 5px;
|
|
padding-top: 0;
|
|
align-items: center;
|
|
}
|
|
|
|
:deep(.q-field--float .q-field__label) {
|
|
transform: translate(-17px, -60%) scale(0.75) !important;
|
|
border-radius: 10px 10px 10px 0px;
|
|
}
|
|
|
|
:deep(.q-field--auto-height.q-field--labeled .q-field__control-container) {
|
|
padding: 0;
|
|
}
|
|
</style> |