refactor(timesheet): finalize support for expenses, approval display, minor timesheet approval rework
This commit is contained in:
parent
88cdb9e5ff
commit
b436428a33
|
|
@ -15,15 +15,16 @@
|
|||
$primary : #30303A;
|
||||
$secondary : #DAE0E7;
|
||||
$accent : #0c9a3b;
|
||||
$accent2 : #0a7d32;
|
||||
|
||||
$dark-shadow-color : #173625;
|
||||
|
||||
$elevation-dark-umbra : rgba($dark-shadow-color, 1);
|
||||
$elevation-dark-penumbra : rgba($dark-shadow-color, 0.5);
|
||||
$elevation-dark-ambient : rgba($dark-shadow-color, 0.3);
|
||||
$elevation-dark-penumbra : rgba($dark-shadow-color, 0.75);
|
||||
$elevation-dark-ambient : rgba($dark-shadow-color, 0.53);
|
||||
|
||||
$dark-shadow-2 : 2px 3px $elevation-dark-umbra, 2px 3px 6px $elevation-dark-penumbra, 2px 3px 14px $elevation-dark-ambient;
|
||||
$layout-shadow-dark : 0 0 10px 5px rgba($dark-shadow-color, 0.5);
|
||||
$layout-shadow-dark : 0 0 5px 5px rgba($dark-shadow-color, 0.5);
|
||||
|
||||
$input-text-color : #455A64;
|
||||
$input-autofill-color : #AAD5C4;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export default {
|
|||
button: {
|
||||
connect: "connect",
|
||||
employee: "employee",
|
||||
facebook:"Facebook",
|
||||
facebook: "Facebook",
|
||||
remember_me: "remember me",
|
||||
},
|
||||
tooltip: {
|
||||
|
|
@ -76,7 +76,7 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
shared:{
|
||||
shared: {
|
||||
error: {
|
||||
no_data_found: "no data found",
|
||||
no_search_results: "no results matching search",
|
||||
|
|
@ -126,20 +126,20 @@ export default {
|
|||
},
|
||||
|
||||
timesheet: {
|
||||
page_header:"Timesheet",
|
||||
page_header: "Timesheet",
|
||||
nav_button: {
|
||||
calendar_date_picker:"Calendar",
|
||||
current_week:"This week",
|
||||
next_week:"Next week",
|
||||
previous_week:"Previous week",
|
||||
calendar_date_picker: "Calendar",
|
||||
current_week: "This week",
|
||||
next_week: "Next week",
|
||||
previous_week: "Previous week",
|
||||
},
|
||||
save_button:"Save",
|
||||
cancel_button:"Cancel",
|
||||
save_button: "Save",
|
||||
cancel_button: "Cancel",
|
||||
remote_button: "Remote work",
|
||||
delete_button: "Delete",
|
||||
shift: {
|
||||
actions: {
|
||||
add:"Add Shift",
|
||||
add: "Add Shift",
|
||||
edit: "Edit shift",
|
||||
delete: "Delete shift",
|
||||
delete_confirmation_msg: "Do you want to delete this shift completly?",
|
||||
|
|
@ -155,56 +155,50 @@ export default {
|
|||
VACATION: "Vacation",
|
||||
REMOTE: "Remote work",
|
||||
},
|
||||
errors: {
|
||||
not_found: "Shift not found",
|
||||
SHIFT_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: {
|
||||
start:"Start (HH:mm)",
|
||||
end:"End (HH:mm)",
|
||||
header_comment:"Shift`s comment",
|
||||
start: "Start (HH:mm)",
|
||||
end: "End (HH:mm)",
|
||||
header_comment: "Shift`s comment",
|
||||
textarea_comment: "Leave a comment here",
|
||||
},
|
||||
},
|
||||
expense: {
|
||||
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",
|
||||
comment_too_long:"Your comment is too long",
|
||||
amount_must_be_positive:"the amount cannot be under 0$",
|
||||
mileave_must_be_positive:"the mileage cannot be under 0",
|
||||
amount_xor_mileage:"you cannot enter an amount and a mileage for the same expense",
|
||||
mileage_required_for_type:"you need to enter a value for mileage when you enter an expense of that type",
|
||||
amount_required_for_type:"you need to enter a value for amount when you enter an expense of that type",
|
||||
},
|
||||
add_expense: 'Add Expense',
|
||||
amount: 'Amount',
|
||||
date: 'Date',
|
||||
empty_list: 'No registered expenses',
|
||||
employee_comment: 'Comment',
|
||||
supervisor_comment: 'Supervisor note',
|
||||
hints: {
|
||||
amount_or_mileage:"Either amount or mileage, not both",
|
||||
comment_required:"A comment required",
|
||||
attach_file:"Attach File"
|
||||
amount_or_mileage: "Either amount or mileage, not both",
|
||||
comment_required: "A comment required",
|
||||
attach_file: "Attach File"
|
||||
},
|
||||
mileage:"mileage",
|
||||
open_btn:"list of expenses",
|
||||
title:"List of all expenses",
|
||||
total_amount:"Total amount",
|
||||
total_mileage:"Total mileage",
|
||||
type:"Type",
|
||||
mileage: "mileage",
|
||||
open_btn: "list of expenses",
|
||||
title: "List of all expenses",
|
||||
total_amount: "Total amount",
|
||||
total_mileage: "Total mileage",
|
||||
type: "Type",
|
||||
types: {
|
||||
PER_DIEM:"Per Diem",
|
||||
EXPENSES:"expense",
|
||||
MILEAGE:"mileage",
|
||||
ON_CALL:"on-call allowance",
|
||||
PER_DIEM: "Per Diem",
|
||||
EXPENSES: "expense",
|
||||
MILEAGE: "mileage",
|
||||
ON_CALL: "on-call allowance",
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
INVALID_SHIFT_TIME: "In and Out shift times are reversed",
|
||||
SHIFT_OVERLAP: "An overlaps occured between 2 or more shifts",
|
||||
INVALID_SHIFT: "A shift contains missing or corrupted data",
|
||||
SHIFT_NOT_FOUND: "Shift missing or deleted",
|
||||
PAY_PERIOD_NOT_FOUND: "No pay period matching given dates",
|
||||
EMPLOYEE_NOT_FOUND: "No employee matching current login details",
|
||||
INVALID_TIMESHEET: "Timesheet data is missing or corrupted",
|
||||
TIMESHEET_NOT_FOUND: "No timesheet found with provided data",
|
||||
INVALID_EXPENSE: "An expense contains missing or corrupted data",
|
||||
EXPENSE_NOT_FOUND: "No expense found with provided data",
|
||||
},
|
||||
},
|
||||
|
||||
timesheet_approvals: {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export default {
|
|||
button: {
|
||||
connect: "connecter",
|
||||
employee: "employé",
|
||||
facebook:"Facebook",
|
||||
facebook: "Facebook",
|
||||
remember_me: "rester connecté",
|
||||
},
|
||||
tooltip: {
|
||||
|
|
@ -127,20 +127,20 @@ export default {
|
|||
},
|
||||
|
||||
timesheet: {
|
||||
page_header:"Carte de temps",
|
||||
page_header: "Carte de temps",
|
||||
nav_button: {
|
||||
calendar_date_picker:"Calendrier",
|
||||
current_week:"Semaine actuelle",
|
||||
next_week:"Prochaine semaine",
|
||||
previous_week:"Semaine précédente",
|
||||
calendar_date_picker: "Calendrier",
|
||||
current_week: "Semaine actuelle",
|
||||
next_week: "Prochaine semaine",
|
||||
previous_week: "Semaine précédente",
|
||||
},
|
||||
save_button:"Enregistrer",
|
||||
cancel_button:"Annuler",
|
||||
save_button: "Enregistrer",
|
||||
cancel_button: "Annuler",
|
||||
remote_button: "Télétravail",
|
||||
delete_button: "Supprimer",
|
||||
shift: {
|
||||
actions: {
|
||||
add:"Ajouter un Quart",
|
||||
add: "Ajouter un Quart",
|
||||
edit: "Modifier un Quart",
|
||||
delete: "Supprimer un Quart",
|
||||
delete_confirmation_msg: "Voulez-vous complètement supprimer ce quart?",
|
||||
|
|
@ -156,56 +156,50 @@ export default {
|
|||
VACATION: "Vacance",
|
||||
REMOTE: "Télétravail",
|
||||
},
|
||||
errors: {
|
||||
not_found: "Aucun quart trouvé",
|
||||
SHIFT_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: {
|
||||
start:"Début (HH:mm)",
|
||||
end:"Fin (HH:mm)",
|
||||
header_comment:"Commentaire du Quart",
|
||||
start: "Début (HH:mm)",
|
||||
end: "Fin (HH:mm)",
|
||||
header_comment: "Commentaire du Quart",
|
||||
textarea_comment: "Laissez votre commentaire ici",
|
||||
},
|
||||
},
|
||||
expense: {
|
||||
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",
|
||||
comment_too_long:"votre commentaire est trop long",
|
||||
amount_must_be_positive:"le montant doit être suppérieur à 0$",
|
||||
mileave_must_be_positive:"le kilométrage doit être suppérieur à 0",
|
||||
amount_xor_mileage:"Vous ne pouvez pas saisir un montant et un kilométrage pour une même dépense",
|
||||
mileage_required_for_type:"Vous devez entrer une valeur en kilométrage pour ce type de dépense",
|
||||
amount_required_for_type:"Vous devez entrer une valeur en montant $ pour ce type de dépense",
|
||||
},
|
||||
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',
|
||||
hints: {
|
||||
amount_or_mileage:"Soit dépense ou kilométrage, pas les deux",
|
||||
comment_required:"un commentaire est requis",
|
||||
attach_file:"Pièce jointe"
|
||||
amount_or_mileage: "Soit dépense ou kilométrage, pas les deux",
|
||||
comment_required: "un commentaire est requis",
|
||||
attach_file: "Pièce jointe"
|
||||
},
|
||||
mileage:"Kilométrage",
|
||||
open_btn:"Liste des Dépenses",
|
||||
title:"Liste des dépenses",
|
||||
total_amount:"Montant total",
|
||||
total_mileage:"Kilométrage total",
|
||||
type:"Type",
|
||||
mileage: "Kilométrage",
|
||||
open_btn: "Liste des Dépenses",
|
||||
title: "Liste des dépenses",
|
||||
total_amount: "Montant total",
|
||||
total_mileage: "Kilométrage total",
|
||||
type: "Type",
|
||||
types: {
|
||||
PER_DIEM:"Per diem",
|
||||
EXPENSES:"dépense",
|
||||
MILEAGE:"kilométrage",
|
||||
ON_CALL:"Prime de garde",
|
||||
PER_DIEM: "Per diem",
|
||||
EXPENSES: "dépense",
|
||||
MILEAGE: "kilométrage",
|
||||
ON_CALL: "Prime de garde",
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
INVALID_SHIFT_TIME: "Les heures d'entrée et de sortie sont inversées",
|
||||
SHIFT_OVERLAP: "Il y a un chevauchement entre deux ou plusieurs quarts",
|
||||
INVALID_SHIFT: "Un quart de travail contient des données manquantes ou corrompues",
|
||||
SHIFT_NOT_FOUND: "Quart de travail manquant ou supprimé",
|
||||
PAY_PERIOD_NOT_FOUND: "Aucune période de paie ne correspond aux dates fournies",
|
||||
EMPLOYEE_NOT_FOUND: "Aucun employé ne correspond aux détails de votre connexion",
|
||||
INVALID_TIMESHEET: "Une feuille de temps contient des données manquantes ou corrompues",
|
||||
TIMESHEET_NOT_FOUND: "Aucune feuille de temps ne correspond au détails fournis",
|
||||
INVALID_EXPENSE: "Une dépense contient des données manquantes ou corrompues",
|
||||
EXPENSE_NOT_FOUND: "Aucune dépense ne correspond aux détails fournis",
|
||||
},
|
||||
},
|
||||
|
||||
timesheet_approvals: {
|
||||
|
|
|
|||
|
|
@ -34,10 +34,11 @@
|
|||
v-model="email"
|
||||
dense
|
||||
outlined
|
||||
color="accent"
|
||||
label-color="accent"
|
||||
class="rounded-5 inset-shadow bg-blue-grey-1"
|
||||
class="rounded-5 inset-shadow bg-white"
|
||||
label-slot
|
||||
input-class="text-weight-medium text-h6 text-primary"
|
||||
input-class="text-h6 text-dark"
|
||||
>
|
||||
<template #label>
|
||||
<span class="text-weight-bolder text-uppercase text-overline"> {{ $t('login.email') }} </span>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@
|
|||
<q-btn-dropdown
|
||||
push
|
||||
rounded
|
||||
class="q-mr-md bg-white text-primary"
|
||||
:label="$t('shared.label.filter')"
|
||||
icon="filter_alt"
|
||||
color="accent"
|
||||
:label="$t('shared.label.filter')"
|
||||
class="q-mr-md"
|
||||
/>
|
||||
|
||||
<!-- Search bar -->
|
||||
|
|
@ -21,14 +22,14 @@
|
|||
debounce="300"
|
||||
class="right-rounded"
|
||||
:label="$t('shared.label.search')"
|
||||
label-color="primary"
|
||||
label-color="accent"
|
||||
bg-color="white"
|
||||
color="primary"
|
||||
color="accent"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon
|
||||
name="search"
|
||||
color="primary"
|
||||
color="accent"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@
|
|||
/* eslint-disable */
|
||||
import { provide, ref } from 'vue';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import DetailedDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-hours-worked.vue';
|
||||
import DetailedDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-shift-types.vue';
|
||||
import DetailedDialogChartExpenses from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-expenses.vue';
|
||||
import DetailsDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-dialog-chart-hours-worked.vue';
|
||||
import DetailsDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-dialog-chart-shift-types.vue';
|
||||
import DetailsDialogChartExpenses from 'src/modules/timesheet-approval/components/details-dialog-chart-expenses.vue';
|
||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||
import ExpenseCrudDialogList from 'src/modules/timesheets/components/expense-crud-dialog-list.vue';
|
||||
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
|
||||
|
||||
const { employeeEmail } = defineProps<{
|
||||
employeeEmail: string;
|
||||
|
|
@ -48,17 +48,17 @@
|
|||
:horizontal="!$q.screen.lt.md"
|
||||
class=" col-auto q-px-sm no-wrap"
|
||||
>
|
||||
<DetailedDialogChartHoursWorked
|
||||
<DetailsDialogChartHoursWorked
|
||||
:key="render_key"
|
||||
class="col"
|
||||
/>
|
||||
|
||||
<DetailedDialogChartShiftTypes
|
||||
<DetailsDialogChartShiftTypes
|
||||
:key="render_key + 1"
|
||||
class="col-2 q-ma-lg"
|
||||
/>
|
||||
|
||||
<DetailedDialogChartExpenses
|
||||
<DetailsDialogChartExpenses
|
||||
:key="render_key + 2"
|
||||
class="col"
|
||||
/>
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
|
||||
<q-card-section class="col-auto">
|
||||
<q-separator />
|
||||
<ExpenseCrudDialogList
|
||||
<ExpenseDialogList
|
||||
horizontal
|
||||
:employee-email="employeeEmail"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -42,18 +42,6 @@
|
|||
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email);
|
||||
// await expenses_store.getPayPeriodExpensesByTimesheetId(employee_email);
|
||||
};
|
||||
|
||||
const getListModeTextColor = (type: string): string => {
|
||||
console.log('type: ', type);
|
||||
if (IS_ABNORMAL_SHIFT.includes(type)) {
|
||||
return ' text-negative text-weight-bolder';
|
||||
}
|
||||
else if (IS_PTO.includes(type)) {
|
||||
return ' text-warning text-weight-bold';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -93,8 +81,8 @@
|
|||
v-model="is_grid_mode"
|
||||
push
|
||||
color="white"
|
||||
text-color="primary"
|
||||
toggle-color="primary"
|
||||
text-color="accent"
|
||||
toggle-color="accent"
|
||||
class="q-mr-md"
|
||||
:options="[
|
||||
{ icon: 'grid_view', value: true },
|
||||
|
|
@ -133,7 +121,6 @@
|
|||
>
|
||||
<span
|
||||
v-if="(props.value > 0 && typeof props.value !== 'boolean') || typeof props.value === 'string'"
|
||||
:class="getListModeTextColor(props.col.name)"
|
||||
>{{ props.value }}</span>
|
||||
<q-icon
|
||||
v-if="typeof props.value === 'boolean'"
|
||||
|
|
@ -155,7 +142,7 @@
|
|||
|
||||
<!-- Template for custome failed-to-load state -->
|
||||
<template #no-data="{ message, filter }">
|
||||
<div class="full-width column items-center text-primary q-gutter-sm">
|
||||
<div class="full-width column items-center text-accent q-gutter-sm">
|
||||
<q-icon
|
||||
size="4em"
|
||||
:name="filter ? 'filter_alt_off' : 'error_outline'"
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { deepEqual } from 'src/utils/deep-equal';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
|
|
@ -18,25 +19,21 @@
|
|||
}
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const ui_store = useUiStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const expenses_store = useExpensesStore();
|
||||
const expenses_api = useExpensesApi();
|
||||
const files = defineModel<File[] | null>('files');
|
||||
|
||||
const is_navigator_open = ref(false);
|
||||
const is_initial_expense = ref(true);
|
||||
|
||||
const COMMENT_MAX_LENGTH = 280;
|
||||
const employee_email = inject<string>('employeeEmail');
|
||||
const rules = useExpenseRules(t);
|
||||
|
||||
const period_start_date = computed(() => timesheet_store.pay_period?.period_start.replaceAll('-', '/') ?? '');
|
||||
const period_end_date = computed(() => timesheet_store.pay_period?.period_end.replaceAll('-', '/') ?? '');
|
||||
|
||||
// const expense_options: { label: string, value: ExpenseType, icon: string }[] = EXPENSE_TYPE.map(expense_type => {
|
||||
// return { label: t(`timesheet.expense.types.${expense_type}`), value: expense_type, icon: getExpenseIcon(expense_type) };
|
||||
// });
|
||||
|
||||
const expense_options: ExpenseOption[] = [
|
||||
{label: t('timesheet.expense.types.PER_DIEM'), value: 'PER_DIEM', icon: getExpenseIcon('PER_DIEM')},
|
||||
{label: t('timesheet.expense.types.EXPENSES'), value: 'EXPENSES', icon: getExpenseIcon('EXPENSES')},
|
||||
|
|
@ -45,21 +42,35 @@
|
|||
]
|
||||
const expense_selected = ref(expense_options.find(expense => expense.value == expenses_store.current_expense.type));
|
||||
|
||||
const emit = defineEmits<{
|
||||
'onClickUpdateCancel': [void];
|
||||
'onClickSaveUpdates': [void];
|
||||
}>();
|
||||
|
||||
const openDatePicker = () => {
|
||||
is_navigator_open.value = true;
|
||||
if (timesheet_store.pay_period !== undefined) {
|
||||
expenses_store.current_expense.date = timesheet_store.pay_period.period_start;
|
||||
if (expenses_store.current_expense.date === undefined) {
|
||||
expenses_store.current_expense.date = timesheet_store.pay_period?.period_start ?? '';
|
||||
}
|
||||
};
|
||||
|
||||
const closeDatePicker = (date: string) => {
|
||||
is_navigator_open.value = false;
|
||||
expenses_store.current_expense.date = date;
|
||||
}
|
||||
|
||||
const requestExpenseCreationOrUpdate = async () => {
|
||||
if (expenses_store.mode === 'create') await expenses_api.createExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
|
||||
else await expenses_api.updateExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
|
||||
await expenses_api.upsertExpense(expenses_store.current_expense);
|
||||
|
||||
if (expenses_store.current_expense.id) {
|
||||
emit('onClickSaveUpdates');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
defineEmits<{
|
||||
'onClickUpdateCancel': [void];
|
||||
}>();
|
||||
watch(expenses_store.current_expense, () => {
|
||||
is_initial_expense.value = deepEqual(expenses_store.current_expense, expenses_store.initial_expense);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -85,7 +96,7 @@
|
|||
v-model="expenses_store.current_expense.date"
|
||||
dense
|
||||
type="date"
|
||||
outlined
|
||||
borderless
|
||||
readonly
|
||||
stack-label
|
||||
color="primary"
|
||||
|
|
@ -114,7 +125,7 @@
|
|||
mask="YYYY-MM-DD"
|
||||
event-color="accent"
|
||||
:options="date => date >= period_start_date && date <= period_end_date"
|
||||
@update:model-value="is_navigator_open = false"
|
||||
@update:model-value="closeDatePicker"
|
||||
/>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
@ -259,7 +270,7 @@
|
|||
<q-icon
|
||||
name="attach_file"
|
||||
size="sm"
|
||||
color="primary"
|
||||
color="accent"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
@ -286,7 +297,8 @@
|
|||
|
||||
<q-btn
|
||||
push
|
||||
color="accent"
|
||||
:disable="is_initial_expense"
|
||||
:color="is_initial_expense ? 'grey-5' : 'accent'"
|
||||
:icon="expenses_store.mode === 'update' ? 'save' : 'upload'"
|
||||
:label="expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
|
||||
class="q-px-sm "
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
icon="clear"
|
||||
color="negative"
|
||||
class="col-auto"
|
||||
style="border-radius: 0 0 0 5px;"
|
||||
style="border-radius: 0 5px 0 10px;"
|
||||
@click="expense_store.close"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
lang="ts"
|
||||
>
|
||||
import { date } from 'quasar';
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, toRaw } from 'vue';
|
||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||
import { deepEqual } from 'src/utils/deep-equal';
|
||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
const approved_class = computed(() => expense.is_approved ? ' bg-accent text-white' : '')
|
||||
const is_authorized_to_approve = computed(() => CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST'))
|
||||
const is_showing_update_form = ref(false);
|
||||
const is_current_expense = computed(() => expense.id === expenses_store.current_expense.id);
|
||||
|
||||
const requestExpenseDeletion = async () => {
|
||||
await expenses_api.deleteExpenseById(expense.id);
|
||||
|
|
@ -45,6 +46,7 @@
|
|||
const onUpdateClicked = () => {
|
||||
if (deepEqual(expense, expenses_store.current_expense)) {
|
||||
expenses_store.mode = 'create';
|
||||
Object.assign(expense, toRaw(expenses_store.initial_expense))
|
||||
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||
is_showing_update_form.value = false;
|
||||
return;
|
||||
|
|
@ -55,6 +57,12 @@
|
|||
expenses_store.initial_expense = unwrapAndClone(expense);
|
||||
is_showing_update_form.value = true;
|
||||
}
|
||||
|
||||
const onSaveUpdatesClicked = () => {
|
||||
is_showing_update_form.value = false;
|
||||
expenses_store.mode = 'create';
|
||||
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -65,7 +73,7 @@
|
|||
:class="background_class + approved_class"
|
||||
@click="onExpenseClicked"
|
||||
>
|
||||
<div class="row full-width items-center">
|
||||
<div class="col row fit items-center">
|
||||
<!-- avatar type icon section -->
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
|
|
@ -77,16 +85,11 @@
|
|||
|
||||
<!-- amount or mileage section -->
|
||||
<q-item-section class="col col-md-2 text-weight-bold">
|
||||
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
||||
<template v-if="typeof expense.mileage === 'number'">
|
||||
<q-item-label v-if="expense.type === 'MILEAGE'">
|
||||
{{ expense.mileage?.toFixed(1) }} km
|
||||
</template>
|
||||
<template v-else>
|
||||
${{ expense.amount.toFixed(2) }}
|
||||
</template>
|
||||
</q-item-label>
|
||||
<q-item-label v-else>
|
||||
${{ expense.amount.toFixed(2) }}
|
||||
$ {{ expense.amount.toFixed(2) }}
|
||||
</q-item-label>
|
||||
|
||||
<!-- date label -->
|
||||
|
|
@ -98,7 +101,8 @@
|
|||
>
|
||||
{{ $d(date.extractDate(expense.date, 'YYYY-MM-DD'), {
|
||||
month: 'short', day: 'numeric', weekday:
|
||||
'long' }) }}
|
||||
'long'
|
||||
}) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
|
|
@ -159,20 +163,29 @@
|
|||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section :side="$q.screen.gt.sm">
|
||||
|
||||
<q-item-section
|
||||
:key="refresh_key"
|
||||
side
|
||||
:class="is_current_expense ? 'invisible' : ''"
|
||||
>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="lg"
|
||||
icon="edit"
|
||||
color="accent"
|
||||
:disable="expense.is_approved"
|
||||
class="q-pa-none z-top"
|
||||
class="q-py-none z-top"
|
||||
:class="expense.is_approved ? 'invisible no-pointer' : ''"
|
||||
@click.stop="onUpdateClicked"
|
||||
/>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section :side="$q.screen.gt.sm">
|
||||
<q-item-section
|
||||
side
|
||||
:class="is_current_expense ? 'invisible' : ''"
|
||||
>
|
||||
<q-icon
|
||||
v-if="expense.is_approved"
|
||||
name="verified"
|
||||
|
|
@ -183,22 +196,24 @@
|
|||
<q-btn
|
||||
v-else
|
||||
flat
|
||||
dense
|
||||
size="lg"
|
||||
icon="close"
|
||||
color="negative"
|
||||
class="q-pa-none z-top"
|
||||
class="q-py-none z-top q-my-xs"
|
||||
@click.stop="requestExpenseDeletion"
|
||||
/>
|
||||
</q-item-section>
|
||||
</div>
|
||||
|
||||
<q-slide-transition
|
||||
@hide="expenses_store.is_hiding_create_form = false"
|
||||
@hide="expenses_store.mode === 'update' ? null : expenses_store.is_hiding_create_form = false"
|
||||
:duration="200"
|
||||
>
|
||||
<ExpenseDialogForm
|
||||
v-if="is_showing_update_form && expenses_store.is_hiding_create_form"
|
||||
v-if="is_current_expense && expenses_store.is_hiding_create_form"
|
||||
@on-click-update-cancel="onUpdateClicked"
|
||||
@on-click-save-updates="onSaveUpdatesClicked"
|
||||
/>
|
||||
</q-slide-transition>
|
||||
</q-item>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
class="q-pa-none rounded-10 shadow-10"
|
||||
:class="$q.screen.lt.md ? ' bg-primary' : 'bg-secondary'"
|
||||
style=" min-width: 70vw;"
|
||||
:style="$q.screen.lt.md ? 'border: solid 2px var(--q-accent);' : ''"
|
||||
:style="$q.dark.isActive ? 'border: solid 2px var(--q-accent);' : ''"
|
||||
>
|
||||
<q-inner-loading :showing="expense_store.is_loading">
|
||||
<q-spinner size="32px" />
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
|
|
@ -28,7 +28,6 @@
|
|||
const is_showing_comment_dialog_mobile = ref(false);
|
||||
|
||||
const COMMENT_MAX_LENGTH = 280;
|
||||
const employee_email = inject<string>('employeeEmail');
|
||||
const rules = useExpenseRules(t);
|
||||
|
||||
const period_start_date = computed(() => timesheet_store.pay_period?.period_start.replaceAll('-', '/') ?? '');
|
||||
|
|
@ -50,8 +49,7 @@
|
|||
};
|
||||
|
||||
const requestExpenseCreationOrUpdate = async () => {
|
||||
if (expenses_store.mode === 'create') await expenses_api.createExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
|
||||
else await expenses_api.updateExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
|
||||
await expenses_api.upsertExpense(expenses_store.current_expense);
|
||||
};
|
||||
|
||||
defineEmits<{
|
||||
|
|
|
|||
|
|
@ -30,10 +30,12 @@
|
|||
];
|
||||
|
||||
const shift = defineModel<Shift>('shift', { required: true });
|
||||
const { dense = false, hasShiftAfter = false } = defineProps<{
|
||||
const { dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
||||
dense?: boolean;
|
||||
hasShiftAfter?: boolean;
|
||||
isTimesheetApproved?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'saveComment': [comment: string, shift_id: number];
|
||||
'requestDelete': [void];
|
||||
|
|
@ -83,8 +85,8 @@
|
|||
<template>
|
||||
<q-slide-item
|
||||
right-color="negative"
|
||||
class="rounded-5 bg-transparent"
|
||||
:class="ui_store.is_mobile_mode ? 'q-my-md' : ''"
|
||||
class="rounded-5 transparent"
|
||||
:class="ui_store.is_mobile_mode ? 'q-my-md' : 'q-mr-xs'"
|
||||
@right="details => slideDeleteShift(details.reset)"
|
||||
>
|
||||
<template
|
||||
|
|
@ -93,16 +95,17 @@
|
|||
>
|
||||
<q-icon name="delete" />
|
||||
</template>
|
||||
<div :class="ui_store.is_mobile_mode ? 'column' : 'row'" >
|
||||
|
||||
<div :class="ui_store.is_mobile_mode ? 'column' : 'row'">
|
||||
<div
|
||||
class="row items-center text-uppercase rounded-5 bg-transparent"
|
||||
class="row items-center text-uppercase rounded-5"
|
||||
:class="ui_store.is_mobile_mode ? 'col q-mb-xs' : 'col-4'"
|
||||
>
|
||||
<!-- mobile comment button -->
|
||||
<q-btn
|
||||
v-if="ui_store.is_mobile_mode && !dense"
|
||||
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
||||
:text-color="shift.comment ? 'accent' : 'grey-5'"
|
||||
:text-color="shift.comment ? ((shift.is_approved || isTimesheetApproved) ? 'white' : 'accent') : 'grey-5'"
|
||||
class="col-auto full-height q-mx-xs rounded-5 shadow-1"
|
||||
>
|
||||
<q-popup-edit
|
||||
|
|
@ -116,13 +119,13 @@
|
|||
color="white"
|
||||
v-model="scope.value"
|
||||
dense
|
||||
:readonly="shift.is_approved"
|
||||
:readonly="(shift.is_approved || isTimesheetApproved)"
|
||||
autofocus
|
||||
counter
|
||||
bottom-slots
|
||||
:maxlength="COMMENT_LENGTH_MAX"
|
||||
class="q-pb-lg"
|
||||
:class="shift.is_approved ? 'cursor-not-allowed' : ''"
|
||||
:class="(shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed' : ''"
|
||||
@keyup.enter="scope.set"
|
||||
>
|
||||
<template #append>
|
||||
|
|
@ -157,8 +160,8 @@
|
|||
v-model="shift_type_selected"
|
||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||
dense
|
||||
:borderless="shift.is_approved"
|
||||
:readonly="shift.is_approved"
|
||||
:borderless="(shift.is_approved || isTimesheetApproved)"
|
||||
:readonly="(shift.is_approved || isTimesheetApproved)"
|
||||
:options-dense="!ui_store.is_mobile_mode"
|
||||
hide-dropdown-icon
|
||||
:menu-offset="[0, 10]"
|
||||
|
|
@ -166,8 +169,8 @@
|
|||
menu-self="top middle"
|
||||
:options="SHIFT_OPTIONS"
|
||||
class="col rounded-5 q-mx-xs bg-dark"
|
||||
:class="shift.is_approved ? 'inset-shadow' : ''"
|
||||
:style="shift.is_approved ? 'background-color: #0002 !important;' : ''"
|
||||
:class="(shift.is_approved || isTimesheetApproved) ? 'inset-shadow' : ''"
|
||||
:style="(shift.is_approved || isTimesheetApproved) ? 'background-color: #0a7d32 !important;' : ''"
|
||||
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||
popup-content-style="border: 2px solid var(--q-accent)"
|
||||
@blur="onBlurShiftTypeSelect"
|
||||
|
|
@ -188,7 +191,7 @@
|
|||
<span
|
||||
style="line-height: 0.9em;"
|
||||
class="col-auto ellipsis"
|
||||
:class="shift.is_approved ? 'text-white' : ''"
|
||||
:class="(shift.is_approved || isTimesheetApproved) ? 'text-white' : ''"
|
||||
>{{ scope.opt.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -200,17 +203,17 @@
|
|||
<q-input
|
||||
v-model="shift.start_time"
|
||||
dense
|
||||
:borderless="shift.is_approved"
|
||||
:readonly="shift.is_approved"
|
||||
:borderless="(shift.is_approved || isTimesheetApproved)"
|
||||
:readonly="(shift.is_approved || isTimesheetApproved)"
|
||||
type="time"
|
||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||
label-slot
|
||||
:label-color="shift.is_approved ? 'white' : 'accent'"
|
||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'cursor-not-allowed text-white' : '')"
|
||||
:label-color="(shift.is_approved || isTimesheetApproved) ? 'white' : 'accent'"
|
||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + ((shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed text-white' : '')"
|
||||
input-style="font-size: 1.2em;"
|
||||
class="col rounded-5 bg-dark"
|
||||
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : '')"
|
||||
:style="shift.is_approved ? 'background-color: #0002 !important;' : ''"
|
||||
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + ((shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : '')"
|
||||
:style="(shift.is_approved || isTimesheetApproved) ? 'background-color: #0a7d32 !important;' : ''"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
|
|
@ -225,16 +228,16 @@
|
|||
v-model="shift.end_time"
|
||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||
dense
|
||||
:borderless="shift.is_approved"
|
||||
:readonly="shift.is_approved"
|
||||
:borderless="(shift.is_approved || isTimesheetApproved)"
|
||||
:readonly="(shift.is_approved || isTimesheetApproved)"
|
||||
type="time"
|
||||
label-slot
|
||||
:label-color="shift.is_approved ? 'white' : 'accent'"
|
||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'cursor-not-allowed text-white' : '')"
|
||||
:label-color="(shift.is_approved || isTimesheetApproved) ? 'white' : 'accent'"
|
||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + ((shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed text-white' : '')"
|
||||
input-style="font-size: 1.2em;"
|
||||
class="col rounded-5 bg-dark"
|
||||
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-ml-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : '')"
|
||||
:style="shift.is_approved ? 'background-color: #0002 !important;' : ''"
|
||||
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-ml-xs ' : 'q-mx-xs ') + ((shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : '')"
|
||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
|
|
@ -257,10 +260,11 @@
|
|||
<!-- desktop comment button -->
|
||||
<q-btn
|
||||
v-else-if="!ui_store.is_mobile_mode"
|
||||
flat
|
||||
push
|
||||
dense
|
||||
:color="shift.is_approved ? 'accent' : 'dark'"
|
||||
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
||||
:text-color="shift.comment ? 'accent' : 'grey-5'"
|
||||
:text-color="shift.is_approved ? '' : (shift.comment ? 'accent' : 'grey-5')"
|
||||
class="col"
|
||||
:class="ui_store.is_mobile_mode ? 'q-mt-xs bg-dark' : ''"
|
||||
>
|
||||
|
|
@ -327,5 +331,10 @@
|
|||
</div>
|
||||
</q-slide-item>
|
||||
|
||||
<q-separator v-if="hasShiftAfter && ui_store.is_mobile_mode" spaced color="accent" class="q-mx-md"/>
|
||||
<q-separator
|
||||
v-if="hasShiftAfter && ui_store.is_mobile_mode"
|
||||
spaced
|
||||
color="accent"
|
||||
class="q-mx-md"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -9,10 +9,9 @@
|
|||
|
||||
const shift_api = useShiftApi();
|
||||
|
||||
const { day, dense = false, outlined = false, approved = false } = defineProps<{
|
||||
const { day, dense = false, approved = false } = defineProps<{
|
||||
day: TimesheetDay;
|
||||
dense?: boolean;
|
||||
outlined?: boolean;
|
||||
approved?: boolean;
|
||||
}>();
|
||||
|
||||
|
|
@ -36,7 +35,7 @@
|
|||
v-for="shift, shift_index in day.shifts"
|
||||
:key="shift_index"
|
||||
v-model:shift="day.shifts[shift_index]!"
|
||||
:outlined="outlined"
|
||||
:is-timesheet-approved="approved"
|
||||
:dense="dense"
|
||||
:has-shift-after="shift_index < day.shifts.length - 1"
|
||||
@request-delete="deleteCurrentShift(shift)"
|
||||
|
|
|
|||
|
|
@ -66,13 +66,14 @@
|
|||
class="col column full-width"
|
||||
>
|
||||
<q-card
|
||||
class="rounded-10 bg-dark"
|
||||
:style="ui_store.is_mobile_mode ? (getDayApproval(day) ? 'border: 6px inset var(--q-accent)' : 'border: 1px solid var(--q-accent);') : ''"
|
||||
class="rounded-10"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : 'bg-dark'"
|
||||
:style="ui_store.is_mobile_mode ? ((getDayApproval(day) || timesheet.is_approved) ? 'border: 6px inset var(--q-accent)' : 'border: 1px solid var(--q-accent);') : ''"
|
||||
>
|
||||
|
||||
<q-card-section
|
||||
class="text-weight-bolder text-uppercase text-h6 q-py-xs"
|
||||
:class="getDayApproval(day) ? 'bg-dark text-accent' : 'bg-primary text-white'"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-dark text-white' : 'bg-primary text-white'"
|
||||
style="line-height: 1em;"
|
||||
>
|
||||
<span> {{ $d(extractDate(day.date, 'YYYY-MM-DD'), {
|
||||
|
|
@ -88,7 +89,7 @@
|
|||
<ShiftListDay
|
||||
outlined
|
||||
:animation-delay-multiplier="day_index"
|
||||
:approved="getDayApproval(day)"
|
||||
:approved="(getDayApproval(day) || timesheet.is_approved)"
|
||||
:day="day"
|
||||
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
||||
/>
|
||||
|
|
@ -96,7 +97,7 @@
|
|||
|
||||
<q-card-actions class="q-pa-none">
|
||||
<q-btn
|
||||
v-if="!getDayApproval(day)"
|
||||
v-if="!(getDayApproval(day) || timesheet.is_approved)"
|
||||
square
|
||||
color="accent"
|
||||
icon="more_time"
|
||||
|
|
@ -107,7 +108,7 @@
|
|||
</q-card-actions>
|
||||
|
||||
<q-badge
|
||||
v-if="getDayApproval(day)"
|
||||
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
||||
floating
|
||||
class="transparent q-pa-none rounded-50"
|
||||
style="transform: translate(15px, -5px);"
|
||||
|
|
@ -115,7 +116,7 @@
|
|||
<q-icon
|
||||
name="verified"
|
||||
size="5em"
|
||||
color="accent"
|
||||
color="white"
|
||||
/>
|
||||
</q-badge>
|
||||
</q-card>
|
||||
|
|
@ -124,25 +125,26 @@
|
|||
<div
|
||||
v-else
|
||||
class="col row full-width"
|
||||
:class="getDayApproval(day) ? 'rounded-10 bg-accent' : ''"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'rounded-10 bg-accent' : ''"
|
||||
>
|
||||
<!-- List of shifts -->
|
||||
|
||||
<div
|
||||
class="col row bg-dark"
|
||||
:class="getDayApproval(day) ? 'bg-transparent' : ''"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-transparent' : ''"
|
||||
style="border-radius: 10px 0 0 10px;"
|
||||
>
|
||||
<!-- Date block -->
|
||||
<ShiftListDateWidget
|
||||
:display-date="day.date"
|
||||
:approved="getDayApproval(day)"
|
||||
:approved="(getDayApproval(day) || timesheet.is_approved)"
|
||||
class="col-auto"
|
||||
/>
|
||||
|
||||
|
||||
<ShiftListDay
|
||||
:day="day"
|
||||
:approved="getDayApproval(day) || timesheet.is_approved"
|
||||
class="col"
|
||||
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
||||
/>
|
||||
|
|
@ -151,7 +153,7 @@
|
|||
|
||||
<div class="col-auto self-stretch">
|
||||
<q-icon
|
||||
v-if="getDayApproval(day)"
|
||||
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
||||
name="verified"
|
||||
color="white"
|
||||
size="xl"
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import { normalizeObject } from "src/utils/normalize-object";
|
||||
import { useExpensesStore } from "src/stores/expense-store";
|
||||
import { expense_validation_schema } from "src/modules/timesheets/models/expense-validation.models";
|
||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||
|
||||
export const useExpensesApi = () => {
|
||||
const expenses_store = useExpensesStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
|
||||
const createExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||
};
|
||||
|
||||
const updateExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||
const upsertExpense = async (expense: Expense): Promise<void> => {
|
||||
const success = await expenses_store.upsertExpense(expense);
|
||||
if (success) {
|
||||
timesheet_store.getTimesheetsByEmployeeEmail();
|
||||
}
|
||||
};
|
||||
|
||||
const deleteExpenseById = async (expense_id: number): Promise<void> => {
|
||||
await expenses_store.deleteExpenseById(expense_id);
|
||||
const success = await expenses_store.deleteExpenseById(expense_id);
|
||||
if (success) {
|
||||
timesheet_store.getTimesheetsByEmployeeEmail();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
createExpenseByEmployeeEmail,
|
||||
updateExpenseByEmployeeEmail,
|
||||
upsertExpense,
|
||||
deleteExpenseById,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,78 +1,78 @@
|
|||
import { type Expense, EXPENSE_TYPE, type ExpenseType } from "src/modules/timesheets/models/expense.models";
|
||||
import type { Normalizer } from "src/utils/normalize-object";
|
||||
// import { type Expense, EXPENSE_TYPE, type ExpenseType } from "src/modules/timesheets/models/expense.models";
|
||||
// import type { Normalizer } from "src/utils/normalize-object";
|
||||
|
||||
export interface ApiErrorPayload {
|
||||
status_code: number;
|
||||
error_code?: string;
|
||||
message?: string;
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
// export interface ApiErrorPayload {
|
||||
// status_code: number;
|
||||
// error_code?: string;
|
||||
// message?: string;
|
||||
// context?: Record<string, unknown>;
|
||||
// };
|
||||
|
||||
export abstract class ApiError extends Error {
|
||||
status_code: number;
|
||||
error_code?: string;
|
||||
context?: Record<string, unknown>;
|
||||
// export abstract class ApiError extends Error {
|
||||
// status_code: number;
|
||||
// error_code?: string;
|
||||
// context?: Record<string, unknown>;
|
||||
|
||||
constructor(payload: ApiErrorPayload, defaultMessage: string) {
|
||||
super(payload.message || defaultMessage);
|
||||
this.status_code = payload.status_code;
|
||||
this.error_code = payload.error_code ?? "unknown";
|
||||
this.context = payload.context ?? {'unknown': 'unknown error has occured', };
|
||||
}
|
||||
};
|
||||
// constructor(payload: ApiErrorPayload, defaultMessage: string) {
|
||||
// super(payload.message || defaultMessage);
|
||||
// this.status_code = payload.status_code;
|
||||
// this.error_code = payload.error_code ?? "unknown";
|
||||
// this.context = payload.context ?? {'unknown': 'unknown error has occured', };
|
||||
// }
|
||||
// };
|
||||
|
||||
export class GenericApiError extends ApiError {
|
||||
constructor(payload: ApiErrorPayload) {
|
||||
super(payload, 'Encountered an error processing request');
|
||||
this.name = 'GenericApiError';
|
||||
}
|
||||
};
|
||||
// export class GenericApiError extends ApiError {
|
||||
// constructor(payload: ApiErrorPayload) {
|
||||
// super(payload, 'Encountered an error processing request');
|
||||
// this.name = 'GenericApiError';
|
||||
// }
|
||||
// };
|
||||
|
||||
export class ExpensesValidationError extends ApiError {
|
||||
constructor(payload: ApiErrorPayload) {
|
||||
super(payload, 'Invalid expense payload');
|
||||
this.name = 'ExpensesValidationError';
|
||||
}
|
||||
};
|
||||
// export class ExpensesValidationError extends ApiError {
|
||||
// constructor(payload: ApiErrorPayload) {
|
||||
// super(payload, 'Invalid expense payload');
|
||||
// this.name = 'ExpensesValidationError';
|
||||
// }
|
||||
// };
|
||||
|
||||
export class ExpensesApiError extends ApiError {
|
||||
constructor(payload: ApiErrorPayload) {
|
||||
super(payload, 'Request failed');
|
||||
this.name = 'ExpensesApiError';
|
||||
}
|
||||
};
|
||||
// export class ExpensesApiError extends ApiError {
|
||||
// constructor(payload: ApiErrorPayload) {
|
||||
// super(payload, 'Request failed');
|
||||
// this.name = 'ExpensesApiError';
|
||||
// }
|
||||
// };
|
||||
|
||||
export const expense_validation_schema: Normalizer<Expense> = {
|
||||
id: v => typeof v === 'number' ? v : -1,
|
||||
date: v => typeof v === 'string' ? v.trim() : '1970-01-01',
|
||||
type: v => EXPENSE_TYPE.includes(v as ExpenseType) ? v as ExpenseType : "EXPENSES",
|
||||
amount: v => typeof v === "number" ? v : -1,
|
||||
mileage: v => typeof v === "number" ? v : undefined,
|
||||
comment: v => typeof v === 'string' ? v.trim() : '',
|
||||
supervisor_comment: v => typeof v === 'string' ? v.trim() : '',
|
||||
is_approved: v => !!v,
|
||||
};
|
||||
// export const expense_validation_schema: Normalizer<Expense> = {
|
||||
// id: v => typeof v === 'number' ? v : -1,
|
||||
// date: v => typeof v === 'string' ? v.trim() : '1970-01-01',
|
||||
// type: v => EXPENSE_TYPE.includes(v as ExpenseType) ? v as ExpenseType : "EXPENSES",
|
||||
// amount: v => typeof v === "number" ? v : -1,
|
||||
// mileage: v => typeof v === "number" ? v : undefined,
|
||||
// comment: v => typeof v === 'string' ? v.trim() : '',
|
||||
// supervisor_comment: v => typeof v === 'string' ? v.trim() : '',
|
||||
// is_approved: v => !!v,
|
||||
// };
|
||||
|
||||
export function toExpensesError(err: unknown): ExpensesValidationError | ExpensesApiError {
|
||||
if (err instanceof ExpensesValidationError || err instanceof ExpensesApiError) {
|
||||
return err;
|
||||
}
|
||||
// export function toExpensesError(err: unknown): ExpensesValidationError | ExpensesApiError {
|
||||
// if (err instanceof ExpensesValidationError || err instanceof ExpensesApiError) {
|
||||
// return err;
|
||||
// }
|
||||
|
||||
if (typeof err === 'object' && err !== null && 'status_code' in err) {
|
||||
const payload = err as ApiErrorPayload;
|
||||
// if (typeof err === 'object' && err !== null && 'status_code' in err) {
|
||||
// const payload = err as ApiErrorPayload;
|
||||
|
||||
// Don't know how to differentiate both types of errors, can be updated here
|
||||
if (payload.error_code?.startsWith('API_')) {
|
||||
return new ExpensesApiError(payload);
|
||||
}
|
||||
// // Don't know how to differentiate both types of errors, can be updated here
|
||||
// if (payload.error_code?.startsWith('API_')) {
|
||||
// return new ExpensesApiError(payload);
|
||||
// }
|
||||
|
||||
return new ExpensesValidationError(payload);
|
||||
}
|
||||
// return new ExpensesValidationError(payload);
|
||||
// }
|
||||
|
||||
// Fallback with ValidationError as default
|
||||
return new ExpensesValidationError({
|
||||
status_code: 500,
|
||||
message: err instanceof Error ? err.message : 'Unknown error',
|
||||
context: { original: err }
|
||||
});
|
||||
}
|
||||
// // Fallback with ValidationError as default
|
||||
// return new ExpensesValidationError({
|
||||
// status_code: 500,
|
||||
// message: err instanceof Error ? err.message : 'Unknown error',
|
||||
// context: { original: err }
|
||||
// });
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXP
|
|||
|
||||
export class Expense {
|
||||
id: number;
|
||||
timesheet_id: number;
|
||||
date: string; //YYYY-MM-DD
|
||||
type: ExpenseType;
|
||||
amount: number;
|
||||
|
|
@ -16,6 +17,7 @@ export class Expense {
|
|||
|
||||
constructor(date: string) {
|
||||
this.id = -1;
|
||||
this.timesheet_id = -1;
|
||||
this.date = date;
|
||||
this.type = 'EXPENSES';
|
||||
this.amount = 0;
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@ import { api } from "src/boot/axios";
|
|||
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||
|
||||
export const ExpenseService = {
|
||||
createExpense: async (expense: Expense) => {
|
||||
createExpense: async (expense: Expense): Promise<{success: boolean, data: Expense, error?: unknown}> => {
|
||||
const response = await api.post('expense/create', expense);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateExpenseById: async (expense: Expense) => {
|
||||
updateExpense: async (expense: Expense): Promise<{success: boolean, data: Expense, error?: unknown}> => {
|
||||
const response = await api.patch(`expense/update`, expense);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteExpenseById: async (expense_id: number): Promise<{ok: boolean, id: number, error?: unknown}> => {
|
||||
deleteExpenseById: async (expense_id: number): Promise<{success: boolean, data: number, error?: unknown}> => {
|
||||
const response = await api.delete(`expense/delete/${expense_id}`);
|
||||
return response.data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,22 +28,25 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
is_hiding_create_form.value = false;
|
||||
};
|
||||
|
||||
const upsertExpensesById = async (expense_id: number, expense: Expense): Promise<void> => {
|
||||
const upsertExpense = async (expense: Expense): Promise<boolean> => {
|
||||
try {
|
||||
if (expense_id < 0) {
|
||||
if (expense.id < 0) {
|
||||
const data = await ExpenseService.createExpense(expense);
|
||||
return data;
|
||||
return data.success;
|
||||
}
|
||||
// TODO: Save response data into proper ref
|
||||
const data = await ExpenseService.updateExpense(expense);
|
||||
return data.success;
|
||||
} catch (err) {
|
||||
// setErrorFrom(err);
|
||||
console.error(err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteExpenseById = async (expense_id: number): Promise<boolean> => {
|
||||
const data = await ExpenseService.deleteExpenseById(expense_id);
|
||||
return data.ok;
|
||||
console.log('data received from expense deletion: ', data);
|
||||
return data.success;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -54,7 +57,7 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
current_expense,
|
||||
initial_expense,
|
||||
open,
|
||||
upsertExpensesById,
|
||||
upsertExpense,
|
||||
deleteExpenseById,
|
||||
close,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user