refactor(types): refactoring of shifts, expenses, timesheet types, interfaces and defaults values.
This commit is contained in:
parent
4470c855cf
commit
c5c0e8b358
|
|
@ -106,7 +106,7 @@ export default defineConfig((ctx) => {
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
avatar: 'https://cdn.quasar.dev/img/boy-avatar.png',
|
avatar: 'https://cdn.quasar.dev/img/boy-avatar.png',
|
||||||
},
|
},
|
||||||
dark: false,
|
dark: "auto",
|
||||||
},
|
},
|
||||||
|
|
||||||
// iconSet: 'material-icons', // Quasar icon set
|
// iconSet: 'material-icons', // Quasar icon set
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ export default {
|
||||||
hints: {
|
hints: {
|
||||||
amount_or_mileage:"Either amount or mileage, not both",
|
amount_or_mileage:"Either amount or mileage, not both",
|
||||||
comment_required:"A comment required",
|
comment_required:"A comment required",
|
||||||
|
attach_file:"Attach File"
|
||||||
},
|
},
|
||||||
mileage:"mileage",
|
mileage:"mileage",
|
||||||
open_btn:"list of expenses",
|
open_btn:"list of expenses",
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ export default {
|
||||||
hints: {
|
hints: {
|
||||||
amount_or_mileage:"Soit dépense ou kilométrage, pas les deux",
|
amount_or_mileage:"Soit dépense ou kilométrage, pas les deux",
|
||||||
comment_required:"un commentaire est requis",
|
comment_required:"un commentaire est requis",
|
||||||
|
attach_file:"Pièce jointe"
|
||||||
},
|
},
|
||||||
mileage:"Kilométrage",
|
mileage:"Kilométrage",
|
||||||
open_btn:"Liste des Dépenses",
|
open_btn:"Liste des Dépenses",
|
||||||
|
|
|
||||||
17
src/modules/shared/composables/use-toggle.ts
Normal file
17
src/modules/shared/composables/use-toggle.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
//date picker state
|
||||||
|
export const useToggle = (initial = false) => {
|
||||||
|
const state = ref<boolean>(initial);
|
||||||
|
|
||||||
|
const setTrue = () => { state.value = true; };
|
||||||
|
const setFalse = () => { state.value = false; };
|
||||||
|
const toggle = () => { state.value = !state.value; };
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
setTrue,
|
||||||
|
setFalse,
|
||||||
|
toggle
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, type ChartData, type ChartDataset } from 'chart.js';
|
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, type ChartData, type ChartDataset } from 'chart.js';
|
||||||
import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface';
|
import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface';
|
||||||
import type { Expense } from 'src/modules/timesheets/types/timesheet-details-interface';
|
import type { Expense } from 'src/modules/timesheets/types/expense.interfaces';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Shift } from 'src/modules/timesheets/types/timesheet-shift-interface';
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import type { Shift } from 'src/modules/timesheets/types/shift.interfaces';
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
shift: Shift;
|
shift: Shift;
|
||||||
|
|
@ -52,7 +53,7 @@ import { ref } from 'vue';
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="props.shift.type !== ''"
|
v-if="props.shift.type"
|
||||||
name="double_arrow"
|
name="double_arrow"
|
||||||
:color="icon_data.color"
|
:color="icon_data.color"
|
||||||
size="24px"
|
size="24px"
|
||||||
|
|
@ -78,7 +79,7 @@ import { ref } from 'vue';
|
||||||
>
|
>
|
||||||
<!-- chat_bubble_outline or announcement -->
|
<!-- chat_bubble_outline or announcement -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="props.shift.type !== ''"
|
v-if="props.shift.type"
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
icon="chat_bubble_outline"
|
icon="chat_bubble_outline"
|
||||||
|
|
@ -87,7 +88,7 @@ import { ref } from 'vue';
|
||||||
|
|
||||||
<!-- insert_drive_file or request_quote -->
|
<!-- insert_drive_file or request_quote -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="props.shift.type !== ''"
|
v-if="props.shift.type"
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
icon="attach_money"
|
icon="attach_money"
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
import TimesheetApprovalEmployeeDetailsShiftsRow from 'src/modules/timesheet-approval/components/timesheet-approval-employee-details-shifts-row.vue';
|
import TimesheetApprovalEmployeeDetailsShiftsRow from 'src/modules/timesheet-approval/components/timesheet-approval-employee-details-shifts-row.vue';
|
||||||
import TimesheetApprovalEmployeeDetailsShiftsRowHeader from 'src/modules/timesheet-approval/components/timesheet-approval-employee-details-shifts-row-header.vue';
|
import TimesheetApprovalEmployeeDetailsShiftsRowHeader from 'src/modules/timesheet-approval/components/timesheet-approval-employee-details-shifts-row-header.vue';
|
||||||
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
||||||
import { default_shift, type Shift } from 'src/modules/timesheets/types/timesheet-shift-interface';
|
|
||||||
import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface';
|
import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface';
|
||||||
|
import type { Shift } from 'src/modules/timesheets/types/shift.interfaces';
|
||||||
|
import { default_shift } from 'src/modules/timesheets/types/shift.defaults';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rawData: PayPeriodEmployeeDetails;
|
rawData: PayPeriodEmployeeDetails;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { default_timesheet_details_week, type TimesheetDetailsWeek } from "src/modules/timesheets/types/timesheet-details-interface";
|
import { defaultTimesheetDetailsWeek } from "src/modules/timesheets/types/timesheet.defaults";
|
||||||
|
import type { TimesheetDetailsWeek } from "src/modules/timesheets/types/timesheet.interfaces";
|
||||||
|
|
||||||
|
|
||||||
export interface PayPeriodEmployeeDetails {
|
export interface PayPeriodEmployeeDetails {
|
||||||
week1: TimesheetDetailsWeek;
|
week1: TimesheetDetailsWeek;
|
||||||
|
|
@ -6,6 +8,6 @@ export interface PayPeriodEmployeeDetails {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const default_pay_period_employee_details = {
|
export const default_pay_period_employee_details = {
|
||||||
week1: default_timesheet_details_week(),
|
week1: defaultTimesheetDetailsWeek(),
|
||||||
week2: default_timesheet_details_week(),
|
week2: defaultTimesheetDetailsWeek(),
|
||||||
}
|
}
|
||||||
185
src/modules/timesheets/components/expenses/expense-form.vue
Normal file
185
src/modules/timesheets/components/expenses/expense-form.vue
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { TimesheetExpense } from '../../types/expense.interfaces';
|
||||||
|
import type { ExpenseType } from '../../types/expense.types';
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
//---------------- v-models ------------------
|
||||||
|
const draft = defineModel<Partial<TimesheetExpense>>('draft');
|
||||||
|
const files = defineModel<File[] | null>('files');
|
||||||
|
const datePickerOpen = defineModel<boolean | null>('datePickerOpen', {default: false });
|
||||||
|
|
||||||
|
//------------------ props ------------------
|
||||||
|
const props = defineProps<{
|
||||||
|
type_options: { label: string; value: ExpenseType }[];
|
||||||
|
show_amount: boolean;
|
||||||
|
is_readonly: boolean;
|
||||||
|
rules: {
|
||||||
|
typeRequired: (val: unknown) => true | string;
|
||||||
|
amountRequired: (val: unknown) => true | string;
|
||||||
|
mileageRequired: (val: unknown) => true | string;
|
||||||
|
commentRequired: (val: unknown) => true | string;
|
||||||
|
commentTooLong: (val: unknown) => true | string;
|
||||||
|
};
|
||||||
|
comment_max_length: number;
|
||||||
|
setType: (val: ExpenseType) => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
//------------------ emits ------------------
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'submit'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-form
|
||||||
|
flat
|
||||||
|
v-if="!is_readonly"
|
||||||
|
@submit.prevent="emit('submit')"
|
||||||
|
>
|
||||||
|
<div class="text-subtitle2 q-py-sm">
|
||||||
|
{{ $t('timesheet.expense.add_expense')}}
|
||||||
|
</div>
|
||||||
|
<div class="row justify-between">
|
||||||
|
|
||||||
|
<!-- date selection input -->
|
||||||
|
<q-input
|
||||||
|
v-model.date="draft!.date"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
readonly
|
||||||
|
stack-label
|
||||||
|
class="col q-px-xs"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('timesheet.expense.date')"
|
||||||
|
>
|
||||||
|
<template #before>
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
icon="event"
|
||||||
|
color="primary"
|
||||||
|
@click="datePickerOpen = true"
|
||||||
|
/>
|
||||||
|
<q-dialog v-model="datePickerOpen">
|
||||||
|
<q-date
|
||||||
|
v-model="draft!.date"
|
||||||
|
@update:model-value="datePickerOpen = false"
|
||||||
|
mask="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<!-- expenses type selection -->
|
||||||
|
<q-select
|
||||||
|
v-model="draft!.type"
|
||||||
|
:options="type_options"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
class="col q-px-xs"
|
||||||
|
color="primary"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
:label="$t('timesheet.expense.type')"
|
||||||
|
:rules="[ rules.typeRequired ]"
|
||||||
|
@update:model-value="val => setType(val as ExpenseType)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- amount input -->
|
||||||
|
<template v-if="show_amount">
|
||||||
|
<q-input
|
||||||
|
key="amount"
|
||||||
|
v-model.number="draft!.amount"
|
||||||
|
filled
|
||||||
|
input-class="text-right"
|
||||||
|
dense
|
||||||
|
stack-label
|
||||||
|
clearable
|
||||||
|
color="primary"
|
||||||
|
class="col q-px-xs"
|
||||||
|
:label="$t('timesheet.expense.amount')"
|
||||||
|
suffix="$"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
:rules="[ rules.amountRequired ]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- mileage input -->
|
||||||
|
<template v-else>
|
||||||
|
<q-input
|
||||||
|
key="mileage"
|
||||||
|
v-model.number="draft!.mileage"
|
||||||
|
filled
|
||||||
|
input-class="text-right"
|
||||||
|
dense
|
||||||
|
stack-label
|
||||||
|
clearable
|
||||||
|
color="primary"
|
||||||
|
class="col q-px-xs"
|
||||||
|
:label="$t('timesheet.expense.mileage')"
|
||||||
|
suffix="km"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
:rules="[ rules.mileageRequired ]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- employee comment input -->
|
||||||
|
<q-input
|
||||||
|
v-model="draft!.comment"
|
||||||
|
filled
|
||||||
|
color="primary"
|
||||||
|
type="text"
|
||||||
|
class="col q-px-sm"
|
||||||
|
dense
|
||||||
|
stack-label
|
||||||
|
clearable
|
||||||
|
:counter="true"
|
||||||
|
:maxlength="comment_max_length"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
:rules="[ rules.commentRequired, rules.commentTooLong ]"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span class="text-weight-bold ">
|
||||||
|
{{ $t('timesheet.expense.employee_comment') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<!-- import attach file section -->
|
||||||
|
<q-file
|
||||||
|
v-model="files"
|
||||||
|
:label="$t('timesheet.expense.hints.attach_file')"
|
||||||
|
filled
|
||||||
|
use-chips
|
||||||
|
multiple
|
||||||
|
stack-label
|
||||||
|
class="col"
|
||||||
|
style="max-width: 300px;"
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon
|
||||||
|
name="attach_file"
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-file>
|
||||||
|
|
||||||
|
<!-- add btn section -->
|
||||||
|
<div>
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
color="primary"
|
||||||
|
icon="add"
|
||||||
|
size="sm"
|
||||||
|
class="q-mt-sm q-ml-sm"
|
||||||
|
type="submit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</template>
|
||||||
98
src/modules/timesheets/components/expenses/expense-list.vue
Normal file
98
src/modules/timesheets/components/expenses/expense-list.vue
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { TimesheetExpense } from '../../types/expense.interfaces';
|
||||||
|
import { expenseTypeIcon } from '../../utils/expense.util';
|
||||||
|
/* eslint-disable */
|
||||||
|
const props = defineProps<{
|
||||||
|
items: TimesheetExpense[];
|
||||||
|
is_readonly: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'remove', index: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- liste des dépenses pré existantes -->
|
||||||
|
<q-list
|
||||||
|
padding
|
||||||
|
class="rounded-borders"
|
||||||
|
|
||||||
|
>
|
||||||
|
<q-item-label v-if="items.length === 0" class="text-italic q-px-sm">
|
||||||
|
{{ $t('timesheet.expense.empty_list') }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item
|
||||||
|
style="border: solid 1px lightgrey; border-radius: 7px;"
|
||||||
|
v-for="(expense, index) in items" :key="index"
|
||||||
|
class="q-my-xs shadow-1"
|
||||||
|
>
|
||||||
|
<!-- avatar type icon section -->
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon :name="expenseTypeIcon(expense.type)" color="primary"/>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- amount or mileage section -->
|
||||||
|
<q-item-section top>
|
||||||
|
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
||||||
|
{{ expense.mileage?.toFixed(1) }} km
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label v-else>
|
||||||
|
{{ expense.amount?.toFixed(2) }} $
|
||||||
|
</q-item-label>
|
||||||
|
|
||||||
|
<!-- date label -->
|
||||||
|
<q-item-label caption lines="2">
|
||||||
|
{{ $d(new Date(expense.date + 'T00:00:00'), { year:'numeric', month:'short', day: 'numeric', weekday: 'short'}) }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- attachment file icon -->
|
||||||
|
<q-item-section side>
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="md"
|
||||||
|
color="primary"
|
||||||
|
class="q-mx-lg"
|
||||||
|
icon="attach_file"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- comment section -->
|
||||||
|
<q-item-section top>
|
||||||
|
<q-item-label lines="1">
|
||||||
|
{{ $t('timesheet.expense.employee_comment') }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label caption lines="2">
|
||||||
|
{{ expense.comment }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- supervisor comment section -->
|
||||||
|
<q-item-section top>
|
||||||
|
<q-item-label lines="1">
|
||||||
|
{{ $t('timesheet.expense.supervisor_comment') }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label v-if="expense.supervisor_comment" caption lines="2">
|
||||||
|
{{ expense.supervisor_comment }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- delete btn -->
|
||||||
|
<q-item-section side>
|
||||||
|
<q-btn
|
||||||
|
v-if="!is_readonly"
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
color="negative"
|
||||||
|
icon="close"
|
||||||
|
@click="emit('remove', index)"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
@ -1,159 +1,115 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { EXPENSE_TYPE, type ExpenseType, type TimesheetExpense } from '../../types/timesheet-expenses-interface';
|
import { useExpenseForm } from '../../composables/use-expense-form';
|
||||||
import { compute_expense_totals, ExpensesValidationError, normalize_expense, validate_expense_UI } from '../../utils/timesheet-expenses-validators';
|
import { useExpenseDraft } from '../../composables/use-expense-draft';
|
||||||
// import { date } from 'quasar';
|
import { useExpenseItems } from '../../composables/use-expense-items';
|
||||||
import { COMMENT_MAX_LENGTH } from '../../composables/use-shift-api';
|
import { COMMENT_MAX_LENGTH } from '../../constants/expense.constants';
|
||||||
|
import { useToggle } from 'src/modules/shared/composables/use-toggle';
|
||||||
|
import ExpenseList from './expense-list.vue';
|
||||||
|
import ExpenseForm from './expense-form.vue';
|
||||||
|
import {
|
||||||
|
buildExpenseTypeOptions,
|
||||||
|
computeExpenseTotals,
|
||||||
|
makeExpenseRules,
|
||||||
|
buildExpenseSavePayload
|
||||||
|
} from '../../utils/expense.util';
|
||||||
|
import { EXPENSE_TYPE } from '../../types/expense.types';
|
||||||
|
import { ExpensesValidationError } from '../../types/expense-validation.interface';
|
||||||
|
import type { TimesheetExpense } from '../../types/expense.interfaces';
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
//props
|
const { t , locale } = useI18n();
|
||||||
|
const rules = makeExpenseRules(t, COMMENT_MAX_LENGTH);
|
||||||
|
|
||||||
|
//------------------ props ------------------
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
pay_period_no: number;
|
pay_period_no: number;
|
||||||
pay_year: number;
|
pay_year: number;
|
||||||
email: string;
|
email: string;
|
||||||
is_approved?: boolean;
|
is_approved?: boolean;
|
||||||
initial_expenses?: TimesheetExpense[];
|
initial_expenses?: TimesheetExpense[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
//emits
|
//------------------ emits ------------------
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e:'close'): void;
|
(e:'close'): void;
|
||||||
(e: 'save', payload: {
|
(e: 'save', payload: {
|
||||||
pay_period_no: number;
|
pay_period_no: number;
|
||||||
pay_year: number;
|
pay_year: number;
|
||||||
email: string;
|
email: string;
|
||||||
expenses: TimesheetExpense[];
|
expenses: TimesheetExpense[];
|
||||||
}): void;
|
}): void;
|
||||||
(e: 'error', err: ExpensesValidationError): void;
|
(e: 'error', err: ExpensesValidationError): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
//q-select mapper
|
//------------------ q-select mapper ------------------
|
||||||
const type_options = computed(()=> EXPENSE_TYPE.map( val => ({ label: val, value: val })));
|
const type_options = computed(()=> {
|
||||||
|
void locale.value;
|
||||||
|
return buildExpenseTypeOptions(EXPENSE_TYPE, t);
|
||||||
|
})
|
||||||
|
|
||||||
//refs & states
|
//------------------ refs and computed ------------------
|
||||||
const items = ref<TimesheetExpense[]>(Array.isArray(props.initial_expenses) ? props.initial_expenses.map(normalize_expense): []);
|
const files = ref<File[] | null>(null);
|
||||||
const formRef = ref<InstanceType<any> | null>(null);
|
|
||||||
const triedSubmit = ref(false);
|
|
||||||
|
|
||||||
const DEFAULT_TYPE: ExpenseType = 'EXPENSES'
|
|
||||||
|
|
||||||
const draft = ref<Partial<TimesheetExpense>>({
|
|
||||||
date:'',
|
|
||||||
type: DEFAULT_TYPE,
|
|
||||||
comment:'',
|
|
||||||
});
|
|
||||||
|
|
||||||
// computeds
|
|
||||||
const totals = computed(()=> compute_expense_totals(items.value));
|
|
||||||
const is_readonly = computed(()=> !!props.is_approved);
|
const is_readonly = computed(()=> !!props.is_approved);
|
||||||
const showMileage = computed(()=> (draft.value.type as string) === 'MILEAGE');
|
|
||||||
const showAmount = computed(()=> !showMileage.value);
|
|
||||||
|
|
||||||
//helpers
|
const { state: is_open_date_picker } = useToggle();
|
||||||
const reset_draft = () => {
|
const { draft, setType, reset, showAmount } = useExpenseDraft();
|
||||||
draft.value.date = '';
|
const { validateAnd } = useExpenseForm();
|
||||||
draft.value.type = 'EXPENSES';
|
const { items, addFromDraft, removeAt, validateAll, payload } = useExpenseItems({
|
||||||
delete draft.value.amount;
|
initial_expenses: props.initial_expenses,
|
||||||
delete draft.value.mileage;
|
draft,
|
||||||
draft.value.comment = '';
|
is_approved: is_readonly,
|
||||||
};
|
});
|
||||||
|
const totals = computed(()=> computeExpenseTotals(items.value));
|
||||||
const set_draft_type = (value: ExpenseType) => {
|
|
||||||
draft.value.type = value;
|
|
||||||
if (value === 'MILEAGE') {
|
|
||||||
delete draft.value.amount;
|
|
||||||
} else {
|
|
||||||
delete draft.value.mileage;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//actions
|
|
||||||
const add_draft_as_item = () => {
|
|
||||||
const candidate: TimesheetExpense = normalize_expense({
|
|
||||||
date: draft.value.date,
|
|
||||||
type: normType(draft.value.type),
|
|
||||||
...(typeof draft.value.amount === 'number' ? { amount: draft.value.amount }: {}),
|
|
||||||
...(typeof draft.value.mileage === 'number' ? { mileage: draft.value.mileage }: {}),
|
|
||||||
comment: String(draft.value.comment ?? '').trim(),
|
|
||||||
} as TimesheetExpense);
|
|
||||||
|
|
||||||
|
//------------------ actions ------------------
|
||||||
|
const onSave = () => {
|
||||||
try {
|
try {
|
||||||
validate_expense_UI(candidate, 'expense_draft');
|
validateAll();
|
||||||
items.value = [ ...items.value, candidate];
|
reset();
|
||||||
reset_draft();
|
emit('save', buildExpenseSavePayload({
|
||||||
} catch (err: any) {
|
|
||||||
const e = err instanceof ExpensesValidationError
|
|
||||||
? err : new ExpensesValidationError({
|
|
||||||
status_code: 400,
|
|
||||||
message: String(err?.message || err)
|
|
||||||
});
|
|
||||||
emit('error', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const remove_item_at = (index: number) => {
|
|
||||||
if(props.is_approved) return;
|
|
||||||
if(index < 0 || index >= items.value.length) return;
|
|
||||||
items.value = items.value.filter((_,i) => i !== index);
|
|
||||||
};
|
|
||||||
|
|
||||||
const validate_all = () => {
|
|
||||||
for(const expense of items.value) {
|
|
||||||
validate_expense_UI(expense, 'expense_item');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const on_save = () => {
|
|
||||||
try {
|
|
||||||
validate_all();
|
|
||||||
const payload = items.value.map(normalize_expense);
|
|
||||||
|
|
||||||
emit('save', {
|
|
||||||
pay_period_no: props.pay_period_no,
|
pay_period_no: props.pay_period_no,
|
||||||
pay_year: props.pay_year,
|
pay_year: props.pay_year,
|
||||||
email: props.email,
|
email: props.email,
|
||||||
expenses: payload,
|
expenses: payload(),
|
||||||
});
|
}));
|
||||||
|
|
||||||
} catch(err: any) {
|
} catch(err: any) {
|
||||||
const e = err instanceof ExpensesValidationError
|
const e = err instanceof ExpensesValidationError
|
||||||
? err: new ExpensesValidationError({
|
? err
|
||||||
status_code: 400,
|
: new ExpensesValidationError({
|
||||||
message: String(err?.message || err)
|
status_code: 400,
|
||||||
});
|
message: String(err?.message || err)
|
||||||
|
});
|
||||||
emit('error', e);
|
emit('error', e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const on_form_submit = async () => {
|
const onFormSubmit = async () => {
|
||||||
triedSubmit.value = true;
|
try {
|
||||||
const ok = await formRef.value?.validate(true);
|
await validateAnd(async () => {
|
||||||
if(!ok) return;
|
addFromDraft();
|
||||||
add_draft_as_item();
|
reset();
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
const e = err instanceof ExpensesValidationError
|
||||||
|
? err
|
||||||
|
: new ExpensesValidationError({
|
||||||
|
status_code: 400,
|
||||||
|
message: String(err?.message || err)
|
||||||
|
});
|
||||||
|
emit('error', e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const on_close = () => emit('close');
|
const onClose = () => emit('close');
|
||||||
|
|
||||||
//icons managament
|
|
||||||
type ExpensesType = 'MILEAGE' | 'EXPENSES' | 'PER_DIEM' | 'PRIME_GARDE' | string;
|
|
||||||
const normType = (type: unknown) => String(type ?? '').trim().toUpperCase();
|
|
||||||
const expenseTypeIcon = (type: ExpensesType) => {
|
|
||||||
const t = normType(type);
|
|
||||||
const map: Record<string, string> = {
|
|
||||||
MILEAGE: 'time_to_leave',
|
|
||||||
EXPENSES: 'receipt_long',
|
|
||||||
PER_DIEM: 'hotel',
|
|
||||||
PRIME_GARDE: 'admin_panel_settings',
|
|
||||||
};
|
|
||||||
return map[String(t)] ?? 'help_outline';
|
|
||||||
};
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div >
|
<div>
|
||||||
<!-- header (title with totals)-->
|
<!-- header (title with totals)-->
|
||||||
<q-item class="row justify-between">
|
<q-item class="row justify-between">
|
||||||
<q-item-label header class="text-h6 col-auto">
|
<q-item-label header class="text-h6 col-auto">
|
||||||
|
|
@ -165,243 +121,43 @@ const expenseTypeIcon = (type: ExpensesType) => {
|
||||||
<q-badge lines="2" class="q-pa-sm q-px-md" :label="$t('timesheet.expense.total_mileage') + ': ' + totals.mileage.toFixed(1)"/>
|
<q-badge lines="2" class="q-pa-sm q-px-md" :label="$t('timesheet.expense.total_mileage') + ': ' + totals.mileage.toFixed(1)"/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<!-- liste des dépenses pré existantes -->
|
<ExpenseList
|
||||||
<q-list
|
:items="items"
|
||||||
padding
|
:is_readonly="is_readonly"
|
||||||
bordered
|
@remove="removeAt"
|
||||||
class="rounded-borders"
|
/>
|
||||||
>
|
<ExpenseForm
|
||||||
<q-item-label v-if="items.length === 0" class="text-italic q-px-sm">
|
v-model:draft="draft"
|
||||||
{{ $t('timesheet.expense.empty_list') }}
|
v-model:files="files"
|
||||||
</q-item-label>
|
v-model:date-picker-open="is_open_date_picker"
|
||||||
<q-item
|
:type_options="type_options"
|
||||||
style="border: solid 1px lightgrey; border-radius: 7px;"
|
:show_amount="showAmount"
|
||||||
v-for="(expense, index) in items" :key="index"
|
:is_readonly="is_readonly"
|
||||||
class="q-my-xs shadow-1"
|
:rules="rules"
|
||||||
>
|
:comment_max_length="COMMENT_MAX_LENGTH"
|
||||||
<!-- avatar type icon section -->
|
:set-type="setType"
|
||||||
<q-item-section avatar>
|
@submit="onFormSubmit"
|
||||||
<q-icon :name="expenseTypeIcon(expense.type)" color="primary"/>
|
/>
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<!-- amount or mileage section -->
|
|
||||||
<q-item-section top>
|
|
||||||
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
|
||||||
{{ expense.mileage?.toFixed(1) }} km
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label v-else>
|
|
||||||
{{ expense.amount?.toFixed(2) }} $
|
|
||||||
</q-item-label>
|
|
||||||
|
|
||||||
<!-- date label -->
|
|
||||||
<q-item-label caption lines="2">
|
|
||||||
{{ $d(new Date(expense.date + 'T00:00:00'), { year:'numeric', month:'short', day: 'numeric', weekday: 'short'}) }}
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<!-- attachment file icon -->
|
|
||||||
<q-item-section side>
|
|
||||||
<q-btn
|
|
||||||
push
|
|
||||||
dense
|
|
||||||
size="md"
|
|
||||||
color="primary"
|
|
||||||
class="q-mx-lg"
|
|
||||||
icon="attach_file"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<!-- comment section -->
|
|
||||||
<q-item-section top>
|
|
||||||
<q-item-label lines="1">
|
|
||||||
{{ $t('timesheet.expense.employee_comment') }}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label caption lines="2">
|
|
||||||
{{ expense.comment }}
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<!-- supervisor comment section -->
|
|
||||||
<q-item-section top>
|
|
||||||
<q-item-label lines="1">
|
|
||||||
{{ $t('timesheet.expense.supervisor_comment') }}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label v-if="expense.supervisor_comment" caption lines="2">
|
|
||||||
{{ expense.supervisor_comment }}
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<!-- delete btn -->
|
|
||||||
<q-item-section side>
|
|
||||||
<q-btn
|
|
||||||
v-if="!is_readonly"
|
|
||||||
push
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
color="negative"
|
|
||||||
icon="close"
|
|
||||||
@click="remove_item_at(index)"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
|
|
||||||
<q-form
|
|
||||||
ref="formRef"
|
|
||||||
flat
|
|
||||||
v-if="!is_readonly"
|
|
||||||
@submit.prevent="on_form_submit"
|
|
||||||
>
|
|
||||||
<div class="text-subtitle2 q-py-sm">
|
|
||||||
{{ $t('timesheet.expense.add_expense')}}
|
|
||||||
</div>
|
|
||||||
<div class="row justify-between">
|
|
||||||
|
|
||||||
<!-- date selection input -->
|
|
||||||
<q-input
|
|
||||||
v-model="draft.date"
|
|
||||||
type="date"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class="col- q-px-xs"
|
|
||||||
color="primary"
|
|
||||||
:label="$t('timesheet.expense.date')"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<!-- expenses type selection -->
|
|
||||||
<q-select
|
|
||||||
v-model="draft.type"
|
|
||||||
:options="type_options"
|
|
||||||
:option-label="opt => $t(`timesheet.expense.types.${opt.label}`)"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
class="col-auto q-px-xs"
|
|
||||||
color="primary"
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
:label="$t('timesheet.expense.type')"
|
|
||||||
:rules="[ val => !! val || $t('timesheet.expense.errors.type_required')]"
|
|
||||||
@update:model-value="val => set_draft_type(val as ExpenseType)"
|
|
||||||
/>
|
|
||||||
<!-- amount input -->
|
|
||||||
<template v-if="showAmount">
|
|
||||||
<q-input
|
|
||||||
key="amount"
|
|
||||||
v-model.number="draft.amount"
|
|
||||||
filled
|
|
||||||
input-class="text-right"
|
|
||||||
dense
|
|
||||||
clearable
|
|
||||||
color="primary"
|
|
||||||
class="col-auto q-px-xs"
|
|
||||||
:label="$t('timesheet.expense.amount')"
|
|
||||||
suffix="$"
|
|
||||||
lazy-rules="ondemand"
|
|
||||||
:rules="[
|
|
||||||
value => (value !== undefined && value !== null && value !== '')
|
|
||||||
|| $t('timesheet.expense.errors.amount_required_for_type')
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- mileage input -->
|
|
||||||
<template v-else>
|
|
||||||
<q-input
|
|
||||||
key="mileage"
|
|
||||||
v-model.number="draft.mileage"
|
|
||||||
filled
|
|
||||||
input-class="text-right"
|
|
||||||
dense
|
|
||||||
clearable
|
|
||||||
color="primary"
|
|
||||||
class="col-auto q-px-xs"
|
|
||||||
:label="$t('timesheet.expense.mileage')"
|
|
||||||
suffix="km"
|
|
||||||
lazy-rules="ondemand"
|
|
||||||
:rules="[
|
|
||||||
value => (value !== undefined && value !== null && value !== '')
|
|
||||||
|| $t('timesheet.expense.errors.mileage_required_for_type')
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- employee comment input -->
|
|
||||||
<q-input
|
|
||||||
v-model="draft.comment"
|
|
||||||
filled
|
|
||||||
color="primary"
|
|
||||||
type="text"
|
|
||||||
class="col q-px-sm"
|
|
||||||
dense
|
|
||||||
clearable
|
|
||||||
:label="$t('timesheet.expense.employee_comment')"
|
|
||||||
:counter="true"
|
|
||||||
:maxlength="COMMENT_MAX_LENGTH"
|
|
||||||
lazy-rules="ondemand"
|
|
||||||
:rules="[
|
|
||||||
value => (value && String(value).trim().length) || $t('timesheet.expense.errors.comment_required'),
|
|
||||||
value => String(value || '').length <= COMMENT_MAX_LENGTH || $t('timesheet.expense.errors.comment_too_long')
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
color="primary"
|
|
||||||
class="q-px-sm row"
|
|
||||||
>
|
|
||||||
<div class="row column">
|
|
||||||
<q-input
|
|
||||||
v-model="draft.comment"
|
|
||||||
type="text"
|
|
||||||
readonly
|
|
||||||
filled
|
|
||||||
class="col-auto justify-end"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
name="attach_file"
|
|
||||||
size="sm"
|
|
||||||
class="col-auto justify-start"
|
|
||||||
/>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<!-- add btn section -->
|
|
||||||
<div>
|
|
||||||
<q-btn
|
|
||||||
push
|
|
||||||
dense
|
|
||||||
color="primary"
|
|
||||||
icon="add"
|
|
||||||
size="sm"
|
|
||||||
class="q-mt-sm q-ml-sm"
|
|
||||||
type="submit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</q-form>
|
|
||||||
|
|
||||||
<q-separator spaced/>
|
<q-separator spaced/>
|
||||||
|
|
||||||
<div class="row col-auto justify-end">
|
<div class="row col-auto justify-end">
|
||||||
<!-- close btn -->
|
<!-- close btn -->
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
|
class="q-mr-sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
:label="$t('timesheet.cancel_button')"
|
:label="$t('timesheet.cancel_button')"
|
||||||
@click="on_close"
|
@click="onClose"
|
||||||
/>
|
/>
|
||||||
<!-- save btn -->
|
<!-- save btn -->
|
||||||
<q-btn
|
<q-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
unelevated
|
unelevated
|
||||||
|
push
|
||||||
:disable="is_readonly || items.length === 0"
|
:disable="is_readonly || items.length === 0"
|
||||||
:label="$t('timesheet.save_button')"
|
:label="$t('timesheet.save_button')"
|
||||||
@click="on_save"
|
@click="onSave"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Shift } from 'src/modules/timesheets/types/timesheet-shift-interface';
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import type { Shift } from '../../types/shift.interfaces';
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -52,7 +52,7 @@ const on_click_delete = () => emit('request-delete', { shift: props.shift });
|
||||||
<q-card-section
|
<q-card-section
|
||||||
horizontal
|
horizontal
|
||||||
class="q-pa-none text-uppercase text-center items-center rounded-10"
|
class="q-pa-none text-uppercase text-center items-center rounded-10"
|
||||||
:class="props.shift.type === '' ? '': 'cursor-pointer'"
|
:class="props.shift.type"
|
||||||
style="line-height: 1;"
|
style="line-height: 1;"
|
||||||
@click.stop="on_click_edit(props.shift.type)"
|
@click.stop="on_click_edit(props.shift.type)"
|
||||||
>
|
>
|
||||||
|
|
@ -79,7 +79,7 @@ const on_click_delete = () => emit('request-delete', { shift: props.shift });
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="props.shift.type !== ''"
|
v-if="props.shift.type"
|
||||||
name="double_arrow"
|
name="double_arrow"
|
||||||
:color="icon_data.color"
|
:color="icon_data.color"
|
||||||
size="24px"
|
size="24px"
|
||||||
|
|
@ -105,7 +105,7 @@ const on_click_delete = () => emit('request-delete', { shift: props.shift });
|
||||||
>
|
>
|
||||||
<!-- comment btn -->
|
<!-- comment btn -->
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="props.shift.type !== ''"
|
v-if="props.shift.type"
|
||||||
:name="comment_icon"
|
:name="comment_icon"
|
||||||
:color="comment_color"
|
:color="comment_color"
|
||||||
class="q-pa-none q-mx-xs"
|
class="q-pa-none q-mx-xs"
|
||||||
|
|
@ -113,7 +113,7 @@ const on_click_delete = () => emit('request-delete', { shift: props.shift });
|
||||||
/>
|
/>
|
||||||
<!-- expenses btn -->
|
<!-- expenses btn -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="props.shift.type !== ''"
|
v-if="props.shift.type"
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
color='grey-8'
|
color='grey-8'
|
||||||
|
|
@ -122,7 +122,7 @@ const on_click_delete = () => emit('request-delete', { shift: props.shift });
|
||||||
/>
|
/>
|
||||||
<!-- delete btn -->
|
<!-- delete btn -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="props.shift.type !== ''"
|
v-if="props.shift.type"
|
||||||
push
|
push
|
||||||
dense
|
dense
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { default_shift, type Shift } from 'src/modules/timesheets/types/timesheet-shift-interface';
|
|
||||||
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
||||||
import type { TimesheetPayPeriodDetailsOverview } from '../../types/timesheet-pay-period-details-overview-interface';
|
import detailedShiftListHeader from './detailed-shift-list-header.vue';
|
||||||
import TimesheetDetailsShiftsRowHeader from './timesheet-details-shifts-row-header.vue';
|
import detailedShiftListRow from './detailed-shift-list-row.vue';
|
||||||
import TimesheetDetailsShiftsRow from './timesheet-details-shifts-row.vue';
|
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
|
import type { TimesheetPayPeriodDetailsOverview } from '../../types/timesheet.interfaces';
|
||||||
|
import type { Shift } from '../../types/shift.interfaces';
|
||||||
|
import { default_shift } from '../../types/shift.defaults';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rawData: TimesheetPayPeriodDetailsOverview;
|
rawData: TimesheetPayPeriodDetailsOverview;
|
||||||
|
|
@ -72,13 +73,13 @@ import { date } from 'quasar';
|
||||||
|
|
||||||
<!-- List of shifts column -->
|
<!-- List of shifts column -->
|
||||||
<q-card-section class="col q-pa-none">
|
<q-card-section class="col q-pa-none">
|
||||||
<TimesheetDetailsShiftsRowHeader />
|
<detailedShiftListHeader />
|
||||||
<TimesheetDetailsShiftsRow
|
<detailedShiftListRow
|
||||||
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
||||||
:key="shift_index"
|
:key="shift_index"
|
||||||
:shift="shift"
|
:shift="shift"
|
||||||
@request-edit=" ({ shift }) => on_request_edit(to_iso_date(day.short_date), shift )"
|
@request-edit=" ({ shift }) => on_request_edit(to_iso_date(day.short_date), shift )"
|
||||||
@request-delete="({ shift }) => on_request_delete(to_iso_date(day.short_date), shift )"
|
@request-delete="({ shift }) => on_request_delete(to_iso_date(day.short_date), shift )"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<!-- add shift btn column -->
|
<!-- add shift btn column -->
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { upsert_shifts_by_date, type UpsertShiftsBody, type ShiftPayload } from '../../composables/use-shift-api';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { SHIFT_KEY, type ShiftKey, type ShiftPayload, type ShiftSelectOption } from '../../types/shift.types';
|
||||||
|
import type { UpsertShiftsBody } from '../../types/shift.interfaces';
|
||||||
|
import { upsertShiftsByDate } from '../../composables/api/use-shift-api';
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
type Option = { value: string; label: string };
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|
@ -12,27 +13,30 @@ const props = defineProps<{
|
||||||
mode: 'create' | 'edit' | 'delete';
|
mode: 'create' | 'edit' | 'delete';
|
||||||
dateIso: string;
|
dateIso: string;
|
||||||
initialShift?: ShiftPayload | null;
|
initialShift?: ShiftPayload | null;
|
||||||
shiftOptions: Option[];
|
shiftOptions: ShiftSelectOption[];
|
||||||
email: string;
|
email: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close' ): void;
|
'close': []
|
||||||
(e: 'saved'): void;
|
'saved': []
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isSubmitting = ref(false);
|
const isSubmitting = ref(false);
|
||||||
const errorBanner = ref<string | null>(null);
|
const errorBanner = ref<string | null>(null);
|
||||||
const conflicts = ref<Array<{start_time: string; end_time: string; type: string}>>([]);
|
const conflicts = ref<Array<{start_time: string; end_time: string; type: string}>>([]);
|
||||||
|
|
||||||
const opened = defineModel<boolean>( { default: false });
|
const opened = defineModel<boolean> ( { default: false });
|
||||||
const startTime = defineModel<string> ('startTime', { default: '' });
|
const startTime = defineModel<string> ('startTime', { default: '' });
|
||||||
const endTime = defineModel<string> ('endTime' , { default: '' });
|
const endTime = defineModel<string> ('endTime' , { default: '' });
|
||||||
const type = defineModel<string> ('type' , { default: '' });
|
const type = defineModel<ShiftKey | ''> ('type' , { default: '' });
|
||||||
const isRemote = defineModel<boolean>('isRemote' , { default: false });
|
const isRemote = defineModel<boolean> ('isRemote' , { default: false });
|
||||||
const comment = defineModel<string> ('comment' , { default: '' });
|
const comment = defineModel<string> ('comment' , { default: '' });
|
||||||
|
|
||||||
|
const isShiftKey = (val: unknown): val is ShiftKey => SHIFT_KEY.includes(val as ShiftKey);
|
||||||
|
|
||||||
const buildNewShiftPayload = (): ShiftPayload => {
|
const buildNewShiftPayload = (): ShiftPayload => {
|
||||||
|
if(!isShiftKey(type.value)) throw new Error('Invalid shift type');
|
||||||
const trimmed = (comment.value ?? '').trim();
|
const trimmed = (comment.value ?? '').trim();
|
||||||
return {
|
return {
|
||||||
start_time: startTime.value,
|
start_time: startTime.value,
|
||||||
|
|
@ -59,7 +63,7 @@ const onSubmit = async () => {
|
||||||
if (!props.initialShift) throw new Error('Missing initial Shift for delete.');
|
if (!props.initialShift) throw new Error('Missing initial Shift for delete.');
|
||||||
body = { old_shift: props.initialShift };
|
body = { old_shift: props.initialShift };
|
||||||
}
|
}
|
||||||
await upsert_shifts_by_date(props.email, props.dateIso, body);
|
await upsertShiftsByDate(props.email, props.dateIso, body);
|
||||||
opened.value = false;
|
opened.value = false;
|
||||||
emit('saved');
|
emit('saved');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -106,8 +110,8 @@ const hydrateFromProps = () => {
|
||||||
const canSubmit = computed(() =>
|
const canSubmit = computed(() =>
|
||||||
props.mode === 'delete' ||
|
props.mode === 'delete' ||
|
||||||
(startTime.value.trim().length === 5 &&
|
(startTime.value.trim().length === 5 &&
|
||||||
endTime.value.trim().length === 5 &&
|
endTime.value.trim().length === 5 &&
|
||||||
type.value.trim().length > 0)
|
isShiftKey(type.value))
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|
@ -120,16 +124,17 @@ watch(
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- create/edit/delete shifts dialog -->
|
<!-- create/edit/delete shifts dialog -->
|
||||||
<template>
|
<template>
|
||||||
<q-dialog
|
<q-dialog v-model="opened"
|
||||||
v-model="opened"
|
persistent
|
||||||
persistent
|
transition-show="fade"
|
||||||
transition-show="fade"
|
transition-hide="fade">
|
||||||
transition-hide="fade"
|
|
||||||
>
|
|
||||||
<q-card class="q-pa-md">
|
<q-card class="q-pa-md">
|
||||||
<div class="row items-center q-mb-sm">
|
<div class="row items-center q-mb-sm">
|
||||||
<q-icon name="schedule" size="24px" class="q-mr-sm"/>
|
<q-icon name="schedule"
|
||||||
|
size="24px"
|
||||||
|
class="q-mr-sm"/>
|
||||||
<div class="text-h6">
|
<div class="text-h6">
|
||||||
{{
|
{{
|
||||||
props.mode === 'create'
|
props.mode === 'create'
|
||||||
|
|
@ -140,7 +145,9 @@ watch(
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<q-space/>
|
<q-space/>
|
||||||
<q-badge outline color="primary">{{ props.dateIso }}</q-badge>
|
<q-badge outline color="primary">
|
||||||
|
{{ props.dateIso }}
|
||||||
|
</q-badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-separator spaced/>
|
<q-separator spaced/>
|
||||||
|
|
@ -227,15 +234,13 @@ watch(
|
||||||
:disable="!canSubmit"
|
:disable="!canSubmit"
|
||||||
@click="onSubmit"
|
@click="onSubmit"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn v-else
|
||||||
v-else
|
color="primary"
|
||||||
color="primary"
|
icon="save_alt"
|
||||||
icon="save_alt"
|
:label="$t('timesheet.save_button')"
|
||||||
:label="$t('timesheet.save_button')"
|
:loading="isSubmitting"
|
||||||
:loading="isSubmitting"
|
:disable="!canSubmit"
|
||||||
:disable="!canSubmit"
|
@click="onSubmit"/>
|
||||||
@click="onSubmit"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import type { ShiftLegendItem } from '../../types/shift.types';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const props = defineProps<{ isLoading: boolean; }>();
|
const props = defineProps<{ isLoading: boolean; }>();
|
||||||
|
|
||||||
type ShiftLegendItem = {
|
|
||||||
type: 'REGULAR'|'EVENING'|'EMERGENCY'|'OVERTIME'|'VACATION'|'HOLIDAY'|'SICK';
|
|
||||||
color: string;
|
|
||||||
label_key: string;
|
|
||||||
text_color?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const legend: ShiftLegendItem[] = [
|
const legend: ShiftLegendItem[] = [
|
||||||
{type:'REGULAR' , color: 'secondary', label_key: 'timesheet.shift.types.REGULAR', text_color: 'grey-8'},
|
{type:'REGULAR' , color: 'secondary', label_key: 'timesheet.shift.types.REGULAR', text_color: 'grey-8'},
|
||||||
{type:'EVENING' , color: 'warning' , label_key: 'timesheet.shift.types.EVENING'},
|
{type:'EVENING' , color: 'warning' , label_key: 'timesheet.shift.types.EVENING'},
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/* eslint-disable */
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import type { CreateShiftPayload } from '../../types/timesheet-shifts-payload-interface';
|
import type { CreateShiftPayload, Shift } from '../../types/shift.interfaces';
|
||||||
import type { Shift } from '../../types/timesheet-shift-interface';
|
|
||||||
|
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rows: Shift[];
|
rows: Shift[];
|
||||||
week_dates: string[];
|
week_dates: string[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,27 @@
|
||||||
import { isProxy, toRaw } from "vue";
|
|
||||||
import { type ExpenseType, type TimesheetExpense } from "../types/timesheet-expenses-interface";
|
|
||||||
import { type PayPeriodExpenses } from "../types/timesheet-expenses-list-interface";
|
|
||||||
import { normalize_expense, validate_expense_UI } from "../utils/timesheet-expenses-validators";
|
|
||||||
import { api } from "src/boot/axios";
|
import { api } from "src/boot/axios";
|
||||||
|
import { isProxy, toRaw } from "vue";
|
||||||
|
import { normalizeExpense, validateExpenseUI } from "../../utils/expenses-validators";
|
||||||
|
import type { ExpenseType } from "../../types/expense.types";
|
||||||
|
import { ExpensesApiError } from "../../types/expense-validation.interface";
|
||||||
|
import type {
|
||||||
|
ExpensePayload,
|
||||||
|
PayPeriodExpenses,
|
||||||
|
TimesheetExpense,
|
||||||
|
UpsertExpensesBody,
|
||||||
|
UpsertExpensesResponse
|
||||||
|
} from "../../types/expense.interfaces";
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
export interface ExpensePayload{
|
const toPlain = <T extends object>(obj:T): T => {
|
||||||
date: string;
|
|
||||||
type: ExpenseType;
|
|
||||||
amount?: number;
|
|
||||||
mileage?: number;
|
|
||||||
comment: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpsertExpensesBody {
|
|
||||||
expenses: ExpensePayload[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpsertExpensesResponse {
|
|
||||||
data: PayPeriodExpenses;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApiErrorPayload {
|
|
||||||
status_code: number;
|
|
||||||
error_code?: string;
|
|
||||||
message?: string;
|
|
||||||
context?: Record<string,unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExpensesApiError extends Error {
|
|
||||||
status_code: number;
|
|
||||||
error_code?: string;
|
|
||||||
context?: Record<string, unknown>;
|
|
||||||
constructor(payload: ApiErrorPayload) {
|
|
||||||
super(payload.message || 'Request failed');
|
|
||||||
this.name = 'ExpensesApiError';
|
|
||||||
this.status_code = payload.status_code;
|
|
||||||
|
|
||||||
if(payload.error_code !== undefined) this.error_code = payload.error_code;
|
|
||||||
if(payload.context !== undefined) this.context = payload.context;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const to_plain = <T extends object>(obj:T): T => {
|
|
||||||
const raw = isProxy(obj) ? toRaw(obj) : obj;
|
const raw = isProxy(obj) ? toRaw(obj) : obj;
|
||||||
if(typeof (globalThis as any).structuredClone === 'function') {
|
if( typeof (globalThis as any).structuredClone === 'function') {
|
||||||
return (globalThis as any).structuredClone(raw);
|
return (globalThis as any).structuredClone(raw);
|
||||||
}
|
}
|
||||||
return JSON.parse(JSON.stringify(raw));
|
return JSON.parse(JSON.stringify(raw));
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalize_payload = (expense: ExpensePayload): ExpensePayload => {
|
const normalizePayload = (expense: ExpensePayload): ExpensePayload => {
|
||||||
const exp = normalize_expense(expense as unknown as TimesheetExpense);
|
const exp = normalizeExpense(expense as unknown as TimesheetExpense);
|
||||||
const out: ExpensePayload = {
|
const out: ExpensePayload = {
|
||||||
date: exp.date,
|
date: exp.date,
|
||||||
type: exp.type as ExpenseType,
|
type: exp.type as ExpenseType,
|
||||||
|
|
@ -62,7 +33,7 @@ const normalize_payload = (expense: ExpensePayload): ExpensePayload => {
|
||||||
}
|
}
|
||||||
|
|
||||||
//GET by email, year and period no
|
//GET by email, year and period no
|
||||||
export const get_pay_period_expenses = async (
|
export const getPayPeriodExpenses = async (
|
||||||
email: string,
|
email: string,
|
||||||
pay_year: number,
|
pay_year: number,
|
||||||
pay_period_no: number
|
pay_period_no: number
|
||||||
|
|
@ -74,7 +45,7 @@ export const get_pay_period_expenses = async (
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get<PayPeriodExpenses>(`/expenses/${encoded_email}/${encoded_year}/${encoded_pay_period_no}`);
|
const { data } = await api.get<PayPeriodExpenses>(`/expenses/${encoded_email}/${encoded_year}/${encoded_pay_period_no}`);
|
||||||
|
|
||||||
const items = Array.isArray(data.expenses) ? data.expenses.map(normalize_expense) : [];
|
const items = Array.isArray(data.expenses) ? data.expenses.map(normalizeExpense) : [];
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
expenses: items,
|
expenses: items,
|
||||||
|
|
@ -92,7 +63,7 @@ export const get_pay_period_expenses = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
//PUT by email, year and period no
|
//PUT by email, year and period no
|
||||||
export const put_pay_period_expenses = async (
|
export const putPayPeriodExpenses = async (
|
||||||
email: string,
|
email: string,
|
||||||
pay_year: number,
|
pay_year: number,
|
||||||
pay_period_no: number,
|
pay_period_no: number,
|
||||||
|
|
@ -102,12 +73,12 @@ export const put_pay_period_expenses = async (
|
||||||
const encoded_year = encodeURIComponent(String(pay_year));
|
const encoded_year = encodeURIComponent(String(pay_year));
|
||||||
const encoded_pay_period_no = encodeURIComponent(String(pay_period_no));
|
const encoded_pay_period_no = encodeURIComponent(String(pay_period_no));
|
||||||
|
|
||||||
const plain = Array.isArray(expenses) ? expenses.map(to_plain): [];
|
const plain = Array.isArray(expenses) ? expenses.map(toPlain): [];
|
||||||
|
|
||||||
const normalized: ExpensePayload[] = plain.map((exp) => {
|
const normalized: ExpensePayload[] = plain.map((exp) => {
|
||||||
const norm = normalize_expense(exp as TimesheetExpense);
|
const norm = normalizeExpense(exp as TimesheetExpense);
|
||||||
validate_expense_UI(norm, 'expense_item');
|
validateExpenseUI(norm, 'expense_item');
|
||||||
return normalize_payload(norm as unknown as ExpensePayload);
|
return normalizePayload(norm as unknown as ExpensePayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
const body: UpsertExpensesBody = {expenses: normalized};
|
const body: UpsertExpensesBody = {expenses: normalized};
|
||||||
|
|
@ -120,7 +91,7 @@ export const put_pay_period_expenses = async (
|
||||||
);
|
);
|
||||||
|
|
||||||
const items = Array.isArray(data?.data?.expenses)
|
const items = Array.isArray(data?.data?.expenses)
|
||||||
? data.data.expenses.map(normalize_expense)
|
? data.data.expenses.map(normalizeExpense)
|
||||||
: [];
|
: [];
|
||||||
return {
|
return {
|
||||||
...(data?.data ?? {
|
...(data?.data ?? {
|
||||||
|
|
@ -145,7 +116,7 @@ export const put_pay_period_expenses = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const post_pay_period_expenses = async (
|
export const postPayPeriodExpenses = async (
|
||||||
email: string,
|
email: string,
|
||||||
pay_year: number,
|
pay_year: number,
|
||||||
pay_period_no: number,
|
pay_period_no: number,
|
||||||
|
|
@ -155,11 +126,11 @@ export const post_pay_period_expenses = async (
|
||||||
const encoded_year = encodeURIComponent(String(pay_year));
|
const encoded_year = encodeURIComponent(String(pay_year));
|
||||||
const encoded_pp = encodeURIComponent(String(pay_period_no));
|
const encoded_pp = encodeURIComponent(String(pay_period_no));
|
||||||
|
|
||||||
const plain = Array.isArray(new_expenses) ? new_expenses.map(to_plain) : [];
|
const plain = Array.isArray(new_expenses) ? new_expenses.map(toPlain) : [];
|
||||||
const normalized: ExpensePayload[] = plain.map((exp) => {
|
const normalized: ExpensePayload[] = plain.map((exp) => {
|
||||||
const norm = normalize_expense(exp as TimesheetExpense);
|
const norm = normalizeExpense(exp as TimesheetExpense);
|
||||||
validate_expense_UI(norm, 'expense_item');
|
validateExpenseUI(norm, 'expense_item');
|
||||||
return normalize_payload(norm as unknown as ExpensePayload);
|
return normalizePayload(norm as unknown as ExpensePayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
const body: UpsertExpensesBody = { expenses: normalized };
|
const body: UpsertExpensesBody = { expenses: normalized };
|
||||||
|
|
@ -171,7 +142,7 @@ export const post_pay_period_expenses = async (
|
||||||
{ headers: { 'content-type': 'application/json' } }
|
{ headers: { 'content-type': 'application/json' } }
|
||||||
);
|
);
|
||||||
const items = Array.isArray(data?.data?.expenses)
|
const items = Array.isArray(data?.data?.expenses)
|
||||||
? data.data.expenses.map(normalize_expense)
|
? data.data.expenses.map(normalizeExpense)
|
||||||
: [];
|
: [];
|
||||||
return {
|
return {
|
||||||
...(data?.data ?? {
|
...(data?.data ?? {
|
||||||
|
|
@ -1,37 +1,9 @@
|
||||||
import { api } from "src/boot/axios";
|
import { api } from "src/boot/axios";
|
||||||
import { isProxy, toRaw } from "vue";
|
import { isProxy, toRaw } from "vue";
|
||||||
|
import { DATE_FORMAT_PATTERN, TIME_FORMAT_PATTERN } from "../../constants/shift.constants";
|
||||||
|
import type { ShiftPayload } from "../../types/shift.types";
|
||||||
|
import type { UpsertShiftsBody, UpsertShiftsResponse } from "../../types/shift.interfaces";
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
export interface ShiftPayload {
|
|
||||||
start_time: string;
|
|
||||||
end_time: string;
|
|
||||||
type: string;
|
|
||||||
is_remote: boolean;
|
|
||||||
comment?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpsertShiftsBody {
|
|
||||||
old_shift?: ShiftPayload;
|
|
||||||
new_shift?: ShiftPayload;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UpsertAction = 'created' | 'updated' | 'deleted';
|
|
||||||
|
|
||||||
export interface DayShift {
|
|
||||||
start_time: string;
|
|
||||||
end_time: string;
|
|
||||||
type: string;
|
|
||||||
is_remote: boolean;
|
|
||||||
comment?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpsertShiftsResponse {
|
|
||||||
action: UpsertAction;
|
|
||||||
day: DayShift[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TIME_FORMAT_PATTERN = /^\d{2}:\d{2}$/;
|
|
||||||
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
||||||
export const COMMENT_MAX_LENGTH = 512 as const;
|
|
||||||
|
|
||||||
//normalize payload to match backend data
|
//normalize payload to match backend data
|
||||||
export const normalize_comment = (input?: string): string | undefined => {
|
export const normalize_comment = (input?: string): string | undefined => {
|
||||||
|
|
@ -40,14 +12,12 @@ export const normalize_comment = (input?: string): string | undefined => {
|
||||||
return trimmed.length ? trimmed : undefined;
|
return trimmed.length ? trimmed : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const normalize_type = (input: string): string => (input ?? '').trim().toUpperCase();
|
|
||||||
|
|
||||||
export const normalize_payload = (payload: ShiftPayload): ShiftPayload => {
|
export const normalize_payload = (payload: ShiftPayload): ShiftPayload => {
|
||||||
const comment = normalize_comment(payload.comment);
|
const comment = normalize_comment(payload.comment);
|
||||||
return {
|
return {
|
||||||
start_time: payload.start_time,
|
start_time: payload.start_time,
|
||||||
end_time: payload.end_time,
|
end_time: payload.end_time,
|
||||||
type: normalize_type(payload.type),
|
type: payload.type,
|
||||||
is_remote: Boolean(payload.is_remote),
|
is_remote: Boolean(payload.is_remote),
|
||||||
...(comment !== undefined ? { comment } : {}),
|
...(comment !== undefined ? { comment } : {}),
|
||||||
};
|
};
|
||||||
|
|
@ -120,7 +90,7 @@ const validateShift = (payload: ShiftPayload, label: 'old_shift'|'new_shift') =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const upsert_shifts_by_date = async (
|
export const upsertShiftsByDate = async (
|
||||||
email: string,
|
email: string,
|
||||||
date: string,
|
date: string,
|
||||||
body: UpsertShiftsBody,
|
body: UpsertShiftsBody,
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useAuthStore } from "src/stores/auth-store";
|
import { useAuthStore } from "src/stores/auth-store";
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store"
|
import { useTimesheetStore } from "src/stores/timesheet-store"
|
||||||
|
/* eslint-disable */
|
||||||
export const useTimesheetApi = () => {
|
export const useTimesheetApi = () => {
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const auth_store = useAuthStore();
|
const auth_store = useAuthStore();
|
||||||
|
|
@ -35,15 +35,24 @@ export const useTimesheetApi = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentPayPeriod = async () => fetchPayPeriod(0);
|
|
||||||
const getNextPayPeriod = async () => fetchPayPeriod(1);
|
const getNextPayPeriod = async () => fetchPayPeriod(1);
|
||||||
const getPreviousPayPeriod = async () => fetchPayPeriod(-1);
|
const getPreviousPayPeriod = async () => fetchPayPeriod(-1);
|
||||||
|
|
||||||
|
const getPreviousPeriodForUser = async (_employee_email: string) => {
|
||||||
|
await getPreviousPayPeriod();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNextPeriodForUser = async (_employee_email: string) => {
|
||||||
|
await getNextPayPeriod();
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getTimesheetsByDate,
|
getTimesheetsByDate,
|
||||||
fetchPayPeriod,
|
fetchPayPeriod,
|
||||||
getCurrentPayPeriod,
|
// getCurrentPayPeriod,
|
||||||
getNextPayPeriod,
|
getNextPayPeriod,
|
||||||
getPreviousPayPeriod,
|
getPreviousPayPeriod,
|
||||||
|
getPreviousPeriodForUser,
|
||||||
|
getNextPeriodForUser,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
36
src/modules/timesheets/composables/use-expense-draft.ts
Normal file
36
src/modules/timesheets/composables/use-expense-draft.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import type { TimesheetExpense } from "../types/expense.interfaces";
|
||||||
|
import type { ExpenseType } from "../types/expense.types";
|
||||||
|
|
||||||
|
export const useExpenseDraft = (initial?: Partial<TimesheetExpense>) => {
|
||||||
|
const DEFAULT_TYPE: ExpenseType = 'EXPENSES';
|
||||||
|
|
||||||
|
const draft = ref<Partial<TimesheetExpense>>({
|
||||||
|
date: '',
|
||||||
|
type: DEFAULT_TYPE,
|
||||||
|
comment: '',
|
||||||
|
...(initial ?? {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
draft.value = {
|
||||||
|
date: '',
|
||||||
|
type: DEFAULT_TYPE,
|
||||||
|
comment: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setType = (value: ExpenseType) => {
|
||||||
|
draft.value.type = value;
|
||||||
|
if(value === 'MILEAGE') {
|
||||||
|
delete draft.value.amount;
|
||||||
|
} else {
|
||||||
|
delete draft.value.mileage;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showMileage = computed(()=> (draft.value.type as string) === 'MILEAGE');
|
||||||
|
const showAmount = computed(()=> !showMileage.value);
|
||||||
|
|
||||||
|
return { draft, setType, reset, showMileage, showAmount };
|
||||||
|
}
|
||||||
21
src/modules/timesheets/composables/use-expense-form.ts
Normal file
21
src/modules/timesheets/composables/use-expense-form.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ref } from "vue";
|
||||||
|
import type { QForm } from "quasar"
|
||||||
|
|
||||||
|
|
||||||
|
export const useExpenseForm = () => {
|
||||||
|
const formRef = ref<QForm | null>(null);
|
||||||
|
const triedSubmit = ref(false);
|
||||||
|
|
||||||
|
const validateAnd = async (fn: ()=> void | Promise<void>) => {
|
||||||
|
triedSubmit.value = true;
|
||||||
|
const ok = await formRef.value?.validate(true);
|
||||||
|
if(!ok) return false;
|
||||||
|
await fn();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
formRef,
|
||||||
|
validateAnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
55
src/modules/timesheets/composables/use-expense-items.ts
Normal file
55
src/modules/timesheets/composables/use-expense-items.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { ref, type Ref } from "vue";
|
||||||
|
import { normalizeExpense, validateExpenseUI } from "../utils/expenses-validators";
|
||||||
|
import { normExpenseType } from "../utils/expense.util";
|
||||||
|
import type { TimesheetExpense } from "../types/expense.interfaces";
|
||||||
|
|
||||||
|
type UseExpenseItemsParams = {
|
||||||
|
initial_expenses?: TimesheetExpense[] | null | undefined;
|
||||||
|
draft: Ref<Partial<TimesheetExpense>>;
|
||||||
|
is_approved: Ref<boolean> | boolean;
|
||||||
|
};
|
||||||
|
export const useExpenseItems = ({
|
||||||
|
initial_expenses,
|
||||||
|
draft,
|
||||||
|
is_approved
|
||||||
|
}: UseExpenseItemsParams) => {
|
||||||
|
const items = ref<TimesheetExpense[]>(
|
||||||
|
Array.isArray(initial_expenses) ? initial_expenses.map(normalizeExpense) : []
|
||||||
|
);
|
||||||
|
|
||||||
|
const addFromDraft = () => {
|
||||||
|
const candidate: TimesheetExpense = normalizeExpense({
|
||||||
|
date: draft.value.date,
|
||||||
|
type: normExpenseType(draft.value.type),
|
||||||
|
...(typeof draft.value.amount === 'number' ? { amount: draft.value.amount }: {}),
|
||||||
|
...(typeof draft.value.mileage === 'number' ? { mileage: draft.value.mileage }: {}),
|
||||||
|
comment: String(draft.value.comment ?? '').trim(),
|
||||||
|
} as TimesheetExpense);
|
||||||
|
|
||||||
|
validateExpenseUI(candidate, 'expense_draft');
|
||||||
|
items.value = [ ...items.value, candidate];
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAt = (index: number) => {
|
||||||
|
const locked = typeof is_approved === 'boolean' ? is_approved : is_approved.value;
|
||||||
|
if(locked) return;
|
||||||
|
if(index < 0 || index >= items.value.length) return;
|
||||||
|
items.value = items.value.filter((_,i)=> i !== index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateAll = () => {
|
||||||
|
for (const expense of items.value) {
|
||||||
|
validateExpenseUI(expense, 'expense_item');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = () => items.value.map(normalizeExpense);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
addFromDraft,
|
||||||
|
removeAt,
|
||||||
|
validateAll,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
};
|
||||||
3
src/modules/timesheets/constants/expense.constants.ts
Normal file
3
src/modules/timesheets/constants/expense.constants.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const COMMENT_MAX_LENGTH = 280;
|
||||||
|
|
||||||
|
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
2
src/modules/timesheets/constants/shift.constants.ts
Normal file
2
src/modules/timesheets/constants/shift.constants.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const TIME_FORMAT_PATTERN = /^\d{2}:\d{2}$/;
|
||||||
|
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
|
@ -1,193 +1,76 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
|
||||||
import { useTimesheetApi } from '../composables/use-timesheet-api';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
import TimesheetNavigation from '../components/timesheet/timesheet-navigation.vue';
|
import { computed, onMounted } from 'vue';
|
||||||
import ShiftsLegend from '../components/shift/shifts-legend.vue';
|
import { useI18n } from 'vue-i18n';
|
||||||
import TimesheetDetailsShifts from '../components/shift/timesheet-details-shifts.vue';
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
import { type ShiftPayload } from '../composables/use-shift-api';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { ExpensesApiError, get_pay_period_expenses, put_pay_period_expenses } from '../composables/use-expense-api';
|
import { useShiftStore } from 'src/stores/shift-store';
|
||||||
import type { PayPeriodExpenses } from '../types/timesheet-expenses-list-interface';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import type { TimesheetExpense } from '../types/timesheet-expenses-interface';
|
import { useTimesheetApi } from '../composables/api/use-timesheet-api';
|
||||||
import TimesheetDetailsExpenses from '../components/expenses/timesheet-details-expenses.vue';
|
import { buildShiftOptions } from '../utils/shift.util';
|
||||||
import ShiftCrudDialog from '../components/shift/shift-crud-dialog.vue';
|
import { formatPayPeriodLabel } from '../utils/timesheet-format.util';
|
||||||
|
import TimesheetNavigation from '../components/timesheet/timesheet-navigation.vue';
|
||||||
|
import ShiftsLegend from '../components/shift/shifts-legend.vue';
|
||||||
|
import ShiftCrudDialog from '../components/shift/shift-crud-dialog.vue';
|
||||||
|
import TimesheetDetailsExpenses from '../components/expenses/timesheet-details-expenses.vue';
|
||||||
|
import { SHIFT_KEY } from '../types/shift.types';
|
||||||
|
import type { TimesheetExpense } from '../types/expense.interfaces';
|
||||||
|
import DetailedShiftList from '../components/shift/detailed-shift-list.vue';
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
/* eslint-disable */
|
//------------------- stores -------------------
|
||||||
const { locale, t } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
|
const auth_store = useAuthStore();
|
||||||
|
const expenses_store = useExpensesStore();
|
||||||
|
const shift_store = useShiftStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const auth_store = useAuthStore();
|
const timesheet_api = useTimesheetApi();
|
||||||
const timesheet_api = useTimesheetApi();
|
|
||||||
|
|
||||||
//expenses refs
|
//------------------- expenses -------------------
|
||||||
const show_expenses_dialog = ref(false);
|
const openExpensesDialog = () => expenses_store.openDialog({
|
||||||
const is_loading_expenses = ref(false);
|
email: auth_store.user.email,
|
||||||
const expenses_data = ref<PayPeriodExpenses | null>(null);
|
pay_year: timesheet_store.current_pay_period.pay_year,
|
||||||
|
pay_period_no: timesheet_store.current_pay_period.pay_period_no,
|
||||||
const notify_error = (err: number) => {
|
t,
|
||||||
const e = err as any;
|
|
||||||
expenses_error.value = (e instanceof ExpensesApiError && t(e.message)) || e?.message || 'Unknown error';
|
|
||||||
};
|
|
||||||
|
|
||||||
const open_expenses_dialog = async () => {
|
|
||||||
show_expenses_dialog.value = true;
|
|
||||||
is_loading_expenses.value = true;
|
|
||||||
expenses_error.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await get_pay_period_expenses(
|
|
||||||
auth_store.user.email,
|
|
||||||
timesheet_store.current_pay_period.pay_year,
|
|
||||||
timesheet_store.current_pay_period.pay_period_no,
|
|
||||||
);
|
|
||||||
} catch(err) {
|
|
||||||
notify_error(err as any);
|
|
||||||
expenses_data.value = {
|
|
||||||
pay_period_no: timesheet_store.current_pay_period.pay_period_no,
|
|
||||||
pay_year: timesheet_store.current_pay_period.pay_year,
|
|
||||||
employee_email: auth_store.user.email,
|
|
||||||
is_approved: false,
|
|
||||||
expenses: [],
|
|
||||||
totals: {amount:0, mileage:0},
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
is_loading_expenses.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const on_save_expenses = async (payload: {
|
|
||||||
pay_period_no: number;
|
|
||||||
pay_year: number;
|
|
||||||
email: string;
|
|
||||||
expenses: TimesheetExpense[];
|
|
||||||
}) => {
|
|
||||||
is_loading_expenses.value = true;
|
|
||||||
expenses_error.value = null;
|
|
||||||
|
|
||||||
try{
|
|
||||||
const updated = await put_pay_period_expenses(
|
|
||||||
payload.email,
|
|
||||||
payload.pay_year,
|
|
||||||
payload.pay_period_no,
|
|
||||||
payload.expenses
|
|
||||||
);
|
|
||||||
expenses_data.value = updated;
|
|
||||||
|
|
||||||
await timesheet_store.getTimesheetsByPayPeriodAndEmail(auth_store.user.email);
|
|
||||||
|
|
||||||
show_expenses_dialog.value = false;
|
|
||||||
} catch(err) {
|
|
||||||
notify_error(err as any);
|
|
||||||
} finally {
|
|
||||||
is_loading_expenses.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const on_close_expenses = () => {
|
|
||||||
show_expenses_dialog.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const date_options: Intl.DateTimeFormatOptions = {
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
};
|
|
||||||
|
|
||||||
const pay_period_label = computed(() => {
|
|
||||||
const label = timesheet_store.current_pay_period?.label ?? '';
|
|
||||||
const dates = label.split('.');
|
|
||||||
if ( dates.length < 2 ) {
|
|
||||||
return { start_date: '—', end_date:'—' }
|
|
||||||
}
|
|
||||||
const start_date = new Intl.DateTimeFormat(locale.value, date_options).format(date.extractDate(dates[0] as string, 'YYYY-MM-DD'));
|
|
||||||
const end_date = new Intl.DateTimeFormat(locale.value, date_options).format(date.extractDate(dates[1] as string, 'YYYY-MM-DD'));
|
|
||||||
return { start_date, end_date };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const is_calendar_limit = computed( () => {
|
const onSaveExpenses = async ( payload: { email: string; pay_year: number; pay_period_no: number; expenses: TimesheetExpense[] }) => {
|
||||||
return timesheet_store.current_pay_period.pay_year === 2024 &&
|
await expenses_store.saveExpenses({...payload, t});
|
||||||
timesheet_store.current_pay_period.pay_period_no <= 1;
|
await timesheet_store.refreshCurrentPeriodForUser(auth_store.user.email);
|
||||||
});
|
};
|
||||||
|
|
||||||
const SHIFT_KEY = ['REGULAR', 'EVENING', 'EMERGENCY', 'HOLIDAY', 'VACATION', 'SICK'] as const;
|
const onCloseExpenses = () => expenses_store.closeDialog();
|
||||||
const shift_options = computed(()=> {
|
|
||||||
void locale.value;
|
|
||||||
return SHIFT_KEY.map(key => ({ value: key, label: t(`timesheet.shift_types.${key}`)}))
|
|
||||||
});
|
|
||||||
|
|
||||||
|
//------------------- pay-period format label -------------------
|
||||||
|
const date_options: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'long', year: 'numeric' };
|
||||||
|
|
||||||
|
const pay_period_label = computed(() => formatPayPeriodLabel(
|
||||||
|
timesheet_store.current_pay_period?.label,
|
||||||
|
locale.value,
|
||||||
|
date.extractDate,
|
||||||
|
date_options
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
//------------------- q-select Shift options -------------------
|
||||||
|
const shift_options = computed(() => buildShiftOptions(SHIFT_KEY, t));
|
||||||
|
|
||||||
|
//------------------- navigation by date -------------------
|
||||||
const onDateSelected = async (date_string: string) => {
|
const onDateSelected = async (date_string: string) => {
|
||||||
await timesheet_api.getTimesheetsByDate(date_string);
|
await timesheet_store.loadByIsoDate(date_string, auth_store.user.email);
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadByDate = async (isoDate: string) => {
|
onMounted(async () => {
|
||||||
await timesheet_store.getPayPeriodByDate(isoDate);
|
await timesheet_store.loadToday(auth_store.user.email);
|
||||||
await timesheet_store.getTimesheetsByPayPeriodAndEmail(auth_store.user.email);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted( async () => {
|
|
||||||
await loadByDate(date.formatDate(new Date(), 'YYYY-MM-DD' ));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ------------------- shifts -------------------
|
||||||
|
const onRequestAdd = ({ date }: { date: string }) => shift_store.openCreate(date);
|
||||||
|
const onRequestEdit = ({ date, shift }: { date: string; shift: any }) => shift_store.openEdit(date, shift);
|
||||||
|
const onRequestDelete = async ({ date, shift }: { date: string; shift: any }) => shift_store.openDelete(date, shift);
|
||||||
const onShiftSaved = async () => {
|
const onShiftSaved = async () => {
|
||||||
await timesheet_store.getTimesheetsByPayPeriodAndEmail(auth_store.user.email);
|
await timesheet_store.refreshCurrentPeriodForUser(auth_store.user.email);
|
||||||
}
|
|
||||||
|
|
||||||
type FormMode = 'create' | 'edit' | 'delete';
|
|
||||||
|
|
||||||
const is_dialog_open = ref<boolean>(false);
|
|
||||||
const form_mode = ref<FormMode>('create');
|
|
||||||
const selected_date = ref<string>('');
|
|
||||||
const old_shift_ref = ref<ShiftPayload | undefined>(undefined);
|
|
||||||
|
|
||||||
const open_create_dialog = (iso_date: string) => {
|
|
||||||
form_mode.value = 'create';
|
|
||||||
selected_date.value = iso_date;
|
|
||||||
old_shift_ref.value = undefined;
|
|
||||||
is_dialog_open.value = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const open_edit_dialog = (iso_date: string, shift: any) => {
|
|
||||||
form_mode.value = 'edit';
|
|
||||||
selected_date.value = iso_date;
|
|
||||||
is_dialog_open.value = true;
|
|
||||||
old_shift_ref.value = {
|
|
||||||
start_time: shift.start_time,
|
|
||||||
end_time: shift.end_time,
|
|
||||||
type: shift.type,
|
|
||||||
is_remote: !!shift.is_remote,
|
|
||||||
...(shift.comment ? { comment: String(shift.comment)} : {}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const open_delete_dialog = (iso_date: string, shift: any) => {
|
|
||||||
form_mode.value = 'delete';
|
|
||||||
selected_date.value = iso_date;
|
|
||||||
old_shift_ref.value = {
|
|
||||||
start_time: shift.start_time,
|
|
||||||
end_time: shift.end_time,
|
|
||||||
type: shift.type,
|
|
||||||
is_remote: !!shift.is_remote,
|
|
||||||
...(shift.comment ? { comment: String(shift.comment)} : {}),
|
|
||||||
};
|
|
||||||
is_dialog_open.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const expenses_error = ref<string|null>(null);
|
|
||||||
|
|
||||||
const close_dialog = () => {
|
|
||||||
expenses_error.value = null;
|
|
||||||
is_dialog_open.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const on_request_add = ({ date }: { date: string }) => open_create_dialog(date);
|
|
||||||
const on_request_edit = ({ date, shift }: { date: string; shift: any }) => open_edit_dialog(date, shift);
|
|
||||||
const on_request_delete = async ({ date, shift }: { date: string; shift: any }) => open_delete_dialog(date, shift);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -226,16 +109,16 @@ const on_request_delete = async ({ date, shift }: { date: string; shift: any })
|
||||||
unelevated
|
unelevated
|
||||||
icon="receipt_long"
|
icon="receipt_long"
|
||||||
:label="$t('timesheet.expense.open_btn')"
|
:label="$t('timesheet.expense.open_btn')"
|
||||||
@click="open_expenses_dialog"
|
@click="openExpensesDialog"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section class="row items-center justify-between q-px-md q-pb-none">
|
<q-card-section class="row items-center justify-between q-px-md q-pb-none">
|
||||||
<TimesheetNavigation
|
<TimesheetNavigation
|
||||||
:is-disabled="timesheet_store.is_loading"
|
:is-disabled="timesheet_store.is_loading"
|
||||||
:is-previous-limit="is_calendar_limit"
|
:is-previous-limit="timesheet_store.is_calendar_limit"
|
||||||
@date-selected="value => onDateSelected(value)"
|
@date-selected="onDateSelected"
|
||||||
@pressed-previous-button="timesheet_api.getPreviousPayPeriod()"
|
@pressed-previous-button="timesheet_api.getPreviousPeriodForUser(auth_store.user.email)"
|
||||||
@pressed-next-button="timesheet_api.getNextPayPeriod()"
|
@pressed-next-button="timesheet_api.getNextPeriodForUser(auth_store.user.email)"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<!-- shift's colored legend -->
|
<!-- shift's colored legend -->
|
||||||
|
|
@ -244,12 +127,12 @@ const on_request_delete = async ({ date, shift }: { date: string; shift: any })
|
||||||
/>
|
/>
|
||||||
<q-card-section horizontal>
|
<q-card-section horizontal>
|
||||||
<!-- display of shifts for 2 timesheets -->
|
<!-- display of shifts for 2 timesheets -->
|
||||||
<TimesheetDetailsShifts
|
<DetailedShiftList
|
||||||
:raw-data="timesheet_store.pay_period_employee_details"
|
:raw-data="timesheet_store.pay_period_employee_details"
|
||||||
:current-pay-period="timesheet_store.current_pay_period"
|
:current-pay-period="timesheet_store.current_pay_period"
|
||||||
@request-add="on_request_add"
|
@request-add="onRequestAdd"
|
||||||
@request-edit="on_request_edit"
|
@request-edit="onRequestEdit"
|
||||||
@request-delete="on_request_delete"
|
@request-delete="onRequestDelete"
|
||||||
/>
|
/>
|
||||||
<q-inner-loading :showing="timesheet_store.is_loading" color="primary"/>
|
<q-inner-loading :showing="timesheet_store.is_loading" color="primary"/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
@ -257,16 +140,17 @@ const on_request_delete = async ({ date, shift }: { date: string; shift: any })
|
||||||
</div>
|
</div>
|
||||||
<!-- read/edit/create/delete expense dialog -->
|
<!-- read/edit/create/delete expense dialog -->
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="show_expenses_dialog"
|
v-model="expenses_store.is_dialog_open"
|
||||||
persistent
|
persistent
|
||||||
>
|
>
|
||||||
<q-card
|
<q-card
|
||||||
class="q-pa-md column"
|
class="q-pa-md column"
|
||||||
style=" min-width: 70vw;"
|
style=" min-width: 70vw;"
|
||||||
>
|
>
|
||||||
<q-inner-loading :showing="is_loading_expenses">
|
<q-inner-loading :showing="expenses_store.is_loading">
|
||||||
<q-spinner size="32px"/>
|
<q-spinner size="32px"/>
|
||||||
</q-inner-loading>
|
</q-inner-loading>
|
||||||
|
|
||||||
<!-- <q-banner
|
<!-- <q-banner
|
||||||
v-if="expenses_error"
|
v-if="expenses_error"
|
||||||
dense
|
dense
|
||||||
|
|
@ -276,29 +160,29 @@ const on_request_delete = async ({ date, shift }: { date: string; shift: any })
|
||||||
</q-banner> -->
|
</q-banner> -->
|
||||||
|
|
||||||
<TimesheetDetailsExpenses
|
<TimesheetDetailsExpenses
|
||||||
v-if="expenses_data"
|
v-if="expenses_store.data"
|
||||||
:pay_period_no="expenses_data.pay_period_no"
|
:pay_period_no="expenses_store.data.pay_period_no"
|
||||||
:pay_year="expenses_data.pay_year"
|
:pay_year="expenses_store.data.pay_year"
|
||||||
:email="expenses_data.employee_email"
|
:email="expenses_store.data.employee_email"
|
||||||
:is_approved="expenses_data.is_approved"
|
:is_approved="expenses_store.data.is_approved"
|
||||||
:initial_expenses="expenses_data.expenses"
|
:initial_expenses="expenses_store.data.expenses"
|
||||||
@save="on_save_expenses"
|
@save="onSaveExpenses"
|
||||||
@close="on_close_expenses"
|
@close="onCloseExpenses"
|
||||||
@error=" "
|
@error=" "
|
||||||
/>
|
/>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<!-- shift crud dialog -->
|
<!-- shift crud dialog -->
|
||||||
<ShiftCrudDialog
|
<ShiftCrudDialog
|
||||||
v-model="is_dialog_open"
|
v-model="shift_store.is_open"
|
||||||
:mode="form_mode"
|
:mode="shift_store.mode"
|
||||||
:date-iso="selected_date"
|
:date-iso="shift_store.date_iso"
|
||||||
:email="auth_store.user.email"
|
:email="auth_store.user.email"
|
||||||
:initial-shift="old_shift_ref || null"
|
:initial-shift="shift_store.initial_shift"
|
||||||
:shift-options="shift_options"
|
:shift-options="shift_options"
|
||||||
@close="close_dialog"
|
@close="shift_store.close"
|
||||||
@saved="onShiftSaved"
|
@saved="onShiftSaved"
|
||||||
/>
|
/>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { api } from "src/boot/axios";
|
import { api } from "src/boot/axios";
|
||||||
import type {Timesheet} from "src/modules/timesheets/types/timesheet-interface";
|
|
||||||
import type { CreateShiftPayload, CreateWeekShiftPayload } from "../types/timesheet-shifts-payload-interface";
|
|
||||||
import type { PayPeriod } from "src/modules/shared/types/pay-period-interface";
|
import type { PayPeriod } from "src/modules/shared/types/pay-period-interface";
|
||||||
import type { PayPeriodEmployeeDetails } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface";
|
import type { PayPeriodEmployeeDetails } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface";
|
||||||
import type { PayPeriodOverview } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-interface";
|
import type { PayPeriodOverview } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-interface";
|
||||||
import type { PayPeriodReportFilters } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-report-interface";
|
import type { PayPeriodReportFilters } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-report-interface";
|
||||||
|
import type { Timesheet } from "../types/timesheet.interfaces";
|
||||||
|
import type { CreateShiftPayload, CreateWeekShiftPayload } from "../types/shift.interfaces";
|
||||||
|
|
||||||
export const timesheetTempService = {
|
export const timesheetTempService = {
|
||||||
//GET
|
//GET
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
//mock data
|
|
||||||
34
src/modules/timesheets/types/expense-validation.interface.ts
Normal file
34
src/modules/timesheets/types/expense-validation.interface.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
export interface ApiErrorPayload {
|
||||||
|
status_code: number;
|
||||||
|
error_code?: string;
|
||||||
|
message?: string;
|
||||||
|
context?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExpensesValidationError extends Error {
|
||||||
|
status_code: number;
|
||||||
|
error_code?: string | undefined;
|
||||||
|
context?: Record<string, unknown> | undefined;
|
||||||
|
|
||||||
|
constructor(payload: ApiErrorPayload) {
|
||||||
|
super(payload.message || 'Invalid expense payload');
|
||||||
|
this.name = 'ExpensesValidationError';
|
||||||
|
this.status_code = payload.status_code;
|
||||||
|
this.error_code = payload.error_code;
|
||||||
|
this.context = payload.context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExpensesApiError extends Error {
|
||||||
|
status_code: number;
|
||||||
|
error_code?: string;
|
||||||
|
context?: Record<string, unknown>;
|
||||||
|
constructor(payload: ApiErrorPayload) {
|
||||||
|
super(payload.message || 'Request failed');
|
||||||
|
this.name = 'ExpensesApiError';
|
||||||
|
this.status_code = payload.status_code;
|
||||||
|
|
||||||
|
if(payload.error_code !== undefined) this.error_code = payload.error_code;
|
||||||
|
if(payload.context !== undefined) this.context = payload.context;
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/modules/timesheets/types/expense.defaults.ts
Normal file
0
src/modules/timesheets/types/expense.defaults.ts
Normal file
47
src/modules/timesheets/types/expense.interfaces.ts
Normal file
47
src/modules/timesheets/types/expense.interfaces.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import type { ExpenseType } from "./expense.types";
|
||||||
|
|
||||||
|
export interface Expense {
|
||||||
|
is_approved: boolean;
|
||||||
|
comment: string;
|
||||||
|
amount: number;
|
||||||
|
supervisor_comment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimesheetExpense {
|
||||||
|
date: string;
|
||||||
|
type: string;
|
||||||
|
amount?: number;
|
||||||
|
mileage?: number;
|
||||||
|
comment?: string;
|
||||||
|
supervisor_comment?: string;
|
||||||
|
is_approved?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PayPeriodExpenses {
|
||||||
|
pay_period_no: number;
|
||||||
|
pay_year: number;
|
||||||
|
employee_email: string;
|
||||||
|
is_approved: boolean;
|
||||||
|
expenses: TimesheetExpense[];
|
||||||
|
totals: {
|
||||||
|
amount: number;
|
||||||
|
mileage: number;
|
||||||
|
reimbursable_total?: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExpensePayload{
|
||||||
|
date: string;
|
||||||
|
type: ExpenseType;
|
||||||
|
amount?: number;
|
||||||
|
mileage?: number;
|
||||||
|
comment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpsertExpensesBody {
|
||||||
|
expenses: ExpensePayload[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpsertExpensesResponse {
|
||||||
|
data: PayPeriodExpenses;
|
||||||
|
}
|
||||||
29
src/modules/timesheets/types/expense.types.ts
Normal file
29
src/modules/timesheets/types/expense.types.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import type { TimesheetExpense } from "./expense.interfaces";
|
||||||
|
|
||||||
|
export const EXPENSE_TYPE = [
|
||||||
|
'PER_DIEM',
|
||||||
|
'MILEAGE',
|
||||||
|
'EXPENSES',
|
||||||
|
'PRIME_GARDE',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type ExpenseType = (typeof EXPENSE_TYPE)[number];
|
||||||
|
|
||||||
|
export const TYPES_WITH_MILEAGE_ONLY: Readonly<ExpenseType[]> = ['MILEAGE'];
|
||||||
|
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = [
|
||||||
|
'PER_DIEM',
|
||||||
|
'EXPENSES',
|
||||||
|
'PRIME_GARDE',
|
||||||
|
];
|
||||||
|
|
||||||
|
export type ExpenseTotals = {
|
||||||
|
amount: number;
|
||||||
|
mileage: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExpenseSavePayload = {
|
||||||
|
pay_period_no: number;
|
||||||
|
pay_year: number;
|
||||||
|
email: string;
|
||||||
|
expenses: TimesheetExpense[];
|
||||||
|
};
|
||||||
11
src/modules/timesheets/types/shift.defaults.ts
Normal file
11
src/modules/timesheets/types/shift.defaults.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import type { Shift } from "./shift.interfaces";
|
||||||
|
|
||||||
|
export const default_shift: Readonly<Shift> = {
|
||||||
|
date: '',
|
||||||
|
start_time: '--:--',
|
||||||
|
end_time: '--:--',
|
||||||
|
type:'REGULAR',
|
||||||
|
comment: '',
|
||||||
|
is_approved: false,
|
||||||
|
is_remote: false,
|
||||||
|
};
|
||||||
44
src/modules/timesheets/types/shift.interfaces.ts
Normal file
44
src/modules/timesheets/types/shift.interfaces.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import type { ShiftKey, ShiftPayload, UpsertAction } from "./shift.types";
|
||||||
|
|
||||||
|
export interface Shift {
|
||||||
|
date: string;
|
||||||
|
type: ShiftKey;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
comment: string;
|
||||||
|
is_approved: boolean;
|
||||||
|
is_remote: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateShiftPayload {
|
||||||
|
date: string;
|
||||||
|
type: ShiftKey;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
comment?: string;
|
||||||
|
is_remote?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateWeekShiftPayload {
|
||||||
|
shifts: CreateShiftPayload[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpsertShiftsBody {
|
||||||
|
old_shift?: ShiftPayload;
|
||||||
|
new_shift?: ShiftPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DayShift {
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
type: string;
|
||||||
|
is_remote: boolean;
|
||||||
|
comment?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpsertShiftsResponse {
|
||||||
|
action: UpsertAction;
|
||||||
|
day: DayShift[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
30
src/modules/timesheets/types/shift.types.ts
Normal file
30
src/modules/timesheets/types/shift.types.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
export const SHIFT_KEY = [
|
||||||
|
'REGULAR',
|
||||||
|
'EVENING',
|
||||||
|
'EMERGENCY',
|
||||||
|
'HOLIDAY',
|
||||||
|
'VACATION',
|
||||||
|
'SICK'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type ShiftKey = typeof SHIFT_KEY[number];
|
||||||
|
|
||||||
|
export type ShiftSelectOption = { value: ShiftKey; label: string };
|
||||||
|
|
||||||
|
export type ShiftPayload = {
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
type: ShiftKey;
|
||||||
|
is_remote: boolean;
|
||||||
|
comment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShiftLegendItem = {
|
||||||
|
type: 'REGULAR'|'EVENING'|'EMERGENCY'|'OVERTIME'|'VACATION'|'HOLIDAY'|'SICK';
|
||||||
|
color: string;
|
||||||
|
label_key: string;
|
||||||
|
text_color?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpsertAction = 'created' | 'updated' | 'deleted';
|
||||||
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
import type { Shift } from "./timesheet-shift-interface";
|
|
||||||
|
|
||||||
export interface TimesheetDetailsWeek {
|
|
||||||
is_approved: boolean;
|
|
||||||
shifts: WeekDay<TimesheetDetailsDailySchedule>;
|
|
||||||
expenses: WeekDay<TimesheetDetailsDailyExpenses>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimesheetDetailsDailySchedule {
|
|
||||||
shifts: Shift[];
|
|
||||||
regular_hours: number;
|
|
||||||
evening_hours: number;
|
|
||||||
emergency_hours: number;
|
|
||||||
overtime_hours: number;
|
|
||||||
total_hours: number;
|
|
||||||
comment: string;
|
|
||||||
short_date: string; // ex. 08/24
|
|
||||||
break_duration?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Expense {
|
|
||||||
is_approved: boolean;
|
|
||||||
comment: string;
|
|
||||||
supervisor_comment: string;
|
|
||||||
amount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type WeekDay<T> = {
|
|
||||||
sun: T;
|
|
||||||
mon: T;
|
|
||||||
tue: T;
|
|
||||||
wed: T;
|
|
||||||
thu: T;
|
|
||||||
fri: T;
|
|
||||||
sat: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface TimesheetDetailsDailyExpenses {
|
|
||||||
cash: Expense[];
|
|
||||||
km: Expense[];
|
|
||||||
[otherType: string]: Expense[]; //for possible future types of expenses
|
|
||||||
}
|
|
||||||
|
|
||||||
//employee timesheet template
|
|
||||||
export interface EmployeeTimesheetDetailsWeek {
|
|
||||||
is_approved: boolean;
|
|
||||||
shifts: WeekDay<TimesheetDetailsDailySchedule>;
|
|
||||||
expenses: WeekDay<TimesheetDetailsDailyExpenses>;
|
|
||||||
}
|
|
||||||
// empty default builder
|
|
||||||
const makeWeek = <T>(factory: () => T): WeekDay<T> => ({
|
|
||||||
sun: factory(),
|
|
||||||
mon: factory(),
|
|
||||||
tue: factory(),
|
|
||||||
wed: factory(),
|
|
||||||
thu: factory(),
|
|
||||||
fri: factory(),
|
|
||||||
sat: factory(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const emptyDailySchedule = (): TimesheetDetailsDailySchedule => ({
|
|
||||||
shifts: [],
|
|
||||||
regular_hours: 0,
|
|
||||||
evening_hours: 0,
|
|
||||||
emergency_hours: 0,
|
|
||||||
overtime_hours: 0,
|
|
||||||
total_hours: 0,
|
|
||||||
comment: "",
|
|
||||||
short_date: "",
|
|
||||||
break_duration: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const emptyDailyExpenses = (): TimesheetDetailsDailyExpenses => ({
|
|
||||||
cash: [],
|
|
||||||
km: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const default_timesheet_details_week = (): TimesheetDetailsWeek => ({
|
|
||||||
is_approved: false,
|
|
||||||
shifts: makeWeek(emptyDailySchedule),
|
|
||||||
expenses: makeWeek(emptyDailyExpenses),
|
|
||||||
});
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
export interface TimesheetExpense {
|
|
||||||
date: string;
|
|
||||||
amount?: number;
|
|
||||||
mileage?: number;
|
|
||||||
comment?: string;
|
|
||||||
supervisor_comment?: string;
|
|
||||||
is_approved?: boolean;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EXPENSE_TYPE = [
|
|
||||||
'PER_DIEM',
|
|
||||||
'MILEAGE',
|
|
||||||
'EXPENSES',
|
|
||||||
'PRIME_GARDE',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export type ExpenseType = typeof EXPENSE_TYPE[number];
|
|
||||||
|
|
||||||
export const TYPES_WITH_MILEAGE_ONLY: Readonly<ExpenseType[]> = ['MILEAGE'];
|
|
||||||
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXPENSES', 'PRIME_GARDE']
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import type { TimesheetExpense } from "./timesheet-expenses-interface";
|
|
||||||
|
|
||||||
export interface PayPeriodExpenses {
|
|
||||||
pay_period_no: number;
|
|
||||||
pay_year: number;
|
|
||||||
employee_email: string;
|
|
||||||
is_approved: boolean;
|
|
||||||
expenses: TimesheetExpense[];
|
|
||||||
totals: {
|
|
||||||
amount: number;
|
|
||||||
mileage: number;
|
|
||||||
reimbursable_total?: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
export interface Timesheet {
|
|
||||||
is_approved: boolean;
|
|
||||||
start_day: string;
|
|
||||||
end_day: string;
|
|
||||||
label: string;
|
|
||||||
shifts: Shifts[];
|
|
||||||
expenses: Expenses[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type Shifts = {
|
|
||||||
bank_type: string;
|
|
||||||
date: string;
|
|
||||||
start_time: string;
|
|
||||||
end_time: string;
|
|
||||||
comment: string;
|
|
||||||
is_approved: boolean;
|
|
||||||
is_remote: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Expenses = {
|
|
||||||
bank_type: string;
|
|
||||||
date: string;
|
|
||||||
amount: number;
|
|
||||||
km: number;
|
|
||||||
comment: string;
|
|
||||||
supervisor_comment: string;
|
|
||||||
is_approved: boolean;
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { default_timesheet_details_week, type TimesheetDetailsWeek } from "./timesheet-details-interface";
|
|
||||||
|
|
||||||
export interface TimesheetPayPeriodDetailsOverview {
|
|
||||||
week1: TimesheetDetailsWeek;
|
|
||||||
week2: TimesheetDetailsWeek;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const default_pay_period_employee_details = {
|
|
||||||
week1: default_timesheet_details_week(),
|
|
||||||
week2: default_timesheet_details_week(),
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
export interface Shift {
|
|
||||||
date : string;
|
|
||||||
type : string;
|
|
||||||
start_time : string;
|
|
||||||
end_time : string;
|
|
||||||
comment : string;
|
|
||||||
is_approved: boolean;
|
|
||||||
is_remote : boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const default_shift: Shift = {
|
|
||||||
date: '',
|
|
||||||
start_time: '--:--',
|
|
||||||
end_time: '--:--',
|
|
||||||
type: '',
|
|
||||||
comment: '',
|
|
||||||
is_approved: false,
|
|
||||||
is_remote: false,
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
export interface CreateShiftPayload {
|
|
||||||
date: string;
|
|
||||||
type: string;
|
|
||||||
start_time: string;
|
|
||||||
end_time: string;
|
|
||||||
comment?: string;
|
|
||||||
is_remote?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface CreateWeekShiftPayload {
|
|
||||||
shifts: CreateShiftPayload[];
|
|
||||||
}
|
|
||||||
39
src/modules/timesheets/types/timesheet.defaults.ts
Normal file
39
src/modules/timesheets/types/timesheet.defaults.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import type { WeekDay } from "./timesheet.types";
|
||||||
|
import type {
|
||||||
|
TimesheetDetailsDailyExpenses,
|
||||||
|
TimesheetDetailsDailySchedule,
|
||||||
|
TimesheetDetailsWeek
|
||||||
|
} from "./timesheet.interfaces";
|
||||||
|
|
||||||
|
const makeWeek = <T>(factory: ()=> T): WeekDay<T> => ({
|
||||||
|
sun: factory(),
|
||||||
|
mon: factory(),
|
||||||
|
tue: factory(),
|
||||||
|
wed: factory(),
|
||||||
|
thu: factory(),
|
||||||
|
fri: factory(),
|
||||||
|
sat: factory(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const emptyDailySchedule = (): TimesheetDetailsDailySchedule => ({
|
||||||
|
shifts: [],
|
||||||
|
regular_hours: 0,
|
||||||
|
evening_hours: 0,
|
||||||
|
emergency_hours: 0,
|
||||||
|
overtime_hours: 0,
|
||||||
|
total_hours: 0,
|
||||||
|
comment: "",
|
||||||
|
short_date: "",
|
||||||
|
break_duration: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emptyDailyExpenses = (): TimesheetDetailsDailyExpenses => ({
|
||||||
|
cash: [],
|
||||||
|
km: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const defaultTimesheetDetailsWeek = (): TimesheetDetailsWeek => ({
|
||||||
|
is_approved: false,
|
||||||
|
shifts: makeWeek(emptyDailySchedule),
|
||||||
|
expenses: makeWeek(emptyDailyExpenses),
|
||||||
|
});
|
||||||
54
src/modules/timesheets/types/timesheet.interfaces.ts
Normal file
54
src/modules/timesheets/types/timesheet.interfaces.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import type { Shift } from "./shift.interfaces";
|
||||||
|
import type {
|
||||||
|
TimesheetExpenseEntry,
|
||||||
|
TimesheetShiftEntry,
|
||||||
|
WeekDay
|
||||||
|
} from "./timesheet.types";
|
||||||
|
|
||||||
|
|
||||||
|
export interface Timesheet {
|
||||||
|
is_approved: boolean;
|
||||||
|
start_day: string;
|
||||||
|
end_day: string;
|
||||||
|
label: string;
|
||||||
|
shifts: TimesheetShiftEntry[];
|
||||||
|
expenses: TimesheetExpenseEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimesheetDetailsWeek {
|
||||||
|
is_approved: boolean;
|
||||||
|
shifts: WeekDay<TimesheetDetailsDailySchedule>
|
||||||
|
expenses: WeekDay<TimesheetDetailsDailyExpenses>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimesheetDetailsDailySchedule {
|
||||||
|
shifts: Shift[];
|
||||||
|
regular_hours: number;
|
||||||
|
evening_hours: number;
|
||||||
|
emergency_hours: number;
|
||||||
|
overtime_hours: number;
|
||||||
|
total_hours: number;
|
||||||
|
comment: string;
|
||||||
|
short_date: string;
|
||||||
|
break_duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DailyExpense {
|
||||||
|
is_approved: boolean;
|
||||||
|
comment: string;
|
||||||
|
amount: number;
|
||||||
|
supervisor_comment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimesheetDetailsDailyExpenses {
|
||||||
|
cash: DailyExpense[];
|
||||||
|
km: DailyExpense[];
|
||||||
|
[otherType: string]: DailyExpense[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface TimesheetPayPeriodDetailsOverview {
|
||||||
|
week1: TimesheetDetailsWeek;
|
||||||
|
week2: TimesheetDetailsWeek;
|
||||||
|
}
|
||||||
|
|
||||||
29
src/modules/timesheets/types/timesheet.types.ts
Normal file
29
src/modules/timesheets/types/timesheet.types.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
export type TimesheetShiftEntry = {
|
||||||
|
bank_type: string;
|
||||||
|
date: string;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
comment: string;
|
||||||
|
is_approved: boolean;
|
||||||
|
is_remote: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TimesheetExpenseEntry = {
|
||||||
|
bank_type: string;
|
||||||
|
date: string;
|
||||||
|
amount: number;
|
||||||
|
km: number;
|
||||||
|
comment: string;
|
||||||
|
is_approved: boolean;
|
||||||
|
supervisor_comment: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WeekDay<T> = {
|
||||||
|
sun: T;
|
||||||
|
mon: T;
|
||||||
|
tue: T;
|
||||||
|
wed: T;
|
||||||
|
thu: T;
|
||||||
|
fri: T;
|
||||||
|
sat: T;
|
||||||
|
};
|
||||||
6
src/modules/timesheets/types/ui.types.ts
Normal file
6
src/modules/timesheets/types/ui.types.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export type FormMode = 'create' | 'edit' | 'delete';
|
||||||
|
|
||||||
|
export type PayPeriodLabel = {
|
||||||
|
start_date: string;
|
||||||
|
end_date: string;
|
||||||
|
};
|
||||||
76
src/modules/timesheets/utils/expense.util.ts
Normal file
76
src/modules/timesheets/utils/expense.util.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import type { TimesheetExpense } from "../types/expense.interfaces";
|
||||||
|
import type { ExpenseSavePayload, ExpenseTotals, ExpenseType } from "../types/expense.types";
|
||||||
|
/* eslint-disable */
|
||||||
|
//------------------ normalization / icons ------------------
|
||||||
|
export const normExpenseType = (type: unknown): string =>
|
||||||
|
String(type ?? '').trim().toUpperCase();
|
||||||
|
|
||||||
|
const icon_map: Record<string,string> = {
|
||||||
|
MILEAGE: 'time_to_leave',
|
||||||
|
EXPENSES: 'receipt_long',
|
||||||
|
PER_DIEM: 'hotel',
|
||||||
|
PRIME_GARDE: 'admin_panel_settings',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expenseTypeIcon = (type: unknown): string => {
|
||||||
|
const t = normExpenseType(type);
|
||||||
|
return (
|
||||||
|
icon_map[t.toLowerCase()] ??
|
||||||
|
icon_map[t.replace('-','_').toLowerCase()] ??
|
||||||
|
'help_outline'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
//------------------ q-select options ------------------
|
||||||
|
export const buildExpenseTypeOptions = ( types: readonly ExpenseType[], t: (key:string) => string):
|
||||||
|
{ label: string; value: ExpenseType } [] =>
|
||||||
|
types.map((val)=> ({
|
||||||
|
label: t(`timesheet.expense.types.${val}`),
|
||||||
|
value: val,
|
||||||
|
}));
|
||||||
|
|
||||||
|
//------------------ totals ------------------
|
||||||
|
export const computeExpenseTotals = (items: readonly TimesheetExpense[]): ExpenseTotals =>
|
||||||
|
items.reduce<ExpenseTotals>(
|
||||||
|
(acc, e) => ({
|
||||||
|
amount: acc.amount + (Number(e.amount) || 0),
|
||||||
|
mileage: acc.mileage + (Number(e.mileage) || 0),
|
||||||
|
}),
|
||||||
|
{ amount: 0, mileage: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
//------------------ Quasar :rules=[] ------------------
|
||||||
|
export const makeExpenseRules = (t: (key: string) => string, max_comment_char: number) => {
|
||||||
|
const isPresent = (val: unknown) => val !== undefined && val !== null && val !== '';
|
||||||
|
|
||||||
|
const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
|
||||||
|
|
||||||
|
const amountRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.amount_required_for_type');
|
||||||
|
|
||||||
|
const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.erros.mileage_required_for_type');
|
||||||
|
|
||||||
|
const commentRequired = (val: unknown) => (String(val ?? '').trim().length > 0) || t('timesheet.expense.errors.comment_required');
|
||||||
|
|
||||||
|
const commentTooLong = (val: unknown) => (String(val ?? '').trim().length <= max_comment_char) || t('timesheet.expense.errors.comment_too_long');
|
||||||
|
|
||||||
|
return {
|
||||||
|
typeRequired,
|
||||||
|
amountRequired,
|
||||||
|
mileageRequired,
|
||||||
|
commentRequired,
|
||||||
|
commentTooLong,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
//------------------ saving payload ------------------
|
||||||
|
export const buildExpenseSavePayload = (args: {
|
||||||
|
pay_period_no: number;
|
||||||
|
pay_year: number;
|
||||||
|
email: string;
|
||||||
|
expenses: TimesheetExpense[];
|
||||||
|
}): ExpenseSavePayload => ({
|
||||||
|
pay_period_no: args.pay_period_no,
|
||||||
|
pay_year: args.pay_year,
|
||||||
|
email: args.email,
|
||||||
|
expenses: args.expenses,
|
||||||
|
});
|
||||||
|
|
@ -1,31 +1,11 @@
|
||||||
import { type ExpenseType, type TimesheetExpense, TYPES_WITH_AMOUNT_ONLY, TYPES_WITH_MILEAGE_ONLY } from "../types/timesheet-expenses-interface";
|
import { COMMENT_MAX_LENGTH, DATE_FORMAT_PATTERN } from "../constants/expense.constants";
|
||||||
|
import { ExpensesValidationError } from "../types/expense-validation.interface";
|
||||||
|
import type { TimesheetExpense } from "../types/expense.interfaces";
|
||||||
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
import {
|
||||||
export const COMMENT_MAX_LENGTH = 512 as const;
|
type ExpenseType,
|
||||||
|
TYPES_WITH_AMOUNT_ONLY,
|
||||||
|
TYPES_WITH_MILEAGE_ONLY
|
||||||
//errors handling
|
} from "../types/expense.types";
|
||||||
export interface ApiErrorPayload {
|
|
||||||
status_code: number;
|
|
||||||
error_code?: string;
|
|
||||||
message?: string;
|
|
||||||
context?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExpensesValidationError extends Error {
|
|
||||||
status_code: number;
|
|
||||||
error_code?: string | undefined;
|
|
||||||
context?: Record<string, unknown> | undefined;
|
|
||||||
|
|
||||||
constructor(payload: ApiErrorPayload) {
|
|
||||||
super(payload.message || 'Invalid expense payload');
|
|
||||||
this.name = 'ExpensesValidationError';
|
|
||||||
this.status_code = payload.status_code;
|
|
||||||
this.error_code = payload.error_code;
|
|
||||||
this.context = payload.context;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//normalization helpers
|
//normalization helpers
|
||||||
export const toNumOrUndefined = (value: unknown): number | undefined => {
|
export const toNumOrUndefined = (value: unknown): number | undefined => {
|
||||||
|
|
@ -34,21 +14,21 @@ export const toNumOrUndefined = (value: unknown): number | undefined => {
|
||||||
return Number.isFinite(num) ? num : undefined;
|
return Number.isFinite(num) ? num : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const normalize_comment = (input?: string): string | undefined => {
|
export const normalizeComment = (input?: string): string | undefined => {
|
||||||
if(typeof input === 'undefined' || input === null) return undefined;
|
if(typeof input === 'undefined' || input === null) return undefined;
|
||||||
const trimmed = String(input).trim();
|
const trimmed = String(input).trim();
|
||||||
return trimmed.length ? trimmed : undefined;
|
return trimmed.length ? trimmed : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const normalize_type = (input: string): string => (input ?? '').trim().toUpperCase();
|
export const normalizeType = (input: string): string => (input ?? '').trim().toUpperCase();
|
||||||
|
|
||||||
export const normalize_expense = (expense: TimesheetExpense): TimesheetExpense => {
|
export const normalizeExpense = (expense: TimesheetExpense): TimesheetExpense => {
|
||||||
const comment = normalize_comment(expense.comment);
|
const comment = normalizeComment(expense.comment);
|
||||||
const amount = toNumOrUndefined(expense.amount);
|
const amount = toNumOrUndefined(expense.amount);
|
||||||
const mileage = toNumOrUndefined(expense.mileage);
|
const mileage = toNumOrUndefined(expense.mileage);
|
||||||
return {
|
return {
|
||||||
date: (expense.date ?? '').trim(),
|
date: (expense.date ?? '').trim(),
|
||||||
type: normalize_type(expense.type),
|
type: normalizeType(expense.type),
|
||||||
...(amount !== undefined ? { amount } : {}),
|
...(amount !== undefined ? { amount } : {}),
|
||||||
...(mileage !== undefined ? { mileage } : {}),
|
...(mileage !== undefined ? { mileage } : {}),
|
||||||
...(comment !== undefined ? { comment } : {}),
|
...(comment !== undefined ? { comment } : {}),
|
||||||
|
|
@ -60,8 +40,8 @@ export const normalize_expense = (expense: TimesheetExpense): TimesheetExpense =
|
||||||
};
|
};
|
||||||
|
|
||||||
//UI validation error messages
|
//UI validation error messages
|
||||||
export const validate_expense_UI = (raw: TimesheetExpense, label: string = 'expense'): void => {
|
export const validateExpenseUI = (raw: TimesheetExpense, label: string = 'expense'): void => {
|
||||||
const expense = normalize_expense(raw);
|
const expense = normalizeExpense(raw);
|
||||||
|
|
||||||
//Date input validation
|
//Date input validation
|
||||||
if(!DATE_FORMAT_PATTERN.test(expense.date)) {
|
if(!DATE_FORMAT_PATTERN.test(expense.date)) {
|
||||||
|
|
@ -139,7 +119,7 @@ export const validate_expense_UI = (raw: TimesheetExpense, label: string = 'expe
|
||||||
//totals per pay-period
|
//totals per pay-period
|
||||||
export const compute_expense_totals = (items: TimesheetExpense[]) => items.reduce(
|
export const compute_expense_totals = (items: TimesheetExpense[]) => items.reduce(
|
||||||
(acc, raw) => {
|
(acc, raw) => {
|
||||||
const expense = normalize_expense(raw);
|
const expense = normalizeExpense(raw);
|
||||||
if(typeof expense.amount === 'number' && expense.amount > 0) acc.amount += expense.amount;
|
if(typeof expense.amount === 'number' && expense.amount > 0) acc.amount += expense.amount;
|
||||||
if(typeof expense.mileage === 'number' && expense.mileage > 0) acc.mileage += expense.mileage;
|
if(typeof expense.mileage === 'number' && expense.mileage > 0) acc.mileage += expense.mileage;
|
||||||
return acc;
|
return acc;
|
||||||
19
src/modules/timesheets/utils/shift.util.ts
Normal file
19
src/modules/timesheets/utils/shift.util.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import type { ShiftKey, ShiftPayload, ShiftSelectOption } from "../types/shift.types";
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export const toShiftPayload = (shift: any): ShiftPayload => ({
|
||||||
|
start_time: String(shift.start_time),
|
||||||
|
end_time: String(shift.end_time),
|
||||||
|
type: String(shift.type).toUpperCase() as ShiftKey,
|
||||||
|
is_remote: !!shift.is_remote,
|
||||||
|
...(shift.comment ? { comment: String(shift.comment) } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const buildShiftOptions = (
|
||||||
|
keys: readonly string[],
|
||||||
|
t:(k: string) => string
|
||||||
|
): ShiftSelectOption[] =>
|
||||||
|
keys.map((key) => ({
|
||||||
|
value: key as any,
|
||||||
|
label: t(`timesheet.shift.types.${key}`),
|
||||||
|
}));
|
||||||
17
src/modules/timesheets/utils/timesheet-format.util.ts
Normal file
17
src/modules/timesheets/utils/timesheet-format.util.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import type { PayPeriodLabel } from "../types/ui.types";
|
||||||
|
|
||||||
|
export const formatPayPeriodLabel = (
|
||||||
|
raw_label: string | undefined,
|
||||||
|
locale: string,
|
||||||
|
extractDate: (_input: string, _mask: string) => Date,
|
||||||
|
opts: Intl.DateTimeFormatOptions
|
||||||
|
): PayPeriodLabel => {
|
||||||
|
const label = raw_label ?? '';
|
||||||
|
const dates = label.split('.');
|
||||||
|
if(dates.length < 2) return { start_date: '—', end_date:'—' };
|
||||||
|
|
||||||
|
const fmt = new Intl.DateTimeFormat(locale, opts);
|
||||||
|
const start = fmt.format(extractDate(dates[0]!, 'YYYY-MM-DD'));
|
||||||
|
const end = fmt.format(extractDate(dates[1]!, 'YYYY-MM-DD'));
|
||||||
|
return { start_date: start, end_date: end };
|
||||||
|
}
|
||||||
88
src/stores/expense-store.ts
Normal file
88
src/stores/expense-store.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { type PayPeriodExpenses } from "src/modules/timesheets/types/expense.interfaces";
|
||||||
|
import { ExpensesApiError } from "src/modules/timesheets/types/expense-validation.interface";
|
||||||
|
import { getPayPeriodExpenses, putPayPeriodExpenses } from "src/modules/timesheets/composables/api/use-expense-api";
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
export const useExpensesStore = defineStore('expenses', () => {
|
||||||
|
const is_dialog_open = ref(false);
|
||||||
|
const is_loading = ref(false);
|
||||||
|
const data = ref<PayPeriodExpenses | null>(null);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
const setErrorFrom = (err: unknown, t?: (_key: string) => string) => {
|
||||||
|
const e = err as any;
|
||||||
|
error.value = (err instanceof ExpensesApiError && t
|
||||||
|
? t(e.message): undefined)
|
||||||
|
|| e?.message
|
||||||
|
|| 'Unknown error';
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDialog = async (
|
||||||
|
params: { email: string; pay_year: number; pay_period_no: number; t?: (_key: string)=> string}) => {
|
||||||
|
is_dialog_open.value = true;
|
||||||
|
is_loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
try {
|
||||||
|
const response = await getPayPeriodExpenses(
|
||||||
|
params.email,
|
||||||
|
params.pay_year,
|
||||||
|
params.pay_period_no,
|
||||||
|
);
|
||||||
|
data.value = response;
|
||||||
|
} catch (err) {
|
||||||
|
setErrorFrom(err, params.t);
|
||||||
|
data.value = {
|
||||||
|
pay_period_no: params.pay_period_no,
|
||||||
|
pay_year: params.pay_year,
|
||||||
|
employee_email: params.email,
|
||||||
|
is_approved: false,
|
||||||
|
expenses: [],
|
||||||
|
totals: { amount: 0, mileage: 0},
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
is_loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveExpenses = async (payload: {
|
||||||
|
email: string;
|
||||||
|
pay_year: number;
|
||||||
|
pay_period_no: number;
|
||||||
|
expenses: any[]; t?: (_key: string) => string
|
||||||
|
}) => {
|
||||||
|
is_loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated = await putPayPeriodExpenses(
|
||||||
|
payload.email,
|
||||||
|
payload.pay_year,
|
||||||
|
payload.pay_period_no,
|
||||||
|
payload.expenses
|
||||||
|
);
|
||||||
|
data.value = updated;
|
||||||
|
is_dialog_open.value = false;
|
||||||
|
} catch (err) {
|
||||||
|
setErrorFrom(err, payload.t);
|
||||||
|
} finally {
|
||||||
|
is_loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
error.value = null;
|
||||||
|
is_dialog_open.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
is_dialog_open,
|
||||||
|
is_loading,
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
openDialog,
|
||||||
|
saveExpenses,
|
||||||
|
closeDialog,
|
||||||
|
};
|
||||||
|
});
|
||||||
50
src/stores/shift-store.ts
Normal file
50
src/stores/shift-store.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { toShiftPayload } from "src/modules/timesheets/utils/shift.util";
|
||||||
|
import type { FormMode } from "src/modules/timesheets/types/ui.types";
|
||||||
|
import type { ShiftPayload } from "src/modules/timesheets/types/shift.types";
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
export const useShiftStore = defineStore('shift', () => {
|
||||||
|
const is_open = ref(false);
|
||||||
|
const mode = ref<FormMode>('create');
|
||||||
|
const date_iso = ref<string>('');
|
||||||
|
const initial_shift = ref<ShiftPayload | null>(null);
|
||||||
|
|
||||||
|
const open = (nextMode: FormMode, date: string, payload: ShiftPayload | null) => {
|
||||||
|
mode.value = nextMode;
|
||||||
|
date_iso.value = date;
|
||||||
|
initial_shift.value = payload;
|
||||||
|
is_open.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreate = (date: string) => {
|
||||||
|
open('create', date, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEdit = (date: string, shift: any) => {
|
||||||
|
open('edit', date, toShiftPayload(shift as any));
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDelete = (date: string, shift: any) => {
|
||||||
|
open('delete', date, toShiftPayload(shift as any));
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
is_open.value = false;
|
||||||
|
mode.value = 'create';
|
||||||
|
date_iso.value = '';
|
||||||
|
initial_shift.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
is_open,
|
||||||
|
mode,
|
||||||
|
date_iso,
|
||||||
|
initial_shift,
|
||||||
|
openCreate,
|
||||||
|
openEdit,
|
||||||
|
openDelete,
|
||||||
|
close,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
|
import { date } from 'quasar';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/services-timesheet-approval';
|
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/services-timesheet-approval';
|
||||||
import { timesheetTempService } from 'src/modules/timesheets/services/timesheet-services';
|
import { timesheetTempService } from 'src/modules/timesheets/services/timesheet-services';
|
||||||
import { default_pay_period_employee_details, type PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface';
|
import { default_pay_period_employee_details, type PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface';
|
||||||
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
||||||
import type { PayPeriodOverviewEmployee } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-employee-interface";
|
import type { PayPeriodOverviewEmployee } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-employee-interface";
|
||||||
import type { PayPeriodReportFilters } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-report-interface';
|
import type { PayPeriodReportFilters } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-report-interface';
|
||||||
import type { Timesheet } from 'src/modules/timesheets/types/timesheet-interface';
|
import type { Timesheet } from 'src/modules/timesheets/types/timesheet.interfaces';
|
||||||
import type { CreateShiftPayload } from 'src/modules/timesheets/types/timesheet-shifts-payload-interface';
|
import type { CreateShiftPayload } from 'src/modules/timesheets/types/shift.interfaces';
|
||||||
|
|
||||||
const default_pay_period: PayPeriod = {
|
const default_pay_period: PayPeriod = {
|
||||||
pay_period_no: -1,
|
pay_period_no: -1,
|
||||||
|
|
@ -127,6 +128,33 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//employee timesheet
|
||||||
|
const loadByIsoDate = async (iso_date: string, employee_email: string) => {
|
||||||
|
const ok = await getPayPeriodByDate(iso_date);
|
||||||
|
if(ok) {
|
||||||
|
await getTimesheetsByPayPeriodAndEmail(employee_email);
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
};
|
||||||
|
|
||||||
|
//employee timesheet
|
||||||
|
const is_calendar_limit = computed(()=>
|
||||||
|
current_pay_period.value.pay_year === 2024 &&
|
||||||
|
current_pay_period.value.pay_period_no <= 1
|
||||||
|
);
|
||||||
|
|
||||||
|
//employee timesheet
|
||||||
|
const refreshCurrentPeriodForUser = async (employee_email: string) => {
|
||||||
|
await getTimesheetsByPayPeriodAndEmail(employee_email);
|
||||||
|
};
|
||||||
|
|
||||||
|
//employee timesheet
|
||||||
|
const loadToday = async (employee_email: string) => {
|
||||||
|
const today = date.formatDate(new Date(), 'YYYY-MM-DD');
|
||||||
|
return loadByIsoDate(today, employee_email);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const getTimesheetsByPayPeriodAndEmail = async (employee_email: string) => {
|
const getTimesheetsByPayPeriodAndEmail = async (employee_email: string) => {
|
||||||
is_loading.value = true;
|
is_loading.value = true;
|
||||||
|
|
||||||
|
|
@ -170,6 +198,10 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
pay_period_employee_details,
|
pay_period_employee_details,
|
||||||
current_timesheet,
|
current_timesheet,
|
||||||
is_loading,
|
is_loading,
|
||||||
|
is_calendar_limit,
|
||||||
|
loadByIsoDate,
|
||||||
|
refreshCurrentPeriodForUser,
|
||||||
|
loadToday,
|
||||||
getPayPeriodByDate,
|
getPayPeriodByDate,
|
||||||
getTimesheetByEmail,
|
getTimesheetByEmail,
|
||||||
createTimesheetShifts,
|
createTimesheetShifts,
|
||||||
|
|
|
||||||
10
src/utils/with-loading.util.ts
Normal file
10
src/utils/with-loading.util.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import type { Ref } from "vue";
|
||||||
|
|
||||||
|
export const withLoading = async <T>(loading_ref: Ref<boolean>, fn: ()=> Promise<T>): Promise<T> => {
|
||||||
|
loading_ref.value = true;
|
||||||
|
try{
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
loading_ref.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user