{{ $t('timesheet.expense.add_expense')}}
@@ -44,7 +58,7 @@ const emit = defineEmits<{
- {{ $t('timesheet.expense.employee_comment') }}
+ {{ $t('timesheet.expense.comment') }}
diff --git a/src/modules/timesheets/components/expenses/expense-list.vue b/src/modules/timesheets/components/expenses/expense-list.vue
index b16b310..9e5f12b 100644
--- a/src/modules/timesheets/components/expenses/expense-list.vue
+++ b/src/modules/timesheets/components/expenses/expense-list.vue
@@ -2,13 +2,14 @@
import type { TimesheetExpense } from '../../types/expense.interfaces';
import { expenseTypeIcon } from '../../utils/expense.util';
/* eslint-disable */
-const props = defineProps<{
+defineProps<{
items: TimesheetExpense[];
is_readonly: boolean;
}>();
-const emit = defineEmits<{
+defineEmits<{
(e: 'remove', index: number): void;
+ (e: 'edit' , index: number): void;
}>();
@@ -36,7 +37,12 @@ const emit = defineEmits<{
- {{ expense.mileage?.toFixed(1) }} km
+
+ {{ expense.mileage?.toFixed(1) }} km
+
+
+ {{ expense.amount?.toFixed(2) }} $
+
{{ expense.amount?.toFixed(2) }} $
@@ -78,7 +84,20 @@ const emit = defineEmits<{
{{ expense.supervisor_comment }}
-
+
+
+
+
+
+
@@ -89,7 +108,7 @@ const emit = defineEmits<{
size="xs"
color="negative"
icon="close"
- @click="emit('remove', index)"
+ @click="$emit('remove', index)"
/>
diff --git a/src/modules/timesheets/components/expenses/timesheet-details-expenses.vue b/src/modules/timesheets/components/expenses/timesheet-details-expenses.vue
index f229af9..40ba68a 100644
--- a/src/modules/timesheets/components/expenses/timesheet-details-expenses.vue
+++ b/src/modules/timesheets/components/expenses/timesheet-details-expenses.vue
@@ -1,68 +1,76 @@
@@ -112,21 +202,34 @@ const onClose = () => emit('close');
-
+
{{ $t('timesheet.expense.title') }}
-
-
-
-
-
+
+
+
+
+
emit('close');
:set-type="setType"
@submit="onFormSubmit"
/>
-
-
-
+
+
+
();
const emit = defineEmits<{
@@ -23,41 +23,41 @@ const emit = defineEmits<{
}>();
const isSubmitting = ref(false);
-const errorBanner = ref(null);
-const conflicts = ref>([]);
+const errorBanner = ref(null);
+const conflicts = ref>([]);
-const opened = defineModel ( { default: false });
-const startTime = defineModel ('startTime', { default: '' });
-const endTime = defineModel ('endTime' , { default: '' });
-const type = defineModel ('type' , { default: '' });
-const isRemote = defineModel ('isRemote' , { default: false });
-const comment = defineModel ('comment' , { default: '' });
+const opened = defineModel({ default: false });
+const startTime = defineModel('startTime', { default: '' });
+const endTime = defineModel('endTime', { default: '' });
+const type = defineModel('type', { default: '' });
+const isRemote = defineModel('isRemote', { default: false });
+const comment = defineModel('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();
+ 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,
+ end_time: endTime.value,
+ type: type.value,
+ is_remote: isRemote.value,
...(trimmed ? { comment: trimmed } : {}),
};
};
const onSubmit = async () => {
- errorBanner.value = null;
- conflicts.value = [];
+ errorBanner.value = null;
+ conflicts.value = [];
isSubmitting.value = true;
- try{
+ try {
let body: UpsertShiftsBody;
- if(props.mode === 'create') {
+ if (props.mode === 'create') {
body = { new_shift: buildNewShiftPayload() };
} else if (props.mode === 'edit') {
- if(!props.initialShift) throw new Error('Missing initial Shift for 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.');
@@ -70,11 +70,11 @@ const onSubmit = async () => {
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)=> ({
+ 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 ?? ''),
+ end_time: String(c.end_time ?? ''),
+ type: String(c.type ?? ''),
}));
} else {
conflicts.value = [];
@@ -84,92 +84,104 @@ const onSubmit = async () => {
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
+ //add conflicts.value error management
} finally {
isSubmitting.value = false;
}
}
const hydrateFromProps = () => {
- if(props.mode === 'edit' || props.mode === 'delete') {
- const shift = props.initialShift;
+ 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 ?? '';
+ 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 = '';
+ endTime.value = '';
+ type.value = '';
+ isRemote.value = false;
+ comment.value = '';
}
};
-const canSubmit = computed(() =>
+const canSubmit = computed(() =>
props.mode === 'delete' ||
(startTime.value.trim().length === 5 &&
- endTime.value.trim().length === 5 &&
- isShiftKey(type.value))
+ endTime.value.trim().length === 5 &&
+ isShiftKey(type.value))
);
watch(
- ()=> [opened.value, props.mode, props.initialShift, props.dateIso],
- ()=> { if (opened.value) hydrateFromProps();},
+ () => [opened.value, props.mode, props.initialShift, props.dateIso],
+ () => { if (opened.value) hydrateFromProps(); },
{ immediate: true }
);
-
-
-
+
+
+
-
+
-
+
- {{
- props.mode === 'create'
- ? $t('timesheet.shift.actions.add')
- : props.mode === 'edit'
- ? $t('timesheet.shift.actions.edit')
- : $t('timesheet.shift.actions.delete')
+ {{
+ props.mode === 'create'
+ ? $t('timesheet.shift.actions.add')
+ : props.mode === 'edit'
+ ? $t('timesheet.shift.actions.edit')
+ : $t('timesheet.shift.actions.delete')
}}
-
-
+
+
{{ props.dateIso }}
-
-
-
-
+
+
+
+
@@ -181,35 +193,55 @@ watch(
:label="$t('timesheet.shift.types.label')"
class="col"
color="primary"
- filled dense
+ filled
+ dense
hide-dropdown-icon
emit-value
map-options
/>
-
+ :label="$t('timesheet.shift.types.REMOTE')"
+ class="col-auto"
+ />
-
-
+
{{ $t('timesheet.shift.actions.delete_confirmation_msg') }}
-
-
{{ errorBanner }}
-
+
+
{{ errorBanner }}
+
Conflits :
- -
+
-
{{ c.start_time }}–{{ c.end_time }} ({{ c.type }})
@@ -219,28 +251,31 @@ watch(
- { opened = false; emit('close');}"
+ { opened = false; emit('close'); }"
/>
-
+
diff --git a/src/modules/timesheets/composables/api/use-expense-api.ts b/src/modules/timesheets/composables/api/use-expense-api.ts
index 899e3d0..3c96d95 100644
--- a/src/modules/timesheets/composables/api/use-expense-api.ts
+++ b/src/modules/timesheets/composables/api/use-expense-api.ts
@@ -4,11 +4,12 @@ import { normalizeExpense, validateExpenseUI } from "../../utils/expenses-valida
import type { ExpenseType } from "../../types/expense.types";
import { ExpensesApiError } from "../../types/expense-validation.interface";
import type {
- ExpensePayload,
- PayPeriodExpenses,
- TimesheetExpense,
- UpsertExpensesBody,
- UpsertExpensesResponse
+ ExpensePayload,
+ PayPeriodExpenses,
+ TimesheetExpense,
+ UpsertExpenseResult,
+ UpsertExpensesBody,
+ UpsertExpensesResponse
} from "../../types/expense.interfaces";
/* eslint-disable */
@@ -167,3 +168,58 @@ export const postPayPeriodExpenses = async (
}
};
+const resolveDateISO = (a?: ExpensePayload, b?: ExpensePayload): string => {
+ const d = a?.date || b?.date;
+ if(!d) throw new Error('date is required in payload');
+ return d;
+};
+
+const makeBody = (obj: {
+ old_expense?: ExpensePayload;
+ new_expense?: ExpensePayload;
+}) => obj;
+
+const postUpsert = async (email: string, date: string, body: {
+ old_expense?: ExpensePayload;
+ new_expense?: ExpensePayload;
+}): Promise
=> {
+
+ try {
+ const url = `/expenses/upsert/${encodeURIComponent(email)}/${date}`;
+ const { data } = await api.post(url, body, {
+ headers: { 'Content-Type': 'application/json'},
+ });
+ return data;
+ } catch(err:any) {
+ const status_code: number = err?.response?.status ?? 500;
+ const data = err?.response?.data ?? {};
+ throw new ExpensesApiError({
+ status_code,
+ error_code: data.error_code,
+ message: data.message || data.error || err.message,
+ context: data.context,
+ });
+ }
+};
+
+//create a new expense
+export const createExpenseByDate = async ( email: string, payload: ExpensePayload): Promise => {
+ const new_expense = normalizePayload(payload);
+ const date = resolveDateISO(new_expense);
+ return postUpsert(email, date, makeBody({ new_expense: new_expense }));
+};
+
+//update an expense
+export const updateExpenseByDate = async ( email: string, old_expense: ExpensePayload, new_expense: ExpensePayload): Promise => {
+ const old_exp = normalizePayload(old_expense);
+ const new_exp = normalizePayload(new_expense);
+ const date = resolveDateISO(new_exp, old_exp);
+ return postUpsert(email, date, makeBody({ old_expense: old_exp,new_expense: new_exp }));
+};
+
+//delete an expense
+export const deleteExpenseByDate = async (email: string, old_expense: ExpensePayload): Promise => {
+ const old = normalizePayload(old_expense);
+ const date = resolveDateISO(undefined, old);
+ return postUpsert(email, date, makeBody({ old_expense: old }));
+};
\ No newline at end of file
diff --git a/src/modules/timesheets/types/expense.interfaces.ts b/src/modules/timesheets/types/expense.interfaces.ts
index b4549f6..983a6d9 100644
--- a/src/modules/timesheets/types/expense.interfaces.ts
+++ b/src/modules/timesheets/types/expense.interfaces.ts
@@ -30,6 +30,7 @@ export interface PayPeriodExpenses {
}
}
+//used by expenses form, either amount or mileage, not both will be sent to the backend
export interface ExpensePayload{
date: string;
type: ExpenseType;
@@ -38,10 +39,25 @@ export interface ExpensePayload{
comment: string;
}
+//amount is required since mileage is returned in $ ( km * modifier )
+export interface ExpenseDay{
+ date: string;
+ type: ExpenseType;
+ amount: number;
+ mileage?: number;
+ comment: string;
+ is_approved: boolean;
+}
+
export interface UpsertExpensesBody {
expenses: ExpensePayload[];
}
export interface UpsertExpensesResponse {
data: PayPeriodExpenses;
+}
+
+export interface UpsertExpenseResult {
+ action: 'created' | 'updated' | 'deleted';
+ day: ExpenseDay[];
}
\ No newline at end of file
diff --git a/src/modules/timesheets/types/shift.interfaces.ts b/src/modules/timesheets/types/shift.interfaces.ts
index 055a4b4..328c3b6 100644
--- a/src/modules/timesheets/types/shift.interfaces.ts
+++ b/src/modules/timesheets/types/shift.interfaces.ts
@@ -1,4 +1,5 @@
-import type { ShiftKey, ShiftPayload, UpsertAction } from "./shift.types";
+import type { ShiftKey, ShiftPayload } from "./shift.types";
+import type { UpsertAction } from "./ui.types";
export interface Shift {
date: string;
diff --git a/src/modules/timesheets/types/shift.types.ts b/src/modules/timesheets/types/shift.types.ts
index db42291..cbe38ad 100644
--- a/src/modules/timesheets/types/shift.types.ts
+++ b/src/modules/timesheets/types/shift.types.ts
@@ -25,6 +25,3 @@ export type ShiftLegendItem = {
label_key: string;
text_color?: string;
};
-
-export type UpsertAction = 'created' | 'updated' | 'deleted';
-
diff --git a/src/modules/timesheets/types/ui.types.ts b/src/modules/timesheets/types/ui.types.ts
index d738779..37ddefa 100644
--- a/src/modules/timesheets/types/ui.types.ts
+++ b/src/modules/timesheets/types/ui.types.ts
@@ -3,4 +3,6 @@ export type FormMode = 'create' | 'edit' | 'delete';
export type PayPeriodLabel = {
start_date: string;
end_date: string;
-};
\ No newline at end of file
+};
+
+export type UpsertAction = 'created' | 'updated' | 'deleted';
\ No newline at end of file
diff --git a/src/modules/timesheets/utils/expense.util.ts b/src/modules/timesheets/utils/expense.util.ts
index 2ad7e3e..9439288 100644
--- a/src/modules/timesheets/utils/expense.util.ts
+++ b/src/modules/timesheets/utils/expense.util.ts
@@ -6,9 +6,9 @@ export const normExpenseType = (type: unknown): string =>
String(type ?? '').trim().toUpperCase();
const icon_map: Record = {
- MILEAGE: 'time_to_leave',
- EXPENSES: 'receipt_long',
- PER_DIEM: 'hotel',
+ MILEAGE: 'time_to_leave',
+ EXPENSES: 'receipt_long',
+ PER_DIEM: 'hotel',
PRIME_GARDE: 'admin_panel_settings',
};
@@ -43,11 +43,11 @@ export const computeExpenseTotals = (items: readonly TimesheetExpense[]): Expens
export const makeExpenseRules = (t: (key: string) => string, max_comment_char: number) => {
const isPresent = (val: unknown) => val !== undefined && val !== null && val !== '';
- const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
+ const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
- const amountRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.amount_required_for_type');
+ const amountRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.amount_required_for_type');
- const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.erros.mileage_required_for_type');
+ const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.mileage_required_for_type');
const commentRequired = (val: unknown) => (String(val ?? '').trim().length > 0) || t('timesheet.expense.errors.comment_required');
diff --git a/src/modules/timesheets/utils/expenses-validators.ts b/src/modules/timesheets/utils/expenses-validators.ts
index 392d776..9325caa 100644
--- a/src/modules/timesheets/utils/expenses-validators.ts
+++ b/src/modules/timesheets/utils/expenses-validators.ts
@@ -115,14 +115,3 @@ export const validateExpenseUI = (raw: TimesheetExpense, label: string = 'expens
});
}
};
-
-//totals per pay-period
-export const compute_expense_totals = (items: TimesheetExpense[]) => items.reduce(
- (acc, raw) => {
- const expense = normalizeExpense(raw);
- if(typeof expense.amount === 'number' && expense.amount > 0) acc.amount += expense.amount;
- if(typeof expense.mileage === 'number' && expense.mileage > 0) acc.mileage += expense.mileage;
- return acc;
- },
- { amount: 0, mileage: 0 }
-);