Merge pull request 'release/nicolas/v1.1' (#81) from release/nicolas/v1.1 into main
Reviewed-on: Targo/targo_frontend#81
This commit is contained in:
commit
64ae14edba
|
|
@ -245,6 +245,8 @@ export default {
|
|||
day: "day",
|
||||
empty: "empty",
|
||||
name: "name",
|
||||
lock: "",
|
||||
unlock: "",
|
||||
},
|
||||
misc: {
|
||||
or: "or",
|
||||
|
|
@ -290,6 +292,8 @@ export default {
|
|||
apply_preset_day: "Apply schedule to day",
|
||||
apply_preset_week: "Apply schedule to week",
|
||||
save_successful: "timesheets saved",
|
||||
unsaved_changes_title: "You have unsaved changes",
|
||||
unsaved_changes_caption: "Save before leaving?",
|
||||
nav_button: {
|
||||
calendar_date_picker: "Calendar",
|
||||
current_week: "This week",
|
||||
|
|
@ -349,9 +353,10 @@ export default {
|
|||
type: "Type",
|
||||
types: {
|
||||
PER_DIEM: "Per Diem",
|
||||
EXPENSES: "expense",
|
||||
EXPENSES: "reimbursement",
|
||||
MILEAGE: "mileage",
|
||||
ON_CALL: "on-call allowance",
|
||||
COMMISSION: "Commission",
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
|
|
|
|||
|
|
@ -245,6 +245,8 @@ export default {
|
|||
day: "jour",
|
||||
empty: "vide",
|
||||
name: "nom",
|
||||
lock: "verrouiller",
|
||||
unlock: "déverrouiller",
|
||||
},
|
||||
misc: {
|
||||
or: "ou",
|
||||
|
|
@ -290,6 +292,8 @@ export default {
|
|||
apply_preset_day: "Appliquer horaire pour la journée",
|
||||
apply_preset_week: "Appliquer horaire pour la semaine",
|
||||
save_successful: "feuilles de temps enregistrées",
|
||||
unsaved_changes_title: "Vous avez des changements non-enregistrés",
|
||||
unsaved_changes_caption: "Sauvegardez avant de quitter?",
|
||||
nav_button: {
|
||||
calendar_date_picker: "Calendrier",
|
||||
current_week: "Semaine actuelle",
|
||||
|
|
@ -349,9 +353,10 @@ export default {
|
|||
type: "Type",
|
||||
types: {
|
||||
PER_DIEM: "Per diem",
|
||||
EXPENSES: "dépense",
|
||||
EXPENSES: "remboursement",
|
||||
MILEAGE: "kilométrage",
|
||||
ON_CALL: "Prime de garde",
|
||||
COMMISSION: "Commission",
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
|
|
|
|||
|
|
@ -11,25 +11,33 @@
|
|||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
|
||||
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
|
||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||
|
||||
// ========== state ========================================
|
||||
|
||||
const { t } = useI18n();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const timesheetStore = useTimesheetStore();
|
||||
const timesheetApprovalApi = useTimesheetApprovalApi();
|
||||
const is_dialog_open = ref(false);
|
||||
const shiftApi = useShiftApi();
|
||||
const isDialogOpen = ref(false);
|
||||
|
||||
const isApproved = computed(() => timesheet_store.timesheets.every(timesheet => timesheet.is_approved));
|
||||
// ========== computed ========================================
|
||||
|
||||
const isApproved = computed(() => timesheetStore.timesheets.every(timesheet => timesheet.is_approved));
|
||||
const approveButtonLabel = computed(() => isApproved.value ?
|
||||
t('timesheet_approvals.table.verified') :
|
||||
t('timesheet_approvals.table.unverified')
|
||||
t('shared.label.unlock') :
|
||||
t('shared.label.lock')
|
||||
);
|
||||
const approveButtonIcon = computed(() => isApproved.value ? 'lock' : 'lock_open');
|
||||
const hasExpenses = computed(() => timesheet_store.timesheets.some(timesheet =>
|
||||
const approveButtonIcon = computed(() => isApproved.value ? 'las la-lock' : 'las la-unlock');
|
||||
const hasExpenses = computed(() => timesheetStore.timesheets.some(timesheet =>
|
||||
Object.values(timesheet.weekly_expenses).some(hours => hours > 0))
|
||||
);
|
||||
|
||||
// ========== methods ========================================
|
||||
|
||||
const onClickApproveAll = async () => {
|
||||
const employeeEmail = timesheet_store.current_pay_period_overview?.email;
|
||||
const isApproved = timesheet_store.timesheets.every(timesheet => timesheet.is_approved);
|
||||
const employeeEmail = timesheetStore.current_pay_period_overview?.email;
|
||||
const isApproved = timesheetStore.timesheets.every(timesheet => timesheet.is_approved);
|
||||
|
||||
if (employeeEmail !== undefined && isApproved !== undefined) {
|
||||
await timesheetApprovalApi.toggleTimesheetsApprovalByEmployeeEmail(
|
||||
|
|
@ -38,19 +46,23 @@
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const onClickSaveTimesheets = async () => {
|
||||
await shiftApi.saveShiftChanges(timesheetStore.current_pay_period_overview?.email);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog
|
||||
v-model="timesheet_store.is_details_dialog_open"
|
||||
v-model="timesheetStore.is_details_dialog_open"
|
||||
full-width
|
||||
full-height
|
||||
transition-show="jump-down"
|
||||
transition-hide="jump-down"
|
||||
backdrop-filter="blur(6px)"
|
||||
@show="is_dialog_open = true"
|
||||
@hide="is_dialog_open = false"
|
||||
@before-hide="timesheet_store.getTimesheetOverviews"
|
||||
@show="isDialogOpen = true"
|
||||
@hide="isDialogOpen = false"
|
||||
@before-hide="timesheetStore.getTimesheetOverviews"
|
||||
>
|
||||
<div
|
||||
class="column bg-secondary hide-scrollbar shadow-12 rounded-15 q-pb-sm no-wrap"
|
||||
|
|
@ -58,13 +70,14 @@
|
|||
>
|
||||
<!-- employee name -->
|
||||
<div class="col-auto row flex-center q-px-none q-py-sm sticky-top bg-secondary full-width shadow-4">
|
||||
<span class="col text-h4 text-weight-bolder text-uppercase q-px-lg">
|
||||
{{ timesheet_store.selected_employee_name }}
|
||||
<span class="col-auto text-h4 text-weight-bolder text-uppercase q-px-lg">
|
||||
{{ timesheetStore.selected_employee_name }}
|
||||
</span>
|
||||
|
||||
<div class="col-auto q-px-lg">
|
||||
<q-btn
|
||||
push
|
||||
:push="isApproved"
|
||||
:outline="!isApproved"
|
||||
dense
|
||||
size="lg"
|
||||
color="accent"
|
||||
|
|
@ -84,11 +97,27 @@
|
|||
</transition>
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<q-space />
|
||||
|
||||
<div class="col-auto q-px-md">
|
||||
<q-btn
|
||||
push
|
||||
dense
|
||||
size="lg"
|
||||
:disable="timesheetStore.is_loading || !timesheetStore.canSaveTimesheets"
|
||||
:color="timesheetStore.is_loading || !timesheetStore.canSaveTimesheets ? 'grey-5' : 'accent'"
|
||||
icon="upload"
|
||||
:label="$t('shared.label.save')"
|
||||
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'full-width' : 'q-ml-md'"
|
||||
@click="onClickSaveTimesheets"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- employee pay period details using chart -->
|
||||
<div
|
||||
v-if="is_dialog_open"
|
||||
v-if="isDialogOpen"
|
||||
class="col-auto q-px-md no-wrap"
|
||||
:class="$q.platform.is.mobile ? 'column' : 'row'"
|
||||
>
|
||||
|
|
@ -108,7 +137,7 @@
|
|||
<div class="col-auto">
|
||||
<TimesheetWrapper
|
||||
mode="approval"
|
||||
:employee-email="timesheet_store.current_pay_period_overview?.email"
|
||||
:employee-email="timesheetStore.current_pay_period_overview?.email"
|
||||
class="col-auto"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
import { type OverviewColumns, pay_period_overview_columns, PayPeriodOverviewFilters, type TimesheetApprovalOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
|
||||
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||
|
||||
// ========== constants ========================================
|
||||
|
||||
|
|
@ -46,11 +47,12 @@
|
|||
|
||||
const q = useQuasar();
|
||||
const uiStore = useUiStore();
|
||||
const auth_store = useAuthStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const timesheet_approval_api = useTimesheetApprovalApi();
|
||||
const authStore = useAuthStore();
|
||||
const timesheetApi = useTimesheetApi();
|
||||
const timesheetStore = useTimesheetStore();
|
||||
const timesheetApprovalApi = useTimesheetApprovalApi();
|
||||
|
||||
const overview_filters = ref<PayPeriodOverviewFilters>({
|
||||
const overviewFilters = ref<PayPeriodOverviewFilters>({
|
||||
is_showing_inactive: false,
|
||||
is_showing_team_only: true,
|
||||
supervisors: [],
|
||||
|
|
@ -63,21 +65,21 @@
|
|||
uiStore.user_preferences.is_timesheet_approval_grid
|
||||
);
|
||||
|
||||
const overview_rows = computed(() =>
|
||||
timesheet_store.pay_period_overviews.filter(overview => overview)
|
||||
const overviewRows = computed(() =>
|
||||
timesheetStore.pay_period_overviews.filter(overview => overview)
|
||||
);
|
||||
|
||||
// ========== methods ========================================
|
||||
|
||||
const onClickedDetails = async (row: TimesheetApprovalOverview) => {
|
||||
timesheet_store.current_pay_period_overview = row;
|
||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(row.email);
|
||||
timesheetStore.current_pay_period_overview = row;
|
||||
await timesheetApi.getTimesheetsByCurrentPayPeriod(row.email);
|
||||
|
||||
timesheet_store.is_details_dialog_open = true;
|
||||
timesheetStore.is_details_dialog_open = true;
|
||||
};
|
||||
|
||||
const onClickApproveAll = async (email: string, is_approved: boolean) => {
|
||||
await timesheet_approval_api.toggleTimesheetsApprovalByEmployeeEmail(email, is_approved);
|
||||
await timesheetApprovalApi.toggleTimesheetsApprovalByEmployeeEmail(email, is_approved);
|
||||
}
|
||||
|
||||
const filterEmployeeRows = (rows: readonly TimesheetApprovalOverview[], terms: PayPeriodOverviewFilters): TimesheetApprovalOverview[] => {
|
||||
|
|
@ -88,7 +90,7 @@
|
|||
}
|
||||
|
||||
if (terms.is_showing_team_only) {
|
||||
result = result.filter(row => row.supervisor !== null && row.supervisor.email === (auth_store.user ? auth_store.user.email : ''));
|
||||
result = result.filter(row => row.supervisor !== null && row.supervisor.email === (authStore.user ? authStore.user.email : ''));
|
||||
}
|
||||
|
||||
if (terms.name_search_string.length > 0) {
|
||||
|
|
@ -114,20 +116,20 @@
|
|||
|
||||
<template>
|
||||
<div class="full-width">
|
||||
<LoadingOverlay v-model="timesheet_store.is_loading" />
|
||||
<LoadingOverlay v-model="timesheetStore.is_loading" />
|
||||
<q-table
|
||||
dense
|
||||
row-key="email"
|
||||
color="accent"
|
||||
separator="none"
|
||||
hide-pagination
|
||||
:rows="overview_rows"
|
||||
:rows="overviewRows"
|
||||
:columns="pay_period_overview_columns"
|
||||
:table-colspan="pay_period_overview_columns.length"
|
||||
:visible-columns="VISIBLE_COLUMNS"
|
||||
:grid="isGridMode"
|
||||
:pagination="{ sortBy: 'is_active' }"
|
||||
:filter="overview_filters"
|
||||
:filter="overviewFilters"
|
||||
:filter-method="filterEmployeeRows"
|
||||
:rows-per-page-options="[0]"
|
||||
class="bg-transparent"
|
||||
|
|
@ -138,20 +140,20 @@
|
|||
:no-results-label="$t('shared.error.no_search_results')"
|
||||
:loading-label="$t('shared.label.loading')"
|
||||
table-header-style="min-width: 80xp; max-width: 80px;"
|
||||
:style="overview_rows.length > 0 ? `max-height: ${maxHeight}px;` : ''"
|
||||
:style="overviewRows.length > 0 ? `max-height: ${maxHeight}px;` : ''"
|
||||
:table-style="{ tableLayout: 'fixed' }"
|
||||
@row-click="(_evt, row: TimesheetApprovalOverview) => onClickedDetails(row)"
|
||||
>
|
||||
<template #top>
|
||||
<OverviewListTopMobile
|
||||
v-if="$q.platform.is.mobile"
|
||||
v-model:filters="overview_filters"
|
||||
v-model:filters="overviewFilters"
|
||||
v-model:visible-columns="VISIBLE_COLUMNS"
|
||||
/>
|
||||
|
||||
<OverviewListTop
|
||||
v-else
|
||||
v-model:filters="overview_filters"
|
||||
v-model:filters="overviewFilters"
|
||||
v-model:visible-columns="VISIBLE_COLUMNS"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -192,7 +194,7 @@
|
|||
mode="out-in"
|
||||
>
|
||||
<div
|
||||
:key="props.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
|
||||
:key="props.rowIndex + (timesheetStore.pay_period?.pay_period_no ?? 0)"
|
||||
class="rounded-5"
|
||||
:style="`animation-delay: ${props.rowIndex / 15}s; opacity: ${props.row.is_active ? '1' : '0.5'};`"
|
||||
>
|
||||
|
|
@ -265,7 +267,7 @@
|
|||
<template #item="props: { row: TimesheetApprovalOverview, rowIndex: number }">
|
||||
<OverviewListItem
|
||||
v-model="props.row.is_approved"
|
||||
:key="props.row.email + timesheet_store.pay_period?.pay_period_no"
|
||||
:key="props.row.email + timesheetStore.pay_period?.pay_period_no"
|
||||
:row="props.row"
|
||||
@click-details="onClickedDetails"
|
||||
@click-approval-all="is_approved => onClickApproveAll(props.row.email, is_approved)"
|
||||
|
|
@ -275,7 +277,7 @@
|
|||
<!-- Template for custome failed-to-load state -->
|
||||
<template #no-data="{ message, filter }">
|
||||
<div
|
||||
v-if="!timesheet_store.is_loading"
|
||||
v-if="!timesheetStore.is_loading"
|
||||
class="full-width column items-center text-accent"
|
||||
>
|
||||
<q-icon
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { ref } from 'vue';
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
|
||||
const expenseStore = useExpensesStore();
|
||||
const isMaximized = ref(false);
|
||||
const imageDimension = ref('400px');
|
||||
|
||||
const onClickMaximizeButton = () => {
|
||||
isMaximized.value = !isMaximized.value;
|
||||
imageDimension.value = isMaximized.value ? '100%' : '400px';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog
|
||||
v-model="expenseStore.isShowingAttachmentDialog"
|
||||
backdrop-filter="blur(4px)"
|
||||
:full-height="isMaximized"
|
||||
:full-width="isMaximized"
|
||||
>
|
||||
<q-card class="q-pa-md flex-center relative-position">
|
||||
<q-img
|
||||
spinner-color="accent"
|
||||
fit="contain"
|
||||
:height="imageDimension"
|
||||
:width="imageDimension"
|
||||
:src="expenseStore.attachmentURL"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
dense
|
||||
size="lg"
|
||||
color="accent"
|
||||
:icon="isMaximized ? 'zoom_in_map' : 'zoom_out_map'"
|
||||
class="absolute-bottom-right q-ma-sm rounded-5"
|
||||
style="opacity: 0.5;"
|
||||
@click="onClickMaximizeButton"
|
||||
/>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
@ -67,6 +67,12 @@
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
const onClickAttachment = async () => {
|
||||
expenses_store.isShowingAttachmentDialog = true;
|
||||
await expenses_store.getAttachmentURL(expense.value.attachment_key);
|
||||
console.log('image url: ', expenses_store.attachmentURL);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -131,6 +137,7 @@
|
|||
:text-color="expense.is_approved ? 'accent' : 'white'"
|
||||
class="col-auto q-px-sm q-mr-sm"
|
||||
icon="attach_file"
|
||||
@click.stop="onClickAttachment"
|
||||
/>
|
||||
|
||||
<q-item-label class="col">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
||||
import ExpenseDialogHeader from 'src/modules/timesheets/components/expense-dialog-header.vue';
|
||||
import ExpenseDialogFormMobile from 'src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue';
|
||||
import ExpenseDialogAttachmentViewer from 'src/modules/timesheets/components/expense-dialog-attachment-viewer.vue';
|
||||
|
||||
import { date } from 'quasar';
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
|
|
@ -32,6 +33,8 @@
|
|||
transition-show="jump-down"
|
||||
transition-hide="jump-down"
|
||||
>
|
||||
<ExpenseDialogAttachmentViewer class="z-top" />
|
||||
|
||||
<q-card
|
||||
class="q-pa-none rounded-10 shadow-24 bg-secondary"
|
||||
style=" min-width: 70vw;"
|
||||
|
|
|
|||
|
|
@ -61,8 +61,13 @@
|
|||
employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL',
|
||||
file.value
|
||||
);
|
||||
|
||||
emit('onUpdateClicked');
|
||||
else
|
||||
await expenses_api.upsertExpense(
|
||||
expenses_store.current_expense,
|
||||
employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL'
|
||||
);
|
||||
|
||||
emit('onUpdateClicked');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
|
|
|||
|
|
@ -5,25 +5,31 @@
|
|||
import { computed, onMounted, ref } from 'vue';
|
||||
import { QSelect, QInput } from 'quasar';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
import { getCurrentDailyMinutesWorked, getTimeStringFromMinutes, SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||
import type { Shift, ShiftOption, ShiftType } from 'src/modules/timesheets/models/shift.models';
|
||||
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
|
||||
|
||||
const ui_store = useUiStore();
|
||||
// ========== state ========================================
|
||||
|
||||
const SHIFT_TYPES_WITH_PREDEFINED_TIMES: ShiftType[] = ['HOLIDAY', 'SICK', 'VACATION'];
|
||||
const COMMENT_LENGTH_MAX = 280;
|
||||
|
||||
const shift = defineModel<Shift>('shift', { required: true });
|
||||
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
||||
const select_ref = ref<QSelect | null>(null);
|
||||
const is_showing_comment_popup = ref(false);
|
||||
const comment_length = computed(() => shift.value.comment?.length ?? 0);
|
||||
const error_message = ref('');
|
||||
|
||||
const { errorMessage = undefined, dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
||||
const {
|
||||
dense = false,
|
||||
hasShiftAfter = false,
|
||||
isTimesheetApproved = false,
|
||||
errorMessage = undefined,
|
||||
expectedDailyHours = 8,
|
||||
currentShifts,
|
||||
} = defineProps<{
|
||||
dense?: boolean;
|
||||
hasShiftAfter?: boolean;
|
||||
isTimesheetApproved?: boolean;
|
||||
errorMessage?: string | undefined;
|
||||
expectedDailyHours?: number;
|
||||
currentShifts: Shift[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -31,8 +37,23 @@
|
|||
'onTimeFieldBlur': [void];
|
||||
}>();
|
||||
|
||||
const ui_store = useUiStore();
|
||||
const shiftTypeSelected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
||||
const select_ref = ref<QSelect | null>(null);
|
||||
const is_showing_comment_popup = ref(false);
|
||||
const error_message = ref('');
|
||||
const isShowingPredefinedTime = ref(shift.value.type === 'HOLIDAY');
|
||||
const predefinedHoursString = ref('');
|
||||
const predefinedHoursBgColor = ref(`bg-${shiftTypeSelected.value?.icon_color ?? ''}`);
|
||||
|
||||
// ========== computed ========================================
|
||||
|
||||
const comment_length = computed(() => shift.value.comment?.length ?? 0);
|
||||
|
||||
// ========== methods =========================================
|
||||
|
||||
const onBlurShiftTypeSelect = () => {
|
||||
if (shift_type_selected.value === undefined) {
|
||||
if (shiftTypeSelected.value === undefined) {
|
||||
shift.value.type = 'REGULAR';
|
||||
shift.value.id = 0;
|
||||
emit('requestDelete');
|
||||
|
|
@ -56,11 +77,35 @@
|
|||
return 'negative';
|
||||
};
|
||||
|
||||
const onShiftTypeChange = (option: ShiftOption) => {
|
||||
shift.value.type = option.value;
|
||||
|
||||
if (SHIFT_TYPES_WITH_PREDEFINED_TIMES.includes(option.value)) {
|
||||
predefinedHoursBgColor.value = `bg-${option.icon_color}`;
|
||||
shift.value.start_time = '00:00';
|
||||
|
||||
if (option.value === 'SICK' || option.value === 'VACATION') {
|
||||
const workedMinutes = getCurrentDailyMinutesWorked(currentShifts);
|
||||
console.log('worked minutes: ', workedMinutes);
|
||||
const expectedWorkedMinutes = expectedDailyHours * 60;
|
||||
const leftOverMinutes = expectedWorkedMinutes - workedMinutes;
|
||||
|
||||
shift.value.end_time = getTimeStringFromMinutes(leftOverMinutes);
|
||||
isShowingPredefinedTime.value = false;
|
||||
} else {
|
||||
isShowingPredefinedTime.value = true;
|
||||
predefinedHoursString.value = getHoursMinutesStringFromHoursFloat(expectedDailyHours);
|
||||
shift.value.end_time = getTimeStringFromMinutes(expectedDailyHours * 60);
|
||||
}
|
||||
} else
|
||||
isShowingPredefinedTime.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (ui_store.focus_next_component) {
|
||||
select_ref.value?.focus();
|
||||
select_ref.value?.showPopup();
|
||||
shift_type_selected.value = undefined;
|
||||
shiftTypeSelected.value = undefined;
|
||||
ui_store.focus_next_component = false;
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +169,7 @@
|
|||
<!-- shift type -->
|
||||
<q-select
|
||||
ref="select"
|
||||
v-model="shift_type_selected"
|
||||
v-model="shiftTypeSelected"
|
||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||
dense
|
||||
:borderless="(shift.is_approved && isTimesheetApproved)"
|
||||
|
|
@ -141,7 +186,7 @@
|
|||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||
popup-content-style="border: 2px solid var(--q-accent)"
|
||||
@blur="onBlurShiftTypeSelect"
|
||||
@update:model-value="option => shift.type = option.value"
|
||||
@update:model-value="onShiftTypeChange"
|
||||
>
|
||||
<template #selected-item="scope">
|
||||
<div
|
||||
|
|
@ -227,7 +272,25 @@
|
|||
</q-select>
|
||||
</div>
|
||||
|
||||
<div class="col row items-start text-uppercase rounded-5 q-pa-xs">
|
||||
<div
|
||||
v-if="isShowingPredefinedTime"
|
||||
class="col row items-start text-uppercase rounded-5 q-pa-xs relative-position"
|
||||
>
|
||||
<div
|
||||
class="absolute-full rounded-5 q-mx-sm q-my-xs"
|
||||
:class="predefinedHoursBgColor"
|
||||
style="opacity: 0.3;"
|
||||
></div>
|
||||
|
||||
<span class="col text-center text-uppercase text-h6 text-bold q-py-xs">
|
||||
{{ getHoursMinutesStringFromHoursFloat(expectedDailyHours) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="col row items-start text-uppercase rounded-5 q-pa-xs"
|
||||
>
|
||||
<!-- punch in field -->
|
||||
<div class="col q-pr-xs">
|
||||
<q-input
|
||||
|
|
|
|||
|
|
@ -6,15 +6,29 @@
|
|||
import { computed, inject, onMounted, ref } from 'vue';
|
||||
import { QSelect, QInput, useQuasar, type QSelectProps, QPopupProxy } from 'quasar';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
import { getCurrentDailyMinutesWorked, getShiftOptions, getTimeStringFromMinutes, SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||
import type { Shift, ShiftOption, ShiftType } from 'src/modules/timesheets/models/shift.models';
|
||||
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
|
||||
|
||||
// ================== State ==================
|
||||
// ========== Constants ========================================
|
||||
|
||||
const SHIFT_TYPES_WITH_PREDEFINED_TIMES: ShiftType[] = ['HOLIDAY', 'SICK', 'VACATION'];
|
||||
const COMMENT_LENGTH_MAX = 280;
|
||||
|
||||
// ========== State ========================================
|
||||
|
||||
const shift = defineModel<Shift>('shift', { required: true });
|
||||
|
||||
const { errorMessage = undefined, isTimesheetApproved = false, holiday = false } = defineProps<{
|
||||
const {
|
||||
errorMessage = undefined,
|
||||
isTimesheetApproved = false,
|
||||
currentShifts,
|
||||
holiday = false,
|
||||
expectedDailyHours = 8,
|
||||
} = defineProps<{
|
||||
currentShifts: Shift[];
|
||||
expectedDailyHours?: number;
|
||||
isTimesheetApproved?: boolean;
|
||||
errorMessage?: string | undefined;
|
||||
holiday?: boolean | undefined;
|
||||
|
|
@ -25,9 +39,6 @@
|
|||
'onTimeFieldBlur': [void];
|
||||
}>();
|
||||
|
||||
|
||||
const COMMENT_LENGTH_MAX = 280;
|
||||
|
||||
const q = useQuasar();
|
||||
const { t } = useI18n();
|
||||
const ui_store = useUiStore();
|
||||
|
|
@ -39,10 +50,15 @@
|
|||
const selectRef = ref<QSelect | null>(null);
|
||||
const shiftErrorMessage = ref<string | undefined>();
|
||||
const is_showing_delete_confirm = ref(false);
|
||||
const isShowingPredefinedTime = ref(shift.value.type === 'HOLIDAY');
|
||||
const popupProxyRef = ref<QPopupProxy | null>(null);
|
||||
const predefinedHoursString = ref('');
|
||||
const predefinedHoursBgColor = ref(`bg-${shiftTypeSelected.value?.icon_color ?? ''}`);
|
||||
|
||||
// ================== Computed ==================
|
||||
|
||||
const hasPTO = computed(() => currentShifts.some(shift => SHIFT_TYPES_WITH_PREDEFINED_TIMES.includes(shift.type)));
|
||||
|
||||
const rightClickMenuIcon = computed(() => shift.value.is_approved ? 'lock_open' : 'lock');
|
||||
|
||||
const rightClickMenuLabel = computed(() => shift.value.is_approved ?
|
||||
|
|
@ -77,7 +93,7 @@
|
|||
menuOffset: [0, 10],
|
||||
menuAnchor: "bottom middle",
|
||||
menuSelf: "top middle",
|
||||
options: SHIFT_OPTIONS,
|
||||
options: getShiftOptions(hasPTO.value, currentShifts.length > 1),
|
||||
class: `col rounded-5 q-mx-xs bg-dark ${!shift.value.is_approved && !isTimesheetApproved ? '' : 'inset-shadow'}`,
|
||||
popupContentClass: "text-uppercase text-weight-bold text-center rounded-5",
|
||||
style: shift.value.is_approved ? (holiday ? 'background-color: #7b1fa2 !important' : 'background-color: #0a7d32 !important;') : '',
|
||||
|
|
@ -128,6 +144,30 @@
|
|||
popupProxyRef.value.hide();
|
||||
}
|
||||
|
||||
const onShiftTypeChange = (option: ShiftOption) => {
|
||||
shift.value.type = option.value;
|
||||
|
||||
if (SHIFT_TYPES_WITH_PREDEFINED_TIMES.includes(option.value)) {
|
||||
predefinedHoursBgColor.value = `bg-${option.icon_color}`;
|
||||
shift.value.start_time = '00:00';
|
||||
|
||||
if (option.value === 'SICK' || option.value === 'VACATION') {
|
||||
const workedMinutes = getCurrentDailyMinutesWorked(currentShifts);
|
||||
console.log('worked minutes: ', workedMinutes);
|
||||
const expectedWorkedMinutes = expectedDailyHours * 60;
|
||||
const leftOverMinutes = expectedWorkedMinutes - workedMinutes;
|
||||
|
||||
shift.value.end_time = getTimeStringFromMinutes(leftOverMinutes);
|
||||
} else {
|
||||
isShowingPredefinedTime.value = true;
|
||||
predefinedHoursString.value = getHoursMinutesStringFromHoursFloat(expectedDailyHours);
|
||||
shift.value.start_time = '00:00';
|
||||
shift.value.end_time = `${expectedDailyHours}:00`;
|
||||
}
|
||||
} else
|
||||
isShowingPredefinedTime.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (ui_store.focus_next_component) {
|
||||
selectRef.value?.focus();
|
||||
|
|
@ -206,7 +246,7 @@
|
|||
v-model="shiftTypeSelected"
|
||||
v-bind="shiftTypeSelectProps"
|
||||
@blur="onBlurShiftTypeSelect"
|
||||
@update:model-value="option => shift.type = option.value"
|
||||
@update:model-value="onShiftTypeChange"
|
||||
>
|
||||
<template #selected-item="scope">
|
||||
<div
|
||||
|
|
@ -291,40 +331,58 @@
|
|||
</q-select>
|
||||
</div>
|
||||
|
||||
<div class="col row items-start text-uppercase rounded-5 q-pa-xs">
|
||||
<!-- punch in field -->
|
||||
<q-input
|
||||
ref="start_time"
|
||||
v-model="shift.start_time"
|
||||
v-bind="timeInputProps"
|
||||
type="time"
|
||||
@blur="onTimeFieldBlur(shift.start_time)"
|
||||
<div class="col row">
|
||||
<!-- If shift type has predefined timestamps -->
|
||||
<div
|
||||
v-if="isShowingPredefinedTime"
|
||||
class="col row q-pa-xs relative-position flex-center"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
:class="shift.is_approved ? ' q-ml-md' : ''"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.in') }}</span>
|
||||
</template>
|
||||
</q-input>
|
||||
<div
|
||||
class="absolute-full rounded-5 q-mx-sm q-my-xs"
|
||||
:class="predefinedHoursBgColor"
|
||||
style="opacity: 0.3;"
|
||||
></div>
|
||||
|
||||
<!-- punch out field -->
|
||||
<q-input
|
||||
ref="end_time"
|
||||
v-model="shift.end_time"
|
||||
v-bind="timeInputProps"
|
||||
type="time"
|
||||
@blur="onTimeFieldBlur(shift.end_time)"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
:class="shift.is_approved ? ' q-ml-md' : ''"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.out') }}</span>
|
||||
</template>
|
||||
</q-input>
|
||||
<span class="col text-center text-uppercase text-h6 text-bold q-py-xs">
|
||||
{{ getHoursMinutesStringFromHoursFloat(expectedDailyHours) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Else show input fields for in-out timestamps -->
|
||||
<div v-else class="col row items-start text-uppercase rounded-5 q-pa-xs">
|
||||
<q-input
|
||||
ref="start_time"
|
||||
v-model="shift.start_time"
|
||||
v-bind="timeInputProps"
|
||||
type="time"
|
||||
@blur="onTimeFieldBlur(shift.start_time)"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
:class="shift.is_approved ? ' q-ml-md' : ''"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.in') }}</span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<!-- punch out field -->
|
||||
<q-input
|
||||
ref="end_time"
|
||||
v-model="shift.end_time"
|
||||
v-bind="timeInputProps"
|
||||
type="time"
|
||||
@blur="onTimeFieldBlur(shift.end_time)"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
:class="shift.is_approved ? ' q-ml-md' : ''"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.out') }}</span>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="row full-height"
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@
|
|||
:is-timesheet-approved="approved"
|
||||
:error-message="shift_error_message"
|
||||
:dense="dense"
|
||||
:current-shifts="day.shifts"
|
||||
:has-shift-after="shift_index < day.shifts.length - 1"
|
||||
@request-delete="deleteCurrentShift(shift)"
|
||||
@on-time-field-blur="onTimeFieldBlur()"
|
||||
|
|
@ -116,6 +117,7 @@
|
|||
v-else
|
||||
v-model:shift="day.shifts[shift_index]!"
|
||||
:holiday="holiday"
|
||||
:current-shifts="day.shifts"
|
||||
:is-timesheet-approved="approved"
|
||||
:error-message="shift_error_message"
|
||||
@request-delete="deleteCurrentShift(shift)"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@
|
|||
const bankedHours = computed(() => timesheetStore.paid_time_off_totals.banked_hours);
|
||||
|
||||
onMounted(async () => {
|
||||
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail();
|
||||
if (timesheetMode === 'normal')
|
||||
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail();
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,14 +10,17 @@
|
|||
import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
|
||||
import ShiftListWeeklyOverview from 'src/modules/timesheets/components/shift-list-weekly-overview.vue';
|
||||
import ShiftListWeeklyOverviewMobile from 'src/modules/timesheets/components/mobile/shift-list-weekly-overview-mobile.vue';
|
||||
import UnsavedChangesDialog from 'src/modules/timesheets/components/unsaved-changes-dialog.vue';
|
||||
|
||||
import { date, Notify } from 'quasar';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { computed, onMounted, provide } from 'vue';
|
||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { RouteNames } from 'src/router/router-constants';
|
||||
|
||||
// ================= state ====================
|
||||
|
||||
|
|
@ -27,18 +30,25 @@
|
|||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const expenses_store = useExpensesStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const timesheet_api = useTimesheetApi();
|
||||
const shift_api = useShiftApi();
|
||||
const router = useRouter();
|
||||
const expenseStore = useExpensesStore();
|
||||
const timesheetStore = useTimesheetStore();
|
||||
const timesheetApi = useTimesheetApi();
|
||||
const shiftApi = useShiftApi();
|
||||
|
||||
// ================== computed ====================
|
||||
|
||||
const has_shift_errors = computed(() => timesheet_store.all_current_shifts.filter(shift => shift.has_error === true).length > 0);
|
||||
const hasShiftErrors = computed(() => timesheetStore.all_current_shifts.filter(shift => shift.has_error === true).length > 0);
|
||||
const isTimesheetsApproved = computed(() => timesheetStore.timesheets.every(timesheet => timesheet.is_approved));
|
||||
// const timesheetStore.canSaveTimesheets = computed(() => {
|
||||
// /* eslint-disable-next-line */
|
||||
// const currentShifts = timesheetStore.timesheets.flatMap(timesheet => timesheet.days.flatMap(day => day.shifts.map(shift => { const { has_error, ...shft } = shift; return shft; })));
|
||||
// const initialShifts = timesheetStore.initial_timesheets.flatMap(timesheet => timesheet.days.flatMap(day => day.shifts));
|
||||
|
||||
const is_timesheets_approved = computed(() => timesheet_store.timesheets.every(timesheet => timesheet.is_approved))
|
||||
// return JSON.stringify(currentShifts) !== JSON.stringify(initialShifts);
|
||||
// });
|
||||
|
||||
const total_hours = computed(() => timesheet_store.timesheets.reduce((sum, timesheet) =>
|
||||
const totalHours = computed(() => timesheetStore.timesheets.reduce((sum, timesheet) =>
|
||||
sum += timesheet.weekly_hours.regular
|
||||
+ timesheet.weekly_hours.evening
|
||||
+ timesheet.weekly_hours.emergency
|
||||
|
|
@ -46,7 +56,7 @@
|
|||
0) //initial value
|
||||
);
|
||||
|
||||
const total_expenses = computed(() => timesheet_store.timesheets.reduce((sum, timesheet) =>
|
||||
const totalExpenses = computed(() => timesheetStore.timesheets.reduce((sum, timesheet) =>
|
||||
sum + timesheet.weekly_expenses.expenses
|
||||
+ timesheet.weekly_expenses.on_call
|
||||
+ timesheet.weekly_expenses.per_diem,
|
||||
|
|
@ -60,25 +70,43 @@
|
|||
|
||||
const onClickSaveTimesheets = async () => {
|
||||
if (mode === 'normal') {
|
||||
await shift_api.saveShiftChanges();
|
||||
await shiftApi.saveShiftChanges();
|
||||
Notify.create({
|
||||
message: t('timesheet.save_successful'),
|
||||
color: 'accent',
|
||||
});
|
||||
} else {
|
||||
await shift_api.saveShiftChanges(timesheet_store.current_pay_period_overview?.email);
|
||||
await shiftApi.saveShiftChanges(timesheetStore.current_pay_period_overview?.email);
|
||||
}
|
||||
}
|
||||
|
||||
const onClickLeave = async () => {
|
||||
timesheetStore.isShowingUnsavedWarning = false;
|
||||
timesheetStore.timesheets = [];
|
||||
timesheetStore.initial_timesheets = [];
|
||||
await router.push({ name: timesheetStore.nextPageNameAfterUnsaveWarning ?? RouteNames.DASHBOARD});
|
||||
}
|
||||
|
||||
const onClickSaveBeforeLeaving = async () => {
|
||||
if (mode === 'approval') return;
|
||||
|
||||
timesheetStore.isShowingUnsavedWarning = false;
|
||||
await onClickSaveTimesheets();
|
||||
await onClickLeave();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (mode === 'normal')
|
||||
await timesheet_api.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||
await timesheetApi.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="column items-center full-height" :class="mode === 'normal' ? 'relative-position' : ' no-wrap'">
|
||||
<LoadingOverlay v-model="timesheet_store.is_loading" />
|
||||
<div
|
||||
class="column items-center full-height"
|
||||
:class="mode === 'normal' ? 'relative-position' : ' no-wrap'"
|
||||
>
|
||||
<LoadingOverlay v-model="timesheetStore.is_loading" />
|
||||
|
||||
<!-- label for approval mode to delimit that this is the timesheet -->
|
||||
<div
|
||||
|
|
@ -90,19 +118,6 @@
|
|||
</span>
|
||||
|
||||
<q-space />
|
||||
|
||||
<!-- desktop save timesheet changes button -->
|
||||
<q-btn
|
||||
v-if="!is_timesheets_approved && $q.screen.width > $q.screen.height"
|
||||
push
|
||||
rounded
|
||||
:disable="timesheet_store.is_loading || has_shift_errors"
|
||||
:color="timesheet_store.is_loading || has_shift_errors ? 'grey-5' : 'accent'"
|
||||
icon="upload"
|
||||
:label="$t('shared.label.save')"
|
||||
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'full-width' : 'q-ml-md'"
|
||||
@click="onClickSaveTimesheets"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -116,8 +131,8 @@
|
|||
<ShiftListWeeklyOverview
|
||||
mode="total-hours"
|
||||
:timesheet-mode="mode"
|
||||
:total-hours="total_hours"
|
||||
:total-expenses="total_expenses"
|
||||
:total-hours="totalHours"
|
||||
:total-expenses="totalExpenses"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -144,9 +159,9 @@
|
|||
<!-- navigation btn -->
|
||||
<PayPeriodNavigator
|
||||
class="col-auto"
|
||||
@date-selected="timesheet_api.getTimesheetsByDate"
|
||||
@pressed-previous-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
|
||||
@pressed-next-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
|
||||
@date-selected="timesheetApi.getTimesheetsByDate"
|
||||
@pressed-previous-button="timesheetApi.getTimesheetsByCurrentPayPeriod"
|
||||
@pressed-next-button="timesheetApi.getTimesheetsByCurrentPayPeriod"
|
||||
/>
|
||||
|
||||
<!-- mobile expenses button -->
|
||||
|
|
@ -160,7 +175,7 @@
|
|||
color="accent"
|
||||
icon="receipt_long"
|
||||
class="full-width"
|
||||
@click="expenses_store.open"
|
||||
@click="expenseStore.open"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -174,16 +189,16 @@
|
|||
color="accent"
|
||||
icon="receipt_long"
|
||||
:label="$t('timesheet.expense.open_btn')"
|
||||
@click="expenses_store.open"
|
||||
@click="expenseStore.open"
|
||||
/>
|
||||
|
||||
<!-- desktop save timesheet changes button -->
|
||||
<q-btn
|
||||
v-if="!is_timesheets_approved && $q.screen.width > $q.screen.height"
|
||||
v-if="!isTimesheetsApproved && $q.screen.width > $q.screen.height"
|
||||
push
|
||||
rounded
|
||||
:disable="timesheet_store.is_loading || has_shift_errors"
|
||||
:color="timesheet_store.is_loading || has_shift_errors ? 'grey-5' : 'accent'"
|
||||
:disable="timesheetStore.is_loading || hasShiftErrors || !timesheetStore.canSaveTimesheets"
|
||||
:color="timesheetStore.is_loading || hasShiftErrors || !timesheetStore.canSaveTimesheets ? 'grey-5' : 'accent'"
|
||||
icon="upload"
|
||||
:label="$t('shared.label.save')"
|
||||
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'full-width' : 'q-ml-md'"
|
||||
|
|
@ -211,14 +226,14 @@
|
|||
:class="$q.platform.is.mobile ? 'fit no-wrap' : 'full-width'"
|
||||
:style="$q.platform.is.mobile && $q.screen.width < $q.screen.height ? 'margin-bottom: 40px' : ''"
|
||||
>
|
||||
<!-- Show if no timesheets found (further than one month from present) -->
|
||||
<!-- If no timesheets found -->
|
||||
<div
|
||||
v-if="timesheet_store.timesheets.length < 1 && !timesheet_store.is_loading"
|
||||
v-if="timesheetStore.timesheets.length < 1 && !timesheetStore.is_loading"
|
||||
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>
|
||||
}}</span>
|
||||
<q-icon
|
||||
name="las la-calendar"
|
||||
color="accent"
|
||||
|
|
@ -228,7 +243,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Else show timesheets if found -->
|
||||
<!-- Else show timesheets -->
|
||||
<ShiftList
|
||||
v-else
|
||||
class="col-auto"
|
||||
|
|
@ -238,7 +253,7 @@
|
|||
<q-btn
|
||||
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
||||
square
|
||||
:disable="timesheet_store.is_loading"
|
||||
:disable="timesheetStore.is_loading || hasShiftErrors || !timesheetStore.canSaveTimesheets"
|
||||
size="lg"
|
||||
color="accent"
|
||||
icon="upload"
|
||||
|
|
@ -249,8 +264,13 @@
|
|||
/>
|
||||
|
||||
<ExpenseDialog
|
||||
:is-approved="is_timesheets_approved"
|
||||
:is-approved="isTimesheetsApproved"
|
||||
class="z-top"
|
||||
/>
|
||||
|
||||
<UnsavedChangesDialog
|
||||
@click-save-no="onClickLeave"
|
||||
@click-save-yes="onClickSaveBeforeLeaving"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
74
src/modules/timesheets/components/unsaved-changes-dialog.vue
Normal file
74
src/modules/timesheets/components/unsaved-changes-dialog.vue
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
|
||||
// ========== state ========================================
|
||||
const emit = defineEmits<{
|
||||
clickSaveNo: [];
|
||||
clickSaveYes: [];
|
||||
}>();
|
||||
|
||||
const timesheetStore = useTimesheetStore();
|
||||
|
||||
// ========== methods ========================================
|
||||
|
||||
const onClickSaveOptionButton = (option: 'cancel' | 'no' | 'yes') => {
|
||||
switch(option) {
|
||||
case 'cancel':
|
||||
timesheetStore.isShowingUnsavedWarning = false;
|
||||
break;
|
||||
case 'no':
|
||||
emit('clickSaveNo');
|
||||
break;
|
||||
case 'yes':
|
||||
emit('clickSaveYes');
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog
|
||||
v-model="timesheetStore.isShowingUnsavedWarning"
|
||||
backdrop-filter="blur(4px)"
|
||||
>
|
||||
<q-card
|
||||
class="bg-dark shadow-12 flex-center"
|
||||
style="border: 2px solid var(--q-accent);"
|
||||
>
|
||||
<div class="column flex-center q-py-sm q-px-md text-center">
|
||||
<span class="col-auto text-bold text-uppercase" style="font-size: 1.2em;">
|
||||
{{ $t('timesheet.unsaved_changes_title') }}
|
||||
</span>
|
||||
<span class="">{{ $t('timesheet.unsaved_changes_caption') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-auto row full-width">
|
||||
<q-btn
|
||||
flat
|
||||
:label="$t('shared.label.cancel')"
|
||||
class="col"
|
||||
@click="onClickSaveOptionButton('cancel')"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
flat
|
||||
color="negative"
|
||||
:label="$t('shared.misc.no')"
|
||||
class="col"
|
||||
@click="onClickSaveOptionButton('no')"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
flat
|
||||
color="accent"
|
||||
:label="$t('shared.misc.yes')"
|
||||
class="col"
|
||||
@click="onClickSaveOptionButton('yes')"
|
||||
/>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
@ -10,9 +10,14 @@ export const useExpensesApi = () => {
|
|||
|
||||
const upsertExpense = async (expense: Expense, employee_email: string, file?: File): Promise<string> => {
|
||||
if (file) {
|
||||
const presignedURL = expenses_store.uploadAttachment(file);
|
||||
const attachmentKey = await expenses_store.uploadAttachment(file);
|
||||
|
||||
if (!presignedURL) return 'PRESIGN_FAILED';
|
||||
if (!attachmentKey)
|
||||
console.error('failed to upload attachment');
|
||||
else {
|
||||
expense.attachment_key = attachmentKey;
|
||||
expense.attachment_name = file.name;
|
||||
}
|
||||
}
|
||||
|
||||
const success = await expenses_store.upsertExpense(expense, employee_email);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ export const useShiftApi = () => {
|
|||
const success = await shift_store.deleteShiftById(shift_id, employee_email);
|
||||
|
||||
if (success) {
|
||||
timesheetStore.timesheets = [];
|
||||
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
||||
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail(employee_email);
|
||||
}
|
||||
|
||||
timesheetStore.is_loading = false;
|
||||
|
|
@ -23,8 +25,9 @@ export const useShiftApi = () => {
|
|||
const create_success = await shift_store.createNewShifts(employee_email);
|
||||
|
||||
if (create_success || update_success){
|
||||
timesheetStore.timesheets = [];
|
||||
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
||||
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail();
|
||||
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail(employee_email);
|
||||
}
|
||||
|
||||
timesheetStore.is_loading = false;
|
||||
|
|
|
|||
|
|
@ -1,65 +1,70 @@
|
|||
import { useShiftStore } from "src/stores/shift-store";
|
||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
|
||||
|
||||
export const useTimesheetApi = () => {
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const timesheetStore = useTimesheetStore();
|
||||
const shiftStore = useShiftStore();
|
||||
|
||||
const getTimesheetsByDate = async (date_string: string, employee_email?: string) => {
|
||||
timesheet_store.timesheets = [];
|
||||
timesheet_store.is_loading = true;
|
||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
|
||||
const getTimesheetsByDate = async (date_string: string, employeeEmail?: string) => {
|
||||
timesheetStore.timesheets = [];
|
||||
timesheetStore.is_loading = true;
|
||||
const success = await timesheetStore.getPayPeriodByDateOrYearAndNumber(date_string);
|
||||
|
||||
if (success) {
|
||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
||||
timesheet_store.is_loading = false;
|
||||
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(employeeEmail);
|
||||
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail(employeeEmail);
|
||||
timesheetStore.is_loading = false;
|
||||
}
|
||||
|
||||
timesheet_store.is_loading = false;
|
||||
timesheetStore.is_loading = false;
|
||||
}
|
||||
|
||||
const getTimesheetsByCurrentPayPeriod = async (employee_email?: string) => {
|
||||
if (timesheet_store.pay_period === undefined) return false;
|
||||
const getTimesheetsByCurrentPayPeriod = async (employeeEmail?: string) => {
|
||||
if (timesheetStore.pay_period === undefined) return false;
|
||||
|
||||
timesheet_store.is_loading = true;
|
||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber();
|
||||
timesheetStore.is_loading = true;
|
||||
const success = await timesheetStore.getPayPeriodByDateOrYearAndNumber();
|
||||
|
||||
if (success) {
|
||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
||||
timesheet_store.is_loading = false;
|
||||
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(employeeEmail);
|
||||
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail(employeeEmail);
|
||||
timesheetStore.is_loading = false;
|
||||
}
|
||||
|
||||
timesheet_store.is_loading = false;
|
||||
timesheetStore.is_loading = false;
|
||||
};
|
||||
|
||||
const applyPreset = async (timesheet_id: number, week_day_index?: number, date?: string, employeeEmail?: string) => {
|
||||
if (timesheet_store.timesheets.map(timesheet => timesheet.timesheet_id).includes(timesheet_id)) {
|
||||
timesheet_store.is_loading = true;
|
||||
try {
|
||||
let response;
|
||||
timesheetStore.is_loading = true;
|
||||
|
||||
if (week_day_index && date)
|
||||
response = await timesheetService.applyPresetToDay(timesheet_id, week_day_index, date, employeeEmail);
|
||||
else
|
||||
response = await timesheetService.applyPresetToWeek(timesheet_id, employeeEmail);
|
||||
const success = await timesheetStore.applyPreset(timesheet_id, week_day_index, date, employeeEmail);
|
||||
|
||||
if (response.success)
|
||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employeeEmail);
|
||||
} catch (error) {
|
||||
console.error('Error applying weekly timesheet: ', error);
|
||||
}
|
||||
|
||||
timesheet_store.is_loading = false;
|
||||
if (!success) {
|
||||
timesheetStore.is_loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const timesheets = JSON.stringify(timesheetStore.timesheets);
|
||||
const initialTimesheets = JSON.stringify(timesheetStore.initial_timesheets);
|
||||
|
||||
if (timesheets !== initialTimesheets) {
|
||||
await shiftStore.updateShifts(employeeEmail);
|
||||
await shiftStore.createNewShifts(employeeEmail);
|
||||
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(employeeEmail);
|
||||
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail(employeeEmail);
|
||||
}
|
||||
|
||||
timesheetStore.is_loading = false;
|
||||
}
|
||||
|
||||
const getTimesheetsBySwiping = async( direction: number ) => {
|
||||
timesheet_store.is_loading = true;
|
||||
const getTimesheetsBySwiping = async (direction: number) => {
|
||||
timesheetStore.is_loading = true;
|
||||
|
||||
timesheet_store.getNextOrPreviousPayPeriod(direction);
|
||||
await timesheet_store.getPayPeriodByDateOrYearAndNumber();
|
||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail();
|
||||
|
||||
timesheet_store.is_loading = false;
|
||||
timesheetStore.getNextOrPreviousPayPeriod(direction);
|
||||
await timesheetStore.getPayPeriodByDateOrYearAndNumber();
|
||||
await timesheetStore.getTimesheetsByOptionalEmployeeEmail();
|
||||
|
||||
timesheetStore.is_loading = false;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -32,4 +32,9 @@ export interface ExpenseOption {
|
|||
label: string;
|
||||
value: ExpenseType;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface AttachmentPresignedURLResponse {
|
||||
url: string;
|
||||
key: string;
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import type { QSelectOption } from "quasar";
|
||||
|
||||
export const SHIFT_TYPES: ShiftType[] = [
|
||||
'REGULAR',
|
||||
'EVENING',
|
||||
|
|
@ -39,9 +41,10 @@ export class Shift {
|
|||
}
|
||||
}
|
||||
|
||||
export interface ShiftOption {
|
||||
export interface ShiftOption extends QSelectOption {
|
||||
label: string;
|
||||
value: ShiftType;
|
||||
icon: string;
|
||||
icon_color: string;
|
||||
disable?: boolean;
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|||
export interface TimesheetResponse {
|
||||
has_preset_schedule: boolean;
|
||||
employee_fullname: string;
|
||||
daily_expected_hours: number;
|
||||
timesheets: Timesheet[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { api } from "src/boot/axios";
|
||||
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
|
||||
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||
import type { AttachmentPresignedURLResponse, Expense } from "src/modules/timesheets/models/expense.models";
|
||||
|
||||
export const ExpenseService = {
|
||||
createExpense: async (expense: Expense): Promise<{ success: boolean, data: Expense, error?: unknown }> => {
|
||||
|
|
@ -18,14 +18,19 @@ export const ExpenseService = {
|
|||
return response.data;
|
||||
},
|
||||
|
||||
getPresignedUploadURL: async (file: File, checksum_crc32: string): Promise<BackendResponse<string>> => {
|
||||
getPresignedUploadURL: async (file: File, checksum_crc32: string): Promise<BackendResponse<AttachmentPresignedURLResponse>> => {
|
||||
const [file_name, file_type] = file.name.split('.');
|
||||
const response = await api.post(`attachments/s3/upload?file-name=${file_name}&file-type=${file_type}&checksumCRC32=${checksum_crc32}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
uploadAttachmentWithPresignedUrl: async (file: File, url: string) => {
|
||||
const response = await api.put(url, file, { headers: { 'Content-Type': file.type, }, withCredentials: false });
|
||||
console.log('response to upload: ', response);
|
||||
uploadAttachmentWithPresignedUrl: async (file: File, url: string): Promise<number> => {
|
||||
const response = await api.put(url, file, { headers: { 'Content-Type': `image/${file.type}`, }, withCredentials: false });
|
||||
return response.status;
|
||||
},
|
||||
|
||||
getPresignedDownloadURL: async (key: string): Promise<BackendResponse<string>> => {
|
||||
const response = await api.get<BackendResponse<string>>(`attachments/s3/download?attachmentKey=${key}`);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,38 +1,82 @@
|
|||
import { date } from "quasar";
|
||||
import type { SchedulePresetShift } from "src/modules/employee-list/models/schedule-presets.models";
|
||||
import type { Shift, ShiftOption } from "src/modules/timesheets/models/shift.models";
|
||||
import type { Shift, ShiftOption, ShiftType } from "src/modules/timesheets/models/shift.models";
|
||||
|
||||
export const isShiftOverlap = (shifts: Shift[] | SchedulePresetShift[]): boolean => {
|
||||
if (shifts.length < 2) return false;
|
||||
if (shifts.length < 2) return false;
|
||||
|
||||
const parsed_shifts = shifts.map(shift => ({
|
||||
start: date.extractDate(`2000-01-01 ${shift.start_time}`, 'YYYY-MM-DD HH:mm').getTime(),
|
||||
end: date.extractDate(`2000-01-01 ${shift.end_time}`, 'YYYY-MM-DD HH:mm').getTime(),
|
||||
}));
|
||||
const parsed_shifts = shifts.map(shift => ({
|
||||
start: date.extractDate(`2000-01-01 ${shift.start_time}`, 'YYYY-MM-DD HH:mm').getTime(),
|
||||
end: date.extractDate(`2000-01-01 ${shift.end_time}`, 'YYYY-MM-DD HH:mm').getTime(),
|
||||
}));
|
||||
|
||||
for (let i = 0; i < parsed_shifts.length; i++) {
|
||||
for (let j = i + 1; j < parsed_shifts.length; j++) {
|
||||
const parsed_shift_a = parsed_shifts[i];
|
||||
const parsed_shift_b = parsed_shifts[j];
|
||||
for (let i = 0; i < parsed_shifts.length; i++) {
|
||||
for (let j = i + 1; j < parsed_shifts.length; j++) {
|
||||
const parsed_shift_a = parsed_shifts[i];
|
||||
const parsed_shift_b = parsed_shifts[j];
|
||||
|
||||
if (parsed_shift_a === undefined || parsed_shift_b === undefined) continue;
|
||||
if (parsed_shift_a === undefined || parsed_shift_b === undefined) continue;
|
||||
|
||||
if (Math.max(parsed_shift_a.start, parsed_shift_b.start) < Math.min(parsed_shift_a.end, parsed_shift_b.end)) {
|
||||
return true; // overlap found
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Math.max(parsed_shift_a.start, parsed_shift_b.start) < Math.min(parsed_shift_a.end, parsed_shift_b.end)) {
|
||||
return true; // overlap found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getCurrentDailyMinutesWorked = (shifts: Shift[]): number => {
|
||||
const shiftTypesToIgnore: ShiftType[] = ['HOLIDAY', 'SICK', 'VACATION'];
|
||||
let minutes = 0;
|
||||
|
||||
shifts.forEach(shift => {
|
||||
if (shiftTypesToIgnore.includes(shift.type)) return;
|
||||
|
||||
const startTime = new Date(`1970-01-01T${shift.start_time}:00`);
|
||||
const endTime = new Date(`1970-01-01T${shift.end_time}:00`);
|
||||
|
||||
const diff = date.getDateDiff(endTime, startTime, 'minutes');
|
||||
minutes += diff;
|
||||
});
|
||||
|
||||
return minutes;
|
||||
}
|
||||
|
||||
export const getTimeStringFromMinutes = (minutes: number): string => {
|
||||
const h = Math.floor(minutes / 60);
|
||||
const m = minutes % 60;
|
||||
|
||||
if (h < 10 ) {
|
||||
if (m < 10)
|
||||
return `0${h}:0${m}`;
|
||||
|
||||
return `0${h}:${m}`;
|
||||
}
|
||||
|
||||
return `${h}:${m}`;
|
||||
}
|
||||
|
||||
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' },
|
||||
// { label: 'timesheet.shift.types.BANKING', value: 'BANKING', icon: 'savings', icon_color: 'pink-3' },
|
||||
{ label: 'timesheet.shift.types.WITHDRAW_BANKED', value: 'WITHDRAW_BANKED', icon: 'attach_money', icon_color: 'yellow-4' },
|
||||
];
|
||||
{ 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' },
|
||||
// { label: 'timesheet.shift.types.BANKING', value: 'BANKING', icon: 'savings', icon_color: 'pink-3' },
|
||||
{ label: 'timesheet.shift.types.WITHDRAW_BANKED', value: 'WITHDRAW_BANKED', icon: 'attach_money', icon_color: 'yellow-4' },
|
||||
];
|
||||
|
||||
export const getShiftOptions = (disablePTO: boolean, isNotUnique: boolean): ShiftOption[] => {
|
||||
return [
|
||||
{ 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', disable: disablePTO },
|
||||
{ label: 'timesheet.shift.types.HOLIDAY', value: 'HOLIDAY', icon: 'event', icon_color: 'purple-5', disable: isNotUnique || disablePTO },
|
||||
{ label: 'timesheet.shift.types.SICK', value: 'SICK', icon: 'medication_liquid', icon_color: 'light-blue-6', disable: disablePTO },
|
||||
// { label: 'timesheet.shift.types.BANKING', value: 'BANKING', icon: 'savings', icon_color: 'pink-3' },
|
||||
// { label: 'timesheet.shift.types.WITHDRAW_BANKED', value: 'WITHDRAW_BANKED', icon: 'attach_money', icon_color: 'yellow-4' },
|
||||
];
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { useAuthStore } from 'src/stores/auth-store';
|
|||
import { RouteNames } from 'src/router/router-constants';
|
||||
import { useChatbotStore } from 'src/stores/chatbot-store';
|
||||
import type { UserModuleAccess } from 'src/modules/shared/models/user.models';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
|
||||
/*
|
||||
* If not building with SSR mode, you can
|
||||
|
|
@ -30,19 +31,29 @@ export default defineRouter(function (/* { store, ssrContext } */) {
|
|||
history: createHistory(process.env.VUE_ROUTER_BASE),
|
||||
});
|
||||
|
||||
Router.beforeEach(async (destination_page) => {
|
||||
Router.beforeEach(async (to, from) => {
|
||||
const auth_store = useAuthStore();
|
||||
const result = await auth_store.getProfile() ?? { status: 400, message: 'unknown error occured' };
|
||||
|
||||
if (destination_page.meta.requires_auth && !auth_store.user || (result.status >= 400 && destination_page.name !== RouteNames.LOGIN)) {
|
||||
if (to.meta.requires_auth && !auth_store.user || (result.status >= 400 && to.name !== RouteNames.LOGIN)) {
|
||||
console.error('no user account found');
|
||||
return { name: 'login' };
|
||||
}
|
||||
|
||||
if (destination_page.meta.required_module && auth_store.user) {
|
||||
if (!auth_store.user.user_module_access.includes(destination_page.meta.required_module as UserModuleAccess))
|
||||
if (to.meta.required_module && auth_store.user) {
|
||||
if (!auth_store.user.user_module_access.includes(to.meta.required_module as UserModuleAccess))
|
||||
return {name: 'error'};
|
||||
}
|
||||
}
|
||||
|
||||
if (from.name === RouteNames.TIMESHEET) {
|
||||
const timesheetStore = useTimesheetStore();
|
||||
|
||||
if(timesheetStore.canSaveTimesheets) {
|
||||
timesheetStore.nextPageNameAfterUnsaveWarning = to.name;
|
||||
timesheetStore.isShowingUnsavedWarning = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Router.afterEach( (destination_page) => {
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
const is_open = ref(false);
|
||||
const is_loading = ref(false);
|
||||
const is_showing_create_form = ref(false);
|
||||
const attachmentURL = ref<string>('');
|
||||
const mode = ref<'create' | 'update' | 'delete'>('create');
|
||||
const current_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
|
||||
const initial_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
|
||||
const isShowingAttachmentDialog = ref(false);
|
||||
const is_save_disabled = computed(() => JSON.stringify(current_expense.value) === JSON.stringify(initial_expense.value))
|
||||
|
||||
const open = (): void => {
|
||||
|
|
@ -51,22 +53,48 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
return data.success;
|
||||
}
|
||||
|
||||
const uploadAttachment = async (file: File) => {
|
||||
/**
|
||||
* Attemps to upload the provided image file to the S3 storage bucket for attachments.
|
||||
*
|
||||
* @param file image file to be uploaded to the S3 storage
|
||||
* @returns Key `string` associated with the uploaded image file if successful,
|
||||
* `undefined` if it fails.
|
||||
*/
|
||||
const uploadAttachment = async (file: File): Promise<string | undefined> => {
|
||||
try {
|
||||
const checksum = await computeCRC32Base64(file);
|
||||
const presignedUrlResponse = await ExpenseService.getPresignedUploadURL(file, checksum);
|
||||
|
||||
if (presignedUrlResponse.success && presignedUrlResponse.data) {
|
||||
const { url, key } = JSON.parse(presignedUrlResponse.data);
|
||||
console.log('key: ', key);
|
||||
|
||||
await ExpenseService.uploadAttachmentWithPresignedUrl(file, url);
|
||||
if (!presignedUrlResponse.success || !presignedUrlResponse.data) {
|
||||
console.error('failed to get presigned URL from server');
|
||||
return;
|
||||
}
|
||||
|
||||
const { url, key } = presignedUrlResponse.data;
|
||||
|
||||
const responseStatus = await ExpenseService.uploadAttachmentWithPresignedUrl(file, url);
|
||||
|
||||
if (responseStatus >= 400) {
|
||||
console.error('an error occured during upload: error ', responseStatus);
|
||||
return;
|
||||
}
|
||||
|
||||
return key;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const getAttachmentURL = async (key?: string) => {
|
||||
if (!key)
|
||||
return;
|
||||
|
||||
const presignedAttachmentURL = await ExpenseService.getPresignedDownloadURL(key);
|
||||
|
||||
if (presignedAttachmentURL.success && presignedAttachmentURL.data)
|
||||
attachmentURL.value = presignedAttachmentURL.data;
|
||||
}
|
||||
|
||||
return {
|
||||
is_open,
|
||||
is_loading,
|
||||
|
|
@ -74,11 +102,14 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
mode,
|
||||
current_expense,
|
||||
initial_expense,
|
||||
isShowingAttachmentDialog,
|
||||
is_save_disabled,
|
||||
attachmentURL,
|
||||
open,
|
||||
upsertExpense,
|
||||
deleteExpenseById,
|
||||
close,
|
||||
uploadAttachment,
|
||||
getAttachmentURL,
|
||||
};
|
||||
});
|
||||
|
|
@ -12,7 +12,8 @@ import type { PaidTimeOff } from 'src/modules/employee-list/models/employee-prof
|
|||
import type { PayPeriodEvent } from 'src/modules/timesheet-approval/models/pay-period-event.models';
|
||||
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
||||
import { type FederalHoliday, TARGO_HOLIDAY_NAMES_FR } from 'src/modules/timesheets/models/federal-holidays.models';
|
||||
|
||||
import type { RouteNames } from 'src/router/router-constants';
|
||||
import type { RouteRecordNameGeneric } from 'vue-router';
|
||||
|
||||
export const useTimesheetStore = defineStore('timesheet', () => {
|
||||
const { t } = useI18n();
|
||||
|
|
@ -21,7 +22,16 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
const timesheets = ref<Timesheet[]>([]);
|
||||
const all_current_shifts = computed(() => timesheets.value.flatMap(week => week.days.flatMap(day => day.shifts)) ?? []);
|
||||
const initial_timesheets = ref<Timesheet[]>([]);
|
||||
const canSaveTimesheets = computed(() => {
|
||||
/* eslint-disable-next-line */
|
||||
const currentShifts = timesheets.value.flatMap(timesheet => timesheet.days.flatMap(day => day.shifts.map(shift => { const { has_error, ...shft } = shift; return shft; })));
|
||||
const initialShifts = initial_timesheets.value.flatMap(timesheet => timesheet.days.flatMap(day => day.shifts));
|
||||
|
||||
return JSON.stringify(currentShifts) !== JSON.stringify(initialShifts);
|
||||
});
|
||||
const paid_time_off_totals = ref<PaidTimeOff>({ sick_hours: 0, vacation_hours: 0, banked_hours: 0 });
|
||||
const isShowingUnsavedWarning = ref(false);
|
||||
const nextPageNameAfterUnsaveWarning = ref<RouteNames | RouteRecordNameGeneric>();
|
||||
|
||||
const pay_period_overviews = ref<TimesheetApprovalOverview[]>([]);
|
||||
const pay_period_infos = ref<PayPeriodOverviewResponse>();
|
||||
|
|
@ -121,7 +131,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
has_timesheet_preset.value = response.data.has_preset_schedule;
|
||||
selected_employee_name.value = response.data.employee_fullname;
|
||||
timesheets.value = response.data.timesheets;
|
||||
initial_timesheets.value = unwrapAndClone(timesheets.value);
|
||||
initial_timesheets.value = unwrapAndClone(response.data.timesheets);
|
||||
} else {
|
||||
selected_employee_name.value = '';
|
||||
timesheets.value = [];
|
||||
|
|
@ -169,6 +179,24 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
return false;
|
||||
};
|
||||
|
||||
const applyPreset = async (timesheet_id: number, week_day_index?: number, date?: string, employeeEmail?: string): Promise<boolean> => {
|
||||
if (timesheets.value.map(timesheet => timesheet.timesheet_id).includes(timesheet_id)) {
|
||||
try {
|
||||
let response;
|
||||
|
||||
if (week_day_index && date)
|
||||
response = await timesheetService.applyPresetToDay(timesheet_id, week_day_index, date, employeeEmail);
|
||||
else
|
||||
response = await timesheetService.applyPresetToWeek(timesheet_id, employeeEmail);
|
||||
|
||||
return response.success ?? false;
|
||||
} catch (error) {
|
||||
console.error('Error applying weekly timesheet: ', error);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const getPayPeriodReport = async (report_filters: TimesheetApprovalCSVReportFilters) => {
|
||||
try {
|
||||
if (!pay_period.value) return false;
|
||||
|
|
@ -239,18 +267,22 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
current_pay_period_overview,
|
||||
pay_period_infos,
|
||||
selected_employee_name,
|
||||
canSaveTimesheets,
|
||||
has_timesheet_preset,
|
||||
timesheets,
|
||||
all_current_shifts,
|
||||
initial_timesheets,
|
||||
federal_holidays,
|
||||
paid_time_off_totals,
|
||||
isShowingUnsavedWarning,
|
||||
nextPageNameAfterUnsaveWarning,
|
||||
getCurrentFederalHolidays,
|
||||
getNextOrPreviousPayPeriod,
|
||||
getPayPeriodByDateOrYearAndNumber,
|
||||
getTimesheetOverviews,
|
||||
getTimesheetsByOptionalEmployeeEmail,
|
||||
toggleTimesheetsApprovalByEmployeeEmail,
|
||||
applyPreset,
|
||||
getPayPeriodReport,
|
||||
openReportDialog,
|
||||
closeReportDialog,
|
||||
|
|
|
|||
|
|
@ -20,14 +20,27 @@ export const getMinutes = (hours: number) => {
|
|||
return minutes > 1 ? minutes.toString() : '0';
|
||||
}
|
||||
|
||||
export const getHoursMinutesStringFromHoursFloat = (hours: number): string => {
|
||||
let flat_hours = Math.floor(hours);
|
||||
let minutes = Math.round((hours - flat_hours) * 60);
|
||||
export const getHoursMinutesStringFromHoursFloat = (hours: number, minutes?: number): string => {
|
||||
let flatHours = Math.floor(hours);
|
||||
let flatMinutes = minutes ?? Math.round((hours - flatHours) * 60);
|
||||
|
||||
if (minutes === 60) {
|
||||
flat_hours += 1;
|
||||
minutes = 0;
|
||||
if (flatMinutes === 60) {
|
||||
flatHours += 1;
|
||||
flatMinutes = 0;
|
||||
}
|
||||
|
||||
return `${flat_hours}h${minutes > 1 ? ' ' + minutes : ''}`
|
||||
return `${flatHours}h${flatMinutes > 1 ? ' ' + flatMinutes : ''}`
|
||||
}
|
||||
|
||||
export const getHoursMinutesBetweenTwoHHmm = (startTime: string, endTime: string): {
|
||||
hours: number,
|
||||
minutes: number,
|
||||
} => {
|
||||
const [startHours, startMinutes] = startTime.split(':');
|
||||
const [endHours, endMinutes] = endTime.split(':');
|
||||
|
||||
return {
|
||||
hours: Number(endHours) - Number(startHours),
|
||||
minutes: Number(endMinutes) - Number(startMinutes),
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user