feat(timesheet): add shift overlap verification to shift entries
Also refactor mobile UI/UX for timesheet: reduced header bloat, made only shifts scrollable, added left or right swipe to travel between pay periods, showing default 'no data' message when beyond 6-month-back 1-month-forward timesheet scope.
This commit is contained in:
parent
db821d1d13
commit
13c339953f
|
|
@ -237,8 +237,9 @@ export default {
|
||||||
errors: {
|
errors: {
|
||||||
INVALID_SHIFT_TIME: "In and Out shift times are reversed",
|
INVALID_SHIFT_TIME: "In and Out shift times are reversed",
|
||||||
SHIFT_OVERLAP: "An overlaps occured between 2 or more shifts",
|
SHIFT_OVERLAP: "An overlaps occured between 2 or more shifts",
|
||||||
|
SHIFT_OVERLAP_SHORT: "Overlap",
|
||||||
INVALID_SHIFT: "A shift contains missing or corrupted data",
|
INVALID_SHIFT: "A shift contains missing or corrupted data",
|
||||||
SHIFT_TIME_REQUIRED: "Valid time required",
|
SHIFT_TIME_REQUIRED: "Time required",
|
||||||
SHIFT_TYPE_REQUIRED: "Shift type required",
|
SHIFT_TYPE_REQUIRED: "Shift type required",
|
||||||
SHIFT_NOT_FOUND: "Shift missing or deleted",
|
SHIFT_NOT_FOUND: "Shift missing or deleted",
|
||||||
PAY_PERIOD_NOT_FOUND: "No pay period matching given dates",
|
PAY_PERIOD_NOT_FOUND: "No pay period matching given dates",
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,7 @@ export default {
|
||||||
errors: {
|
errors: {
|
||||||
INVALID_SHIFT_TIME: "Les heures d'entrée et de sortie sont inversées",
|
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",
|
SHIFT_OVERLAP: "Il y a un chevauchement entre deux ou plusieurs quarts",
|
||||||
|
SHIFT_OVERLAP_SHORT: "Chevauchement",
|
||||||
INVALID_SHIFT: "Un quart de travail contient des données manquantes ou corrompues",
|
INVALID_SHIFT: "Un quart de travail contient des données manquantes ou corrompues",
|
||||||
SHIFT_TIME_REQUIRED: "Heure requise",
|
SHIFT_TIME_REQUIRED: "Heure requise",
|
||||||
SHIFT_TYPE_REQUIRED: "Type requis",
|
SHIFT_TYPE_REQUIRED: "Type requis",
|
||||||
|
|
|
||||||
|
|
@ -33,30 +33,13 @@
|
||||||
emit('date-selected', value);
|
emit('date-selected', value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNextOrPreviousPayPeriod = (direction: number) => {
|
|
||||||
const pay_period = timesheet_store.pay_period;
|
|
||||||
if (!pay_period) return;
|
|
||||||
|
|
||||||
pay_period.pay_period_no += direction;
|
|
||||||
|
|
||||||
if (pay_period.pay_period_no > 26) {
|
|
||||||
pay_period.pay_period_no = 1;
|
|
||||||
pay_period.pay_year += direction;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pay_period.pay_period_no < 1) {
|
|
||||||
pay_period.pay_period_no = 26;
|
|
||||||
pay_period.pay_year += direction;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNextPayPeriod = () => {
|
const getNextPayPeriod = () => {
|
||||||
getNextOrPreviousPayPeriod(NEXT);
|
timesheet_store.getNextOrPreviousPayPeriod(NEXT);
|
||||||
emit('pressed-next-button');
|
emit('pressed-next-button');
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPreviousPayPeriod = () => {
|
const getPreviousPayPeriod = () => {
|
||||||
getNextOrPreviousPayPeriod(PREVIOUS);
|
timesheet_store.getNextOrPreviousPayPeriod(PREVIOUS);
|
||||||
emit('pressed-previous-button');
|
emit('pressed-previous-button');
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@
|
||||||
let expenses = 0;
|
let expenses = 0;
|
||||||
let mileage = 0;
|
let mileage = 0;
|
||||||
timesheet_store.timesheets.forEach(timesheet => {
|
timesheet_store.timesheets.forEach(timesheet => {
|
||||||
expenses += timesheet.weekly_expenses.expenses ?? 0;
|
expenses += timesheet.weekly_expenses.expenses;
|
||||||
mileage += timesheet.weekly_expenses.mileage ?? 0;
|
expenses += timesheet.weekly_expenses.on_call;
|
||||||
|
expenses += timesheet.weekly_expenses.per_diem;
|
||||||
|
mileage += timesheet.weekly_expenses.mileage;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { expenses, mileage };
|
return { expenses, mileage };
|
||||||
|
|
|
||||||
|
|
@ -8,39 +8,33 @@
|
||||||
import { QSelect, QInput } from 'quasar';
|
import { QSelect, QInput } from 'quasar';
|
||||||
import { Shift, type ShiftOption } from 'src/modules/timesheets/models/shift.models';
|
import { Shift, type ShiftOption } from 'src/modules/timesheets/models/shift.models';
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import { useShiftRules } from 'src/modules/timesheets/utils/shift.util';
|
import { useShiftRules, SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||||
|
|
||||||
let timer: NodeJS.Timeout;
|
let timer: NodeJS.Timeout;
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const ui_store = useUiStore();
|
const ui_store = useUiStore();
|
||||||
const shift_rules = useShiftRules(t('timesheet.errors.SHIFT_TIME_REQUIRED'),);
|
|
||||||
|
|
||||||
const COMMENT_LENGTH_MAX = 280;
|
const COMMENT_LENGTH_MAX = 280;
|
||||||
|
|
||||||
const SHIFT_OPTIONS: ShiftOption[] = [
|
|
||||||
{ label: t('timesheet.shift.types.REGULAR'), value: 'REGULAR', icon: 'wb_sunny', icon_color: 'blue-grey-3' },
|
|
||||||
{ label: t('timesheet.shift.types.EVENING'), value: 'EVENING', icon: 'bedtime', icon_color: 'indigo-8' },
|
|
||||||
{ label: t('timesheet.shift.types.EMERGENCY'), value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-8' },
|
|
||||||
{ label: t('timesheet.shift.types.VACATION'), value: 'VACATION', icon: 'beach_access', icon_color: 'yellow-8' },
|
|
||||||
{ label: t('timesheet.shift.types.HOLIDAY'), value: 'HOLIDAY', icon: 'forest', icon_color: 'green-8' },
|
|
||||||
{ label: t('timesheet.shift.types.SICK'), value: 'SICK', icon: 'medication_liquid', icon_color: 'light-blue-6' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const shift = defineModel<Shift>('shift', { required: true });
|
const shift = defineModel<Shift>('shift', { required: true });
|
||||||
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
||||||
const select_ref = useTemplateRef<QSelect>('select');
|
const select_ref = useTemplateRef<QSelect>('select');
|
||||||
const start_time_ref = useTemplateRef<QInput>('start_time');
|
const start_time_ref = useTemplateRef<QInput>('start_time');
|
||||||
const end_time_ref = useTemplateRef<QInput>('end_time');
|
const end_time_ref = useTemplateRef<QInput>('end_time');
|
||||||
|
|
||||||
const { dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
const { dayShifts = [], dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
||||||
|
dayShifts: Shift[];
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
hasShiftAfter?: boolean;
|
hasShiftAfter?: boolean;
|
||||||
isTimesheetApproved?: boolean;
|
isTimesheetApproved?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const shift_rules = useShiftRules(t('timesheet.errors.SHIFT_TIME_REQUIRED'), t('timesheet.errors.SHIFT_OVERLAP_SHORT'), dayShifts);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'saveComment': [comment: string, shift_id: number];
|
'saveComment': [comment: string, shift_id: number];
|
||||||
'requestDelete': [void];
|
'requestDelete': [void];
|
||||||
|
'onTimeFieldBlur': [void];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const onBlurShiftTypeSelect = () => {
|
const onBlurShiftTypeSelect = () => {
|
||||||
|
|
@ -194,7 +188,7 @@
|
||||||
class="col-auto ellipsis"
|
class="col-auto ellipsis"
|
||||||
:class="!shift.is_approved ? '' : 'text-white'"
|
:class="!shift.is_approved ? '' : 'text-white'"
|
||||||
>
|
>
|
||||||
{{ scope.opt.label }}
|
{{ $t(scope.opt.label) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -235,7 +229,8 @@
|
||||||
:offset="[0, 10]"
|
:offset="[0, 10]"
|
||||||
class="text-uppercase text-weight-medium text-white bg-accent"
|
class="text-uppercase text-weight-medium text-white bg-accent"
|
||||||
>
|
>
|
||||||
{{ $t('timesheet.remote_button') }}
|
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
||||||
|
$t('timesheet.shift.types.OFFICE') }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-toggle>
|
</q-toggle>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -256,13 +251,14 @@
|
||||||
lazy-rules
|
lazy-rules
|
||||||
no-error-icon
|
no-error-icon
|
||||||
hide-bottom-space
|
hide-bottom-space
|
||||||
:rules="[shift_rules.isTimeRequired]"
|
:rules="[shift_rules.isTimeRequiredRule, shift_rules.isShiftOverlapRule]"
|
||||||
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
||||||
class="col rounded-5 bg-dark"
|
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 && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')"
|
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + (!shift.is_approved && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')"
|
||||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
||||||
input-style="font-size: 1.2em;"
|
input-style="font-size: 1.2em;"
|
||||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||||
|
@blur="emit('onTimeFieldBlur')"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span
|
<span
|
||||||
|
|
@ -286,13 +282,14 @@
|
||||||
lazy-rules
|
lazy-rules
|
||||||
no-error-icon
|
no-error-icon
|
||||||
hide-bottom-space
|
hide-bottom-space
|
||||||
:rules="[shift_rules.isTimeRequired]"
|
:rules="[shift_rules.isTimeRequiredRule, shift_rules.isShiftOverlapRule]"
|
||||||
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
||||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
||||||
input-style="font-size: 1.2em;"
|
input-style="font-size: 1.2em;"
|
||||||
class="col rounded-5 bg-dark"
|
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' : (isTimesheetApproved ? 'inset-shadow' : ''))"
|
: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' : (isTimesheetApproved ? 'inset-shadow' : ''))"
|
||||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||||
|
@blur="emit('onTimeFieldBlur')"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span
|
<span
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@
|
||||||
v-for="shift, shift_index in day.shifts"
|
v-for="shift, shift_index in day.shifts"
|
||||||
:key="shift_index"
|
:key="shift_index"
|
||||||
v-model:shift="day.shifts[shift_index]!"
|
v-model:shift="day.shifts[shift_index]!"
|
||||||
|
:day-shifts="day.shifts"
|
||||||
:is-timesheet-approved="approved"
|
:is-timesheet-approved="approved"
|
||||||
:dense="dense"
|
:dense="dense"
|
||||||
:has-shift-after="shift_index < day.shifts.length - 1"
|
:has-shift-after="shift_index < day.shifts.length - 1"
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const timesheet_api = useTimesheetApi();
|
const timesheet_api = useTimesheetApi();
|
||||||
|
|
||||||
const animation_style = computed(() => ui_store.is_mobile_mode ? 'fadeInLeft' : 'fadeInDown');
|
const mobile_animation_direction = ref('fadeInLeft');
|
||||||
|
const animation_style = computed(() => ui_store.is_mobile_mode ? mobile_animation_direction.value : 'fadeInDown');
|
||||||
|
|
||||||
const timesheet_page = ref<QScrollArea | null>(null);
|
const timesheet_page = ref<QScrollArea | null>(null);
|
||||||
const scroll_y = computed(() => timesheet_page.value?.getScrollPosition().top ?? 0)
|
const scroll_y = computed(() => timesheet_page.value?.getScrollPosition().top ?? 0)
|
||||||
|
|
@ -45,23 +46,52 @@
|
||||||
if (day.shifts.length < 1) return false;
|
if (day.shifts.length < 1) return false;
|
||||||
return day.shifts.every(shift => shift.is_approved === true);
|
return day.shifts.every(shift => shift.is_approved === true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSwipe = async (direction: 'left' | 'up' | 'down' | 'right' | undefined, distance: {x?: number, y?: number}) => {
|
||||||
|
mobile_animation_direction.value = direction === 'left' ? 'fadeInRight' : 'fadeInLeft';
|
||||||
|
if (distance.x && Math.abs(distance.x) > 10 ) {
|
||||||
|
await timesheet_api.getTimesheetsBySwiping( direction === 'left' ? 1 : -1 )
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="col column fit">
|
<div class="col column fit relative-position" v-touch-swipe="value => handleSwipe(value.direction, value.distance ?? {x: 0, y: 0})">
|
||||||
<q-scroll-area
|
<q-scroll-area
|
||||||
ref="timesheet_page"
|
ref="timesheet_page"
|
||||||
:horizontal-offset="[0, 3]"
|
:horizontal-offset="[0, 3]"
|
||||||
class="col hide-scrollbar q-mt-sm"
|
class="absolute-full hide-scrollbar q-mt-sm"
|
||||||
:thumb-style="{ opacity: '0' }"
|
:thumb-style="{ opacity: '0' }"
|
||||||
:bar-style="{ opacity: '0' }"
|
:bar-style="{ opacity: '0' }"
|
||||||
|
style="min-height: 50vh;"
|
||||||
>
|
>
|
||||||
<div :class="$q.platform.is.mobile ? 'column' : 'row'">
|
<!-- Show if no timesheets found (further than one month from present) -->
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="timesheet, timesheet_index in timesheet_store.timesheets"
|
v-if="timesheet_store.timesheets.length < 1"
|
||||||
|
class="col-auto column flex-center fit q-py-lg"
|
||||||
|
style="min-height: 20vh;"
|
||||||
|
>
|
||||||
|
<span class="text-uppercase text-weight-bolder text-center">{{ $t('shared.error.no_data_found')
|
||||||
|
}}</span>
|
||||||
|
<q-icon
|
||||||
|
name="las la-calendar"
|
||||||
|
color="accent"
|
||||||
|
size="10em"
|
||||||
|
class="absolute"
|
||||||
|
style="opacity: 0.2;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Else show timesheets if found -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="col fit"
|
||||||
|
:class="$q.platform.is.mobile ? 'column' : 'row'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="timesheet, timesheet_index of timesheet_store.timesheets"
|
||||||
:key="timesheet.timesheet_id"
|
:key="timesheet.timesheet_id"
|
||||||
class="col"
|
class="col fit"
|
||||||
>
|
>
|
||||||
<transition
|
<transition
|
||||||
appear
|
appear
|
||||||
|
|
@ -220,12 +250,10 @@
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-scroll-area>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-page-sticky
|
<q-page-sticky
|
||||||
position="bottom-right"
|
position="bottom-right"
|
||||||
:offset="[15, 15]"
|
:offset="[0, -35]"
|
||||||
class="z-top"
|
class="z-top"
|
||||||
>
|
>
|
||||||
<transition
|
<transition
|
||||||
|
|
@ -244,4 +272,7 @@
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
</q-page-sticky>
|
</q-page-sticky>
|
||||||
|
</q-scroll-area>
|
||||||
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
|
|
||||||
<TimesheetErrorWidget class="col-auto"/>
|
<TimesheetErrorWidget class="col-auto"/>
|
||||||
|
|
||||||
<ShiftList :mode="mode" />
|
<ShiftList />
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="mode === 'approval' || $q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
v-if="mode === 'approval' || $q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { useExpensesStore } from "src/stores/expense-store";
|
import { useExpensesStore } from "src/stores/expense-store";
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
import { Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
|
import { date } from "quasar";
|
||||||
|
|
||||||
export const useExpensesApi = () => {
|
export const useExpensesApi = () => {
|
||||||
const expenses_store = useExpensesStore();
|
const expenses_store = useExpensesStore();
|
||||||
|
|
@ -10,6 +11,7 @@ export const useExpensesApi = () => {
|
||||||
const upsertExpense = async (expense: Expense): Promise<void> => {
|
const upsertExpense = async (expense: Expense): Promise<void> => {
|
||||||
const success = await expenses_store.upsertExpense(expense);
|
const success = await expenses_store.upsertExpense(expense);
|
||||||
if (success) {
|
if (success) {
|
||||||
|
expenses_store.current_expense = new Expense(date.formatDate( new Date(), 'YYYY-MM-DD'));
|
||||||
timesheet_store.getTimesheetsByOptionalEmployeeEmail();
|
timesheet_store.getTimesheetsByOptionalEmployeeEmail();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,20 @@ export const useTimesheetApi = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTimesheetsBySwiping = async( direction: number ) => {
|
||||||
|
timesheet_store.is_loading = true;
|
||||||
|
|
||||||
|
timesheet_store.getNextOrPreviousPayPeriod(direction);
|
||||||
|
await timesheet_store.getPayPeriodByDateOrYearAndNumber();
|
||||||
|
await timesheet_store.getTimesheetsByOptionalEmployeeEmail();
|
||||||
|
|
||||||
|
timesheet_store.is_loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getTimesheetsByDate,
|
getTimesheetsByDate,
|
||||||
getTimesheetsByCurrentPayPeriod,
|
getTimesheetsByCurrentPayPeriod,
|
||||||
|
getTimesheetsBySwiping,
|
||||||
applyPreset,
|
applyPreset,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -20,13 +20,13 @@ export const timesheetService = {
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getTimesheetsByPayPeriodAndOptionalEmail: async (year: number, period_number: number, employee_email?: string): Promise<TimesheetResponse> => {
|
getTimesheetsByPayPeriodAndOptionalEmail: async (year: number, period_number: number, employee_email?: string): Promise<BackendResponse<TimesheetResponse>> => {
|
||||||
if (employee_email !== undefined) {
|
if (employee_email !== undefined) {
|
||||||
const response = await api.get<{ success: boolean, data: TimesheetResponse, error?: string }>(`timesheets/${year}/${period_number}?employee_email=${employee_email}`);
|
const response = await api.get<BackendResponse<TimesheetResponse>>(`timesheets/${year}/${period_number}?employee_email=${employee_email}`);
|
||||||
return response.data.data;
|
return response.data;
|
||||||
} else {
|
} else {
|
||||||
const response = await api.get<{ success: boolean, data: TimesheetResponse, error?: string }>(`timesheets/${year}/${period_number}`);
|
const response = await api.get<BackendResponse<TimesheetResponse>>(`timesheets/${year}/${period_number}`);
|
||||||
return response.data.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import type { ExpenseType } from "src/modules/timesheets/models/expense.models";
|
||||||
export const getExpenseIcon = (type: ExpenseType) => {
|
export const getExpenseIcon = (type: ExpenseType) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'MILEAGE': return 'time_to_leave';
|
case 'MILEAGE': return 'time_to_leave';
|
||||||
case 'EXPENSES': return 'receipt_long';
|
case 'EXPENSES': return 'las la-coins';
|
||||||
case 'PER_DIEM': return 'hotel';
|
case 'PER_DIEM': return 'hotel';
|
||||||
case 'ON_CALL': return 'phone_android';
|
case 'ON_CALL': return 'phone_android';
|
||||||
default: return 'help_outline';
|
default: return 'help_outline';
|
||||||
|
|
@ -15,7 +15,7 @@ export const useExpenseRules = (t: (_key: string) => string) => {
|
||||||
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.errors.mileage_required_for_type');
|
const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.mileage_required_for_type');
|
||||||
const commentRequired = (val: unknown) => (typeof val === 'string' ? val.trim().length > 0 : false) || t('timesheet.expense.errors.comment_required');
|
const commentRequired = (val: unknown) => (typeof val === 'string' ? val.trim().length > 0 : false) || t('timesheet.expense.hints.comment_required');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
typeRequired,
|
typeRequired,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { date, patterns, type ValidationRule } from "quasar";
|
import { date, patterns, type ValidationRule } from "quasar";
|
||||||
import type { SchedulePresetShift } from "src/modules/employee-list/models/schedule-presets.models";
|
import type { SchedulePresetShift } from "src/modules/employee-list/models/schedule-presets.models";
|
||||||
import type { Shift } from "src/modules/timesheets/models/shift.models";
|
import type { Shift, ShiftOption } from "src/modules/timesheets/models/shift.models";
|
||||||
|
|
||||||
export const isShiftOverlap = (shifts: Shift[] | SchedulePresetShift[]): boolean => {
|
export const isShiftOverlap = (shifts: Shift[] | SchedulePresetShift[]): boolean => {
|
||||||
if (shifts.length < 2) return false;
|
if (shifts.length < 2) return false;
|
||||||
|
|
@ -26,10 +26,21 @@ export const isShiftOverlap = (shifts: Shift[] | SchedulePresetShift[]): boolean
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useShiftRules = (time_required_error: string) => {
|
export const useShiftRules = (time_required_error: string, overlap_error_string: string, day_shifts: Shift[]) => {
|
||||||
const isTimeRequired: ValidationRule<string> = (time_string: string) => (!!time_string && patterns.testPattern.time(time_string)) || time_required_error;
|
const isTimeRequiredRule: ValidationRule<string> = (time_string: string) => (!!time_string && patterns.testPattern.time(time_string)) || time_required_error;
|
||||||
|
const isShiftOverlapRule: ValidationRule<string> = (_time_string: string) => !isShiftOverlap(day_shifts) || overlap_error_string;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isTimeRequired,
|
isTimeRequiredRule,
|
||||||
|
isShiftOverlapRule
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SHIFT_OPTIONS: ShiftOption[] = [
|
||||||
|
{ label: 'timesheet.shift.types.REGULAR', value: 'REGULAR', icon: 'wb_sunny', icon_color: 'accent' },
|
||||||
|
{ label: 'timesheet.shift.types.EVENING', value: 'EVENING', icon: 'bedtime', icon_color: 'orange-5' },
|
||||||
|
{ label: 'timesheet.shift.types.EMERGENCY', value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-5' },
|
||||||
|
{ label: 'timesheet.shift.types.VACATION', value: 'VACATION', icon: 'beach_access', icon_color: 'deep-orange-5' },
|
||||||
|
{ label: 'timesheet.shift.types.HOLIDAY', value: 'HOLIDAY', icon: 'event', icon_color: 'purple-5' },
|
||||||
|
{ label: 'timesheet.shift.types.SICK', value: 'SICK', icon: 'medication_liquid', icon_color: 'light-blue-6' },
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
const open = (): void => {
|
const open = (): void => {
|
||||||
is_open.value = true;
|
is_open.value = true;
|
||||||
if (timesheet_store.pay_period !== undefined) {
|
if (timesheet_store.pay_period !== undefined) {
|
||||||
current_expense.value = new Expense(timesheet_store.pay_period.period_start);
|
current_expense.value = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
initial_expense.value = new Expense(timesheet_store.pay_period.period_start);
|
initial_expense.value = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
}
|
}
|
||||||
mode.value = 'create';
|
mode.value = 'create';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,22 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
const is_approval_grid_mode = ref<boolean>(true);
|
const is_approval_grid_mode = ref<boolean>(true);
|
||||||
const pay_period_report = ref();
|
const pay_period_report = ref();
|
||||||
|
|
||||||
|
const getNextOrPreviousPayPeriod = (direction: number) => {
|
||||||
|
if (!pay_period.value) return;
|
||||||
|
|
||||||
|
pay_period.value.pay_period_no += direction;
|
||||||
|
|
||||||
|
if (pay_period.value.pay_period_no > 26) {
|
||||||
|
pay_period.value.pay_period_no = 1;
|
||||||
|
pay_period.value.pay_year += direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pay_period.value.pay_period_no < 1) {
|
||||||
|
pay_period.value.pay_period_no = 26;
|
||||||
|
pay_period.value.pay_year += direction;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getPayPeriodByDateOrYearAndNumber = async (date?: string): Promise<boolean> => {
|
const getPayPeriodByDateOrYearAndNumber = async (date?: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
if (date !== undefined) {
|
if (date !== undefined) {
|
||||||
|
|
@ -80,9 +96,15 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
response = await timesheetService.getTimesheetsByPayPeriodAndOptionalEmail(pay_period.value.pay_year, pay_period.value.pay_period_no);
|
response = await timesheetService.getTimesheetsByPayPeriodAndOptionalEmail(pay_period.value.pay_year, pay_period.value.pay_period_no);
|
||||||
}
|
}
|
||||||
|
|
||||||
selected_employee_name.value = response.employee_fullname;
|
if (response.success && response.data) {
|
||||||
timesheets.value = response.timesheets;
|
selected_employee_name.value = response.data.employee_fullname;
|
||||||
|
timesheets.value = response.data.timesheets;
|
||||||
initial_timesheets.value = unwrapAndClone(timesheets.value);
|
initial_timesheets.value = unwrapAndClone(timesheets.value);
|
||||||
|
} else {
|
||||||
|
selected_employee_name.value = '';
|
||||||
|
timesheets.value = [];
|
||||||
|
initial_timesheets.value = [];
|
||||||
|
}
|
||||||
is_loading.value = false;
|
is_loading.value = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('There was an error retrieving timesheet details for this employee: ', error);
|
console.error('There was an error retrieving timesheet details for this employee: ', error);
|
||||||
|
|
@ -121,6 +143,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
timesheets,
|
timesheets,
|
||||||
all_current_shifts,
|
all_current_shifts,
|
||||||
initial_timesheets,
|
initial_timesheets,
|
||||||
|
getNextOrPreviousPayPeriod,
|
||||||
getPayPeriodByDateOrYearAndNumber,
|
getPayPeriodByDateOrYearAndNumber,
|
||||||
getTimesheetOverviews,
|
getTimesheetOverviews,
|
||||||
getTimesheetsByOptionalEmployeeEmail,
|
getTimesheetsByOptionalEmployeeEmail,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user