Merge pull request 'dev/matthieu/timesheet-form' (#18) from dev/matthieu/timesheet-form into main
Reviewed-on: Targo/targo_frontend#18
This commit is contained in:
commit
4470c855cf
|
|
@ -160,10 +160,12 @@ export default {
|
|||
},
|
||||
},
|
||||
expense: {
|
||||
add_expense:"Add Expense",
|
||||
amount:"Amount",
|
||||
date:"Date",
|
||||
empty_list:"No registered expenses",
|
||||
add_expense:'Add Expense',
|
||||
amount:'Amount',
|
||||
date:'Date',
|
||||
empty_list:'No registered expenses',
|
||||
employee_comment:'Comment',
|
||||
supervisor_comment:'Supervisor note',
|
||||
errors: {
|
||||
date_required_or_invalid:"the date is missing or invalid",
|
||||
comment_required:"A comment required",
|
||||
|
|
|
|||
|
|
@ -160,10 +160,12 @@ export default {
|
|||
},
|
||||
},
|
||||
expense: {
|
||||
add_expense:"Ajouter une dépense",
|
||||
amount:"Montant",
|
||||
date:"Date",
|
||||
empty_list:"Aucun dépense enregistrée",
|
||||
add_expense:'Ajouter une dépense',
|
||||
amount:'Montant',
|
||||
date:'Date',
|
||||
empty_list:'Aucun dépense enregistrée',
|
||||
employee_comment:'Commentaire',
|
||||
supervisor_comment:'Note du Superviseur',
|
||||
errors: {
|
||||
date_required_or_invalid:"La date est manquante ou invalide",
|
||||
comment_required:"un commentaire est requis",
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@
|
|||
import { computed, ref } from 'vue';
|
||||
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 { useI18n } from 'vue-i18n';
|
||||
// import { date } from 'quasar';
|
||||
import { COMMENT_MAX_LENGTH } from '../../composables/use-shift-api';
|
||||
|
||||
/* eslint-disable */
|
||||
const { t } = useI18n();
|
||||
|
||||
//props
|
||||
const props = defineProps<{
|
||||
|
|
@ -31,27 +30,28 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
//q-select mapper
|
||||
const type_options = computed(()=> EXPENSE_TYPE.map((val)=> ({
|
||||
label: t(`timesheet.expense.types.${val}`, val),
|
||||
value: val,
|
||||
})));
|
||||
const type_options = computed(()=> EXPENSE_TYPE.map( val => ({ label: val, value: val })));
|
||||
|
||||
//refs & states
|
||||
const items = ref<TimesheetExpense[]>(Array.isArray(props.initial_expenses) ? props.initial_expenses.map(normalize_expense): []);
|
||||
const formRef = ref<InstanceType<any> | null>(null);
|
||||
const triedSubmit = ref(false);
|
||||
|
||||
const DEFAULT_TYPE: ExpenseType = 'EXPENSES'
|
||||
|
||||
const draft = ref<Partial<TimesheetExpense>>({
|
||||
date:'',
|
||||
type: 'EXPENSES',
|
||||
type: DEFAULT_TYPE,
|
||||
comment:'',
|
||||
});
|
||||
|
||||
// computeds
|
||||
const totals = computed(()=> compute_expense_totals(items.value));
|
||||
const remaining_comment_chars = computed(()=> {
|
||||
const comment = String(draft.value.comment ?? '');
|
||||
return COMMENT_MAX_LENGTH - comment.length;
|
||||
});
|
||||
const is_readonly = computed(()=> !!props.is_approved);
|
||||
const showMileage = computed(()=> (draft.value.type as string) === 'MILEAGE');
|
||||
const showAmount = computed(()=> !showMileage.value);
|
||||
|
||||
//actions
|
||||
//helpers
|
||||
const reset_draft = () => {
|
||||
draft.value.date = '';
|
||||
draft.value.type = 'EXPENSES';
|
||||
|
|
@ -60,10 +60,20 @@ const reset_draft = () => {
|
|||
draft.value.comment = '';
|
||||
};
|
||||
|
||||
const set_draft_type = (value: ExpenseType) => {
|
||||
draft.value.type = value;
|
||||
if (value === 'MILEAGE') {
|
||||
delete draft.value.amount;
|
||||
} else {
|
||||
delete draft.value.mileage;
|
||||
}
|
||||
};
|
||||
|
||||
//actions
|
||||
const add_draft_as_item = () => {
|
||||
const candidate: TimesheetExpense = normalize_expense({
|
||||
date: String(draft.value.date ?? '').trim(),
|
||||
type: String(draft.value.type ?? '').trim(),
|
||||
date: draft.value.date,
|
||||
type: normType(draft.value.type),
|
||||
...(typeof draft.value.amount === 'number' ? { amount: draft.value.amount }: {}),
|
||||
...(typeof draft.value.mileage === 'number' ? { mileage: draft.value.mileage }: {}),
|
||||
comment: String(draft.value.comment ?? '').trim(),
|
||||
|
|
@ -117,79 +127,157 @@ const on_save = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const on_form_submit = async () => {
|
||||
triedSubmit.value = true;
|
||||
const ok = await formRef.value?.validate(true);
|
||||
if(!ok) return;
|
||||
add_draft_as_item();
|
||||
};
|
||||
|
||||
const on_close = () => emit('close');
|
||||
|
||||
|
||||
//read-only guard for supervisor comment and approved expenses
|
||||
const is_readonly = computed(()=> !!props.is_approved);
|
||||
|
||||
|
||||
const set_draft_type = (value: ExpenseType) => (draft.value.type = value);
|
||||
const set_draft_amount = (value: number | null) => {
|
||||
if(value === null || value === undefined || Number.isNaN(Number(value))) {
|
||||
delete draft.value.amount;
|
||||
} else {
|
||||
draft.value.amount = Number(value);
|
||||
}
|
||||
//icons managament
|
||||
type ExpensesType = 'MILEAGE' | 'EXPENSES' | 'PER_DIEM' | 'PRIME_GARDE' | string;
|
||||
const normType = (type: unknown) => String(type ?? '').trim().toUpperCase();
|
||||
const expenseTypeIcon = (type: ExpensesType) => {
|
||||
const t = normType(type);
|
||||
const map: Record<string, string> = {
|
||||
MILEAGE: 'time_to_leave',
|
||||
EXPENSES: 'receipt_long',
|
||||
PER_DIEM: 'hotel',
|
||||
PRIME_GARDE: 'admin_panel_settings',
|
||||
};
|
||||
const set_draft_mileage = (value: number | null) => {
|
||||
if(value === null || value === undefined || Number.isNaN(Number(value))) {
|
||||
delete draft.value.mileage;
|
||||
} else {
|
||||
draft.value.mileage = Number(value);
|
||||
}
|
||||
return map[String(t)] ?? 'help_outline';
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card class="q-pa-md q-gutter-md" flat bordered>
|
||||
<div >
|
||||
<!-- header (title with totals)-->
|
||||
<div class="row items-center justify-between">
|
||||
<div class="text-h6"> {{ $t('timesheet.expense.title') }}</div>
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<q-badge :label="$t('timesheet.expense.total_amount') + ': ' + totals.amount.toFixed(2)"/>
|
||||
<q-badge :label="$t('timesheet.expense.total_mileage') + ': ' + totals.mileage.toFixed(2)"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator/>
|
||||
|
||||
<q-item class="row justify-between">
|
||||
<q-item-label header class="text-h6 col-auto">
|
||||
{{ $t('timesheet.expense.title') }}
|
||||
</q-item-label>
|
||||
<q-item-section class="items-center col-auto">
|
||||
<q-badge lines="1" class="q-pa-sm q-px-md" :label="$t('timesheet.expense.total_amount') + ': ' + totals.amount.toFixed(2)"/>
|
||||
<q-separator spaced/>
|
||||
<q-badge lines="2" class="q-pa-sm q-px-md" :label="$t('timesheet.expense.total_mileage') + ': ' + totals.mileage.toFixed(1)"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<!-- liste des dépenses pré existantes -->
|
||||
<div class="column q-gutter-sm">
|
||||
<div
|
||||
v-if="items.length === 0"
|
||||
class="text-italic text-secondary"
|
||||
>{{ $t('timesheet.expense.empty_list') }}
|
||||
</div>
|
||||
|
||||
<q-card
|
||||
v-for="(expense, index) in items"
|
||||
:key="index"
|
||||
flat
|
||||
<q-list
|
||||
padding
|
||||
bordered
|
||||
class="q-pa-sm"
|
||||
class="rounded-borders"
|
||||
>
|
||||
<!-- date section -->
|
||||
<div class="row items-start q-col-gutter-md">
|
||||
<div class="col-12 col-md-2">
|
||||
<div class="text-caption text-secondary">{{ $t('timesheet.expense.date') }} </div>
|
||||
<div class="text-body2"> {{ expense.date }}</div>
|
||||
<q-item-label v-if="items.length === 0" class="text-italic q-px-sm">
|
||||
{{ $t('timesheet.expense.empty_list') }}
|
||||
</q-item-label>
|
||||
<q-item
|
||||
style="border: solid 1px lightgrey; border-radius: 7px;"
|
||||
v-for="(expense, index) in items" :key="index"
|
||||
class="q-my-xs shadow-1"
|
||||
>
|
||||
<!-- avatar type icon section -->
|
||||
<q-item-section avatar>
|
||||
<q-icon :name="expenseTypeIcon(expense.type)" color="primary"/>
|
||||
</q-item-section>
|
||||
|
||||
<!-- amount or mileage section -->
|
||||
<q-item-section top>
|
||||
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
||||
{{ expense.mileage?.toFixed(1) }} km
|
||||
</q-item-label>
|
||||
<q-item-label v-else>
|
||||
{{ expense.amount?.toFixed(2) }} $
|
||||
</q-item-label>
|
||||
|
||||
<!-- date label -->
|
||||
<q-item-label caption lines="2">
|
||||
{{ $d(new Date(expense.date + 'T00:00:00'), { year:'numeric', month:'short', day: 'numeric', weekday: 'short'}) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<!-- attachment file icon -->
|
||||
<q-item-section side>
|
||||
<q-btn
|
||||
push
|
||||
dense
|
||||
size="md"
|
||||
color="primary"
|
||||
class="q-mx-lg"
|
||||
icon="attach_file"
|
||||
/>
|
||||
</q-item-section>
|
||||
|
||||
<!-- comment section -->
|
||||
<q-item-section top>
|
||||
<q-item-label lines="1">
|
||||
{{ $t('timesheet.expense.employee_comment') }}
|
||||
</q-item-label>
|
||||
<q-item-label caption lines="2">
|
||||
{{ expense.comment }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<!-- supervisor comment section -->
|
||||
<q-item-section top>
|
||||
<q-item-label lines="1">
|
||||
{{ $t('timesheet.expense.supervisor_comment') }}
|
||||
</q-item-label>
|
||||
<q-item-label v-if="expense.supervisor_comment" caption lines="2">
|
||||
{{ expense.supervisor_comment }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<!-- delete btn -->
|
||||
<q-item-section side>
|
||||
<q-btn
|
||||
v-if="!is_readonly"
|
||||
push
|
||||
dense
|
||||
size="xs"
|
||||
color="negative"
|
||||
icon="close"
|
||||
@click="remove_item_at(index)"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<q-form
|
||||
ref="formRef"
|
||||
flat
|
||||
v-if="!is_readonly"
|
||||
@submit.prevent="on_form_submit"
|
||||
>
|
||||
<div class="text-subtitle2 q-py-sm">
|
||||
{{ $t('timesheet.expense.add_expense')}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- expense type section -->
|
||||
<div class="row items-start q-col-gutter-md">
|
||||
<div class="col-12 col-md-2">
|
||||
<div class="text-body2"> {{ $t('timesheet.expense.types') + expense.type, expense.type }} </div>
|
||||
<div class="text-body2">
|
||||
{{ $t('timesheet.expense.types') + expense.type, expense.type }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-3">
|
||||
<div class="row justify-between">
|
||||
|
||||
<!-- date selection input -->
|
||||
<q-input
|
||||
v-model="draft.date"
|
||||
type="date"
|
||||
dense
|
||||
filled
|
||||
class="col- q-px-xs"
|
||||
color="primary"
|
||||
:label="$t('timesheet.expense.date')"
|
||||
clearable
|
||||
>
|
||||
</q-input>
|
||||
|
||||
<!-- expenses type selection -->
|
||||
<q-select
|
||||
v-model="draft.type"
|
||||
:options="type_options"
|
||||
:option-label="opt => $t(`timesheet.expense.types.${opt.label}`)"
|
||||
filled
|
||||
dense
|
||||
class="col-auto q-px-xs"
|
||||
color="primary"
|
||||
emit-value
|
||||
map-options
|
||||
|
|
@ -197,134 +285,109 @@ const set_draft_mileage = (value: number | null) => {
|
|||
:rules="[ val => !! val || $t('timesheet.expense.errors.type_required')]"
|
||||
@update:model-value="val => set_draft_type(val as ExpenseType)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- amount section -->
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="text-caption text-secondary"> {{ $t('timesheet.expense.amount') }}</div>
|
||||
<div class="text-body2">
|
||||
<span v-if="typeof expense.amount === 'number'">{{ expense.amount.toFixed(2) }}</span>
|
||||
<span v-else class="text-grey-6">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- comment section -->
|
||||
<div class="col-12 col-md">
|
||||
<div class="text-caption text-secondary">{{ $t('timesheet.expense.employee_comment') }}</div>
|
||||
<div class="text-body2">{{ expense.comment }}</div>
|
||||
</div>
|
||||
|
||||
<!-- supervisor comment section -->
|
||||
<div class="col-12 col-md">
|
||||
<div class="text-caption text-secondary">{{ $t('timesheet.expense.supervisor_comment') }}</div>
|
||||
<div class="text-body2">
|
||||
<span v-if="expense.supervisor_comment">{{ expense.supervisor_comment }}</span>
|
||||
<span v-else class="text-grey-6">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- delete btn -->
|
||||
<div class="col-auto q-ml-auto">
|
||||
<q-btn
|
||||
v-if="!is_readonly"
|
||||
flat
|
||||
round
|
||||
size="sm"
|
||||
color="negative"
|
||||
icon="delete"
|
||||
:aria-label="$t('timesheet.delete_button')"
|
||||
@click="remove_item_at(index)"
|
||||
/>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-separator spaced/>
|
||||
|
||||
<div v-if="!is_readonly" class="column q-gutter-sm">
|
||||
<div class="text-subtitle2">{{ $t('timesheet.expense.add_expense')}}</div>
|
||||
<!-- add a new expense btn -->
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-sm-3">
|
||||
<!-- date selection input -->
|
||||
<q-input
|
||||
v-model="draft.date"
|
||||
filled
|
||||
color="primary"
|
||||
type="date"
|
||||
:label="$t('timesheet.expense.date')"
|
||||
:rules="[ value =>!!value || $t('timesheet.expense.errors.date_required_or_invalid')]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-3">
|
||||
<!-- amount input -->
|
||||
<template v-if="showAmount">
|
||||
<q-input
|
||||
:model-value="draft.amount"
|
||||
@update:model-value="val => set_draft_amount(val as any)"
|
||||
key="amount"
|
||||
v-model.number="draft.amount"
|
||||
filled
|
||||
input-class="text-right"
|
||||
dense
|
||||
clearable
|
||||
color="primary"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
:disable="(draft.type as string) === 'MILEAGE'"
|
||||
class="col-auto q-px-xs"
|
||||
:label="$t('timesheet.expense.amount')"
|
||||
:hint="$t('timesheet.expense.hints.amount_or_mileage')"
|
||||
suffix="$"
|
||||
lazy-rules="ondemand"
|
||||
:rules="[
|
||||
value => (value !== undefined && value !== null && value !== '')
|
||||
|| $t('timesheet.expense.errors.amount_required_for_type')
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="col-12 col-sm-3">
|
||||
<!-- mileage input -->
|
||||
<template v-else>
|
||||
<q-input
|
||||
:model-value="draft.mileage"
|
||||
@update:model-value="val => set_draft_mileage(val as any)"
|
||||
key="mileage"
|
||||
v-model.number="draft.mileage"
|
||||
filled
|
||||
input-class="text-right"
|
||||
dense
|
||||
clearable
|
||||
color="primary"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
:disable="(draft.type as string) !== 'MILEAGE'"
|
||||
class="col-auto q-px-xs"
|
||||
:label="$t('timesheet.expense.mileage')"
|
||||
:hint="$t('timesheet.expense.hints.amount_or_mileage')"
|
||||
suffix="km"
|
||||
lazy-rules="ondemand"
|
||||
:rules="[
|
||||
value => (value !== undefined && value !== null && value !== '')
|
||||
|| $t('timesheet.expense.errors.mileage_required_for_type')
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="col-12">
|
||||
<!-- employee comment input -->
|
||||
<q-input
|
||||
v-model="draft.comment"
|
||||
filled
|
||||
color="primary"
|
||||
type="textarea"
|
||||
type="text"
|
||||
class="col q-px-sm"
|
||||
dense
|
||||
clearable
|
||||
:label="$t('timesheet.expense.employee_comment')"
|
||||
:counter="true"
|
||||
:maxlength="COMMENT_MAX_LENGTH"
|
||||
lazy-rules="ondemand"
|
||||
:rules="[
|
||||
value => (value && String(value).trim().length) || $t('timesheet.expense.errors.comment_required'),
|
||||
value => String(value || '').length <= COMMENT_MAX_LENGTH || $t('timesheet.expense.errors.comment_too_long')
|
||||
]"
|
||||
:hint="$t('timesheet.expense.hints.comment_required')"
|
||||
/>
|
||||
<div class="text-right text-caption text-secondary q-mt-xs">
|
||||
{{ remaining_comment_chars }} {{ $t('general.chars_left') }}
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
color="primary"
|
||||
class="q-px-sm row"
|
||||
>
|
||||
<div class="row column">
|
||||
<q-input
|
||||
v-model="draft.comment"
|
||||
type="text"
|
||||
readonly
|
||||
filled
|
||||
class="col-auto justify-end"
|
||||
>
|
||||
<q-icon
|
||||
name="attach_file"
|
||||
size="sm"
|
||||
class="col-auto justify-start"
|
||||
/>
|
||||
</q-input>
|
||||
|
||||
</div>
|
||||
</q-btn>
|
||||
|
||||
<!-- add btn section -->
|
||||
<div>
|
||||
<q-btn
|
||||
push
|
||||
dense
|
||||
color="primary"
|
||||
icon="add"
|
||||
size="sm"
|
||||
class="q-mt-sm q-ml-sm"
|
||||
type="submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<!-- add expense btn -->
|
||||
<q-btn
|
||||
color="primary"
|
||||
unelevated
|
||||
:label="$t('timesheet.expense.add_expense')"
|
||||
icon="add"
|
||||
@click="add_draft_as_item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-form>
|
||||
|
||||
<q-separator spaced/>
|
||||
|
||||
<div class="row justify-end q-gutter-sm">
|
||||
<div class="row col-auto justify-end">
|
||||
<!-- close btn -->
|
||||
<q-btn
|
||||
flat
|
||||
|
|
@ -341,5 +404,5 @@ const set_draft_mileage = (value: number | null) => {
|
|||
@click="on_save"
|
||||
/>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -261,19 +261,19 @@ const on_request_delete = async ({ date, shift }: { date: string; shift: any })
|
|||
persistent
|
||||
>
|
||||
<q-card
|
||||
class="q-pa=md"
|
||||
style="min-width:900px; max-width: 95vw;"
|
||||
class="q-pa-md column"
|
||||
style=" min-width: 70vw;"
|
||||
>
|
||||
<q-inner-loading :showing="is_loading_expenses">
|
||||
<q-spinner size="32px"/>
|
||||
</q-inner-loading>
|
||||
<q-banner
|
||||
<!-- <q-banner
|
||||
v-if="expenses_error"
|
||||
dense
|
||||
class="bg-red-2 text-negative q-mt-sm"
|
||||
class="bg-red-2 col-auto text-negative q-mt-sm"
|
||||
>
|
||||
{{ expenses_error }}
|
||||
</q-banner>
|
||||
</q-banner> -->
|
||||
|
||||
<TimesheetDetailsExpenses
|
||||
v-if="expenses_data"
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ export const EXPENSE_TYPE = [
|
|||
'PER_DIEM',
|
||||
'MILEAGE',
|
||||
'EXPENSES',
|
||||
'PRIME_DISPO',
|
||||
'PRIME_GARDE',
|
||||
] as const;
|
||||
|
||||
export type ExpenseType = typeof EXPENSE_TYPE[number];
|
||||
|
||||
export const TYPES_WITH_MILEAGE_ONLY: Readonly<ExpenseType[]> = ['MILEAGE'];
|
||||
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXPENSES', 'PRIME_DISPO']
|
||||
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXPENSES', 'PRIME_GARDE']
|
||||
Loading…
Reference in New Issue
Block a user