targo-frontend/src/modules/timesheets/components/shift/shift-crud-dialog.vue

282 lines
9.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { SHIFT_KEY, type ShiftKey, type ShiftPayload, type ShiftSelectOption } from '../../types/shift.types';
import type { UpsertShiftsBody } from '../../types/shift.interfaces';
import { upsertShiftsByDate } from '../../composables/api/use-shift-api';
/* eslint-disable */
const { t } = useI18n();
const props = defineProps<{
mode: 'create' | 'edit' | 'delete';
dateIso: string;
initialShift?: ShiftPayload | null;
shiftOptions: ShiftSelectOption[];
email: string;
}>();
const emit = defineEmits<{
'close': []
'saved': []
}>();
const isSubmitting = ref(false);
const errorBanner = ref<string | null>(null);
const conflicts = ref<Array<{ start_time: string; end_time: string; type: string }>>([]);
const opened = defineModel<boolean>({ default: false });
const startTime = defineModel<string>('startTime', { default: '' });
const endTime = defineModel<string>('endTime', { default: '' });
const type = defineModel<ShiftKey | ''>('type', { default: '' });
const isRemote = defineModel<boolean>('isRemote', { default: false });
const comment = defineModel<string>('comment', { default: '' });
const isShiftKey = (val: unknown): val is ShiftKey => SHIFT_KEY.includes(val as ShiftKey);
const buildNewShiftPayload = (): ShiftPayload => {
if (!isShiftKey(type.value)) throw new Error('Invalid shift type');
const trimmed = (comment.value ?? '').trim();
return {
start_time: startTime.value,
end_time: endTime.value,
type: type.value,
is_remote: isRemote.value,
...(trimmed ? { comment: trimmed } : {}),
};
};
const onSubmit = async () => {
errorBanner.value = null;
conflicts.value = [];
isSubmitting.value = true;
try {
let body: UpsertShiftsBody;
if (props.mode === 'create') {
body = { new_shift: buildNewShiftPayload() };
} else if (props.mode === 'edit') {
if (!props.initialShift) throw new Error('Missing initial Shift for edit');
body = { old_shift: props.initialShift, new_shift: buildNewShiftPayload() };
} else {
if (!props.initialShift) throw new Error('Missing initial Shift for delete.');
body = { old_shift: props.initialShift };
}
await upsertShiftsByDate(props.email, props.dateIso, body);
opened.value = false;
emit('saved');
} catch (error: any) {
const status = error?.status_code ?? error.response?.status ?? 500;
const apiConflicts = error?.response?.data?.conflicts ?? error?.data?.conflicts;
if (Array.isArray(apiConflicts)) {
conflicts.value = apiConflicts.map((c: any) => ({
start_time: String(c.start_time ?? ''),
end_time: String(c.end_time ?? ''),
type: String(c.type ?? ''),
}));
} else {
conflicts.value = [];
}
if (status === 404) errorBanner.value = t('timesheet.shift.errors.not_found')
else if (status === 409) errorBanner.value = t('timesheet.shift.errors.overlap')
else if (status === 422) errorBanner.value = t('timesheet.shift.errors.invalid')
else errorBanner.value = t('timesheet.shift.errors.unknown')
//add conflicts.value error management
} finally {
isSubmitting.value = false;
}
}
const hydrateFromProps = () => {
if (props.mode === 'edit' || props.mode === 'delete') {
const shift = props.initialShift;
startTime.value = shift?.start_time ?? '';
endTime.value = shift?.end_time ?? '';
type.value = shift?.type ?? '';
isRemote.value = !!shift?.is_remote;
comment.value = (shift as any)?.comment ?? '';
} else {
startTime.value = '';
endTime.value = '';
type.value = '';
isRemote.value = false;
comment.value = '';
}
};
const canSubmit = computed(() =>
props.mode === 'delete' ||
(startTime.value.trim().length === 5 &&
endTime.value.trim().length === 5 &&
isShiftKey(type.value))
);
watch(
() => [opened.value, props.mode, props.initialShift, props.dateIso],
() => { if (opened.value) hydrateFromProps(); },
{ immediate: true }
);
</script>
<!-- create/edit/delete shifts dialog -->
<template>
<q-dialog
v-model="opened"
persistent
transition-show="fade"
transition-hide="fade"
>
<q-card class="q-pa-md">
<div class="row items-center q-mb-sm">
<q-icon
name="schedule"
size="24px"
class="q-mr-sm"
/>
<div class="text-h6">
{{
props.mode === 'create'
? $t('timesheet.shift.actions.add')
: props.mode === 'edit'
? $t('timesheet.shift.actions.edit')
: $t('timesheet.shift.actions.delete')
}}
</div>
<q-space />
<q-badge
outline
color="primary"
>
{{ props.dateIso }}
</q-badge>
</div>
<q-separator spaced />
<div
v-if="props.mode !== 'delete'"
class="column q-gutter-md"
>
<div class="row ">
<div class="col">
<q-input
v-model="startTime"
:label="$t('timesheet.shift.fields.start')"
filled
dense
inputmode="numeric"
mask="##:##"
/>
</div>
<div class="col">
<q-input
v-model="endTime"
:label="$t('timesheet.shift.fields.end')"
filled
dense
inputmode="numeric"
mask="##:##"
/>
</div>
</div>
<div class="row items-center">
<q-select
v-model="type"
options-dense
:options="props.shiftOptions"
:label="$t('timesheet.shift.types.label')"
class="col"
color="primary"
filled
dense
hide-dropdown-icon
emit-value
map-options
/>
<q-toggle
v-model="isRemote"
:label="$t('timesheet.shift.types.REMOTE')"
class="col-auto"
/>
</div>
<q-input
v-model="comment"
type="textarea"
autogrow
filled
dense
:label="$t('timesheet.shift.fields.header_comment')"
:counter="true"
:maxlength="512"
/>
</div>
<div
v-else
class="q-pa-md"
>
{{ $t('timesheet.shift.actions.delete_confirmation_msg') }}
</div>
<div
v-if="errorBanner"
class="q-mt-md"
>
<q-banner
dense
class="bg-red-2 text-negative"
>{{ errorBanner }}</q-banner>
<div
v-if="conflicts.length"
class="q-mt-xs"
>
<div class="text-caption">Conflits :</div>
<ul class="q-pl-md q-mt-xs">
<li
v-for="(c, i) in conflicts"
:key="i"
>
{{ c.start_time }}{{ c.end_time }} ({{ c.type }})
</li>
</ul>
</div>
</div>
<q-separator spaced />
<div class="row justify-end q-gutter-sm">
<q-btn
flat
color="grey-8"
:label="$t('timesheet.cancel_button')"
@click="() => { opened = false; emit('close'); }"
/>
<q-btn
v-if="props.mode === 'delete'"
outline
color="negative"
icon="cancel"
:label="$t('timesheet.delete_button')"
:loading="isSubmitting"
:disable="!canSubmit"
@click="onSubmit"
/>
<q-btn
v-else
color="primary"
icon="save_alt"
:label="$t('timesheet.save_button')"
:loading="isSubmitting"
:disable="!canSubmit"
@click="onSubmit"
/>
</div>
</q-card>
</q-dialog>
</template>