targo-frontend/src/modules/timesheets/components/shift/shift-crud-dialog.vue
2025-09-18 14:38:23 -04:00

242 lines
8.7 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 { upsert_shifts_by_date, type UpsertShiftsBody, type ShiftPayload } from '../../composables/use-shift-api';
import { useI18n } from 'vue-i18n';
/* eslint-disable */
type Option = { value: string; label: string };
const { t } = useI18n();
const props = defineProps<{
mode: 'create' | 'edit' | 'delete';
dateIso: string;
initialShift?: ShiftPayload | null;
shiftOptions: Option[];
email: string;
}>();
const emit = defineEmits<{
(e: 'close' ): void;
(e: 'saved'): void;
}>();
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<string> ('type' , { default: '' });
const isRemote = defineModel<boolean>('isRemote' , { default: false });
const comment = defineModel<string> ('comment' , { default: '' });
const buildNewShiftPayload = (): ShiftPayload => {
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 upsert_shifts_by_date(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 &&
type.value.trim().length > 0)
);
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>