refactor(shift): extracted crud for shift from overview.
This commit is contained in:
parent
b488848ac3
commit
71956ef4b2
|
|
@ -250,7 +250,8 @@ export default {
|
||||||
timeSheetValidations: 'Time sheet approvals',
|
timeSheetValidations: 'Time sheet approvals',
|
||||||
},
|
},
|
||||||
timesheet: {
|
timesheet: {
|
||||||
//employee's timesheet page
|
title:'Timesheet',
|
||||||
|
date_ranges_to:'to',
|
||||||
days: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
|
days: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
|
||||||
nav_button: {
|
nav_button: {
|
||||||
calendar_date_picker:'Calendar',
|
calendar_date_picker:'Calendar',
|
||||||
|
|
@ -262,15 +263,15 @@ export default {
|
||||||
cancel_button:'Cancel',
|
cancel_button:'Cancel',
|
||||||
remote_button: 'Remote work',
|
remote_button: 'Remote work',
|
||||||
delete_button: 'Delete',
|
delete_button: 'Delete',
|
||||||
|
shift: {
|
||||||
|
actions: {
|
||||||
|
add:'Add Shift',
|
||||||
|
edit: 'Edit shift',
|
||||||
|
delete: 'Delete shift',
|
||||||
delete_confirmation_msg: 'Do you want to delete this shift completly?',
|
delete_confirmation_msg: 'Do you want to delete this shift completly?',
|
||||||
|
},
|
||||||
add_shift:'Add Shift',
|
types: {
|
||||||
edit_shift: 'Edit shift',
|
label: 'Shift`s Type',
|
||||||
delete_shift: 'Delete shift',
|
|
||||||
|
|
||||||
shift_types_label: 'Shift`s Type',
|
|
||||||
shift_types: {
|
|
||||||
EMERGENCY: 'Emergency',
|
EMERGENCY: 'Emergency',
|
||||||
EVENING: 'Evening',
|
EVENING: 'Evening',
|
||||||
HOLIDAY: 'Holiday',
|
HOLIDAY: 'Holiday',
|
||||||
|
|
@ -280,29 +281,39 @@ export default {
|
||||||
VACATION: 'Vacation',
|
VACATION: 'Vacation',
|
||||||
REMOTE: 'Remote work',
|
REMOTE: 'Remote work',
|
||||||
},
|
},
|
||||||
|
errors: {
|
||||||
|
not_found:'Shift not found',
|
||||||
|
overlap:'An overlaps occured between 2 or more shifts',
|
||||||
|
invalid:'Invalid shift`s entry',
|
||||||
|
unknown:'Unknown error',
|
||||||
|
comment_required:'A comment is required',
|
||||||
|
comment_too_long:'Your comment is too long',
|
||||||
|
},
|
||||||
fields: {
|
fields: {
|
||||||
start:'Start (HH:mm)',
|
start:'Start (HH:mm)',
|
||||||
end:'End (HH:mm)',
|
end:'End (HH:mm)',
|
||||||
header_comment:'Shift`s comment',
|
header_comment:'Shift`s comment',
|
||||||
textarea_comment: 'Leave a comment here',
|
textarea_comment: 'Leave a comment here',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
expense: {
|
expense: {
|
||||||
add_expense:'Add Expense',
|
add_expense:'Add Expense',
|
||||||
amount:'Amount',
|
amount:'Amount',
|
||||||
date:'Date',
|
date:'Date',
|
||||||
empty_list:'No registered expenses',
|
empty_list:'No registered expenses',
|
||||||
errors: {
|
errors: {
|
||||||
date_required_or_invalid:'',
|
date_required_or_invalid:'the date is missing or invalid',
|
||||||
comment_required:'',
|
comment_required:'A comment required',
|
||||||
comment_too_long:'',
|
comment_too_long:'Your comment is too long',
|
||||||
amount_must_be_positive:'',
|
amount_must_be_positive:'the amount cannot be under 0$',
|
||||||
mileave_must_be_positive:'',
|
mileave_must_be_positive:'the mileage cannot be under 0',
|
||||||
amount_xor_mileage:'',
|
amount_xor_mileage:'you cannot enter an amount and a mileage for the same expense',
|
||||||
mileage_required_for_type:'',
|
mileage_required_for_type:'you need to enter a value for mileage when you enter an expense of that type',
|
||||||
amount_required_for_type:'',
|
amount_required_for_type:'you need to enter a value for amount when you enter an expense of that type',
|
||||||
},
|
},
|
||||||
hints: {
|
hints: {
|
||||||
amount_or_mileage:'Either amount or mileage, not both',
|
amount_or_mileage:'Either amount or mileage, not both',
|
||||||
|
comment_required:'A comment required',
|
||||||
},
|
},
|
||||||
mileage:'Mileage',
|
mileage:'Mileage',
|
||||||
open_btn:'List of expenses',
|
open_btn:'List of expenses',
|
||||||
|
|
@ -311,10 +322,10 @@ export default {
|
||||||
total_mileage:'Total mileage',
|
total_mileage:'Total mileage',
|
||||||
type:'Type',
|
type:'Type',
|
||||||
types: {
|
types: {
|
||||||
PER_DIEM:'',
|
PER_DIEM:'Per Diem',
|
||||||
EXPENSES:'',
|
EXPENSES:'expense',
|
||||||
MILEAGE:'',
|
MILEAGE:'mileage',
|
||||||
PRIME_GARDE:'',
|
PRIME_GARDE:'on-call allowance',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -300,7 +300,8 @@ export default {
|
||||||
noDataLabel: 'Je n’ai rien trouvé pour toi',
|
noDataLabel: 'Je n’ai rien trouvé pour toi',
|
||||||
},
|
},
|
||||||
timesheet: {
|
timesheet: {
|
||||||
//employee's timesheet page
|
title:'Carte de temps',
|
||||||
|
date_ranges_to:'au',
|
||||||
days: ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'],
|
days: ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'],
|
||||||
nav_button: {
|
nav_button: {
|
||||||
calendar_date_picker:'Calendrier',
|
calendar_date_picker:'Calendrier',
|
||||||
|
|
@ -312,26 +313,38 @@ export default {
|
||||||
cancel_button:'Annuler',
|
cancel_button:'Annuler',
|
||||||
remote_button: 'Télétravail',
|
remote_button: 'Télétravail',
|
||||||
delete_button: 'Supprimer',
|
delete_button: 'Supprimer',
|
||||||
delete_confirmation_msg: 'Voulez-vous supprimer complètement ce quart?',
|
shift: {
|
||||||
add_shift:'Ajouter une quart',
|
actions: {
|
||||||
edit_shift: 'Modifier un quart',
|
add:'Ajouter un Quart',
|
||||||
delete_shift: 'Supprimer un quart',
|
edit: 'Modifier un Quart',
|
||||||
shift_types_label: 'Type de quart',
|
delete: 'Supprimer un Quart',
|
||||||
shift_types: {
|
delete_confirmation_msg: 'Voulez-vous complètement supprimer ce quart?',
|
||||||
|
},
|
||||||
|
types: {
|
||||||
|
label: 'Type de Quart',
|
||||||
EMERGENCY: 'Urgence',
|
EMERGENCY: 'Urgence',
|
||||||
EVENING: 'Soir',
|
EVENING: 'Soir',
|
||||||
HOLIDAY: 'Férié',
|
HOLIDAY: 'Férié',
|
||||||
OVERTIME: 'Supplémentaire',
|
OVERTIME: 'Supplémentaire',
|
||||||
SICK: 'Absence',
|
|
||||||
REGULAR: 'Régulier',
|
REGULAR: 'Régulier',
|
||||||
|
SICK: 'Maladie',
|
||||||
VACATION: 'Vacance',
|
VACATION: 'Vacance',
|
||||||
REMOTE: 'Télétravail',
|
REMOTE: 'Télétravail',
|
||||||
},
|
},
|
||||||
|
errors: {
|
||||||
|
not_found:'Aucun quart trouvé',
|
||||||
|
overlap:'Il y a un chevauchement entre deux ou plusieurs quarts',
|
||||||
|
invalid:'Entrée du quart invalide',
|
||||||
|
unknown:'Erreur inconnue',
|
||||||
|
comment_required:'un commentaire est requis',
|
||||||
|
comment_too_long:'votre commentaire est trop long',
|
||||||
|
},
|
||||||
fields: {
|
fields: {
|
||||||
start:'Entrée (HH:mm)',
|
start:'Début (HH:mm)',
|
||||||
end:'Sortie (HH:mm)',
|
end:'Fin (HH:mm)',
|
||||||
header_comment:'Commentaire du Quart',
|
header_comment:'Commentaire du Quart',
|
||||||
textarea_comment:'Laissez votre commentaire',
|
textarea_comment: 'Laissez votre commentaire ici',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
expense: {
|
expense: {
|
||||||
add_expense:'Ajouter une dépense',
|
add_expense:'Ajouter une dépense',
|
||||||
|
|
@ -339,17 +352,18 @@ export default {
|
||||||
date:'Date',
|
date:'Date',
|
||||||
empty_list:'Aucun dépense enregistrée',
|
empty_list:'Aucun dépense enregistrée',
|
||||||
errors: {
|
errors: {
|
||||||
date_required_or_invalid:'',
|
date_required_or_invalid:'La date est manquante ou invalide',
|
||||||
comment_required:'',
|
comment_required:'un commentaire est requis',
|
||||||
comment_too_long:'',
|
comment_too_long:'votre commentaire est trop long',
|
||||||
amount_must_be_positive:'',
|
amount_must_be_positive:'le montant doit être suppérieur à 0$',
|
||||||
mileave_must_be_positive:'',
|
mileave_must_be_positive:'le kilométrage doit être suppérieur à 0',
|
||||||
amount_xor_mileage:'',
|
amount_xor_mileage:'Vous ne pouvez pas saisir un montant et un kilométrage pour une même dépense',
|
||||||
mileage_required_for_type:'',
|
mileage_required_for_type:'Vous devez entrer une valeur en kilométrage pour ce type de dépense',
|
||||||
amount_required_for_type:'',
|
amount_required_for_type:'Vous devez entrer une valeur en montant $ pour ce type de dépense',
|
||||||
},
|
},
|
||||||
hints: {
|
hints: {
|
||||||
amount_or_mileage:'Soit dépense ou kilométrage, pas les deux',
|
amount_or_mileage:'Soit dépense ou kilométrage, pas les deux',
|
||||||
|
comment_required:'un commentaire est requis',
|
||||||
},
|
},
|
||||||
mileage:'Kilométrage',
|
mileage:'Kilométrage',
|
||||||
open_btn:'Liste des Dépenses',
|
open_btn:'Liste des Dépenses',
|
||||||
|
|
@ -358,10 +372,10 @@ export default {
|
||||||
total_mileage:'Kilométrage total',
|
total_mileage:'Kilométrage total',
|
||||||
type:'Type',
|
type:'Type',
|
||||||
types: {
|
types: {
|
||||||
PER_DIEM:'',
|
PER_DIEM:'Per diem',
|
||||||
EXPENSES:'',
|
EXPENSES:'dépense',
|
||||||
MILEAGE:'',
|
MILEAGE:'kilométrage',
|
||||||
PRIME_GARDE:'',
|
PRIME_GARDE:'Prime de garde',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { EXPENSE_TYPE, type ExpenseType, type TimesheetExpense } from '../../types/timesheet-expenses-interface';
|
import { EXPENSE_TYPE, type ExpenseType, type TimesheetExpense } from '../../types/timesheet-expenses-interface';
|
||||||
import { compute_expense_totals, ExpensesValidationError, normalize_expense, validate_expense_UI } from '../../utils/timesheet-expenses-validators';
|
import { compute_expense_totals, ExpensesValidationError, normalize_expense, validate_expense_UI } from '../../utils/timesheet-expenses-validators';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { COMMENT_MAX_LENGTH } from '../../composables/use-shift-api';
|
import { COMMENT_MAX_LENGTH } from '../../composables/use-shift-api';
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|
@ -37,7 +38,7 @@ const type_options = computed(()=> EXPENSE_TYPE.map((val)=> ({
|
||||||
|
|
||||||
//refs & states
|
//refs & states
|
||||||
const items = ref<TimesheetExpense[]>(Array.isArray(props.initial_expenses) ? props.initial_expenses.map(normalize_expense): []);
|
const items = ref<TimesheetExpense[]>(Array.isArray(props.initial_expenses) ? props.initial_expenses.map(normalize_expense): []);
|
||||||
const draft = reactive<Partial<TimesheetExpense>>({
|
const draft = ref<Partial<TimesheetExpense>>({
|
||||||
date:'',
|
date:'',
|
||||||
type: 'EXPENSES',
|
type: 'EXPENSES',
|
||||||
comment:'',
|
comment:'',
|
||||||
|
|
@ -46,26 +47,26 @@ const draft = reactive<Partial<TimesheetExpense>>({
|
||||||
// computeds
|
// computeds
|
||||||
const totals = computed(()=> compute_expense_totals(items.value));
|
const totals = computed(()=> compute_expense_totals(items.value));
|
||||||
const remaining_comment_chars = computed(()=> {
|
const remaining_comment_chars = computed(()=> {
|
||||||
const comment = String(draft.comment ?? '');
|
const comment = String(draft.value.comment ?? '');
|
||||||
return COMMENT_MAX_LENGTH - comment.length;
|
return COMMENT_MAX_LENGTH - comment.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
//actions
|
//actions
|
||||||
const reset_draft = () => {
|
const reset_draft = () => {
|
||||||
draft.date = '';
|
draft.value.date = '';
|
||||||
draft.type = 'EXPENSES';
|
draft.value.type = 'EXPENSES';
|
||||||
delete draft.amount;
|
delete draft.value.amount;
|
||||||
delete draft.mileage;
|
delete draft.value.mileage;
|
||||||
draft.comment = '';
|
draft.value.comment = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const add_draft_as_item = () => {
|
const add_draft_as_item = () => {
|
||||||
const candidate: TimesheetExpense = normalize_expense({
|
const candidate: TimesheetExpense = normalize_expense({
|
||||||
date: String(draft.date ?? '').trim(),
|
date: String(draft.value.date ?? '').trim(),
|
||||||
type: String(draft.type ?? '').trim(),
|
type: String(draft.value.type ?? '').trim(),
|
||||||
...(typeof draft.amount === 'number' ? { amount: draft.amount }: {}),
|
...(typeof draft.value.amount === 'number' ? { amount: draft.value.amount }: {}),
|
||||||
...(typeof draft.mileage === 'number' ? { mileage: draft.mileage }: {}),
|
...(typeof draft.value.mileage === 'number' ? { mileage: draft.value.mileage }: {}),
|
||||||
comment: String(draft.comment ?? '').trim(),
|
comment: String(draft.value.comment ?? '').trim(),
|
||||||
} as TimesheetExpense);
|
} as TimesheetExpense);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -123,19 +124,19 @@ const on_close = () => emit('close');
|
||||||
const is_readonly = computed(()=> !!props.is_approved);
|
const is_readonly = computed(()=> !!props.is_approved);
|
||||||
|
|
||||||
|
|
||||||
const set_draft_type = (value: ExpenseType) => (draft.type = value);
|
const set_draft_type = (value: ExpenseType) => (draft.value.type = value);
|
||||||
const set_draft_amount = (value: number | null) => {
|
const set_draft_amount = (value: number | null) => {
|
||||||
if(value === null || value === undefined || Number.isNaN(Number(value))) {
|
if(value === null || value === undefined || Number.isNaN(Number(value))) {
|
||||||
delete draft.amount;
|
delete draft.value.amount;
|
||||||
} else {
|
} else {
|
||||||
draft.amount = Number(value);
|
draft.value.amount = Number(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const set_draft_mileage = (value: number | null) => {
|
const set_draft_mileage = (value: number | null) => {
|
||||||
if(value === null || value === undefined || Number.isNaN(Number(value))) {
|
if(value === null || value === undefined || Number.isNaN(Number(value))) {
|
||||||
delete draft.mileage;
|
delete draft.value.mileage;
|
||||||
} else {
|
} else {
|
||||||
draft.mileage = Number(value);
|
draft.value.mileage = Number(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
242
src/modules/timesheets/components/shift/shift-crud-dialog.vue
Normal file
242
src/modules/timesheets/components/shift/shift-crud-dialog.vue
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
<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>
|
||||||
|
|
@ -13,13 +13,13 @@ type ShiftLegendItem = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const legend: ShiftLegendItem[] = [
|
const legend: ShiftLegendItem[] = [
|
||||||
{type:'REGULAR' , color: 'secondary', label_key: 'timesheet.shift_types.REGULAR', text_color: 'grey-8'},
|
{type:'REGULAR' , color: 'secondary', label_key: 'timesheet.shift.types.REGULAR', text_color: 'grey-8'},
|
||||||
{type:'EVENING' , color: 'warning' , label_key: 'timesheet.shift_types.EVENING'},
|
{type:'EVENING' , color: 'warning' , label_key: 'timesheet.shift.types.EVENING'},
|
||||||
{type:'EMERGENCY', color: 'amber-10' , label_key: 'timesheet.shift_types.EMERGENCY'},
|
{type:'EMERGENCY', color: 'amber-10' , label_key: 'timesheet.shift.types.EMERGENCY'},
|
||||||
{type:'OVERTIME' , color: 'negative' , label_key: 'timesheet.shift_types.OVERTIME'},
|
{type:'OVERTIME' , color: 'negative' , label_key: 'timesheet.shift.types.OVERTIME'},
|
||||||
{type:'VACATION' , color: 'purple-10', label_key: 'timesheet.shift_types.VACATION'},
|
{type:'VACATION' , color: 'purple-10', label_key: 'timesheet.shift.types.VACATION'},
|
||||||
{type:'HOLIDAY' , color: 'purple-8' , label_key: 'timesheet.shift_types.HOLIDAY'},
|
{type:'HOLIDAY' , color: 'purple-8' , label_key: 'timesheet.shift.types.HOLIDAY'},
|
||||||
{type:'SICK' , color: 'grey-8' , label_key: 'timesheet.shift_types.SICK'},
|
{type:'SICK' , color: 'grey-8' , label_key: 'timesheet.shift.types.SICK'},
|
||||||
]
|
]
|
||||||
|
|
||||||
const shift_type_legend = computed(()=>
|
const shift_type_legend = computed(()=>
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,12 @@ import { date } from 'quasar';
|
||||||
import TimesheetNavigation from '../components/timesheet/timesheet-navigation.vue';
|
import TimesheetNavigation from '../components/timesheet/timesheet-navigation.vue';
|
||||||
import ShiftsLegend from '../components/shift/shifts-legend.vue';
|
import ShiftsLegend from '../components/shift/shifts-legend.vue';
|
||||||
import TimesheetDetailsShifts from '../components/shift/timesheet-details-shifts.vue';
|
import TimesheetDetailsShifts from '../components/shift/timesheet-details-shifts.vue';
|
||||||
import { upsert_shifts_by_date, type ShiftPayload, type UpsertShiftsBody } from '../composables/use-shift-api';
|
import { type ShiftPayload } from '../composables/use-shift-api';
|
||||||
import { ExpensesApiError, get_pay_period_expenses, put_pay_period_expenses } from '../composables/use-expense-api';
|
import { ExpensesApiError, get_pay_period_expenses, put_pay_period_expenses } from '../composables/use-expense-api';
|
||||||
import type { PayPeriodExpenses } from '../types/timesheet-expenses-list-interface';
|
import type { PayPeriodExpenses } from '../types/timesheet-expenses-list-interface';
|
||||||
import type { TimesheetExpense } from '../types/timesheet-expenses-interface';
|
import type { TimesheetExpense } from '../types/timesheet-expenses-interface';
|
||||||
|
import TimesheetDetailsExpenses from '../components/expenses/timesheet-details-expenses.vue';
|
||||||
|
import ShiftCrudDialog from '../components/shift/shift-crud-dialog.vue';
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
const { locale, t } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
|
|
@ -26,13 +28,13 @@ const expenses_data = ref<PayPeriodExpenses | null>(null);
|
||||||
|
|
||||||
const notify_error = (err: number) => {
|
const notify_error = (err: number) => {
|
||||||
const e = err as any;
|
const e = err as any;
|
||||||
error_banner.value = (e instanceof ExpensesApiError && t(e.message)) || e?.message || 'Unknown error';
|
expenses_error.value = (e instanceof ExpensesApiError && t(e.message)) || e?.message || 'Unknown error';
|
||||||
};
|
};
|
||||||
|
|
||||||
const open_expenses_dialog = async () => {
|
const open_expenses_dialog = async () => {
|
||||||
show_expenses_dialog.value = true;
|
show_expenses_dialog.value = true;
|
||||||
is_loading_expenses.value = true;
|
is_loading_expenses.value = true;
|
||||||
error_banner.value = null;
|
expenses_error.value = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await get_pay_period_expenses(
|
const data = await get_pay_period_expenses(
|
||||||
|
|
@ -62,7 +64,7 @@ const on_save_expenses = async (payload: {
|
||||||
expenses: TimesheetExpense[];
|
expenses: TimesheetExpense[];
|
||||||
}) => {
|
}) => {
|
||||||
is_loading_expenses.value = true;
|
is_loading_expenses.value = true;
|
||||||
error_banner.value = null;
|
expenses_error.value = null;
|
||||||
|
|
||||||
try{
|
try{
|
||||||
const updated = await put_pay_period_expenses(
|
const updated = await put_pay_period_expenses(
|
||||||
|
|
@ -129,6 +131,10 @@ onMounted( async () => {
|
||||||
await loadByDate(date.formatDate(new Date(), 'YYYY-MM-DD' ));
|
await loadByDate(date.formatDate(new Date(), 'YYYY-MM-DD' ));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onShiftSaved = async () => {
|
||||||
|
await timesheet_store.getTimesheetsByPayPeriodAndEmail(auth_store.user.email);
|
||||||
|
}
|
||||||
|
|
||||||
type FormMode = 'create' | 'edit' | 'delete';
|
type FormMode = 'create' | 'edit' | 'delete';
|
||||||
|
|
||||||
const is_dialog_open = ref<boolean>(false);
|
const is_dialog_open = ref<boolean>(false);
|
||||||
|
|
@ -136,32 +142,16 @@ const form_mode = ref<FormMode>('create');
|
||||||
const selected_date = ref<string>('');
|
const selected_date = ref<string>('');
|
||||||
const old_shift_ref = ref<ShiftPayload | undefined>(undefined);
|
const old_shift_ref = ref<ShiftPayload | undefined>(undefined);
|
||||||
|
|
||||||
const start_time = ref<string>('');
|
|
||||||
const end_time = ref<string>('');
|
|
||||||
const type = ref<string>('');
|
|
||||||
const is_remote = ref<boolean>(false);
|
|
||||||
const comment = ref<string>('');
|
|
||||||
|
|
||||||
const open_create_dialog = (iso_date: string) => {
|
const open_create_dialog = (iso_date: string) => {
|
||||||
form_mode.value = 'create';
|
form_mode.value = 'create';
|
||||||
selected_date.value = iso_date;
|
selected_date.value = iso_date;
|
||||||
old_shift_ref.value = undefined;
|
old_shift_ref.value = undefined;
|
||||||
start_time.value = '';
|
|
||||||
end_time.value = '';
|
|
||||||
type.value = '';
|
|
||||||
is_remote.value = false;
|
|
||||||
comment.value = '';
|
|
||||||
is_dialog_open.value = true;
|
is_dialog_open.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const open_edit_dialog = (iso_date: string, shift: any) => {
|
const open_edit_dialog = (iso_date: string, shift: any) => {
|
||||||
form_mode.value = 'edit';
|
form_mode.value = 'edit';
|
||||||
selected_date.value = iso_date;
|
selected_date.value = iso_date;
|
||||||
start_time.value = shift.start_time,
|
|
||||||
end_time.value = shift.end_time,
|
|
||||||
type.value = shift.type,
|
|
||||||
is_remote.value = shift.is_remote,
|
|
||||||
comment.value = shift.comment,
|
|
||||||
is_dialog_open.value = true;
|
is_dialog_open.value = true;
|
||||||
old_shift_ref.value = {
|
old_shift_ref.value = {
|
||||||
start_time: shift.start_time,
|
start_time: shift.start_time,
|
||||||
|
|
@ -185,65 +175,13 @@ const open_delete_dialog = (iso_date: string, shift: any) => {
|
||||||
is_dialog_open.value = true;
|
is_dialog_open.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const build_new_shift_payload = () => {
|
|
||||||
const base = {
|
|
||||||
start_time: start_time.value,
|
|
||||||
end_time: end_time.value,
|
|
||||||
type: type.value,
|
|
||||||
is_remote: !!is_remote.value,
|
|
||||||
};
|
|
||||||
const trimmed_comment = (comment.value ?? '').trim();
|
|
||||||
return {
|
|
||||||
...base,
|
|
||||||
...(trimmed_comment.length > 0 ? { comment: trimmed_comment }: {}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const is_submitting = ref<boolean>(false);
|
|
||||||
const error_banner = ref<string|null>(null);
|
|
||||||
const conflicts = ref<Array<{start_time: string; end_time: string; type: string }>>([]);
|
|
||||||
|
|
||||||
const submit_dialog = async () => {
|
|
||||||
error_banner.value = null;
|
|
||||||
conflicts.value = [];
|
|
||||||
is_submitting.value = true;
|
|
||||||
|
|
||||||
try {
|
const expenses_error = ref<string|null>(null);
|
||||||
const email = auth_store.user.email;
|
|
||||||
const date_iso = selected_date.value;
|
|
||||||
let body: UpsertShiftsBody;
|
|
||||||
|
|
||||||
if(form_mode.value === 'create') {
|
|
||||||
body = { new_shift: build_new_shift_payload() };
|
|
||||||
} else if (form_mode.value === 'edit') {
|
|
||||||
body = { old_shift: old_shift_ref.value!, new_shift: build_new_shift_payload() };
|
|
||||||
} else {
|
|
||||||
body = { old_shift: old_shift_ref.value! };
|
|
||||||
}
|
|
||||||
|
|
||||||
await upsert_shifts_by_date(email, date_iso, body);
|
|
||||||
await timesheet_store.getTimesheetsByPayPeriodAndEmail(email);
|
|
||||||
|
|
||||||
close_dialog();
|
|
||||||
is_dialog_open.value = false;
|
|
||||||
} catch (e:any) {
|
|
||||||
const status = e?.status_code ?? e?.response?.status ?? 500;
|
|
||||||
if (status === 404) {
|
|
||||||
error_banner.value = 'Ce quart a été modifié ou supprimé. Rafraichissez la page.';
|
|
||||||
} else if (status === 409) {
|
|
||||||
error_banner.value = 'Chevauchement détecté avec un autre quart';
|
|
||||||
} else if (status === 422) {
|
|
||||||
error_banner.value = 'Certains champs sont invalides. Vérifiez le formulaire.';
|
|
||||||
} else {
|
|
||||||
error_banner.value = e?.message || 'Erreur inconnue';
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
is_submitting.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const close_dialog = () => {
|
const close_dialog = () => {
|
||||||
error_banner.value = null;
|
expenses_error.value = null;
|
||||||
is_dialog_open.value = false;
|
is_dialog_open.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -257,7 +195,7 @@ const on_request_delete = async ({ date, shift }: { date: string; shift: any })
|
||||||
<q-page padding class="q-pa-md bg-secondary" >
|
<q-page padding class="q-pa-md bg-secondary" >
|
||||||
<!-- title and dates -->
|
<!-- title and dates -->
|
||||||
<div class="text-h4 row justify-center text-center q-mt-lg text-uppercase text-weight-bolder text-grey-8">
|
<div class="text-h4 row justify-center text-center q-mt-lg text-uppercase text-weight-bolder text-grey-8">
|
||||||
{{ $t('pageTitles.timeSheets') }}
|
{{ $t('timesheet.title') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="row items-center justify-center q-py-none q-my-none">
|
<div class="row items-center justify-center q-py-none q-my-none">
|
||||||
<div
|
<div
|
||||||
|
|
@ -270,7 +208,7 @@ const on_request_delete = async ({ date, shift }: { date: string; shift: any })
|
||||||
class="text-grey-8 text-uppercase q-mx-md"
|
class="text-grey-8 text-uppercase q-mx-md"
|
||||||
:class="$q.screen.lt.md ? 'text-weight-medium text-caption' : 'text-weight-bold'"
|
:class="$q.screen.lt.md ? 'text-weight-medium text-caption' : 'text-weight-bold'"
|
||||||
>
|
>
|
||||||
{{ $t('timesheet.dateRangesTo') }}
|
{{ $t('timesheet.date_ranges_to') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="text-primary text-uppercase text-center text-weight-bold"
|
class="text-primary text-uppercase text-center text-weight-bold"
|
||||||
|
|
@ -312,7 +250,6 @@ const on_request_delete = async ({ date, shift }: { date: string; shift: any })
|
||||||
@request-add="on_request_add"
|
@request-add="on_request_add"
|
||||||
@request-edit="on_request_edit"
|
@request-edit="on_request_edit"
|
||||||
@request-delete="on_request_delete"
|
@request-delete="on_request_delete"
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<q-inner-loading :showing="timesheet_store.is_loading" color="primary"/>
|
<q-inner-loading :showing="timesheet_store.is_loading" color="primary"/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
@ -330,8 +267,15 @@ const on_request_delete = async ({ date, shift }: { date: string; shift: any })
|
||||||
<q-inner-loading :showing="is_loading_expenses">
|
<q-inner-loading :showing="is_loading_expenses">
|
||||||
<q-spinner size="32px"/>
|
<q-spinner size="32px"/>
|
||||||
</q-inner-loading>
|
</q-inner-loading>
|
||||||
|
<q-banner
|
||||||
|
v-if="expenses_error"
|
||||||
|
dense
|
||||||
|
class="bg-red-2 text-negative q-mt-sm"
|
||||||
|
>
|
||||||
|
{{ expenses_error }}
|
||||||
|
</q-banner>
|
||||||
|
|
||||||
<TimesheetExpense
|
<TimesheetDetailsExpenses
|
||||||
v-if="expenses_data"
|
v-if="expenses_data"
|
||||||
:pay_period_no="expenses_data.pay_period_no"
|
:pay_period_no="expenses_data.pay_period_no"
|
||||||
:pay_year="expenses_data.pay_year"
|
:pay_year="expenses_data.pay_year"
|
||||||
|
|
@ -344,114 +288,17 @@ const on_request_delete = async ({ date, shift }: { date: string; shift: any })
|
||||||
/>
|
/>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
<!-- create/edit/delete shifts dialog -->
|
|
||||||
<q-dialog
|
<!-- shift crud dialog -->
|
||||||
|
<ShiftCrudDialog
|
||||||
v-model="is_dialog_open"
|
v-model="is_dialog_open"
|
||||||
persistent
|
:mode="form_mode"
|
||||||
transition-show="fade"
|
:date-iso="selected_date"
|
||||||
transition-hide="fade"
|
:email="auth_store.user.email"
|
||||||
>
|
:initial-shift="old_shift_ref || null"
|
||||||
<q-card class="q-pa-md">
|
:shift-options="shift_options"
|
||||||
<div class="row items-center q-mb-sm">
|
@close="close_dialog"
|
||||||
<q-icon name="schedule" size="24px" class="q-mr-sm"/>
|
@saved="onShiftSaved"
|
||||||
<div class="text-h6">
|
|
||||||
{{ form_mode === 'create' ? $t('timesheet.add_shift') : form_mode === 'edit' ? $t('timesheet.edit_shift') : $t('timesheet.delete_shift') }}
|
|
||||||
</div>
|
|
||||||
<q-space/>
|
|
||||||
<q-badge outline color="primary"> {{ selected_date }}</q-badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-separator spaced/>
|
|
||||||
|
|
||||||
<div v-if="form_mode !== 'delete'" class="column q-gutter-md">
|
|
||||||
<div class="row ">
|
|
||||||
<div class="col">
|
|
||||||
<q-input
|
|
||||||
v-model="start_time"
|
|
||||||
:label="$t('timesheet.fields.start')"
|
|
||||||
filled dense
|
|
||||||
inputmode="numeric"
|
|
||||||
mask="##:##"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<q-input
|
|
||||||
v-model="end_time"
|
|
||||||
:label="$t('timesheet.fields.end')"
|
|
||||||
filled dense
|
|
||||||
inputmode="numeric"
|
|
||||||
mask="##:##"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center">
|
|
||||||
<q-select
|
|
||||||
v-model="type"
|
|
||||||
options-dense
|
|
||||||
:options="shift_options"
|
|
||||||
:label="$t('timesheet.shift_types_label')"
|
|
||||||
class="col"
|
|
||||||
color="primary"
|
|
||||||
filled dense
|
|
||||||
hide-dropdown-icon
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
/>
|
|
||||||
<q-toggle
|
|
||||||
v-model="is_remote"
|
|
||||||
:label="$t('timesheet.shift_types.REMOTE')"
|
|
||||||
class="col-auto" />
|
|
||||||
</div>
|
|
||||||
<q-input
|
|
||||||
v-model="comment"
|
|
||||||
type="textarea"
|
|
||||||
autogrow filled dense
|
|
||||||
:label="$t('timesheet.fields.header_comment')"
|
|
||||||
:counter="true" :maxlength="512" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="q-pa-md">
|
|
||||||
{{ $t('timesheet.delete_confirmation_msg') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="error_banner" class="q-mt-md">
|
|
||||||
<q-banner dense class="bg-red-2 text-negative">{{ error_banner }}</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="close_dialog" />
|
|
||||||
<q-btn
|
|
||||||
v-if="form_mode === 'delete'"
|
|
||||||
outline color="negative"
|
|
||||||
icon="cancel"
|
|
||||||
:label="$t('timesheet.delete_button')"
|
|
||||||
:loading="is_submitting"
|
|
||||||
@click="submit_dialog"
|
|
||||||
/>
|
|
||||||
<q-btn
|
|
||||||
v-else
|
|
||||||
color="primary"
|
|
||||||
icon="save_alt"
|
|
||||||
:label="$t('timesheet.save_button')"
|
|
||||||
:loading="is_submitting"
|
|
||||||
@click="submit_dialog"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
Loading…
Reference in New Issue
Block a user