Merge pull request 'fix(all): fix scroll issue in timesheet mobile, add approval for individual expenses in approvals' (#39) from dev/nicolas/staging-prep into main

Reviewed-on: Targo/targo_frontend#39
This commit is contained in:
Nicolas 2026-01-07 09:49:54 -05:00
commit 00bc56ea61
10 changed files with 121 additions and 36 deletions

View File

@ -123,6 +123,8 @@ export default {
page_header: "account login",
email: "e-mail",
password: "password",
connected: "Connected",
redirecting: "Redirecting...",
button: {
connect: "connect",
employee: "employee",
@ -323,6 +325,7 @@ export default {
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",
UPDATE_ERROR: "Error while updating data",
},
},
@ -355,6 +358,8 @@ export default {
},
tooltip: {
button_detailed_view: "detailed view",
approve: "Approve",
unapprove: "remove approval",
},
},
descriptions: {

View File

@ -123,6 +123,8 @@ export default {
page_header: "connexion au compte",
email: "courriel",
password: "mot de passe",
connected: "Connecté",
redirecting: "redirection en cours...",
button: {
connect: "connecter",
employee: "employé",
@ -324,6 +326,7 @@ export default {
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",
UPDATE_ERROR: "Une erreur est survenu lors de la mise à jour",
},
},
@ -356,6 +359,8 @@ export default {
},
tooltip: {
button_detailed_view: "vue détaillée",
approve: "mettre status approuvé",
unapprove: "enlever status approuvé",
},
},
descriptions: {

View File

@ -1,4 +1,7 @@
<script setup lang="ts">
<script
setup
lang="ts"
>
import { onMounted } from 'vue';
onMounted(() => {
@ -6,7 +9,7 @@
setTimeout(() => {
window.opener.postMessage({ type: 'authSuccess' });
window.close();
}, 1500);
}, 2000);
}
});
</script>
@ -15,18 +18,35 @@
<q-layout class="bg-secondary">
<q-page-container>
<q-page class="column items-center justify-center q-pa-xl">
<transition appear enter-active-class="animated slow flipInX" leave-active-class="animated flipOutX">
<q-card class="col-3 items-center">
<q-card-section class="row justify-center ">
<q-icon name="check_circle" color="accent" size="xl" />
<transition
appear
enter-active-class="animated flipInX"
>
<q-card class="col-3 items-center rounded-10 q-px-lg">
<q-card-section class="row justify-center ">
<q-icon
name="check_circle"
color="accent"
size="xl"
/>
</q-card-section>
<q-separator inset color="accent" />
<q-card-section class="row justify-center">
<span class="row text-h3">Login Successful!</span>
<q-separator
inset
color="accent"
/>
<q-card-section class="column items-center">
<span class="col-auto text-h4 text-uppercase">{{ $t('login.connected') }}</span>
<span class="col-auto text-h6 text-uppercase">{{ $t('login.redirecting') }}</span>
<q-spinner
color="accent"
size="5em"
:thickness="4"
class="col-auto"
/>
</q-card-section>
</q-card>
</transition>
</q-page>
</q-page-container>
</q-layout>
</q-layout>
</template>

View File

@ -46,7 +46,7 @@
</div>
<div class="col-auto">
<ExpenseDialogList />
<ExpenseDialogList mode="approval" />
</div>
<!-- list of shifts -->

View File

@ -3,7 +3,7 @@
lang="ts"
>
import type { TimesheetApprovalOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
import { getHoursMinutesStringFromHoursFloat, getMinutes } from 'src/utils/date-and-time-utils';
import { getHoursMinutesStringFromHoursFloat, getMinutes } from 'src/utils/date-and-time-utils';
const modelApproval = defineModel<boolean>();
@ -14,7 +14,7 @@ import { getHoursMinutesStringFromHoursFloat, getMinutes } from 'src/utils/date-
const emit = defineEmits<{
'clickDetails': [overview: TimesheetApprovalOverview];
'clickApprovalAll' : [is_approved: boolean];
'clickApprovalAll': [is_approved: boolean];
}>();
</script>
@ -45,11 +45,10 @@ import { getHoursMinutesStringFromHoursFloat, getMinutes } from 'src/utils/date-
<!-- Buttons to view detailed shifts or view employee timesheet -->
<q-btn
flat
dense
square
unelevated
class="col-auto q-pa-none q-ma-none"
class="col-auto q-ma-none rounded-5"
color="accent"
icon="las la-chart-pie"
@click="emit('clickDetails', row)"
@ -57,6 +56,7 @@ import { getHoursMinutesStringFromHoursFloat, getMinutes } from 'src/utils/date-
<q-tooltip
anchor="top middle"
self="center middle"
:offset="[0, 20]"
class="bg-accent text-uppercase text-weight-bold"
>
{{ $t('timesheet_approvals.tooltip.button_detailed_view') }}
@ -64,7 +64,7 @@ import { getHoursMinutesStringFromHoursFloat, getMinutes } from 'src/utils/date-
<q-icon
name="las la-chart-bar"
color="accent"
color="white"
/>
</q-btn>
</q-card-section>
@ -152,9 +152,7 @@ import { getHoursMinutesStringFromHoursFloat, getMinutes } from 'src/utils/date-
v-if="row.is_active"
class="col row full-width"
>
<div
class="col text-uppercase"
>
<div class="col text-uppercase">
<span class="text-h6 q-ml-sm text-weight-bolder">{{ 'Total : ' + Math.floor(row.total_hours)
}}</span>
<span class="text-uppercase text-weight-medium text-caption">H</span>
@ -176,7 +174,17 @@ import { getHoursMinutesStringFromHoursFloat, getMinutes } from 'src/utils/date-
class="text-uppercase"
:class="row.is_approved ? '' : 'text-accent'"
@update:model-value="value => $emit('clickApprovalAll', value)"
/>
>
<q-tooltip
anchor="top middle"
self="center middle"
:offset="[0, 20]"
class="bg-accent text-uppercase text-weight-bold"
>
{{ row.is_approved ? $t('timesheet_approvals.tooltip.unapprove') :
$t('timesheet_approvals.tooltip.approve') }}
</q-tooltip>
</q-checkbox>
</div>
</div>
@ -190,8 +198,10 @@ import { getHoursMinutesStringFromHoursFloat, getMinutes } from 'src/utils/date-
class="col-auto"
size="lg"
/>
<span class="col q-pl-sm text-uppercase text-weight-bold text-h5">{{
$t('timesheet_approvals.table.inactive') }}</span>
<span class="col q-pl-sm text-uppercase text-weight-bold text-h5">
{{ $t('timesheet_approvals.table.inactive') }}
</span>
</div>
</q-card-section>
</q-card>

View File

@ -4,32 +4,57 @@
>
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
import { date } from 'quasar';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { date, Notify } from 'quasar';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
import { useExpensesStore } from 'src/stores/expense-store';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
import type { Expense } from 'src/modules/timesheets/models/expense.models';
const { t } = useI18n();
const expense = defineModel<Expense>({ required: true });
const expenses_store = useExpensesStore();
const expenses_api = useExpensesApi();
const expenses_store = useExpensesStore();
const timesheet_store = useTimesheetStore();
const is_showing_update_form = ref(false);
const { mode = 'normal' } = defineProps<{
mode?: 'approval' | 'normal';
}>();
const requestExpenseDeletion = async () => {
await expenses_api.deleteExpenseById(expense.value.id);
}
const onClickExpenseUpdate = () => {
if (expense.value.is_approved) return;
expenses_store.mode = 'update';
expenses_store.current_expense = expense.value;
expenses_store.initial_expense = unwrapAndClone(expense.value);
}
const onClickApproval = async () => {
expenses_store.current_expense = unwrapAndClone(expense.value);
expenses_store.current_expense.is_approved = !expenses_store.current_expense.is_approved;
const success = await expenses_store.upsertExpense(expenses_store.current_expense, timesheet_store.current_pay_period_overview?.email);
if (success) {
expense.value.is_approved = !expense.value.is_approved;
} else {
expenses_store.current_expense.is_approved = !expenses_store.current_expense.is_approved;
Notify.create({
message: t('timesheet.errors.UPDATE_ERROR'),
color: "negative"
});
}
}
</script>
<template>
@ -127,11 +152,31 @@
<div class="col-auto">
<q-btn
v-if="mode === 'approval'"
flat
size="lg"
:icon="expense.is_approved ? 'lock' : 'lock_open'"
:color="expense.is_approved ? 'white' : 'accent'"
class="relative-position"
@click.stop="onClickApproval"
>
<q-tooltip
anchor="top middle"
self="center middle"
:offset="[0, 20]"
class="bg-accent text-uppercase text-weight-bold"
>
{{ expense.is_approved ? $t('timesheet_approvals.tooltip.unapprove') : $t('timesheet_approvals.tooltip.approve') }}
</q-tooltip>
</q-btn>
<q-icon
v-if="expense.is_approved"
name="verified"
color="white"
size="lg"
size="2.5em"
class="q-px-sm"
/>
<q-btn
@ -141,7 +186,7 @@
size="lg"
icon="close"
color="negative"
class="q-py-none q-my-xs"
class="q-py-xs q-px-sm"
@click.stop="requestExpenseDeletion"
/>
</div>

View File

@ -9,8 +9,8 @@
const timesheet_store = useTimesheetStore();
const { horizontal = false } = defineProps<{
horizontal?: boolean;
const { mode = 'normal' } = defineProps<{
mode?: 'approval' | 'normal';
}>();
const expenses_list = computed(() => {
@ -26,7 +26,6 @@
<q-list
padding
class="q-px-lg"
:class="horizontal ? 'row flex-center' : ''"
>
<q-item-label
v-if="expenses_list.length < 1"
@ -50,6 +49,7 @@
v-else
v-model="expenses_list[index]!"
:index="index"
:mode="mode"
/>
</div>
</q-list>

View File

@ -29,7 +29,7 @@
const handleSwipe: TouchSwipeValue = (details) => {
mobile_animation_direction.value = details.direction === 'left' ? 'fadeInRight' : 'fadeInLeft';
if (details.distance && details.distance.x && Math.abs(details.distance.x) > 10) {
if (details.distance && details.distance.x && Math.abs(details.distance.x) > 30) {
timesheet_api.getTimesheetsBySwiping(details.direction === 'left' ? 1 : -1).catch(error => console.error(error));
}
};
@ -46,7 +46,7 @@
<div
class="column fit relative-position"
:style="$q.platform.is.mobile && $q.screen.width < $q.screen.height ? 'margin-bottom: 40px' : ''"
v-touch-swipe="handleSwipe"
v-touch-swipe.horizontal="handleSwipe"
>
<q-scroll-area
ref="timesheet_page"

View File

@ -7,8 +7,8 @@ export const ExpenseService = {
return response.data;
},
updateExpense: async (expense: Expense): Promise<{success: boolean, data: Expense, error?: unknown}> => {
const response = await api.patch(`expense/update`, expense);
updateExpense: async (expense: Expense, email?: string): Promise<{success: boolean, data: Expense, error?: unknown}> => {
const response = await api.patch(`expense/update${email ? '?employee_email=' + email : ''}`, expense);
return response.data;
},

View File

@ -29,13 +29,13 @@ export const useExpensesStore = defineStore('expenses', () => {
is_showing_create_form.value = true;
};
const upsertExpense = async (expense: Expense): Promise<boolean> => {
const upsertExpense = async (expense: Expense, email?: string): Promise<boolean> => {
try {
if (expense.id < 0) {
const data = await ExpenseService.createExpense(expense);
return data.success;
}
const data = await ExpenseService.updateExpense(expense);
const data = await ExpenseService.updateExpense(expense, email);
return data.success;
} catch (err) {
// setErrorFrom(err);