targo-frontend/src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue
Nicolas Drolet 20fcc0206c fix(all): Many fixes and adjustements, see full commit comment:
Dashboard: Reworked carousel and added useful links. Help page: made title sections more obvious, minor UI adjustments to spacing, appearance. Timesheets: Make mobile timesheet automaticall scroll to today's date when loading. Layout: Fix UI bugs where menu labels would not appear in mobile and tray would load automatically on mobile.
2026-01-02 12:38:35 -05:00

329 lines
15 KiB
Vue

<script
setup
lang="ts"
>
import { computed, onMounted, ref } from 'vue';
import { QSelect, QInput } from 'quasar';
import { useUiStore } from 'src/stores/ui-store';
import { SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
import type { Shift } from 'src/modules/timesheets/models/shift.models';
const ui_store = useUiStore();
const COMMENT_LENGTH_MAX = 280;
const shift = defineModel<Shift>('shift', { required: true });
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
const select_ref = ref<QSelect | null>(null);
const is_showing_comment_popup = ref(false);
const comment_length = computed(() => shift.value.comment?.length ?? 0);
const error_message = ref('');
const { errorMessage = undefined, dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
dense?: boolean;
hasShiftAfter?: boolean;
isTimesheetApproved?: boolean;
errorMessage?: string | undefined;
}>();
const emit = defineEmits<{
'requestDelete': [void];
'onTimeFieldBlur': [void];
}>();
const onBlurShiftTypeSelect = () => {
if (shift_type_selected.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;
error_message.value = 'timesheet.errors.SHIFT_TIME_REQUIRED';
} else {
shift.value.has_error = false;
error_message.value = '';
emit('onTimeFieldBlur');
}
}
const getCommentCounterColor = (comment_length: number) => {
if (comment_length < 200) return 'primary';
if (comment_length < 250) return 'warning';
return 'negative';
};
onMounted(() => {
if (ui_store.focus_next_component) {
select_ref.value?.focus();
select_ref.value?.showPopup();
shift_type_selected.value = undefined;
ui_store.focus_next_component = false;
}
if (errorMessage)
error_message.value = errorMessage;
});
</script>
<template>
<div class="row q-px-xs">
<div class="col column">
<div class="col row items-center text-uppercase q-px-xs rounded-5">
<!-- comment button -->
<q-btn
v-if="ui_store.is_mobile_mode && !dense"
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
:text-color="shift.comment ? ((shift.is_approved && isTimesheetApproved) ? 'white' : 'accent') : 'grey-5'"
class="col-auto full-height q-mx-xs rounded-5 shadow-1"
@click="is_showing_comment_popup = true"
>
<q-dialog v-model="is_showing_comment_popup">
<q-input
color="white"
v-model="shift.comment"
dense
:readonly="(shift.is_approved || isTimesheetApproved)"
autofocus
counter
bottom-slots
stack-label
:label="$t('timesheet.shift.fields.header_comment')"
:maxlength="COMMENT_LENGTH_MAX"
:class="(shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed' : ''"
>
<template #append>
<q-icon name="edit" />
</template>
<template #counter>
<div class="row flex-center">
<q-space />
<q-knob
v-model="comment_length"
readonly
:max="COMMENT_LENGTH_MAX"
size="1.6em"
:thickness="0.4"
:color="getCommentCounterColor(comment_length)"
track-color="grey-4"
class="col-auto q-mr-xs"
/>
<span
:class="'col-auto text-weight-bolder text-' + getCommentCounterColor(comment_length)"
>{{ 280 - comment_length }}</span>
</div>
</template>
</q-input>
</q-dialog>
</q-btn>
<!-- shift type -->
<q-select
ref="select"
v-model="shift_type_selected"
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
dense
:borderless="(shift.is_approved && isTimesheetApproved)"
:readonly="(shift.is_approved && isTimesheetApproved)"
options-dense
hide-dropdown-icon
:menu-offset="[0, 10]"
menu-anchor="bottom middle"
menu-self="top middle"
:options="SHIFT_OPTIONS"
class="col rounded-5 bg-dark"
:class="!shift.is_approved && !isTimesheetApproved ? '' : 'inset-shadow'"
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
popup-content-style="border: 2px solid var(--q-accent)"
@blur="onBlurShiftTypeSelect"
@update:model-value="option => shift.type = option.value"
>
<template #selected-item="scope">
<div
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
:tabindex="scope.tabindex"
>
<q-icon
:name="scope.opt.icon"
:color="scope.opt.icon_color"
size="sm"
class="col-auto"
:class="shift.is_approved ? 'q-mx-xs' : 'q-mr-xs'"
/>
<span
style="line-height: 1.2em;"
class="col-auto ellipsis"
:class="!shift.is_approved ? '' : 'text-white'"
>
{{ $t(scope.opt.label) }}
</span>
</div>
</template>
<template #after>
<q-icon
v-if="shift.is_approved"
:name="shift.is_remote ? 'las la-laptop' : 'las la-building'"
size="1.2em"
color="white"
class="q-mr-sm"
>
<q-tooltip
anchor="top middle"
self="bottom middle"
:offset="[0, 10]"
:hide-delay="1000"
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="accent"
icon="las la-building"
checked-icon="las la-laptop"
>
<q-tooltip
anchor="top middle"
self="bottom middle"
:offset="[0, 10]"
:hide-delay="1000"
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 #option="scope">
<q-item>
<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>
</q-select>
</div>
<div class="col row items-start text-uppercase rounded-5 q-pa-xs">
<!-- punch in field -->
<div class="col q-pr-xs">
<q-input
v-model="shift.start_time"
dense
:borderless="(shift.is_approved && isTimesheetApproved)"
:readonly="(shift.is_approved && isTimesheetApproved)"
type="time"
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
label-slot
lazy-rules
no-error-icon
hide-bottom-space
:error="shift.has_error"
:error-message="errorMessage || error_message !== '' ? $t(errorMessage ?? error_message) : ''"
:label-color="!shift.is_approved ? 'accent' : 'white'"
class="rounded-5 bg-dark"
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (!shift.is_approved && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')"
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
input-style="font-size: 1.2em;"
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
@blur="onTimeFieldBlur(shift.start_time)"
>
<template #label>
<span
class="text-weight-bolder"
:class="shift.is_approved ? ' q-ml-md' : ''"
style="font-size: 0.95em;"
>{{ $t('shared.misc.in') }}</span>
</template>
</q-input>
</div>
<!-- punch out field -->
<div class="col">
<q-input
v-model="shift.end_time"
standout
dense
:borderless="(shift.is_approved && isTimesheetApproved)"
:readonly="(shift.is_approved && isTimesheetApproved)"
type="time"
label-slot
no-error-icon
hide-bottom-space
:error="shift.has_error"
:error-message="errorMessage || error_message !== '' ? $t(errorMessage ?? error_message) : ''"
:label-color="!shift.is_approved ? 'accent' : 'white'"
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
input-style="font-size: 1.2em;"
class="rounded-5 bg-dark"
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (shift.is_approved ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : (isTimesheetApproved ? 'inset-shadow' : ''))"
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
@blur="onTimeFieldBlur(shift.end_time)"
>
<template #label>
<span
class="text-weight-bolder"
:class="shift.is_approved ? ' q-ml-md' : ''"
style="font-size: 0.95em;"
>{{ $t('shared.misc.out') }}</span>
</template>
</q-input>
</div>
</div>
</div>
<div class="col-auto">
<q-btn
v-if="!shift.is_approved"
flat
dense
color="negative"
icon="las la-trash"
size="lg"
class="full-height"
@click="$emit('requestDelete')"
/>
</div>
<q-separator
v-if="hasShiftAfter"
spaced
class="q-mx-md col-12"
/>
</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;
}
</style>