fix(auth): change auth signup to use authentik test account rather than seed account for supervisor
This commit is contained in:
parent
ebc3bde10c
commit
dc615340bc
|
|
@ -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: "auto",
|
dark: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
// iconSet: 'material-icons', // Quasar icon set
|
// iconSet: 'material-icons', // Quasar icon set
|
||||||
|
|
@ -127,15 +127,7 @@ export default defineConfig((ctx) => {
|
||||||
|
|
||||||
// animations: 'all', // --- includes all animations
|
// animations: 'all', // --- includes all animations
|
||||||
// https://v2.quasar.dev/options/animations
|
// https://v2.quasar.dev/options/animations
|
||||||
animations: [
|
animations: 'all',
|
||||||
'fadeIn',
|
|
||||||
'fadeOut',
|
|
||||||
'fadeInUp',
|
|
||||||
'zoomIn',
|
|
||||||
'zoomOut',
|
|
||||||
'flipInX',
|
|
||||||
'flipOutX',
|
|
||||||
],
|
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#sourcefiles
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#sourcefiles
|
||||||
// sourceFiles: {
|
// sourceFiles: {
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ export default {
|
||||||
cancel: "cancel",
|
cancel: "cancel",
|
||||||
update: "update",
|
update: "update",
|
||||||
modify: "modify",
|
modify: "modify",
|
||||||
|
close: "close",
|
||||||
},
|
},
|
||||||
misc: {
|
misc: {
|
||||||
or: "or",
|
or: "or",
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ export default {
|
||||||
cancel: "annuler",
|
cancel: "annuler",
|
||||||
update: "mettre à jour",
|
update: "mettre à jour",
|
||||||
modify: "modifier",
|
modify: "modifier",
|
||||||
|
close: "fermer",
|
||||||
},
|
},
|
||||||
misc: {
|
misc: {
|
||||||
or: "ou",
|
or: "ou",
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { RouterView } from 'vue-router';
|
import { RouterView } from 'vue-router';
|
||||||
import HeaderBar from 'src/modules/layouts/components/main-layout-header-bar.vue';
|
import HeaderBar from 'src/layouts/components/main-layout-header-bar.vue';
|
||||||
import FooterBar from 'src/modules/layouts/components/main-layout-footer-bar.vue';
|
import FooterBar from 'src/layouts/components/main-layout-footer-bar.vue';
|
||||||
import RightDrawer from 'src/modules/layouts/components/main-layout-right-drawer.vue';
|
import LeftDrawer from 'src/layouts/components/main-layout-left-drawer.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-layout view="hHh lpR fFf">
|
<q-layout view="hHh lpR fFf">
|
||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
<RightDrawer />
|
<LeftDrawer />
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
<router-view class="q-pa-sm bg-secondary" />
|
<router-view class="q-pa-sm bg-secondary" />
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { User } from "src/modules/shared/types/user-interface";
|
import type { User } from "src/modules/shared/models/user.models";
|
||||||
|
|
||||||
export interface AuthState {
|
export interface AuthState {
|
||||||
token: string;
|
token: string;
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="row q-mb-lg q-mt-lg" >
|
<div class="row" >
|
||||||
<!-- navigation to previous week -->
|
<!-- navigation to previous week -->
|
||||||
<q-btn
|
<q-btn
|
||||||
push rounded
|
push rounded
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="is_showing_calendar_picker = true"
|
@click="is_showing_calendar_picker = true"
|
||||||
:disable="timesheet_store.is_loading"
|
:disable="timesheet_store.is_loading"
|
||||||
class="q-px-lg"
|
class="q-px-xl"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
anchor="top middle"
|
anchor="top middle"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
<!-- Filters toggle -->
|
<!-- Filters toggle -->
|
||||||
<q-btn-dropdown
|
<q-btn-dropdown
|
||||||
push
|
push
|
||||||
class="q-mr-md bg-white text-primary left-rounded"
|
rounded
|
||||||
|
class="q-mr-md bg-white text-primary"
|
||||||
:label="$t('shared.label.filter')"
|
:label="$t('shared.label.filter')"
|
||||||
icon="filter_alt"
|
icon="filter_alt"
|
||||||
/>
|
/>
|
||||||
|
|
@ -16,6 +17,7 @@
|
||||||
v-model="search_model"
|
v-model="search_model"
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
|
rounded
|
||||||
debounce="300"
|
debounce="300"
|
||||||
class="right-rounded"
|
class="right-rounded"
|
||||||
:label="$t('shared.label.search')"
|
:label="$t('shared.label.search')"
|
||||||
|
|
@ -23,21 +25,11 @@
|
||||||
bg-color="white"
|
bg-color="white"
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
<template #before>
|
<template #prepend>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="search"
|
name="search"
|
||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.left-rounded {
|
|
||||||
border-radius: 50% 0 0 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-rounded {
|
|
||||||
border-radius: 0 50% 50% 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { Bar } from 'vue-chartjs';
|
import { Bar } from 'vue-chartjs';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
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 { Expense } from 'src/modules/timesheets/models/expense.models';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
|
@ -15,70 +17,55 @@
|
||||||
ChartJS.defaults.maintainAspectRatio = false;
|
ChartJS.defaults.maintainAspectRatio = false;
|
||||||
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
|
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
|
||||||
|
|
||||||
defineProps<{
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
||||||
}>();
|
|
||||||
|
|
||||||
// const expenses_dataset = ref<ChartDataset<'bar'>[]>([]);
|
const expenses_dataset = ref<ChartDataset<'bar'>[]>([]);
|
||||||
// const expenses_labels = ref<string[]>([]);
|
const expenses_labels = ref<string[]>([]);
|
||||||
|
|
||||||
// const getExpensesData = (): ChartData<'bar'> => {
|
const getExpensesData = (): ChartData<'bar'> => {
|
||||||
// if (timesheetDetails) {
|
const all_days = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.expenses));
|
||||||
// const all_weeks = [timesheetDetails.week1, timesheetDetails.week2];
|
const all_days_dates = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.shifts))
|
||||||
// const all_days = all_weeks.flatMap( week => Object.values(week.expenses));
|
|
||||||
// const all_days_dates = all_weeks.flatMap( week => Object.values(week.shifts))
|
|
||||||
|
|
||||||
// const all_costs = all_days.map( day => getTotalAmounts(day.cash));
|
const all_costs = all_days.map(day => day.total_expenses);
|
||||||
// const all_mileage = all_days.map( day => getTotalAmounts(day.km));
|
const all_mileage = all_days.map(day => day.total_mileage);
|
||||||
|
|
||||||
|
|
||||||
// expenses_dataset.value = [
|
|
||||||
// {
|
|
||||||
// label: t('timesheet_approvals.table.expenses'),
|
|
||||||
// data: all_costs,
|
|
||||||
// backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(),
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// label: t('timesheet_approvals.table.mileage'),
|
|
||||||
// data: all_mileage,
|
|
||||||
// backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(),
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
|
|
||||||
// expenses_labels.value = all_days_dates.map( day => day.short_date);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return {
|
expenses_dataset.value = [
|
||||||
// datasets: expenses_dataset.value,
|
{
|
||||||
// labels: expenses_labels.value
|
label: t('timesheet_approvals.table.expenses'),
|
||||||
// };
|
data: all_costs,
|
||||||
// };
|
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('timesheet_approvals.table.mileage'),
|
||||||
|
data: all_mileage,
|
||||||
|
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
const getTotalAmounts = (expenses: Expense[]): number => {
|
expenses_labels.value = all_days_dates.map(day => day.short_date);
|
||||||
let total_amount = 0;
|
|
||||||
|
|
||||||
for (const expense of expenses) {
|
return {
|
||||||
total_amount += expense.amount;
|
datasets: expenses_dataset.value,
|
||||||
}
|
labels: expenses_labels.value
|
||||||
|
};
|
||||||
return total_amount;
|
};
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Bar
|
<Bar
|
||||||
:data="getExpensesData()"
|
:data="getExpensesData()"
|
||||||
:options="({
|
:options="({
|
||||||
indexAxis: $q.screen.lt.md? 'y' : 'x',
|
indexAxis: $q.screen.lt.md ? 'y' : 'x',
|
||||||
plugins: {
|
plugins: {
|
||||||
|
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: t('timesheet_approvals.chart.expenses_title'),
|
text: t('timesheet_approvals.chart.expenses_title'),
|
||||||
},
|
},
|
||||||
legend:{
|
legend: {
|
||||||
labels:{
|
labels: {
|
||||||
boxWidth: 15,
|
boxWidth: 15,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
import { Bar } from 'vue-chartjs';
|
import { Bar } from 'vue-chartjs';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, type ChartData, type ChartOptions, type Plugin, type ChartDataset } from 'chart.js';
|
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, type ChartData, type ChartDataset } from 'chart.js';
|
||||||
import type { TimesheetDetails } from 'src/modules/timesheets/models/pay-period-details.models';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
|
@ -15,19 +15,14 @@
|
||||||
ChartJS.defaults.maintainAspectRatio = false;
|
ChartJS.defaults.maintainAspectRatio = false;
|
||||||
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
|
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
|
||||||
|
|
||||||
const props = defineProps<{
|
const { pay_period_details } = useTimesheetStore();
|
||||||
rawData: TimesheetDetails | undefined;
|
|
||||||
options?: ChartOptions<"bar"> | undefined;
|
|
||||||
plugins?: Plugin<"bar">[] | undefined;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const hours_worked_labels = ref<string[]>([]);
|
const hours_worked_labels = ref<string[]>([]);
|
||||||
const hours_worked_dataset = ref<ChartDataset<'bar'>[]>([]);
|
const hours_worked_dataset = ref<ChartDataset<'bar'>[]>([]);
|
||||||
|
|
||||||
const getHoursWorkedData = (): ChartData<'bar'> => {
|
const getHoursWorkedData = (): ChartData<'bar'> => {
|
||||||
if (props.rawData) {
|
|
||||||
const all_weeks = [props.rawData.week1, props.rawData.week2];
|
const all_days = pay_period_details.weeks.flatMap( week => Object.values(week.shifts));
|
||||||
const all_days = all_weeks.flatMap( week => Object.values(week.shifts));
|
|
||||||
const datasetConfig = [
|
const datasetConfig = [
|
||||||
{
|
{
|
||||||
key: 'regular_hours',
|
key: 'regular_hours',
|
||||||
|
|
@ -58,7 +53,7 @@
|
||||||
}));
|
}));
|
||||||
|
|
||||||
hours_worked_labels.value = all_days.map(day => day.short_date);
|
hours_worked_labels.value = all_days.map(day => day.short_date);
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
labels: hours_worked_labels.value,
|
labels: hours_worked_labels.value,
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,49 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
/* eslint-disable */
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { colors } from 'quasar';
|
import { colors } from 'quasar';
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { Doughnut } from 'vue-chartjs';
|
import { Doughnut } from 'vue-chartjs';
|
||||||
import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale, type ChartDataset } from 'chart.js';
|
import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale, type ChartDataset } from 'chart.js';
|
||||||
import type { PayPeriodEmployeeOverview } from 'src/modules/timesheet-approval/types/pay-period-employee-overview';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
|
||||||
ChartJS.register(Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale);
|
ChartJS.register(Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale);
|
||||||
ChartJS.defaults.font.family = '"Roboto", sans-serif';
|
ChartJS.defaults.font.family = '"Roboto", sans-serif';
|
||||||
ChartJS.defaults.maintainAspectRatio = false;
|
ChartJS.defaults.maintainAspectRatio = false;
|
||||||
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
|
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
|
||||||
|
|
||||||
const props = defineProps<{
|
const { current_pay_period_overview } = useTimesheetStore();
|
||||||
rawData: PayPeriodEmployeeOverview | undefined;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const shift_type_labels = ref<string[]>([]);
|
const shift_type_labels = ref<string[]>([]);
|
||||||
const shift_type_totals = ref<ChartDataset<'doughnut'>[]>([{ data: [40, 0, 2, 5], }]);
|
const shift_type_totals = ref<ChartDataset<'doughnut'>[]>([{ data: [40, 0, 2, 5], }]);
|
||||||
|
|
||||||
if (props.rawData){
|
|
||||||
shift_type_totals.value = [{
|
|
||||||
data: [
|
|
||||||
props.rawData.regular_hours,
|
|
||||||
props.rawData.evening_hours,
|
|
||||||
props.rawData.emergency_hours,
|
|
||||||
props.rawData.overtime_hours,
|
|
||||||
],
|
|
||||||
backgroundColor: [
|
|
||||||
colors.getPaletteColor('green-5'), // Regular
|
|
||||||
colors.getPaletteColor('green-9'), // Evening
|
|
||||||
getComputedStyle(document.body).getPropertyValue('--q-warning').trim(), // Emergency
|
|
||||||
getComputedStyle(document.body).getPropertyValue('--q-negative').trim(), // Overtime
|
|
||||||
]
|
|
||||||
}];
|
|
||||||
|
|
||||||
shift_type_labels.value = [
|
shift_type_totals.value = [{
|
||||||
props.rawData.regular_hours.toString() + 'h',
|
data: [
|
||||||
props.rawData.evening_hours.toString() + 'h',
|
current_pay_period_overview.regular_hours,
|
||||||
props.rawData.emergency_hours.toString() + 'h',
|
current_pay_period_overview.evening_hours,
|
||||||
props.rawData.overtime_hours.toString() + 'h',
|
current_pay_period_overview.emergency_hours,
|
||||||
|
current_pay_period_overview.overtime_hours,
|
||||||
|
],
|
||||||
|
backgroundColor: [
|
||||||
|
colors.getPaletteColor('green-5'), // Regular
|
||||||
|
colors.getPaletteColor('green-9'), // Evening
|
||||||
|
getComputedStyle(document.body).getPropertyValue('--q-warning').trim(), // Emergency
|
||||||
|
getComputedStyle(document.body).getPropertyValue('--q-negative').trim(), // Overtime
|
||||||
]
|
]
|
||||||
}
|
}];
|
||||||
|
|
||||||
|
shift_type_labels.value = [
|
||||||
|
current_pay_period_overview.regular_hours.toString() + 'h',
|
||||||
|
current_pay_period_overview.evening_hours.toString() + 'h',
|
||||||
|
current_pay_period_overview.emergency_hours.toString() + 'h',
|
||||||
|
current_pay_period_overview.overtime_hours.toString() + 'h',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
labels: shift_type_labels.value,
|
labels: shift_type_labels.value,
|
||||||
|
|
@ -59,9 +57,9 @@
|
||||||
<Doughnut
|
<Doughnut
|
||||||
:data="data"
|
:data="data"
|
||||||
:options="({
|
:options="({
|
||||||
plugins:{
|
plugins: {
|
||||||
legend:{
|
legend: {
|
||||||
labels:{
|
labels: {
|
||||||
boxWidth: 15,
|
boxWidth: 15,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,73 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
// import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { shift_type_legend } from 'src/modules/timesheet-approval/models/detailed-dialog-shift-color.model';
|
import DetailedDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-hours-worked.vue';
|
||||||
import DetailedDialogChartHoursWorked from 'src/modules/timesheet-approval/components/graphs/detailed-chart-hours-worked.vue';
|
import DetailedDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-shift-types.vue';
|
||||||
import DetailedDialogChartShiftTypes from 'src/modules/timesheet-approval/components/graphs/detailed-chart-shift-types.vue';
|
import DetailedDialogChartExpenses from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-expenses.vue';
|
||||||
import DetailedDialogChartExpenses from 'src/modules/timesheet-approval/components/graphs/detailed-chart-expenses.vue';
|
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||||
import type { PayPeriodOverview } from 'src/modules/timesheet-approval/models/pay-period-overview.models';
|
|
||||||
import type { PayPeriodDetails } from 'src/modules/timesheets/models/pay-period-details.models';
|
|
||||||
|
|
||||||
const dialog_model = defineModel<boolean>('dialog', { default: false });
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
isLoading: boolean;
|
employeeEmail: string;
|
||||||
payPeriodOverview: PayPeriodOverview;
|
|
||||||
payPeriodDetails: PayPeriodDetails;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const dialog_model = defineModel<boolean>('dialog', { default: false });
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
||||||
// const timesheet_store = useTimesheetStore();
|
// const timesheet_store = useTimesheetStore();
|
||||||
const is_showing_graph = ref(true);
|
const is_showing_graph = ref(true);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="dialog_model"
|
v-model="dialog_model"
|
||||||
full-width
|
full-width
|
||||||
transition-show="jump-down"
|
transition-show="jump-down"
|
||||||
transition-hide="jump-down"
|
transition-hide="jump-down"
|
||||||
>
|
>
|
||||||
<q-card
|
<!-- loader -->
|
||||||
|
<q-card
|
||||||
|
v-if="timesheet_store.is_loading"
|
||||||
|
class="column flex-center text-center"
|
||||||
|
>
|
||||||
|
<q-spinner
|
||||||
|
color="primary"
|
||||||
|
size="5em"
|
||||||
|
:thickness="10"
|
||||||
|
class="col-auto"
|
||||||
|
/>
|
||||||
|
<div class="col-auto text-primary text-h6 text-weight-bold text-center ">
|
||||||
|
{{ $t('shared.loading') }}
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-card
|
||||||
|
v-else
|
||||||
class="q-pa-sm shadow-12 rounded-15 column no-wrap relative"
|
class="q-pa-sm shadow-12 rounded-15 column no-wrap relative"
|
||||||
:style="$q.screen.lt.md ? '' : 'width: 60vw !important;'"
|
:style="$q.screen.lt.md ? '' : 'width: 60vw !important;'"
|
||||||
>
|
>
|
||||||
<!-- loader -->
|
|
||||||
<q-card-section
|
|
||||||
v-if="isLoading"
|
|
||||||
class="column flex-center text-center"
|
|
||||||
>
|
|
||||||
<q-spinner
|
|
||||||
color="primary"
|
|
||||||
size="5em"
|
|
||||||
:thickness="10"
|
|
||||||
class="col-auto"
|
|
||||||
/>
|
|
||||||
<div class="col-auto text-primary text-h6 text-weight-bold text-center ">
|
|
||||||
{{ $t('shared.loading') }}
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- employee name -->
|
<!-- employee name -->
|
||||||
<q-card-section
|
<q-card-section
|
||||||
v-if="!isLoading"
|
|
||||||
class="text-h5 text-weight-bolder text-center text-primary q-pa-none text-uppercase col-auto"
|
class="text-h5 text-weight-bolder text-center text-primary q-pa-none text-uppercase col-auto"
|
||||||
>
|
>
|
||||||
<span> {{ payPeriodDetails.employee_full_name }} </span>
|
<span>{{ timesheet_store.pay_period_details.employee_full_name }}</span>
|
||||||
|
|
||||||
<q-separator
|
<q-separator
|
||||||
spaced
|
spaced
|
||||||
size="2px"
|
size="2px"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-card-actions
|
<q-card-actions
|
||||||
align="center"
|
align="center"
|
||||||
class="q-pa-none"
|
class="q-pa-none"
|
||||||
>
|
>
|
||||||
<q-btn-toggle
|
<q-btn-toggle
|
||||||
v-model="is_showing_graph"
|
v-model="is_showing_graph"
|
||||||
color="white"
|
color="white"
|
||||||
text-color="primary"
|
text-color="primary"
|
||||||
toggle-color="primary"
|
toggle-color="primary"
|
||||||
:options="[
|
:options="[
|
||||||
{ icon: 'bar_chart', value: true },
|
{ icon: 'bar_chart', value: true },
|
||||||
|
|
@ -77,67 +78,48 @@
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<!-- employee timesheet for supervisor editting -->
|
<!-- employee timesheet for supervisor editting -->
|
||||||
<q-card-section
|
<q-card-section
|
||||||
v-if="!is_showing_graph"
|
v-if="!is_showing_graph"
|
||||||
class="q-pa-none"
|
class="q-pa-none"
|
||||||
>
|
>
|
||||||
<!-- shift type color legend -->
|
<!-- list of shifts -->
|
||||||
<q-card-section class="q-py-xs q-px-none text-center q-my-s">
|
<q-card-section
|
||||||
<q-badge
|
:horizontal="$q.screen.gt.sm"
|
||||||
v-for="shift_type, index in shift_type_legend"
|
class="q-pa-none rounded-10"
|
||||||
:key="index"
|
|
||||||
:color="shift_type.background_color"
|
|
||||||
:label="$t(shift_type.type_label)"
|
|
||||||
:text-color="shift_type.font_color"
|
|
||||||
class="q-px-md q-py-xs q-mx-xs q-my-none text-uppercase text-weight-bolder justify-center"
|
|
||||||
style="width: 120px; font-size: 0.8em;"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- list of shifts, broken down into weekly columns -->
|
|
||||||
<q-card-section
|
|
||||||
:horizontal="$q.screen.gt.sm"
|
|
||||||
class="q-pa-none bg-secondary rounded-10"
|
|
||||||
>
|
>
|
||||||
<!-- IMPORTANT: Timesheet shift list goes here!!! -->
|
<TimesheetWrapper
|
||||||
|
dense
|
||||||
|
:employee-email="employeeEmail"
|
||||||
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<!-- employee timesheet details with chart -->
|
<!-- employee timesheet details with chart -->
|
||||||
<q-card-section
|
<q-card-section
|
||||||
v-if="is_showing_graph"
|
v-if="is_showing_graph"
|
||||||
class="q-pa-md col column full-width no-wrap"
|
class="q-pa-md col column full-width no-wrap"
|
||||||
>
|
>
|
||||||
<q-card-section
|
<q-card-section
|
||||||
:horizontal="!$q.screen.lt.md"
|
:horizontal="!$q.screen.lt.md"
|
||||||
class="q-pa-none col no-wrap"
|
class="q-pa-none col no-wrap"
|
||||||
style="min-height: 300px;"
|
style="min-height: 300px;"
|
||||||
>
|
>
|
||||||
<DetailedDialogChartHoursWorked
|
<DetailedDialogChartHoursWorked class="col-7" />
|
||||||
:raw-data="payPeriodDetails"
|
|
||||||
class="col-7"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-separator
|
<q-separator
|
||||||
spaced
|
spaced
|
||||||
:vertical="!$q.screen.lt.md"
|
:vertical="!$q.screen.lt.md"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="column col justify-center no-wrap q-pa-none">
|
<div class="column col justify-center no-wrap q-pa-none">
|
||||||
<DetailedDialogChartShiftTypes
|
<DetailedDialogChartShiftTypes class="col-5" />
|
||||||
:raw-data="payPeriodOverview"
|
|
||||||
class="col-5"
|
<q-separator
|
||||||
|
spaced
|
||||||
|
:vertical="!$q.screen.lt.md"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-separator
|
<DetailedDialogChartExpenses class="col" />
|
||||||
spaced
|
|
||||||
:vertical="!$q.screen.lt.md"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DetailedDialogChartExpenses
|
|
||||||
:raw-data="payPeriodDetails"
|
|
||||||
class="col"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PayPeriodEmployeeOverview } from 'src/modules/timesheet-approval/types/pay-period-employee-overview';
|
import type { PayPeriodOverview } from 'src/modules/timesheet-approval/models/pay-period-overview.models';
|
||||||
|
|
||||||
const modelApproval = defineModel<boolean>();
|
const modelApproval = defineModel<boolean>();
|
||||||
const { row } = defineProps<{ row: PayPeriodEmployeeOverview; }>();
|
const { row } = defineProps<{ row: PayPeriodOverview; }>();
|
||||||
const emit = defineEmits<{ clickDetails: []; }>();
|
const emit = defineEmits<{
|
||||||
|
'clickDetails': [overview: PayPeriodOverview];
|
||||||
|
}>();
|
||||||
|
|
||||||
const stack_label_class = "text-weight-bold text-primary text-uppercase text-caption q-pa-none q-my-none ellipsis";
|
const stack_label_class = "text-weight-bold text-primary text-uppercase text-caption q-pa-none q-my-none ellipsis";
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -25,7 +27,7 @@
|
||||||
class="col-auto q-pa-none q-ma-none"
|
class="col-auto q-pa-none q-ma-none"
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="work_history"
|
icon="work_history"
|
||||||
@click="emit('clickDetails')"
|
@click="emit('clickDetails', row)"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
anchor="top middle"
|
anchor="top middle"
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,89 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
import { ref } from 'vue';
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import OverviewListItem from 'src/modules/timesheet-approval/components/employee-overview/overview-list-item.vue';
|
import OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue';
|
||||||
import QTableFilters from 'src/modules/shared/components/utils/q-table-filters.vue';
|
import QTableFilters from 'src/modules/shared/components/q-table-filters.vue';
|
||||||
import { pay_period_employee_overview_columns, type PayPeriodEmployeeOverview } from 'src/modules/timesheet-approval/types/pay-period-employee-overview';
|
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
||||||
|
import { pay_period_overview_columns, type PayPeriodOverview } from 'src/modules/timesheet-approval/models/pay-period-overview.models';
|
||||||
|
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
||||||
// const FORWARD = 1
|
|
||||||
// const BACKWARD = -1
|
|
||||||
const filter = ref<string | number | null>('');
|
const filter = ref<string | number | null>('');
|
||||||
|
|
||||||
const onClickedDetails = async ( employee_email: string ) => {
|
const employeeEmail = defineModel();
|
||||||
await timesheet_store.getPayPeriodEmployeeDetailsByEmployeeEmail(employee_email);
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'clickedDetailsButton': [email: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const overview_rows = computed(() => timesheet_store.pay_period_overviews[0]?.regular_hours === -1 ?
|
||||||
|
[] :
|
||||||
|
timesheet_store.pay_period_overviews
|
||||||
|
)
|
||||||
|
|
||||||
|
const onClickedDetails = async (employee_email: string, row: PayPeriodOverview) => {
|
||||||
|
employeeEmail.value = employee_email;
|
||||||
|
emit('clickedDetailsButton', employee_email);
|
||||||
|
timesheet_store.current_pay_period_overview = row;
|
||||||
|
await timesheet_store.getPayPeriodDetailsByEmployeeEmail(employee_email);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="q-pa-md">
|
<div class="q-pa-md">
|
||||||
<q-table
|
<q-table
|
||||||
:rows="timesheet_store.pay_period_employee_overview_list"
|
:rows="overview_rows"
|
||||||
:columns="pay_period_employee_overview_columns"
|
:columns="pay_period_overview_columns"
|
||||||
row-key="email"
|
row-key="email"
|
||||||
:filter="filter"
|
:filter="filter"
|
||||||
grid
|
grid
|
||||||
dense
|
dense
|
||||||
hide-pagination
|
hide-pagination
|
||||||
color="primary"
|
color="primary"
|
||||||
:rows-per-page-options="[0]"
|
:rows-per-page-options="[0]"
|
||||||
card-container-class="justify-center"
|
card-container-class="justify-center"
|
||||||
:loading="timesheet_store.is_loading"
|
:loading="timesheet_store.is_loading"
|
||||||
:no-data-label="$t('shared.error.no_data_found')"
|
:no-data-label="$t('shared.error.no_data_found')"
|
||||||
:no-results-label="$t('shared.error.no_search_results')"
|
:no-results-label="$t('shared.error.no_search_results')"
|
||||||
:loading-label="$t('shared.label.loading')"
|
:loading-label="$t('shared.label.loading')"
|
||||||
>
|
>
|
||||||
<template #top>
|
<template #top>
|
||||||
<div class="full-width" :class="$q.screen.lt.md ? 'text-center q-gutter-sm' : 'row'">
|
<div
|
||||||
<!-- Calendar Picker goes here -->
|
class="full-width"
|
||||||
|
:class="$q.screen.lt.md ? 'text-center' : 'row'"
|
||||||
|
>
|
||||||
|
<PayPeriodNavigator />
|
||||||
|
|
||||||
<q-space />
|
<q-space />
|
||||||
|
|
||||||
<!-- Grid-or-List toggle goes here -->
|
<!-- Grid-or-List toggle goes here -->
|
||||||
|
|
||||||
<QTableFilters v-model="filter"/>
|
<QTableFilters v-model="filter" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Template for individual employee cards -->
|
<!-- Template for individual employee cards -->
|
||||||
<template #item="props: { row: PayPeriodEmployeeOverview, key: string }">
|
<template #item="props: { row: PayPeriodOverview, key: string }">
|
||||||
<OverviewListItem
|
<OverviewListItem
|
||||||
v-model="props.row.is_approved"
|
v-model="props.row.is_approved"
|
||||||
:row="props.row"
|
:row="props.row"
|
||||||
@click-details="onClickedDetails(props.row.email)"
|
@click-details="overview => onClickedDetails(props.row.email, overview)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Template for custome failed-to-load state -->
|
<!-- Template for custome failed-to-load state -->
|
||||||
<template #no-data="{ message, filter }">
|
<template #no-data="{ message, filter }">
|
||||||
<div class="full-width column items-center text-primary q-gutter-sm">
|
<div class="full-width column items-center text-primary q-gutter-sm">
|
||||||
<span class="text-h6 q-mt-xl">
|
<q-icon
|
||||||
|
size="4em"
|
||||||
|
:name="filter ? 'filter_alt_off' : 'error_outline'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="text-h6">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</span>
|
</span>
|
||||||
<q-icon
|
|
||||||
size="4em"
|
|
||||||
:name="filter ? 'filter_alt_off' : 'error_outline'"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</q-table>
|
</q-table>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { default_timesheet_approval_cvs_report_filters, type TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { default_pay_period_report_filters, type PayPeriodReportFilters } from 'src/modules/timesheet-approval/types/pay-period-report-options';
|
|
||||||
|
const report_filter_options = ref<TimesheetApprovalCSVReportFilters>(default_timesheet_approval_cvs_report_filters);
|
||||||
const report_filter_options = ref<PayPeriodReportFilters>(default_pay_period_report_filters);
|
|
||||||
|
|
||||||
const company_options = [
|
const company_options = [
|
||||||
{ label: 'Targo', value: report_filter_options.value.companies.targo },
|
{ label: 'Targo', value: report_filter_options.value.companies.targo },
|
||||||
{ label: 'Solucom', value: report_filter_options.value.companies.solucom },
|
{ label: 'Solucom', value: report_filter_options.value.companies.solucom },
|
||||||
|
|
@ -16,15 +19,18 @@
|
||||||
{ label: 'shared.shift_type.vacation', value: report_filter_options.value.types.vacation },
|
{ label: 'shared.shift_type.vacation', value: report_filter_options.value.types.vacation },
|
||||||
];
|
];
|
||||||
|
|
||||||
const is_download_button_disabled = computed(() => {
|
const is_download_button_disabled = computed(() => {
|
||||||
return company_options.map( option => option.value ).filter( value => value === true ).length > 0 ||
|
return company_options.map(option => option.value).filter(value => value === true).length > 0 ||
|
||||||
type_options.map( option => option.value ).filter( value => value === true ).length > 0;
|
type_options.map(option => option.value).filter(value => value === true).length > 0;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-btn-group rounded push>
|
<q-btn-group
|
||||||
<q-btn
|
rounded
|
||||||
|
push
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
rounded
|
rounded
|
||||||
push
|
push
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
@ -42,14 +48,17 @@
|
||||||
<q-list class="row">
|
<q-list class="row">
|
||||||
<q-item class="col">
|
<q-item class="col">
|
||||||
<q-item-label class="text-weight-bolder text-primary q-ma-none q-pa-none text-uppercase">
|
<q-item-label class="text-weight-bolder text-primary q-ma-none q-pa-none text-uppercase">
|
||||||
{{$t('timesheet_approvals.print_report.company')}}
|
{{ $t('timesheet_approvals.print_report.company') }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
|
|
||||||
<q-item-section row no-wrap>
|
<q-item-section
|
||||||
<q-checkbox
|
row
|
||||||
|
no-wrap
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
v-for="option, index in company_options"
|
v-for="option, index in company_options"
|
||||||
:key="index"
|
:key="index"
|
||||||
v-model="option.value"
|
v-model="option.value"
|
||||||
:val="option.label"
|
:val="option.label"
|
||||||
:label="option.label"
|
:label="option.label"
|
||||||
/>
|
/>
|
||||||
|
|
@ -59,13 +68,17 @@
|
||||||
<q-separator
|
<q-separator
|
||||||
spaced
|
spaced
|
||||||
vertical
|
vertical
|
||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-item class="col">
|
<q-item class="col">
|
||||||
<q-item-section row no-wrap>
|
<q-item-section
|
||||||
<p class="text-weight-bolder text-primary q-ma-none q-pa-none text-uppercase">{{$t('timesheet_approvals.print_report.type')}}</p>
|
row
|
||||||
<q-checkbox
|
no-wrap
|
||||||
|
>
|
||||||
|
<p class="text-weight-bolder text-primary q-ma-none q-pa-none text-uppercase">
|
||||||
|
{{ $t('timesheet_approvals.print_report.type') }}</p>
|
||||||
|
<q-checkbox
|
||||||
v-for="option, index in type_options"
|
v-for="option, index in type_options"
|
||||||
:key="index"
|
:key="index"
|
||||||
v-model="option.value"
|
v-model="option.value"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,16 @@ export interface PayPeriodOverview {
|
||||||
is_approved: boolean;
|
is_approved: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface PayPeriodOverviewResponse {
|
||||||
|
pay_period_no: number;
|
||||||
|
pay_year: number;
|
||||||
|
period_start: string;
|
||||||
|
period_end: string;
|
||||||
|
payday: string;
|
||||||
|
label: string;
|
||||||
|
employees_overview: PayPeriodOverview[];
|
||||||
|
}
|
||||||
|
|
||||||
export const default_pay_period_overview: PayPeriodOverview = {
|
export const default_pay_period_overview: PayPeriodOverview = {
|
||||||
email: '',
|
email: '',
|
||||||
employee_name: '',
|
employee_name: '',
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export interface TimesheetApprovalCSVReportFilters {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const default_pay_period_report_filters: TimesheetApprovalCSVReportFilters = {
|
export const default_timesheet_approval_cvs_report_filters: TimesheetApprovalCSVReportFilters = {
|
||||||
types: {
|
types: {
|
||||||
shifts: true,
|
shifts: true,
|
||||||
expenses: true,
|
expenses: true,
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted } from 'vue';
|
|
||||||
import { date } from 'quasar';
|
|
||||||
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
|
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
|
||||||
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
|
||||||
import EmployeeOverviewList from 'src/modules/timesheet-approval/components/employee-overview/overview-list.vue';
|
|
||||||
import DetailedDialog from 'src/modules/timesheet-approval/components/detailed-dialog.vue';
|
|
||||||
|
|
||||||
const timesheet_approval_api = useTimesheetApprovalApi();
|
|
||||||
const timesheet_store = useTimesheetStore();
|
|
||||||
|
|
||||||
onMounted( async () => {
|
|
||||||
await timesheet_approval_api.getPayPeriodOverviewByDate(date.formatDate( new Date(), 'YYYY-MM-DD'));
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-page
|
|
||||||
padding
|
|
||||||
class="q-pa-md bg-secondary "
|
|
||||||
>
|
|
||||||
<PageHeaderTemplate
|
|
||||||
title="timesheet_approvals.page_title"
|
|
||||||
:start-date="timesheet_store.pay_period.period_start"
|
|
||||||
:end-date="timesheet_store.pay_period.period_end"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DetailedDialog
|
|
||||||
:is-loading="timesheet_store.is_loading"
|
|
||||||
:employee-overview="timesheet_store.pay_period_employee_overview"
|
|
||||||
:timesheet-details="timesheet_store.pay_period_employee_details"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EmployeeOverviewList />
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { api } from "src/boot/axios";
|
import { api } from "src/boot/axios";
|
||||||
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
|
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
|
||||||
import type { PayPeriodOverview } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
import type { PayPeriodOverviewResponse } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
||||||
|
|
||||||
export const timesheetApprovalService = {
|
export const timesheetApprovalService = {
|
||||||
getPayPeriodOverviewsBySupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverview[]> => {
|
getPayPeriodOverviewsBySupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverviewResponse> => {
|
||||||
const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`);
|
const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,50 +2,53 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { ref } from 'vue';
|
import { inject, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import type { ExpenseType, Expense } from 'src/modules/timesheets/models/expense.models';
|
import { default_expense, EXPENSE_TYPE, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
import { makeExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||||
|
import { useExpensesApi } from 'src/modules/timesheets/composables/api/use-expense-api';
|
||||||
|
|
||||||
const expense_store = useExpensesStore();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const expenses_store = useExpensesStore();
|
||||||
|
const expenses_api = useExpensesApi();
|
||||||
const files = defineModel<File[] | null>('files');
|
const files = defineModel<File[] | null>('files');
|
||||||
const is_navigator_open = ref(false);
|
const is_navigator_open = ref(false);
|
||||||
|
|
||||||
//------------------ props ------------------
|
const COMMENT_MAX_LENGTH = 280;
|
||||||
defineProps<{
|
const employee_email = inject<string>('employeeEmail');
|
||||||
type_options: { label: string; value: ExpenseType }[];
|
const rules = makeExpenseRules(t);
|
||||||
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 cancelUpdateMode = () => {
|
||||||
const emit = defineEmits<{
|
expenses_store.current_expense = default_expense;
|
||||||
'submit': [void];
|
expenses_store.initial_expense = default_expense;
|
||||||
}>();
|
expenses_store.mode = 'create';
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestExpenseCreationOrUpdate = async () => {
|
||||||
|
if (expenses_store.mode === 'create') await expenses_api.createExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense.date);
|
||||||
|
else await expenses_api.updateExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense.date);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-form
|
<q-form
|
||||||
flat
|
flat
|
||||||
v-if="!is_readonly"
|
v-if="!expenses_store.pay_period_expenses.is_approved"
|
||||||
@submit.prevent="$emit('submit')"
|
@submit.prevent="requestExpenseCreationOrUpdate"
|
||||||
>
|
>
|
||||||
<div class="text-subtitle2 q-py-sm">
|
<div class="text-subtitle2 q-py-sm">
|
||||||
{{ $t('timesheet.expense.add_expense') }}
|
{{ $t('timesheet.expense.add_expense') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-between">
|
<div
|
||||||
|
class="row justify-between rounded-5"
|
||||||
|
:class="expenses_store.mode === 'update' ? 'bg-accent' : ''"
|
||||||
|
>
|
||||||
|
|
||||||
<!-- date selection input -->
|
<!-- date selection input -->
|
||||||
<q-input
|
<q-input
|
||||||
v-model="expense_store.current_expense.date"
|
v-model="expenses_store.current_expense.date"
|
||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
readonly
|
readonly
|
||||||
|
|
@ -64,7 +67,7 @@
|
||||||
/>
|
/>
|
||||||
<q-dialog v-model="is_navigator_open">
|
<q-dialog v-model="is_navigator_open">
|
||||||
<q-date
|
<q-date
|
||||||
v-model="expense_store.current_expense.date"
|
v-model="expenses_store.current_expense.date"
|
||||||
@update:model-value="is_navigator_open = false"
|
@update:model-value="is_navigator_open = false"
|
||||||
mask="YYYY-MM-DD"
|
mask="YYYY-MM-DD"
|
||||||
/>
|
/>
|
||||||
|
|
@ -74,8 +77,8 @@
|
||||||
|
|
||||||
<!-- expenses type selection -->
|
<!-- expenses type selection -->
|
||||||
<q-select
|
<q-select
|
||||||
v-model="expense_store.current_expense.type"
|
v-model="expenses_store.current_expense.type"
|
||||||
:options="type_options"
|
:options="EXPENSE_TYPE"
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
class="col q-px-xs"
|
class="col q-px-xs"
|
||||||
|
|
@ -85,14 +88,13 @@
|
||||||
:label="$t('timesheet.expense.type')"
|
:label="$t('timesheet.expense.type')"
|
||||||
:rules="[rules.typeRequired]"
|
:rules="[rules.typeRequired]"
|
||||||
:option-label="label => $t(label)"
|
:option-label="label => $t(label)"
|
||||||
@update:model-value="val => setType(val as ExpenseType)"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- amount input -->
|
<!-- amount input -->
|
||||||
<template v-if="show_amount">
|
<template v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenses_store.current_expense.type)">
|
||||||
<q-input
|
<q-input
|
||||||
key="amount"
|
key="amount"
|
||||||
v-model.number="expense_store.current_expense.amount"
|
v-model.number="expenses_store.current_expense.amount"
|
||||||
filled
|
filled
|
||||||
input-class="text-right"
|
input-class="text-right"
|
||||||
dense
|
dense
|
||||||
|
|
@ -111,7 +113,7 @@
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<q-input
|
<q-input
|
||||||
key="mileage"
|
key="mileage"
|
||||||
v-model.number="expense_store.current_expense.mileage"
|
v-model.number="expenses_store.current_expense.mileage"
|
||||||
filled
|
filled
|
||||||
input-class="text-right"
|
input-class="text-right"
|
||||||
dense
|
dense
|
||||||
|
|
@ -128,7 +130,7 @@
|
||||||
|
|
||||||
<!-- employee comment input -->
|
<!-- employee comment input -->
|
||||||
<q-input
|
<q-input
|
||||||
v-model="expense_store.current_expense.comment"
|
v-model="expenses_store.current_expense.comment"
|
||||||
filled
|
filled
|
||||||
color="primary"
|
color="primary"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -137,9 +139,9 @@
|
||||||
stack-label
|
stack-label
|
||||||
clearable
|
clearable
|
||||||
:counter="true"
|
:counter="true"
|
||||||
:maxlength="comment_max_length"
|
:maxlength="COMMENT_MAX_LENGTH"
|
||||||
lazy-rules="ondemand"
|
lazy-rules="ondemand"
|
||||||
:rules="[rules.commentRequired, rules.commentTooLong]"
|
:rules="[rules.commentRequired]"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-weight-bold ">
|
<span class="text-weight-bold ">
|
||||||
|
|
@ -171,6 +173,15 @@
|
||||||
|
|
||||||
<!-- add btn section -->
|
<!-- add btn section -->
|
||||||
<div>
|
<div>
|
||||||
|
<q-btn
|
||||||
|
v-if="expenses_store.mode === 'update'"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="sm"
|
||||||
|
class="q-mt-sm q-ml-sm"
|
||||||
|
@click="cancelUpdateMode"
|
||||||
|
/>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
push
|
||||||
dense
|
dense
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
|
||||||
|
const expense_store = useExpensesStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-item class="row justify-between">
|
||||||
|
<q-item-label
|
||||||
|
header
|
||||||
|
class="text-h6 col-auto"
|
||||||
|
>
|
||||||
|
{{ $t('timesheet.expense.title') }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-section class="items-center col-auto">
|
||||||
|
<q-badge
|
||||||
|
lines="1"
|
||||||
|
class="q-pa-sm q-px-md"
|
||||||
|
:label="$t('timesheet.expense.total_amount') + ': ' + expense_store.pay_period_expenses_totals.amount.toFixed(2)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-separator spaced />
|
||||||
|
|
||||||
|
<q-badge
|
||||||
|
lines="2"
|
||||||
|
class="q-pa-sm q-px-md"
|
||||||
|
:label="$t('timesheet.expense.total_mileage') + ': ' + expense_store.pay_period_expenses_totals.mileage.toFixed(1)"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
151
src/modules/timesheets/components/expense-crud-dialog-list.vue
Normal file
151
src/modules/timesheets/components/expense-crud-dialog-list.vue
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
|
import { expenseTypeIcon } from 'src/modules/timesheets/utils/expense.util';
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
import { useExpensesApi } from 'src/modules/timesheets/composables/api/use-expense-api';
|
||||||
|
import { default_expense, type Expense } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
import { computed, inject } from 'vue';
|
||||||
|
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const expenses_store = useExpensesStore();
|
||||||
|
const expenses_api = useExpensesApi();
|
||||||
|
|
||||||
|
const expenses_list = computed(() => timesheet_store.pay_period_details.weeks.flatMap(week =>
|
||||||
|
Object.values(week.expenses).flatMap(day => day.expenses)));
|
||||||
|
|
||||||
|
const employee_email = inject('employeeEmail', '');
|
||||||
|
|
||||||
|
const setExpenseToModify = (expense: Expense) => {
|
||||||
|
expenses_store.mode = 'update';
|
||||||
|
expenses_store.current_expense = expense;
|
||||||
|
expenses_store.initial_expense = unwrapAndClone(expense);
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestExpenseDeletion = async (expense: Expense) => {
|
||||||
|
expenses_store.mode = 'delete';
|
||||||
|
expenses_store.initial_expense = expense;
|
||||||
|
expenses_store.current_expense = default_expense;
|
||||||
|
await expenses_api.deleteExpenseByEmployeeEmail(employee_email, expenses_store.initial_expense.date);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- liste des dépenses pré existantes -->
|
||||||
|
<q-list
|
||||||
|
padding
|
||||||
|
class="rounded-borders"
|
||||||
|
>
|
||||||
|
<q-item-label
|
||||||
|
v-if="expenses_store.pay_period_expenses.expenses.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 expenses_list"
|
||||||
|
:key="index"
|
||||||
|
class="q-my-xs shadow-1"
|
||||||
|
:class="expenses_store.mode === 'update' ? 'bg-accent' : ''"
|
||||||
|
>
|
||||||
|
<!-- 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'">
|
||||||
|
<template v-if="typeof expense.mileage === 'number'">
|
||||||
|
{{ expense.mileage?.toFixed(1) }} km
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ expense.amount.toFixed(2) }} $
|
||||||
|
</template>
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label v-else>
|
||||||
|
{{ expense.amount.toFixed(2) }} $
|
||||||
|
</q-item-label>
|
||||||
|
|
||||||
|
<!-- date label -->
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
lines="2"
|
||||||
|
>
|
||||||
|
{{ $d(new Date(expense.date), { 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>
|
||||||
|
|
||||||
|
<q-item-section
|
||||||
|
v-if="!expenses_store.pay_period_expenses.is_approved && !expense.is_approved"
|
||||||
|
side
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
color="primary"
|
||||||
|
icon="edit"
|
||||||
|
@click="setExpenseToModify(expense)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
color="negative"
|
||||||
|
icon="close"
|
||||||
|
@click="requestExpenseDeletion(expense)"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
</template>
|
||||||
58
src/modules/timesheets/components/expense-crud-dialog.vue
Normal file
58
src/modules/timesheets/components/expense-crud-dialog.vue
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
import ExpenseCrudDialogList from 'src/modules/timesheets/components/expense-crud-dialog-list.vue';
|
||||||
|
import ExpenseCrudDialogForm from 'src/modules/timesheets/components/expense-crud-dialog-form.vue';
|
||||||
|
import ExpenseCrudDialogHeader from 'src/modules/timesheets/components/expense-crud-dialog-header.vue';
|
||||||
|
|
||||||
|
const expense_store = useExpensesStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-dialog
|
||||||
|
v-model="expense_store.is_open"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<q-card
|
||||||
|
class="q-pa-md"
|
||||||
|
style=" min-width: 70vw;"
|
||||||
|
>
|
||||||
|
<q-inner-loading :showing="expense_store.is_loading">
|
||||||
|
<q-spinner size="32px" />
|
||||||
|
</q-inner-loading>
|
||||||
|
|
||||||
|
<q-card-section>
|
||||||
|
<!-- <q-banner
|
||||||
|
v-if="expenses_error"
|
||||||
|
dense
|
||||||
|
class="bg-red-2 col-auto text-negative q-mt-sm"
|
||||||
|
>
|
||||||
|
{{ expenses_error }}
|
||||||
|
</q-banner> -->
|
||||||
|
|
||||||
|
<ExpenseCrudDialogHeader />
|
||||||
|
|
||||||
|
<ExpenseCrudDialogList />
|
||||||
|
|
||||||
|
<ExpenseCrudDialogForm />
|
||||||
|
|
||||||
|
<q-separator spaced />
|
||||||
|
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<!-- close btn -->
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
class="col-auto q-mr-sm"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('shared.label.close')"
|
||||||
|
@click="expense_store.close"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { TimesheetExpense } from '../../types/expense.interfaces';
|
|
||||||
import { expenseTypeIcon } from '../../utils/expense.util';
|
|
||||||
/* eslint-disable */
|
|
||||||
defineProps<{
|
|
||||||
items: TimesheetExpense[];
|
|
||||||
is_readonly: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
defineEmits<{
|
|
||||||
(e: 'remove', index: number): void;
|
|
||||||
(e: 'edit' , 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'">
|
|
||||||
<template v-if="typeof expense.mileage === 'number'">
|
|
||||||
{{ expense.mileage?.toFixed(1) }} km
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{ expense.amount?.toFixed(2) }} $
|
|
||||||
</template>
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label v-else>
|
|
||||||
{{ expense.amount?.toFixed(2) }} $
|
|
||||||
</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="primary"
|
|
||||||
icon="edit"
|
|
||||||
@click="$emit('edit', index)"
|
|
||||||
/>
|
|
||||||
</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,121 +0,0 @@
|
||||||
<script
|
|
||||||
setup
|
|
||||||
lang="ts"
|
|
||||||
>
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
|
||||||
import { useExpenseForm } from '../../composables/use-expense-form';
|
|
||||||
import { COMMENT_MAX_LENGTH } from '../../constants/expense.constants';
|
|
||||||
import ExpenseList from './expense-list.vue';
|
|
||||||
import ExpenseCrudDialogForm from 'src/modules/timesheets/components/expenses/expense-crud-dialog-form.vue';
|
|
||||||
import { computeExpenseTotals, makeExpenseRules, buildExpenseSavePayload } from '../../utils/expense.util';
|
|
||||||
import { EXPENSE_TYPE } from 'src/modules/timesheets/models/expense.models';
|
|
||||||
import type { Expense, ExpenseType, PayPeriodExpenses } from 'src/modules/timesheets/models/expense.models';
|
|
||||||
import { toQSelectOptions } from 'src/utils/to-qselect-options';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const rules = makeExpenseRules(t, COMMENT_MAX_LENGTH);
|
|
||||||
const expense_store = useExpensesStore();
|
|
||||||
|
|
||||||
const { employeeEmail } = defineProps<{
|
|
||||||
employeeEmail: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const type_options = toQSelectOptions<ExpenseType>(EXPENSE_TYPE, 'timesheet.expense.types.');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//------------------ refs and computed ------------------
|
|
||||||
const files = ref<File[] | null>(null);
|
|
||||||
const { validateAnd } = useExpenseForm();
|
|
||||||
|
|
||||||
const totals = computed(() => computeExpenseTotals(expenses));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-dialog
|
|
||||||
v-model="expense_store.is_open"
|
|
||||||
persistent
|
|
||||||
>
|
|
||||||
<q-card
|
|
||||||
class="q-pa-md"
|
|
||||||
style=" min-width: 70vw;"
|
|
||||||
>
|
|
||||||
<q-inner-loading :showing="expense_store.is_loading">
|
|
||||||
<q-spinner size="32px" />
|
|
||||||
</q-inner-loading>
|
|
||||||
|
|
||||||
<q-card-section>
|
|
||||||
<!-- <q-banner
|
|
||||||
v-if="expenses_error"
|
|
||||||
dense
|
|
||||||
class="bg-red-2 col-auto text-negative q-mt-sm"
|
|
||||||
>
|
|
||||||
{{ expenses_error }}
|
|
||||||
</q-banner> -->
|
|
||||||
|
|
||||||
<!-- header (title with totals)-->
|
|
||||||
<q-item class="row justify-between">
|
|
||||||
<q-item-label
|
|
||||||
header
|
|
||||||
class="text-h6 col-auto"
|
|
||||||
>
|
|
||||||
{{ $t('timesheet.expense.title') }}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-section class="items-center col-auto">
|
|
||||||
<q-badge
|
|
||||||
lines="1"
|
|
||||||
class="q-pa-sm q-px-md"
|
|
||||||
:label="$t('timesheet.expense.total_amount') + ': ' + totals.amount.toFixed(2)"
|
|
||||||
/>
|
|
||||||
<q-separator spaced />
|
|
||||||
<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>
|
|
||||||
<ExpenseList
|
|
||||||
:items="expense_store.current_expenses"
|
|
||||||
@remove="expense_store.upsertOrDeletePayPeriodExpenseByEmployeeEmail(employeeEmail, expense_store.current_expenses.expenses)"
|
|
||||||
/>
|
|
||||||
<ExpenseCrudDialogForm
|
|
||||||
:current-expenses:"expense_store.current_expenses.exp"
|
|
||||||
v-model:files="files"
|
|
||||||
:is_readonly="expense_store.current_expenses.is_approved"
|
|
||||||
:type_options="type_options"
|
|
||||||
:rules="rules"
|
|
||||||
:comment_max_length="COMMENT_MAX_LENGTH"
|
|
||||||
:set-type="setType"
|
|
||||||
@submit="expense_store.upsertOrDeletePayPeriodExpenseByEmployeeEmail(employeeEmail, expense_store.current_expenses.expenses)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-separator spaced />
|
|
||||||
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<!-- close btn -->
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
class="col-auto q-mr-sm"
|
|
||||||
color="primary"
|
|
||||||
:label="$t('timesheet.cancel_button')"
|
|
||||||
@click="close"
|
|
||||||
/>
|
|
||||||
<!-- save btn -->
|
|
||||||
<q-btn
|
|
||||||
push
|
|
||||||
color="primary"
|
|
||||||
class="col-auto"
|
|
||||||
:disable="pay_period_expenses.is_approved || expenses.length === 0"
|
|
||||||
:label="$t('timesheet.save_button')"
|
|
||||||
@click="onSave"
|
|
||||||
/>
|
|
||||||
</q-card-actions>
|
|
||||||
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,267 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { useExpenseForm } from '../../composables/use-expense-form';
|
|
||||||
import { useExpenseDraft } from '../../composables/use-expense-draft';
|
|
||||||
import { useExpenseItems } from '../../composables/use-expense-items';
|
|
||||||
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 { COMMENT_MAX_LENGTH } from '../../constants/expense.constants';
|
|
||||||
import { ExpensesValidationError } from '../../types/expense-validation.interface';
|
|
||||||
import { EXPENSE_TYPE } from '../../types/expense.types';
|
|
||||||
import type { ExpenseType } from '../../types/expense.types';
|
|
||||||
import type { ExpenseDay, TimesheetExpense } from '../../types/expense.interfaces';
|
|
||||||
import {
|
|
||||||
createExpenseByDate,
|
|
||||||
deleteExpenseByDate,
|
|
||||||
getPayPeriodExpenses,
|
|
||||||
updateExpenseByDate
|
|
||||||
} from '../../composables/api/use-expense-api';
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
|
||||||
const rules = makeExpenseRules(t, COMMENT_MAX_LENGTH);
|
|
||||||
|
|
||||||
//------------------ props ------------------
|
|
||||||
const {email, pay_period_no, pay_year, is_approved, initial_expenses} = defineProps<{
|
|
||||||
pay_period_no: number;
|
|
||||||
pay_year: number;
|
|
||||||
email: string;
|
|
||||||
is_approved?: boolean;
|
|
||||||
initial_expenses?: TimesheetExpense[];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
//------------------ emits ------------------
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'close'): void;
|
|
||||||
(e: 'save', payload: {
|
|
||||||
pay_period_no: number;
|
|
||||||
pay_year: number;
|
|
||||||
email: string;
|
|
||||||
expenses: TimesheetExpense[];
|
|
||||||
}): void;
|
|
||||||
(e: 'error', err: ExpensesValidationError): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
//------------------ q-select mapper ------------------
|
|
||||||
const type_options = computed(() => {
|
|
||||||
void locale.value;
|
|
||||||
return buildExpenseTypeOptions(EXPENSE_TYPE, t);
|
|
||||||
})
|
|
||||||
|
|
||||||
//------------------ refs and computed ------------------
|
|
||||||
const files = ref<File[] | null>(null);
|
|
||||||
const is_readonly = computed(() => !!is_approved);
|
|
||||||
const editing_old = ref<ExpenseDay | null>(null);
|
|
||||||
|
|
||||||
const { state: is_open_date_picker } = useToggle();
|
|
||||||
const { draft, setType, reset, showAmount } = useExpenseDraft();
|
|
||||||
const { formRef, validateAnd } = useExpenseForm();
|
|
||||||
const { items, validateAll, payload } = useExpenseItems({
|
|
||||||
initial_expenses: initial_expenses,
|
|
||||||
is_approved: is_readonly,
|
|
||||||
draft,
|
|
||||||
});
|
|
||||||
const totals = computed(() => computeExpenseTotals(items.value));
|
|
||||||
|
|
||||||
//------------------ actions ------------------
|
|
||||||
const onSave = () => {
|
|
||||||
try {
|
|
||||||
validateAll();
|
|
||||||
reset();
|
|
||||||
emit('save', buildExpenseSavePayload({
|
|
||||||
pay_period_no: pay_period_no,
|
|
||||||
pay_year: pay_year,
|
|
||||||
email: email,
|
|
||||||
expenses: payload(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
} catch (err: any) {
|
|
||||||
const e = err instanceof ExpensesValidationError
|
|
||||||
? err
|
|
||||||
: new ExpensesValidationError({
|
|
||||||
status_code: 400,
|
|
||||||
message: String(err?.message || err)
|
|
||||||
});
|
|
||||||
emit('error', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshFromServer = async () => {
|
|
||||||
const fresh = await getPayPeriodExpenses(email, pay_year, pay_period_no);
|
|
||||||
items.value = Array.isArray(fresh.expenses) ? fresh.expenses : [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFormSubmit = async () => {
|
|
||||||
try {
|
|
||||||
await validateAnd(async () => {
|
|
||||||
if (is_readonly.value) throw new Error(t('common.read_only') || 'Read-only');
|
|
||||||
|
|
||||||
const day = draft.value;
|
|
||||||
if (!day?.date || !day?.type || !day?.comment) {
|
|
||||||
throw new ExpensesValidationError({ status_code: 400, message: 'Missing required fields' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const is_mileage = String(day.type).toUpperCase() === 'MILEAGE';
|
|
||||||
const new_payload = {
|
|
||||||
date: day.date,
|
|
||||||
type: day.type as ExpenseType,
|
|
||||||
comment: day.comment,
|
|
||||||
...(is_mileage && typeof day.mileage === 'number'
|
|
||||||
? { mileage: day.mileage }
|
|
||||||
: !is_mileage && typeof day.amount === 'number'
|
|
||||||
? { amount: day.amount }
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
if(editing_old.value) {
|
|
||||||
await updateExpenseByDate(email, editing_old.value, new_payload as any);
|
|
||||||
editing_old.value = null;
|
|
||||||
} else {
|
|
||||||
await createExpenseByDate(email, new_payload as any);
|
|
||||||
}
|
|
||||||
await refreshFromServer();
|
|
||||||
reset();
|
|
||||||
});
|
|
||||||
} catch (err: any) {
|
|
||||||
const e = err instanceof ExpensesValidationError ? err : new ExpensesValidationError({
|
|
||||||
status_code: 400,
|
|
||||||
message: String(err?.message || err)
|
|
||||||
});
|
|
||||||
emit('error', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRemove = async (index: number) => {
|
|
||||||
try {
|
|
||||||
if (is_readonly.value) throw new Error(t('common.read_only') || 'Read-only');
|
|
||||||
|
|
||||||
const item = items.value[index];
|
|
||||||
if (!item) return;
|
|
||||||
const is_mileage = String(item.type).toUpperCase() === 'MILEAGE';
|
|
||||||
|
|
||||||
const old_payload: any = {
|
|
||||||
date: item.date,
|
|
||||||
type: item.type as ExpenseType,
|
|
||||||
comment: item.comment ?? '',
|
|
||||||
...(is_mileage && typeof item.mileage === 'number'
|
|
||||||
? { mileage: item.mileage }
|
|
||||||
: !is_mileage && typeof item.amount === 'number'
|
|
||||||
? { amount: item.amount }
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
await deleteExpenseByDate(email, old_payload as any);
|
|
||||||
await refreshFromServer();
|
|
||||||
} catch (err: any) {
|
|
||||||
const e = err instanceof ExpensesValidationError ? err : new ExpensesValidationError({
|
|
||||||
status_code: 400, message: String(err?.message || err)
|
|
||||||
});
|
|
||||||
emit('error', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onEdit = async (index: number) => {
|
|
||||||
if(is_readonly) return;
|
|
||||||
const item = items.value[index];
|
|
||||||
if(!item) return;
|
|
||||||
const old_amount = Number(item.amount) || 0;
|
|
||||||
editing_old.value = {
|
|
||||||
date: item.date,
|
|
||||||
type: item.type as ExpenseType,
|
|
||||||
amount: old_amount,
|
|
||||||
comment: item.comment ?? '',
|
|
||||||
is_approved: !!item.is_approved,
|
|
||||||
};
|
|
||||||
|
|
||||||
const is_mileage = String(item.type).toUpperCase() === 'MILEAGE';
|
|
||||||
const next: Partial<TimesheetExpense> = {
|
|
||||||
date: item.date,
|
|
||||||
type: item.type,
|
|
||||||
comment: item.comment ?? '',
|
|
||||||
...(is_mileage
|
|
||||||
? (typeof item.mileage === 'number' ? { mileage: item.mileage } : {})
|
|
||||||
: (typeof item.amount === 'number' ? { amnount: item.amount } : {})),
|
|
||||||
};
|
|
||||||
(draft as any).value = next;
|
|
||||||
setType(item.type as ExpenseType);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClose = () => emit('close');
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<!-- header (title with totals)-->
|
|
||||||
<q-item class="row justify-between">
|
|
||||||
<q-item-label
|
|
||||||
header
|
|
||||||
class="text-h6 col-auto"
|
|
||||||
>
|
|
||||||
{{ $t('timesheet.expense.title') }}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-section class="items-center col-auto">
|
|
||||||
<q-badge
|
|
||||||
lines="1"
|
|
||||||
class="q-pa-sm q-px-md"
|
|
||||||
:label="$t('timesheet.expense.total_amount') + ': ' + totals.amount.toFixed(2)"
|
|
||||||
/>
|
|
||||||
<q-separator spaced />
|
|
||||||
<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>
|
|
||||||
<ExpenseList
|
|
||||||
:items="items"
|
|
||||||
:is_readonly="is_readonly"
|
|
||||||
@remove="onRemove"
|
|
||||||
@edit="onEdit"
|
|
||||||
/>
|
|
||||||
<ExpenseForm
|
|
||||||
ref="formRef"
|
|
||||||
v-model:draft="draft"
|
|
||||||
v-model:files="files"
|
|
||||||
v-model:date-picker-open="is_open_date_picker"
|
|
||||||
:type_options="type_options"
|
|
||||||
:show_amount="showAmount"
|
|
||||||
:is_readonly="is_readonly"
|
|
||||||
:rules="rules"
|
|
||||||
:comment_max_length="COMMENT_MAX_LENGTH"
|
|
||||||
:set-type="setType"
|
|
||||||
@submit="onFormSubmit"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-separator spaced />
|
|
||||||
|
|
||||||
<div class="row col-auto justify-end">
|
|
||||||
<!-- close btn -->
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
class="q-mr-sm"
|
|
||||||
color="primary"
|
|
||||||
:label="$t('timesheet.cancel_button')"
|
|
||||||
@click="onClose"
|
|
||||||
/>
|
|
||||||
<!-- save btn -->
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
unelevated
|
|
||||||
push
|
|
||||||
:disable="is_readonly || items.length === 0"
|
|
||||||
:label="$t('timesheet.save_button')"
|
|
||||||
@click="onSave"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<!-- punch-in timestamps -->
|
<!-- punch-in timestamps -->
|
||||||
<q-card-section class="col q-pa-none">
|
<q-card-section class="col q-pa-none">
|
||||||
<q-item-label class="text-weight-bolder text-primary" style="font-size: 0.7em;">
|
<q-item-label class="text-weight-bolder text-primary" style="font-size: 0.7em;">
|
||||||
{{ $t('shiftColumns.labelIn') }}
|
{{ $t('shared.misc.in') }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
<!-- punch-out timestamps -->
|
<!-- punch-out timestamps -->
|
||||||
<q-card-section class="col q-pa-none">
|
<q-card-section class="col q-pa-none">
|
||||||
<q-item-label class="text-weight-bolder text-primary" style="font-size: 0.7em;">
|
<q-item-label class="text-weight-bolder text-primary" style="font-size: 0.7em;">
|
||||||
{{ $t('shiftColumns.labelOut') }}
|
{{ $t('shared.misc.out') }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
72
src/modules/timesheets/components/shift-list-legend.vue
Normal file
72
src/modules/timesheets/components/shift-list-legend.vue
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import type { ShiftLegendItem } from 'src/modules/timesheets/models/shift.models';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const is_showing_legend = ref(false);
|
||||||
|
|
||||||
|
const legend: ShiftLegendItem[] = [
|
||||||
|
{ type: 'REGULAR', color: 'secondary', label_type: 'timesheet.shift.types.REGULAR', text_color: 'grey-8' },
|
||||||
|
{ type: 'EVENING', color: 'warning', label_type: 'timesheet.shift.types.EVENING' },
|
||||||
|
{ type: 'EMERGENCY', color: 'amber-10', label_type: 'timesheet.shift.types.EMERGENCY' },
|
||||||
|
{ type: 'OVERTIME', color: 'negative', label_type: 'timesheet.shift.types.OVERTIME' },
|
||||||
|
{ type: 'VACATION', color: 'purple-10', label_type: 'timesheet.shift.types.VACATION' },
|
||||||
|
{ type: 'HOLIDAY', color: 'purple-5', label_type: 'timesheet.shift.types.HOLIDAY' },
|
||||||
|
{ type: 'SICK', color: 'grey-8', label_type: 'timesheet.shift.types.SICK' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const shift_type_legend = computed(() =>
|
||||||
|
legend.map(item => ({ ...item, label: t(item.label_type) }))
|
||||||
|
);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="items-center"
|
||||||
|
:class="$q.screen.lt.md ? 'column' : 'row'"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
rounded
|
||||||
|
color="primary"
|
||||||
|
class="col-auto q-ma-sm"
|
||||||
|
@click="is_showing_legend = !is_showing_legend"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<q-icon
|
||||||
|
:name="is_showing_legend ? 'close' : 'info_outline'"
|
||||||
|
size="md"
|
||||||
|
class="col-auto"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<transition
|
||||||
|
appear
|
||||||
|
enter-active-class="animated fadeIn"
|
||||||
|
leave-active-class="animated fadeOut"
|
||||||
|
class="col-auto"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="is_showing_legend"
|
||||||
|
class="q-pa-xs bg-white rounded-5 shadow-2 text-center q-ma-xs"
|
||||||
|
>
|
||||||
|
<q-badge
|
||||||
|
v-for="shift_type in shift_type_legend"
|
||||||
|
:key="shift_type.type"
|
||||||
|
:color="shift_type.color"
|
||||||
|
:label="shift_type.label"
|
||||||
|
:text-color="shift_type.text_color || 'white'"
|
||||||
|
class="q-px-md q-py-xs q-mx-xs q-my-none text-uppercase text-weight-bolder justify-center"
|
||||||
|
style="font-size: 0.8em;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -3,8 +3,9 @@
|
||||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||||
|
|
||||||
|
|
||||||
const { shift } = defineProps<{
|
const { shift, dense = false } = defineProps<{
|
||||||
shift: Shift;
|
shift: Shift;
|
||||||
|
dense?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
@ -14,11 +15,12 @@
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const has_comment = computed(() => {
|
const has_comment = computed(() => {
|
||||||
const comment = (shift as any).description ?? (shift as any).comment ?? '';
|
const comment = shift.comment ?? '';
|
||||||
return typeof comment === 'string' && comment.trim().length > 0;
|
return typeof comment === 'string' && comment.trim().length > 0;
|
||||||
})
|
})
|
||||||
const comment_icon = computed(() => (has_comment.value ? 'announcement' : 'chat_bubble_outline'));
|
const comment_icon = computed(() => (has_comment.value ? 'announcement' : 'chat_bubble_outline'));
|
||||||
const comment_color = computed(() => (has_comment.value ? 'primary' : 'grey-8'));
|
const comment_color = computed(() => (has_comment.value ? 'primary' : 'grey-8'));
|
||||||
|
const hour_font_size = computed(() => dense ? '0.9em' : '1.5em' )
|
||||||
|
|
||||||
const get_shift_color = (type: string): string => {
|
const get_shift_color = (type: string): string => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
|
@ -27,7 +29,7 @@
|
||||||
case 'EMERGENCY': return 'amber-10';
|
case 'EMERGENCY': return 'amber-10';
|
||||||
case 'OVERTIME': return 'negative';
|
case 'OVERTIME': return 'negative';
|
||||||
case 'VACATION': return 'purple-10';
|
case 'VACATION': return 'purple-10';
|
||||||
case 'HOLIDAY': return 'purple-10';
|
case 'HOLIDAY': return 'purple-5';
|
||||||
case 'SICK': return 'grey-8';
|
case 'SICK': return 'grey-8';
|
||||||
default: return 'transparent';
|
default: return 'transparent';
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +63,7 @@
|
||||||
<q-item-label
|
<q-item-label
|
||||||
class="text-weight-bolder q-pa-xs rounded-5"
|
class="text-weight-bolder q-pa-xs rounded-5"
|
||||||
:class="'bg-' + get_shift_color(shift.type) + ' text-' + get_text_color(shift.type)"
|
:class="'bg-' + get_shift_color(shift.type) + ' text-' + get_text_color(shift.type)"
|
||||||
style="font-size: 1.5em; line-height: 80% !important;"
|
:style="'font-size: ' + hour_font_size + '; line-height: 80% !important;'"
|
||||||
>
|
>
|
||||||
{{ shift.start_time }}
|
{{ shift.start_time }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
|
|
@ -3,26 +3,23 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
import ShiftListHeader from 'src/modules/timesheets/components/shift/shift-list-header.vue';
|
import ShiftListHeader from 'src/modules/timesheets/components/shift-list-header.vue';
|
||||||
import ShiftListRow from 'src/modules/timesheets/components/shift/shift-list-row.vue';
|
import ShiftListRow from 'src/modules/timesheets/components/shift-list-row.vue';
|
||||||
import ShiftListLegend from 'src/modules/timesheets/components/shift/shift-list-legend.vue';
|
|
||||||
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
|
||||||
import { useShiftStore } from 'src/stores/shift-store';
|
import { useShiftStore } from 'src/stores/shift-store';
|
||||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/api/use-timesheet-api';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { type Shift, default_shift } from 'src/modules/timesheets/models/shift.models';
|
import { type Shift, default_shift } from 'src/modules/timesheets/models/shift.models';
|
||||||
import type { PayPeriodDetails } from 'src/modules/timesheets/models/pay-period-details.models';
|
import { computed } from 'vue';
|
||||||
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const timesheet_store = useTimesheetStore();
|
||||||
rawData: PayPeriodDetails;
|
const { openCreate, openDelete, openUpdate } = useShiftStore();
|
||||||
currentPayPeriod: PayPeriod;
|
const { dense = false } = defineProps<{
|
||||||
|
dense?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const timesheet_api = useTimesheetApi();
|
const font_size = computed(() => dense ? '1.5em' : '2.5em')
|
||||||
const { openCreate, openDelete, openUpdate } = useShiftStore();
|
|
||||||
|
|
||||||
const get_date_from_short = (short_date: string): Date => {
|
const get_date_from_short = (short_date: string): Date => {
|
||||||
return new Date(props.currentPayPeriod.pay_year.toString() + '/' + short_date);
|
return new Date(timesheet_store.pay_period.pay_year.toString() + '/' + short_date);
|
||||||
};
|
};
|
||||||
|
|
||||||
const to_iso_date = (short_date: string): string => {
|
const to_iso_date = (short_date: string): string => {
|
||||||
|
|
@ -34,37 +31,35 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDate = (shift_date: string): Date => {
|
const getDate = (shift_date: string): Date => {
|
||||||
return new Date(props.currentPayPeriod.pay_year.toString() + '/' + shift_date);
|
return new Date(timesheet_store.pay_period.pay_year.toString() + '/' + shift_date);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- shift's colored legend -->
|
|
||||||
<ShiftListLegend :is-loading="false" />
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="week, index in props.rawData.weeks"
|
v-for="week, index in timesheet_store.pay_period_details.weeks"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="q-px-xs q-pt-xs rounded-5 col"
|
class="col q-px-xs q-pt-xs q-mx-sm rounded-5"
|
||||||
>
|
>
|
||||||
<q-card
|
<q-card
|
||||||
v-for="day, day_index in week.shifts"
|
v-for="day, day_index in week.shifts"
|
||||||
:key="day_index"
|
:key="day_index + index"
|
||||||
flat
|
|
||||||
bordered
|
|
||||||
class="row items-center rounded-10 q-mb-xs"
|
class="row items-center rounded-10 q-mb-xs"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- Dates column -->
|
<!-- Dates column -->
|
||||||
<q-card-section class="col-auto q-pa-xs text-white">
|
<q-card-section class="col-auto q-pa-xs text-white q-mr-md">
|
||||||
<div class="bg-primary rounded-10 q-pa-xs text-center">
|
<div
|
||||||
|
class="bg-primary rounded-10 q-pa-xs text-center"
|
||||||
|
:style="'width: ' + dense? '60px' : '75px;'"
|
||||||
|
>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
style="font-size: 0.7em;"
|
style="font-size: 0.7em;"
|
||||||
class="text-uppercase"
|
class="text-uppercase"
|
||||||
>{{ $d(getDate(day.short_date), { weekday: $q.screen.lt.md ? 'short' : 'long' }) }}</q-item-label>
|
>{{ $d(getDate(day.short_date), { weekday: $q.screen.lt.md ? 'short' : 'long' }) }}</q-item-label>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
class="text-weight-bolder"
|
class="text-weight-bolder"
|
||||||
style="font-size: 2.5em; line-height: 90% !important;"
|
:style="'font-size: ' + font_size + '; line-height: 90% !important;'"
|
||||||
>{{ day.short_date.split('/')[1] }}</q-item-label>
|
>{{ day.short_date.split('/')[1] }}</q-item-label>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
style="font-size: 0.7em;"
|
style="font-size: 0.7em;"
|
||||||
|
|
@ -75,14 +70,18 @@
|
||||||
|
|
||||||
<!-- List of shifts column -->
|
<!-- List of shifts column -->
|
||||||
<q-card-section class="col q-pa-none">
|
<q-card-section class="col q-pa-none">
|
||||||
<ShiftListHeader />
|
<ShiftListHeader v-if="day.shifts.length > 0"/>
|
||||||
<ShiftListRow
|
<div
|
||||||
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
v-if="day.shifts.length > 0"
|
||||||
:key="shift_index"
|
>
|
||||||
:shift="shift"
|
<ShiftListRow
|
||||||
@request-update="value => openUpdate(to_iso_date(day.short_date), value)"
|
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
||||||
@request-delete="value => openDelete(to_iso_date(day.short_date), value)"
|
:key="shift_index"
|
||||||
/>
|
:shift="shift"
|
||||||
|
@request-update="value => openUpdate(to_iso_date(day.short_date), value)"
|
||||||
|
@request-delete="value => openDelete(to_iso_date(day.short_date), value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<!-- add shift btn column -->
|
<!-- add shift btn column -->
|
||||||
<q-card-section class="q-pr-xs col-auto">
|
<q-card-section class="q-pr-xs col-auto">
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import type { ShiftLegendItem } from 'src/modules/timesheets/models/shift.models';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const props = defineProps<{ isLoading: boolean; }>();
|
|
||||||
|
|
||||||
const legend: ShiftLegendItem[] = [
|
|
||||||
{type:'REGULAR' , color: 'secondary', label_type: 'timesheet.shift.types.REGULAR', text_color: 'grey-8'},
|
|
||||||
{type:'EVENING' , color: 'warning' , label_type: 'timesheet.shift.types.EVENING'},
|
|
||||||
{type:'EMERGENCY', color: 'amber-10' , label_type: 'timesheet.shift.types.EMERGENCY'},
|
|
||||||
{type:'OVERTIME' , color: 'negative' , label_type: 'timesheet.shift.types.OVERTIME'},
|
|
||||||
{type:'VACATION' , color: 'purple-10', label_type: 'timesheet.shift.types.VACATION'},
|
|
||||||
{type:'HOLIDAY' , color: 'purple-8' , label_type: 'timesheet.shift.types.HOLIDAY'},
|
|
||||||
{type:'SICK' , color: 'grey-8' , label_type: 'timesheet.shift.types.SICK'},
|
|
||||||
]
|
|
||||||
|
|
||||||
const shift_type_legend = computed(()=>
|
|
||||||
legend.map(item => ({ ...item, label: t(item.label_type)} ))
|
|
||||||
);
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-card class="q-px-xs q-pt-xs col rounded-10 q-mx-xs q-py-xs">
|
|
||||||
<q-card-section
|
|
||||||
class="q-py-xs q-pa-none text-center q-my-s"
|
|
||||||
v-if="!props.isLoading"
|
|
||||||
>
|
|
||||||
<q-badge
|
|
||||||
v-for="shift_type in shift_type_legend"
|
|
||||||
:key="shift_type.type"
|
|
||||||
:color="shift_type.color"
|
|
||||||
:label="shift_type.label"
|
|
||||||
:text-color="shift_type.text_color || 'white'"
|
|
||||||
class="q-px-md q-py-xs q-mx-xs q-my-none text-uppercase text-weight-bolder justify-center"
|
|
||||||
style="width: 120px; font-size: 0.8em;"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
88
src/modules/timesheets/components/timesheet-wrapper.vue
Normal file
88
src/modules/timesheets/components/timesheet-wrapper.vue
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import ShiftList from 'src/modules/timesheets/components/shift-list.vue';
|
||||||
|
import ShiftCrudDialog from 'src/modules/timesheets/components/shift-crud-dialog.vue';
|
||||||
|
import ExpenseCrudDialog from 'src/modules/timesheets/components/expense-crud-dialog.vue';
|
||||||
|
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
||||||
|
import ShiftListLegend from 'src/modules/timesheets/components/shift-list-legend.vue';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
import { useTimesheetApi } from 'src/modules/timesheets/composables/api/use-timesheet-api';
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
import { provide } from 'vue';
|
||||||
|
|
||||||
|
const { open } = useExpensesStore();
|
||||||
|
|
||||||
|
const { employeeEmail, dense = false } = defineProps<{
|
||||||
|
employeeEmail: string;
|
||||||
|
dense?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { is_loading } = useTimesheetStore();
|
||||||
|
const { getPayPeriodDetailsByDate, getPreviousPayPeriodDetails, getNextPayPeriodDetails } = useTimesheetApi();
|
||||||
|
|
||||||
|
provide('employeeEmail', employeeEmail);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-card
|
||||||
|
flat
|
||||||
|
class="q-mt-md bg-secondary full-width"
|
||||||
|
>
|
||||||
|
<q-inner-loading
|
||||||
|
:showing="is_loading"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-card-section
|
||||||
|
:horizontal="$q.screen.gt.sm"
|
||||||
|
class="q-px-lg items-center"
|
||||||
|
:class="$q.screen.lt.md ? 'column' : ''"
|
||||||
|
>
|
||||||
|
<!-- navigation btn -->
|
||||||
|
<PayPeriodNavigator
|
||||||
|
@date-selected="getPayPeriodDetailsByDate"
|
||||||
|
@pressed-previous-button="getPreviousPayPeriodDetails"
|
||||||
|
@pressed-next-button="getNextPayPeriodDetails"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- mobile expenses button -->
|
||||||
|
<q-btn
|
||||||
|
v-if="$q.screen.lt.md"
|
||||||
|
push
|
||||||
|
rounded
|
||||||
|
color="primary"
|
||||||
|
icon="receipt_long"
|
||||||
|
:label="$t('timesheet.expense.open_btn')"
|
||||||
|
class="q-mt-sm"
|
||||||
|
@click="open(employeeEmail)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- shift's colored legend -->
|
||||||
|
<ShiftListLegend :is-loading="false" />
|
||||||
|
|
||||||
|
<q-space />
|
||||||
|
|
||||||
|
<!-- desktop expenses button -->
|
||||||
|
<q-btn
|
||||||
|
v-if="$q.screen.gt.sm"
|
||||||
|
push
|
||||||
|
rounded
|
||||||
|
color="primary"
|
||||||
|
icon="receipt_long"
|
||||||
|
:label="$t('timesheet.expense.open_btn')"
|
||||||
|
@click="open(employeeEmail)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section :horizontal="$q.screen.gt.sm">
|
||||||
|
<ShiftList :dense="dense"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<ExpenseCrudDialog />
|
||||||
|
|
||||||
|
<ShiftCrudDialog :employee-email="employeeEmail" />
|
||||||
|
</template>
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
<script
|
|
||||||
setup
|
|
||||||
lang="ts"
|
|
||||||
>
|
|
||||||
import ShiftList from 'src/modules/timesheets/components/shift/shift-list.vue';
|
|
||||||
import ShiftCrudDialog from 'src/modules/timesheets/components/shift/shift-crud-dialog.vue';
|
|
||||||
import ExpenseCrudDialog from 'src/modules/timesheets/components/expenses/expense-crud-dialog.vue';
|
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
|
||||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/api/use-timesheet-api';
|
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
|
||||||
import { onMounted } from 'vue';
|
|
||||||
|
|
||||||
const { openExpensesDialog } = useExpensesStore();
|
|
||||||
|
|
||||||
const { employeeEmail } = defineProps<{
|
|
||||||
employeeEmail: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const { pay_period, pay_period_details, is_loading } = useTimesheetStore();
|
|
||||||
const { getPayPeriodDetailsByDate, getPreviousPayPeriodDetails, getNextPayPeriodDetails } = useTimesheetApi();
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-card
|
|
||||||
flat
|
|
||||||
class=" col q-mt-md bg-secondary"
|
|
||||||
>
|
|
||||||
<q-inner-loading
|
|
||||||
:showing="is_loading"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-card-section horizontal>
|
|
||||||
<!-- expenses button -->
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
unelevated
|
|
||||||
icon="receipt_long"
|
|
||||||
:label="$t('timesheet.expense.open_btn')"
|
|
||||||
@click="openExpensesDialog"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- navigation btn -->
|
|
||||||
<PayPeriodNavigator
|
|
||||||
@date-selected="getPayPeriodDetailsByDate"
|
|
||||||
@pressed-previous-button="getPreviousPayPeriodDetails"
|
|
||||||
@pressed-next-button="getNextPayPeriodDetails"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section>
|
|
||||||
<ShiftList
|
|
||||||
:raw-data="pay_period_details"
|
|
||||||
:current-pay-period="pay_period"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<!-- dialog for Expenses or Shifts -->
|
|
||||||
<ExpenseCrudDialog :email="employeeEmail" />
|
|
||||||
|
|
||||||
<!-- shift crud dialog -->
|
|
||||||
<ShiftCrudDialog :employee-email="employeeEmail" />
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { normalizeObject } from "src/utils/normalize-object";
|
import { normalizeObject } from "src/utils/normalize-object";
|
||||||
import { useExpensesStore } from "src/stores/expense-store";
|
import { useExpensesStore } from "src/stores/expense-store";
|
||||||
import { expense_validation_schema, type ExpensesApiError } from "src/modules/timesheets/models/expense.validation";
|
import { expense_validation_schema } from "src/modules/timesheets/models/expense.validation";
|
||||||
import type { Expense, UpsertExpense } from "src/modules/timesheets/models/expense.models";
|
import type { Expense, UpsertExpense } from "src/modules/timesheets/models/expense.models";
|
||||||
|
|
||||||
export const useExpensesApi = () => {
|
export const useExpensesApi = () => {
|
||||||
|
|
@ -11,26 +11,26 @@ export const useExpensesApi = () => {
|
||||||
new_expense?: Expense;
|
new_expense?: Expense;
|
||||||
}) => obj as UpsertExpense;
|
}) => obj as UpsertExpense;
|
||||||
|
|
||||||
const createExpenseByEmployeeEmail = async (employee_email: string): Promise<void> => {
|
const createExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||||
const upsert_expense = toUpsertExpense({
|
const upsert_expense = toUpsertExpense({
|
||||||
new_expense: normalizeObject(expenses_store.current_expense, expense_validation_schema),
|
new_expense: normalizeObject(expenses_store.current_expense, expense_validation_schema),
|
||||||
});
|
});
|
||||||
await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, upsert_expense);
|
await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateExpenseByEmployeeEmail = async (employee_email: string): Promise<void> => {
|
const updateExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||||
const upsert_expense = toUpsertExpense({
|
const upsert_expense = toUpsertExpense({
|
||||||
old_expense: normalizeObject(expenses_store.initial_expense, expense_validation_schema),
|
old_expense: normalizeObject(expenses_store.initial_expense, expense_validation_schema),
|
||||||
new_expense: normalizeObject(expenses_store.current_expense, expense_validation_schema),
|
new_expense: normalizeObject(expenses_store.current_expense, expense_validation_schema),
|
||||||
});
|
});
|
||||||
await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, upsert_expense);
|
await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteExpenseByEmployeeEmail = async (employee_email: string): Promise<void> => {
|
const deleteExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||||
const upsert_expense = toUpsertExpense({
|
const upsert_expense = toUpsertExpense({
|
||||||
old_expense: normalizeObject(expenses_store.initial_expense, expense_validation_schema),
|
old_expense: normalizeObject(expenses_store.initial_expense, expense_validation_schema),
|
||||||
});
|
});
|
||||||
await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, upsert_expense);
|
await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
||||||
import { DATE_FORMAT_PATTERN, TIME_FORMAT_PATTERN } from "src/modules/timesheets/constants/shift.constants";
|
import { TIME_FORMAT_PATTERN } from "src/modules/timesheets/constants/shift.constants";
|
||||||
import { GenericApiError } from "src/modules/timesheets/models/expense.validation";
|
import { GenericApiError } from "src/modules/timesheets/models/expense.validation";
|
||||||
import { useShiftStore } from "src/stores/shift-store";
|
import { useShiftStore } from "src/stores/shift-store";
|
||||||
import { default_shift, type Shift, type UpsertShift } from "src/modules/timesheets/models/shift.models";
|
import { default_shift, type Shift, type UpsertShift } from "src/modules/timesheets/models/shift.models";
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import type { TimesheetExpense } from "../types/expense.interfaces";
|
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
import type { ExpenseType } from "../types/expense.types";
|
import type { ExpenseType } from "src/modules/timesheets/models/expense.models";
|
||||||
|
|
||||||
export const useExpenseDraft = (initial?: Partial<TimesheetExpense>) => {
|
export const useExpenseDraft = (initial?: Partial<Expense>) => {
|
||||||
const DEFAULT_TYPE: ExpenseType = 'EXPENSES';
|
const DEFAULT_TYPE: ExpenseType = 'EXPENSES';
|
||||||
|
|
||||||
const draft = ref<Partial<TimesheetExpense>>({
|
const draft = ref<Partial<Expense>>({
|
||||||
date: '',
|
date: '',
|
||||||
type: DEFAULT_TYPE,
|
type: DEFAULT_TYPE,
|
||||||
comment: '',
|
comment: '',
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,59 @@
|
||||||
import { ref, type Ref } from "vue";
|
// import { ref, type Ref } from "vue";
|
||||||
import { normalizeExpense, validateExpenseUI } from "../utils/expenses-validators";
|
// import { normalizeObject } from "src/utils/normalize-object";
|
||||||
import { normExpenseType } from "../utils/expense.util";
|
// import { normExpenseType } from "../utils/expense.util";
|
||||||
import type { Expense, PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
|
// import type { Expense, PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
|
||||||
import { useExpensesStore } from "src/stores/expense-store";
|
// import { useExpensesStore } from "src/stores/expense-store";
|
||||||
import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
// import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
||||||
|
// import { expense_validation_schema } from "src/modules/timesheets/models/expense.validation";
|
||||||
|
|
||||||
const expenses_store = useExpensesStore();
|
// const expenses_store = useExpensesStore();
|
||||||
|
|
||||||
export const useExpenseItems = () => {
|
// export const useExpenseItems = () => {
|
||||||
let expenses = unwrapAndClone(expenses_store.pay_period_expenses.expenses.map(normalizeExpense));
|
// let expenses = unwrapAndClone(expenses_store.pay_period_expenses.expenses.map(normalizeExpense));
|
||||||
|
|
||||||
const normalizePayload = (expense: Expense): Expense => {
|
// const normalizePayload = (expense: Expense): Expense => {
|
||||||
const exp = normalizeExpense(expense);
|
// const exp = normalizeObject(expense, expense_validation_schema);
|
||||||
const out: Expense = {
|
// const out: Expense = {
|
||||||
date: exp.date,
|
// date: exp.date,
|
||||||
type: exp.type as ExpenseType,
|
// type: exp.type as ExpenseType,
|
||||||
comment: exp.comment || '',
|
// comment: exp.comment || '',
|
||||||
};
|
// };
|
||||||
if(typeof exp.amount === 'number') out.amount = exp.amount;
|
// if(typeof exp.amount === 'number') out.amount = exp.amount;
|
||||||
if(typeof exp.mileage === 'number') out.mileage = exp.mileage;
|
// if(typeof exp.mileage === 'number') out.mileage = exp.mileage;
|
||||||
return out;
|
// return out;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const addFromDraft = () => {
|
// const addFromDraft = () => {
|
||||||
const candidate: Expense = normalizeExpense({
|
// const candidate: Expense = normalizeExpense({
|
||||||
date: draft.date,
|
// date: draft.date,
|
||||||
type: normExpenseType(draft.type),
|
// type: normExpenseType(draft.type),
|
||||||
...(typeof draft.amount === 'number' ? { amount: draft.amount }: {}),
|
// ...(typeof draft.amount === 'number' ? { amount: draft.amount }: {}),
|
||||||
...(typeof draft.mileage === 'number' ? { mileage: draft.mileage }: {}),
|
// ...(typeof draft.mileage === 'number' ? { mileage: draft.mileage }: {}),
|
||||||
comment: String(draft.comment ?? '').trim(),
|
// comment: String(draft.comment ?? '').trim(),
|
||||||
} as Expense);
|
// } as Expense);
|
||||||
|
|
||||||
validateExpenseUI(candidate, 'expense_draft');
|
// validateExpenseUI(candidate, 'expense_draft');
|
||||||
expenses = [ ...expenses, candidate];
|
// expenses = [ ...expenses, candidate];
|
||||||
};
|
// };
|
||||||
|
|
||||||
const removeAt = (index: number) => {
|
// const removeAt = (index: number) => {
|
||||||
if(index < 0 || index >= expenses.length) return;
|
// if(index < 0 || index >= expenses.length) return;
|
||||||
expenses = expenses.filter((_,i)=> i !== index);
|
// expenses = expenses.filter((_,i)=> i !== index);
|
||||||
};
|
// };
|
||||||
|
|
||||||
const validateAll = () => {
|
// const validateAll = () => {
|
||||||
for (const expense of expenses) {
|
// for (const expense of expenses) {
|
||||||
validateExpenseUI(expense, 'expense_item');
|
// validateExpenseUI(expense, 'expense_item');
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
const payload = () => expenses.map(normalizeExpense);
|
// const payload = () => expenses.map(normalizeExpense);
|
||||||
|
|
||||||
return {
|
// return {
|
||||||
expenses,
|
// expenses,
|
||||||
addFromDraft,
|
// addFromDraft,
|
||||||
removeAt,
|
// removeAt,
|
||||||
validateAll,
|
// validateAll,
|
||||||
payload,
|
// payload,
|
||||||
};
|
// };
|
||||||
};
|
// };
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1 @@
|
||||||
export const COMMENT_MAX_LENGTH = 280;
|
|
||||||
|
|
||||||
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
|
@ -7,7 +7,7 @@ export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXP
|
||||||
export interface Expense {
|
export interface Expense {
|
||||||
date: string;
|
date: string;
|
||||||
type: ExpenseType;
|
type: ExpenseType;
|
||||||
amount?: number;
|
amount: number;
|
||||||
mileage?: number;
|
mileage?: number;
|
||||||
comment: string;
|
comment: string;
|
||||||
supervisor_comment?: string;
|
supervisor_comment?: string;
|
||||||
|
|
@ -26,10 +26,9 @@ export interface PayPeriodExpenses {
|
||||||
totals?: ExpenseTotals;
|
totals?: ExpenseTotals;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimesheetDetailsWeekDayExpenses {
|
export interface UpsertExpense {
|
||||||
cash: Expense[];
|
old_expense: Expense;
|
||||||
km: Expense[];
|
new_expense: Expense;
|
||||||
[otherType: string]: Expense[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const default_expense: Expense = {
|
export const default_expense: Expense = {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Expense, EXPENSE_TYPE, ExpenseType } from "src/modules/timesheets/models/expense.models";
|
import { type Expense, EXPENSE_TYPE, type ExpenseType } from "src/modules/timesheets/models/expense.models";
|
||||||
import { Normalizer } from "src/utils/normalize-object";
|
import type { Normalizer } from "src/utils/normalize-object";
|
||||||
|
|
||||||
export interface ApiErrorPayload {
|
export interface ApiErrorPayload {
|
||||||
status_code: number;
|
status_code: number;
|
||||||
|
|
@ -43,12 +43,12 @@ export class ExpensesApiError extends ApiError {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const expense_validation_schema: Normalizer<Expense> = {
|
export const expense_validation_schema: Normalizer<Expense> = {
|
||||||
date: v => String(v ?? "1970-01-01").trim(),
|
date: v => typeof v === 'string' ? v.trim() : '1970-01-01',
|
||||||
type: v => EXPENSE_TYPE.includes(v) ? v as ExpenseType : "EXPENSES",
|
type: v => EXPENSE_TYPE.includes(v as ExpenseType) ? v as ExpenseType : "EXPENSES",
|
||||||
amount: v => typeof v === "number" ? v : undefined,
|
amount: v => typeof v === "number" ? v : -1,
|
||||||
mileage: v => typeof v === "number" ? v : undefined,
|
mileage: v => typeof v === "number" ? v : undefined,
|
||||||
comment: v => String(v ?? "").trim(),
|
comment: v => typeof v === 'string' ? v.trim() : '',
|
||||||
supervisor_comment: v => String(v ?? "").trim(),
|
supervisor_comment: v => typeof v === 'string' ? v.trim() : '',
|
||||||
is_approved: v => !!v,
|
is_approved: v => !!v,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Shift } from "./shift.models";
|
import type { Shift } from "./shift.models";
|
||||||
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
import { default_expense, type Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
|
|
||||||
export type Week<T> = {
|
export type Week<T> = {
|
||||||
sun: T;
|
sun: T;
|
||||||
|
|
@ -34,9 +34,9 @@ export interface PayPeriodDetailsWeekDayShifts {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PayPeriodDetailsWeekDayExpenses {
|
export interface PayPeriodDetailsWeekDayExpenses {
|
||||||
cash: Expense[];
|
expenses: Expense[];
|
||||||
km: Expense[];
|
total_expenses: number;
|
||||||
[otherType: string]: Expense[];
|
total_mileage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeWeek = <T>(factory: ()=> T): Week<T> => ({
|
const makeWeek = <T>(factory: ()=> T): Week<T> => ({
|
||||||
|
|
@ -61,8 +61,9 @@ const emptyDailySchedule = (): PayPeriodDetailsWeekDayShifts => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const emptyDailyExpenses = (): PayPeriodDetailsWeekDayExpenses => ({
|
const emptyDailyExpenses = (): PayPeriodDetailsWeekDayExpenses => ({
|
||||||
cash: [],
|
expenses: [default_expense,],
|
||||||
km: [],
|
total_expenses: -1,
|
||||||
|
total_mileage: -1,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const defaultPayPeriodDetailsWeek = (): PayPeriodDetailsWeek => ({
|
export const defaultPayPeriodDetailsWeek = (): PayPeriodDetailsWeek => ({
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
<script
|
|
||||||
setup
|
|
||||||
lang="ts"
|
|
||||||
>
|
|
||||||
import { date } from 'quasar';
|
|
||||||
import { computed, onMounted } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
|
||||||
import { useTimesheetApi } from '../composables/api/use-timesheet-api';
|
|
||||||
import { formatPayPeriodLabel } from '../utils/timesheet-format.util';
|
|
||||||
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
|
||||||
|
|
||||||
//------------------- stores -------------------
|
|
||||||
const { locale } = useI18n();
|
|
||||||
const auth_store = useAuthStore();
|
|
||||||
const timesheet_api = useTimesheetApi();
|
|
||||||
|
|
||||||
//------------------- pay-period format label -------------------
|
|
||||||
const date_options: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'long', year: 'numeric' };
|
|
||||||
|
|
||||||
const pay_period_label = computed(() => formatPayPeriodLabel(
|
|
||||||
pay_period.label,
|
|
||||||
locale.value,
|
|
||||||
date.extractDate,
|
|
||||||
date_options
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await timesheet_api.getPayPeriodDetailsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-page
|
|
||||||
padding
|
|
||||||
class="q-pa-md bg-secondary"
|
|
||||||
>
|
|
||||||
<PageHeaderTemplate
|
|
||||||
:title="$t('timesheet.title')"
|
|
||||||
:start-date="pay_period_label.start_date"
|
|
||||||
:end-date="pay_period_label.end_date"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
@ -3,7 +3,7 @@ import type { UpsertShift } from "src/modules/timesheets/models/shift.models";
|
||||||
import type { PayPeriod } from "src/modules/shared/models/pay-period.models";
|
import type { PayPeriod } from "src/modules/shared/models/pay-period.models";
|
||||||
import type { PayPeriodDetails } from "src/modules/timesheets/models/pay-period-details.models";
|
import type { PayPeriodDetails } from "src/modules/timesheets/models/pay-period-details.models";
|
||||||
import type { PayPeriodOverview } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
import type { PayPeriodOverview } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
||||||
import type { PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
|
import type { Expense, PayPeriodExpenses, UpsertExpense } from "src/modules/timesheets/models/expense.models";
|
||||||
|
|
||||||
export const timesheetService = {
|
export const timesheetService = {
|
||||||
getPayPeriodDetailsByEmployeeEmail: async (email: string): Promise<PayPeriodDetails> => {
|
getPayPeriodDetailsByEmployeeEmail: async (email: string): Promise<PayPeriodDetails> => {
|
||||||
|
|
@ -31,8 +31,20 @@ export const timesheetService = {
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
upsertOrDeletePayPeriodDetailsByDateAndEmployeeEmail: async (email: string, payload: UpsertShift[] | PayPeriodExpenses, pay_period: PayPeriod, date?: string): Promise<PayPeriodDetails> => {
|
getExpensesByPayPeriodAndEmployeeEmail: async (email: string, year: string, period_number: string): Promise<PayPeriodExpenses> => {
|
||||||
if (date) return (await api.put(`/shifts/upsert/${email}/${date}`, payload)).data;
|
const response = await api.get(`/expenses/${email}/${year}/${period_number}`);
|
||||||
else return (await api.put(`/expenses/${email}/${pay_period.pay_year}/${pay_period.pay_period_no}`, payload, { headers: {'Content-Type': 'application/json'}})).data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
upsertOrDeleteShiftsByDateAndEmployeeEmail: async (email: string, payload: UpsertShift[], date: string): Promise<PayPeriodDetails> => {
|
||||||
|
const response = await api.put(`/shifts/upsert/${email}/${date}`, payload);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail: async (email: string, date: string, payload: UpsertExpense): Promise<Expense[]> => {
|
||||||
|
const headers = { 'Content-Type': 'application/json' }
|
||||||
|
|
||||||
|
const response = await api.put(`/expenses/upsert/${email}/${date}`, payload, { headers });
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type { Expense, ExpenseTotals, ExpenseType, PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
|
import type { Expense, ExpenseTotals } from "src/modules/timesheets/models/expense.models";
|
||||||
|
|
||||||
//------------------ normalization / icons ------------------
|
//------------------ normalization / icons ------------------
|
||||||
export const normExpenseType = (type: unknown): string =>
|
export const normExpenseType = (type: unknown): string =>
|
||||||
String(type ?? '').trim().toUpperCase();
|
typeof type === 'string' ? type.trim().toUpperCase() : '';
|
||||||
|
|
||||||
const icon_map: Record<string,string> = {
|
const icon_map: Record<string,string> = {
|
||||||
MILEAGE: 'time_to_leave',
|
MILEAGE: 'time_to_leave',
|
||||||
|
|
@ -31,7 +31,7 @@ export const computeExpenseTotals = (items: readonly Expense[]): ExpenseTotals =
|
||||||
);
|
);
|
||||||
|
|
||||||
//------------------ Quasar :rules=[] ------------------
|
//------------------ Quasar :rules=[] ------------------
|
||||||
export const makeExpenseRules = (t: (key: string) => string, max_comment_char: number) => {
|
export const makeExpenseRules = (t: (_key: string) => string) => {
|
||||||
const isPresent = (val: unknown) => val !== undefined && val !== null && val !== '';
|
const isPresent = (val: unknown) => val !== undefined && val !== null && val !== '';
|
||||||
|
|
||||||
const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
|
const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
|
||||||
|
|
@ -40,15 +40,12 @@ export const makeExpenseRules = (t: (key: string) => string, max_comment_char: n
|
||||||
|
|
||||||
const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.mileage_required_for_type');
|
const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.mileage_required_for_type');
|
||||||
|
|
||||||
const commentRequired = (val: unknown) => (String(val ?? '').trim().length > 0) || t('timesheet.expense.errors.comment_required');
|
const commentRequired = (val: unknown) => (typeof val === 'string' ? val.trim().length > 0 : false ) || 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 {
|
return {
|
||||||
typeRequired,
|
typeRequired,
|
||||||
amountRequired,
|
amountRequired,
|
||||||
mileageRequired,
|
mileageRequired,
|
||||||
commentRequired,
|
commentRequired,
|
||||||
commentTooLong,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { PayPeriodLabel } from "../types/ui.types";
|
import type { PayPeriodLabel } from "src/modules/timesheets/models/ui.models";
|
||||||
|
|
||||||
export const formatPayPeriodLabel = (
|
export const formatPayPeriodLabel = (
|
||||||
raw_label: string | undefined,
|
raw_label: string | undefined,
|
||||||
|
|
|
||||||
46
src/pages/timesheet-approval-page.vue
Normal file
46
src/pages/timesheet-approval-page.vue
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { date } from 'quasar';
|
||||||
|
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||||
|
import OverviewList from 'src/modules/timesheet-approval/components/overview-list.vue';
|
||||||
|
import DetailscrudDialog from 'src/modules/timesheet-approval/components/details-crud-dialog.vue';
|
||||||
|
|
||||||
|
const timesheet_approval_api = useTimesheetApprovalApi();
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const is_details_dialog_open = ref(false);
|
||||||
|
const employee_email = ref('');
|
||||||
|
|
||||||
|
const onDetailsClicked = (email: string) => {
|
||||||
|
employee_email.value = email;
|
||||||
|
is_details_dialog_open.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted( async () => {
|
||||||
|
await timesheet_approval_api.getPayPeriodOverviewsByDate(date.formatDate( new Date(), 'YYYY-MM-DD'));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-page
|
||||||
|
padding
|
||||||
|
class="q-pa-md bg-secondary "
|
||||||
|
>
|
||||||
|
<PageHeaderTemplate
|
||||||
|
title="timesheet_approvals.page_title"
|
||||||
|
:start-date="timesheet_store.pay_period.period_start"
|
||||||
|
:end-date="timesheet_store.pay_period.period_end"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailscrudDialog
|
||||||
|
v-model:dialog="is_details_dialog_open"
|
||||||
|
:employee-email="employee_email"
|
||||||
|
:is-loading="timesheet_store.is_loading"
|
||||||
|
:employee-overview="timesheet_store.current_pay_period_overview"
|
||||||
|
:timesheet-details="timesheet_store.pay_period_details"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OverviewList @clickedDetailsButton="onDetailsClicked"/>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
37
src/pages/timesheet-page.vue
Normal file
37
src/pages/timesheet-page.vue
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { date } from 'quasar';
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
|
import { useTimesheetApi } from 'src/modules/timesheets/composables/api/use-timesheet-api';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||||
|
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||||
|
|
||||||
|
const { user } = useAuthStore();
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const timesheet_api = useTimesheetApi();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await timesheet_api.getPayPeriodDetailsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-page
|
||||||
|
padding
|
||||||
|
class="q-pa-md bg-secondary"
|
||||||
|
>
|
||||||
|
<PageHeaderTemplate
|
||||||
|
:title="$t('timesheet.page_header')"
|
||||||
|
:start-date="timesheet_store.pay_period.period_start"
|
||||||
|
:end-date="timesheet_store.pay_period.period_end"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TimesheetWrapper :employee-email="user.email" />
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
@ -15,22 +15,22 @@ const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: 'timesheet-approvals',
|
path: 'timesheet-approvals',
|
||||||
name: RouteNames.TIMESHEET_APPROVALS,
|
name: RouteNames.TIMESHEET_APPROVALS,
|
||||||
component: () => import('src/modules/timesheet-approval/pages/timesheet-approval.vue'),
|
component: () => import('src/pages/timesheet-approval-page.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'employees',
|
path: 'employees',
|
||||||
name: RouteNames.EMPLOYEE_LIST,
|
name: RouteNames.EMPLOYEE_LIST,
|
||||||
component: () => import('src/modules/employee-list/pages/supervisor-crew-page.vue'),
|
component: () => import('src/pages/supervisor-crew-page.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'timesheet-temp',
|
path: 'timesheet-temp',
|
||||||
name: RouteNames.TIMESHEET_TEMP,
|
name: RouteNames.TIMESHEET_TEMP,
|
||||||
component: () => import('src/modules/timesheets/pages/timesheet-details-overview.vue')
|
component: () => import('src/pages/timesheet-page.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'user/profile',
|
path: 'user/profile',
|
||||||
name: RouteNames.PROFILE,
|
name: RouteNames.PROFILE,
|
||||||
component: () => import('src/modules/profile/pages/profile-container.vue'),
|
component: () => import('src/pages/profile-page.vue'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { AuthService } from "../modules/auth/services/services-auth";
|
import { AuthService } from "../modules/auth/services/services-auth";
|
||||||
import type { User } from "src/modules/shared/types/user-interface";
|
import type { User } from "src/modules/shared/models/user.models";
|
||||||
|
|
||||||
export type CompanyRole = 'guest' | 'supervisor' | 'accounting' | 'human_resources' | 'employee';
|
export type CompanyRole = 'guest' | 'supervisor' | 'accounting' | 'human_resources' | 'employee';
|
||||||
|
|
||||||
const TestUsers: Record<CompanyRole, User> = {
|
const TestUsers: Record<CompanyRole, User> = {
|
||||||
guest: { firstName: 'Unknown', lastName: 'Unknown', email: 'guest@guest.com', role: 'guest' },
|
guest: { firstName: 'Unknown', lastName: 'Unknown', email: 'guest@guest.com', role: 'guest' },
|
||||||
supervisor: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
supervisor: { firstName: 'User', lastName: 'Test', email: 'user@targointernet.com', role: 'supervisor' },
|
||||||
accounting: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
accounting: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
||||||
human_resources: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
human_resources: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
||||||
employee: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
employee: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
const user = ref(TestUsers.guest);
|
const user = ref<User>(TestUsers.guest);
|
||||||
const authError = ref("");
|
const authError = ref("");
|
||||||
const isAuthorizedUser = computed(() => user.value.role !== 'guest');
|
const isAuthorizedUser = computed(() => user.value.role !== 'guest');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,86 +1,103 @@
|
||||||
import { ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
import { default_expense, default_pay_period_expenses, type Expense, type PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
|
import { default_expense, default_pay_period_expenses, type UpsertExpense, type Expense, type PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
|
||||||
import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
|
||||||
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
|
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
|
||||||
import { ExpensesApiError } from "src/modules/timesheets/models/expense.validation";
|
import { ExpensesApiError, type GenericApiError } from "src/modules/timesheets/models/expense.validation";
|
||||||
|
import { computeExpenseTotals } from "src/modules/timesheets/utils/expense.util";
|
||||||
|
import type { UpsertAction } from "src/modules/timesheets/models/shift.models";
|
||||||
|
|
||||||
const { pay_period } = useTimesheetStore();
|
|
||||||
|
|
||||||
const encodeData = ( email: string, year: number, period_number: number ) => {
|
|
||||||
return { email: encodeURIComponent(email), year: encodeURIComponent(year), period_number: encodeURIComponent(period_number)};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useExpensesStore = defineStore('expenses', () => {
|
export const useExpensesStore = defineStore('expenses', () => {
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
const is_open = ref(false);
|
const is_open = ref(false);
|
||||||
const is_loading = ref(false);
|
const is_loading = ref(false);
|
||||||
|
const mode = ref<UpsertAction>('create');
|
||||||
const pay_period_expenses = ref<PayPeriodExpenses>(default_pay_period_expenses);
|
const pay_period_expenses = ref<PayPeriodExpenses>(default_pay_period_expenses);
|
||||||
|
const pay_period_expenses_totals = computed(() => computeExpenseTotals(pay_period_expenses.value.expenses))
|
||||||
const current_expense = ref<Expense>(default_expense);
|
const current_expense = ref<Expense>(default_expense);
|
||||||
const initial_expense = ref<Expense>(default_expense);
|
const initial_expense = ref<Expense>(default_expense);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
const setErrorFrom = (err: unknown) => {
|
// const setErrorFrom = (err: unknown) => {
|
||||||
const e = err as any;
|
// const e = err as any;
|
||||||
error.value = e?.message || 'Unknown error';
|
// error.value = e?.message || 'Unknown error';
|
||||||
};
|
// };
|
||||||
|
|
||||||
const open = async (employee_email: string): Promise<void> => {
|
const open = async (employee_email: string): Promise<void> => {
|
||||||
is_open.value = true;
|
is_open.value = true;
|
||||||
is_loading.value = true;
|
is_loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
current_expense.value = default_expense;
|
||||||
|
initial_expense.value = default_expense;
|
||||||
|
|
||||||
await getPayPeriodExpensesByEmployeeEmail(employee_email);
|
await getPayPeriodExpensesByEmployeeEmail(employee_email);
|
||||||
is_loading.value = false;
|
is_loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPayPeriodExpensesByEmployeeEmail = async (employee_email: string): Promise<void> => {
|
|
||||||
const encoded_data = encodeData(employee_email, pay_period.pay_year, pay_period.pay_period_no);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const expenses = await timesheetService.getExpensesByPayPeriodAndEmployeeEmail(encoded_data.email, encoded_data.year, encoded_data.period_number);
|
|
||||||
pay_period_expenses.value = expenses;
|
|
||||||
} catch(err:any) {
|
|
||||||
const status_code: number = err?.response?.status ?? 500;
|
|
||||||
const data = err?.response?.data ?? {};
|
|
||||||
error.value = data.message || data.error || err.message;
|
|
||||||
|
|
||||||
throw new ExpensesApiError({
|
|
||||||
status_code,
|
|
||||||
error_code: data.error_code,
|
|
||||||
message: data.message || data.error || err.message,
|
|
||||||
context: data.context,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const upsertOrDeleteExpensesByEmployeeEmail = async (employee_email: string, expenses: Expense[]): Promise<void> => {
|
|
||||||
is_loading.value = true;
|
|
||||||
error.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const encoded_data = encodeData(employee_email, pay_period.pay_year, pay_period.pay_period_no);
|
|
||||||
const payload = { is_approved: false, expenses };
|
|
||||||
|
|
||||||
const updated_expenses = await timesheetService.upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail(encoded_data.email, encoded_data.year, encoded_data.period_number, payload);
|
|
||||||
pay_period_expenses.value.expenses = updated_expenses;
|
|
||||||
is_open.value = false;
|
|
||||||
} catch (err) {
|
|
||||||
setErrorFrom(err);
|
|
||||||
} finally {
|
|
||||||
is_loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
error.value = null;
|
error.value = null;
|
||||||
is_open.value = false;
|
is_open.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPayPeriodExpensesByEmployeeEmail = async (employee_email: string): Promise<void> => {
|
||||||
|
is_loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const expenses = await timesheetService.getExpensesByPayPeriodAndEmployeeEmail(
|
||||||
|
encodeURIComponent(employee_email),
|
||||||
|
encodeURIComponent(timesheet_store.pay_period.pay_year),
|
||||||
|
encodeURIComponent(timesheet_store.pay_period.pay_period_no),
|
||||||
|
);
|
||||||
|
pay_period_expenses.value = expenses;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (typeof err === 'object') {
|
||||||
|
const error = err as GenericApiError;
|
||||||
|
const status_code: number = error.status_code ?? 500;
|
||||||
|
// const data = error.context ?? '';
|
||||||
|
// error.value = data.message || data.error || err.message;
|
||||||
|
|
||||||
|
throw new ExpensesApiError({
|
||||||
|
status_code,
|
||||||
|
// error_code: data.error_code,
|
||||||
|
// message: data.message || data.error || err.message,
|
||||||
|
// context: data.context,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
is_loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const upsertOrDeleteExpensesByEmployeeEmail = async (employee_email: string, date: string, expense: UpsertExpense): Promise<void> => {
|
||||||
|
is_loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated_expenses = await timesheetService.upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail(
|
||||||
|
encodeURIComponent(employee_email),
|
||||||
|
encodeURIComponent(date),
|
||||||
|
expense,
|
||||||
|
);
|
||||||
|
console.log('updated expenses received: ', updated_expenses)
|
||||||
|
pay_period_expenses.value.expenses = updated_expenses;
|
||||||
|
} catch (err) {
|
||||||
|
// setErrorFrom(err);
|
||||||
|
console.log('error doing some expense thing: ', err)
|
||||||
|
} finally {
|
||||||
|
is_loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
is_open,
|
is_open,
|
||||||
is_loading,
|
is_loading,
|
||||||
|
mode,
|
||||||
pay_period_expenses,
|
pay_period_expenses,
|
||||||
|
pay_period_expenses_totals,
|
||||||
current_expense,
|
current_expense,
|
||||||
initial_expense,
|
initial_expense,
|
||||||
error,
|
error,
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ import { defineStore } from "pinia";
|
||||||
import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
||||||
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
|
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
import { default_shift, type UpsertAction, type Shift, UpsertShift } from "src/modules/timesheets/models/shift.models";
|
import { default_shift, type UpsertAction, type Shift, type UpsertShift } from "src/modules/timesheets/models/shift.models";
|
||||||
import { GenericApiError } from "src/modules/timesheets/models/expense.validation";
|
|
||||||
|
|
||||||
export const useShiftStore = defineStore('shift', () => {
|
export const useShiftStore = defineStore('shift', () => {
|
||||||
const is_open = ref(false);
|
const is_open = ref(false);
|
||||||
|
|
@ -31,7 +30,7 @@ export const useShiftStore = defineStore('shift', () => {
|
||||||
open('update', date, shift, unwrapAndClone(shift));
|
open('update', date, shift, unwrapAndClone(shift));
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDelete = (date: string, shift: any) => {
|
const openDelete = (date: string, shift: Shift) => {
|
||||||
open('delete', date, default_shift, shift);
|
open('delete', date, default_shift, shift);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,17 +47,18 @@ export const useShiftStore = defineStore('shift', () => {
|
||||||
const encoded_date = encodeURIComponent(current_shift.value.date);
|
const encoded_date = encodeURIComponent(current_shift.value.date);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await timesheetService.upsertOrDeletePayPeriodShifts(encoded_email, encoded_date, [ upsert_shift, ]);
|
const result = await timesheetService.upsertOrDeleteShiftsByDateAndEmployeeEmail(encoded_email, [ upsert_shift, ], encoded_date);
|
||||||
timesheet_store.pay_period_details = result;
|
timesheet_store.pay_period_details = result;
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
const status_code: number = err?.response?.status ?? 500;
|
console.log('error doing thing: ', err)
|
||||||
const data = err?.response?.data ?? {};
|
// const status_code: number = err?.response?.status ?? 500;
|
||||||
throw new GenericApiError({
|
// const data = err?.response?.data ?? {};
|
||||||
status_code,
|
// throw new GenericApiError({
|
||||||
error_code: data.error_code,
|
// status_code,
|
||||||
message: data.message || data.error || err.message,
|
// error_code: data.error_code,
|
||||||
context: data.context,
|
// message: data.message || data.error || err.message,
|
||||||
});
|
// context: data.context,
|
||||||
|
// });
|
||||||
} finally {
|
} finally {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,93 +4,91 @@ import { withLoading } from 'src/utils/store-helpers';
|
||||||
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/timesheet-approval-service';
|
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/timesheet-approval-service';
|
||||||
import { timesheetService } from 'src/modules/timesheets/services/timesheet-service';
|
import { timesheetService } from 'src/modules/timesheets/services/timesheet-service';
|
||||||
import { default_pay_period_overview, type PayPeriodOverview } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
import { default_pay_period_overview, type PayPeriodOverview } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
||||||
import { default_pay_period, type PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
import { default_pay_period, type PayPeriod } from 'src/modules/shared/models/pay-period.models';
|
||||||
import { default_pay_period_details, type PayPeriodDetails } from 'src/modules/timesheets/models/pay-period-details.models';
|
import { default_pay_period_details, type PayPeriodDetails } from 'src/modules/timesheets/models/pay-period-details.models';
|
||||||
import { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
||||||
|
|
||||||
export const useTimesheetStore = defineStore('timesheet', () => {
|
export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
const is_loading = ref<boolean>(false);
|
const is_loading = ref<boolean>(false);
|
||||||
const pay_period = ref<PayPeriod>(default_pay_period);
|
const pay_period = ref<PayPeriod>(default_pay_period);
|
||||||
const pay_period_overviews = ref<PayPeriodOverview[]>([ default_pay_period_overview, ]);
|
const pay_period_overviews = ref<PayPeriodOverview[]>([default_pay_period_overview,]);
|
||||||
const current_pay_period_overview = ref<PayPeriodOverview>(default_pay_period_overview);
|
const current_pay_period_overview = ref<PayPeriodOverview>(default_pay_period_overview);
|
||||||
const pay_period_details = ref<PayPeriodDetails>(default_pay_period_details);
|
const pay_period_details = ref<PayPeriodDetails>(default_pay_period_details);
|
||||||
const pay_period_report = ref();
|
const pay_period_report = ref();
|
||||||
const is_calendar_limit = computed( ()=>
|
const is_calendar_limit = computed(() =>
|
||||||
pay_period.value.pay_year === 2024 &&
|
pay_period.value.pay_year === 2024 &&
|
||||||
pay_period.value.pay_period_no <= 1
|
pay_period.value.pay_period_no <= 1
|
||||||
);
|
);
|
||||||
|
|
||||||
const getPayPeriodByDateOrYearAndNumber = (date_or_year: string | number, period_number?: number): Promise<boolean> => {
|
const getPayPeriodByDateOrYearAndNumber = async (date_or_year: string | number, period_number?: number): Promise<boolean> => {
|
||||||
return withLoading( is_loading, async () => {
|
is_loading.value = true;
|
||||||
try {
|
|
||||||
let response;
|
|
||||||
|
|
||||||
if (typeof date_or_year === 'string') {
|
try {
|
||||||
response = await timesheetService.getPayPeriodByDate(date_or_year);
|
if (typeof date_or_year === 'string') {
|
||||||
return true;
|
pay_period.value = await timesheetService.getPayPeriodByDate(date_or_year);
|
||||||
}
|
|
||||||
else if ( typeof date_or_year === 'number' && period_number ) {
|
|
||||||
response = await timesheetService.getPayPeriodByYearAndPeriodNumber(date_or_year, period_number);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else response = default_pay_period;
|
|
||||||
|
|
||||||
pay_period.value = response;
|
|
||||||
return false;
|
|
||||||
} catch(error){
|
|
||||||
console.error('Could not get current pay period: ', error );
|
|
||||||
pay_period.value = default_pay_period;
|
|
||||||
pay_period_overviews.value = [ default_pay_period_overview, ];
|
|
||||||
//TODO: More in-depth error-handling here
|
|
||||||
}
|
}
|
||||||
|
else if (typeof date_or_year === 'number' && period_number) {
|
||||||
|
pay_period.value = await timesheetService.getPayPeriodByYearAndPeriodNumber(date_or_year, period_number);
|
||||||
|
}
|
||||||
|
else pay_period.value = default_pay_period;
|
||||||
|
is_loading.value = false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Could not get current pay period: ', error);
|
||||||
|
pay_period.value = default_pay_period;
|
||||||
|
pay_period_overviews.value = [default_pay_period_overview,];
|
||||||
|
//TODO: More in-depth error-handling here
|
||||||
|
is_loading.value = false;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const getPayPeriodDetailsByEmployeeEmail = async (employee_email: string) => {
|
|
||||||
return withLoading( is_loading, async () => {
|
|
||||||
try {
|
|
||||||
const response = await timesheetService.getPayPeriodDetailsByPayPeriodAndEmployeeEmail(
|
|
||||||
pay_period.value.pay_year,
|
|
||||||
pay_period.value.pay_period_no,
|
|
||||||
employee_email
|
|
||||||
);
|
|
||||||
pay_period_details.value = response;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('There was an error retrieving timesheet details for this employee: ', error);
|
|
||||||
// TODO: More in-depth error-handling here
|
|
||||||
}
|
|
||||||
|
|
||||||
pay_period_details.value = default_pay_period_details;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPayPeriodOverviewsBySupervisorEmail = async (pay_year: number, period_number: number, supervisor_email: string): Promise<boolean> => {
|
const getPayPeriodOverviewsBySupervisorEmail = async (pay_year: number, period_number: number, supervisor_email: string): Promise<boolean> => {
|
||||||
return withLoading( is_loading, async () => {
|
is_loading.value = true;
|
||||||
try {
|
|
||||||
const response = await timesheetApprovalService.getPayPeriodOverviewsBySupervisorEmail( pay_year, period_number, supervisor_email );
|
|
||||||
pay_period_overviews.value = response;
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('There was an error retrieving Employee Pay Period overviews: ', error);
|
|
||||||
pay_period_overviews.value = [ default_pay_period_overview, ];
|
|
||||||
// TODO: More in-depth error-handling here
|
|
||||||
}
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await timesheetApprovalService.getPayPeriodOverviewsBySupervisorEmail(pay_year, period_number, supervisor_email);
|
||||||
|
pay_period_overviews.value = response.employees_overview;
|
||||||
|
is_loading.value = false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('There was an error retrieving Employee Pay Period overviews: ', error);
|
||||||
|
pay_period_overviews.value = [default_pay_period_overview,];
|
||||||
|
// TODO: More in-depth error-handling here
|
||||||
|
is_loading.value = false;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPayPeriodDetailsByEmployeeEmail = async (employee_email: string) => {
|
||||||
|
is_loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await timesheetService.getPayPeriodDetailsByPayPeriodAndEmployeeEmail(
|
||||||
|
pay_period.value.pay_year,
|
||||||
|
pay_period.value.pay_period_no,
|
||||||
|
employee_email
|
||||||
|
);
|
||||||
|
pay_period_details.value = response;
|
||||||
|
console.log('pay period details: ', response, pay_period_details.value.employee_full_name)
|
||||||
|
is_loading.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('There was an error retrieving timesheet details for this employee: ', error);
|
||||||
|
// TODO: More in-depth error-handling here
|
||||||
|
pay_period_details.value = default_pay_period_details;
|
||||||
|
is_loading.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPayPeriodReportByYearAndPeriodNumber = async (year: number, period_number: number, report_filters?: TimesheetApprovalCSVReportFilters) => {
|
const getPayPeriodReportByYearAndPeriodNumber = async (year: number, period_number: number, report_filters?: TimesheetApprovalCSVReportFilters) => {
|
||||||
return withLoading( is_loading, async () => {
|
return withLoading(is_loading.value, async () => {
|
||||||
try {
|
try {
|
||||||
const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber(
|
const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber(
|
||||||
year,
|
year,
|
||||||
period_number,
|
period_number,
|
||||||
report_filters
|
report_filters
|
||||||
);
|
);
|
||||||
pay_period_report.value = response;
|
pay_period_report.value = response;
|
||||||
|
|
@ -105,10 +103,10 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
is_loading,
|
is_loading,
|
||||||
is_calendar_limit,
|
is_calendar_limit,
|
||||||
pay_period,
|
pay_period,
|
||||||
pay_period_overviews,
|
pay_period_overviews,
|
||||||
current_pay_period_overview,
|
current_pay_period_overview,
|
||||||
pay_period_details,
|
pay_period_details,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
export type Normalizer<T> = {
|
export type Normalizer<T> = {
|
||||||
[K in keyof T]: (val: unknown) => T[K];
|
[K in keyof T]: (_val: unknown) => T[K];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const normalizeObject = <T>(raw: any, schema: Normalizer<T>): T => {
|
export const normalizeObject = <T>(raw: Partial<Record<keyof T, unknown>>, schema: Normalizer<T>): T => {
|
||||||
const result = {} as T;
|
const result = {} as T;
|
||||||
for (const key in schema) {
|
for (const key in schema) {
|
||||||
result[key] = schema[key](raw[key]);
|
result[key] = schema[key](raw[key]);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export const withLoading = async <T>( loading_state: boolean, fn: () => Promise<T> ) => {
|
export const withLoading = async <T>( loading_state: boolean, fn: () => Promise<T> ) => {
|
||||||
loading_state = true;
|
loading_state = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await fn();
|
return await fn();
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export const toQSelectOptions = <T>(values: readonly T[], i18n_domain?: string): { label: string; value: T }[] => {
|
export const toQSelectOptions = <T>(values: readonly T[], i18n_domain?: string): { label: string; value: T }[] => {
|
||||||
return values.map(value => ({
|
return values.map(value => ({
|
||||||
label: ((i18n_domain ?? "") + value).toString(),
|
label: ((i18n_domain ?? "") + value).toString(),
|
||||||
value: value as T
|
value: value
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
@ -7,8 +7,8 @@ export const unwrapAndClone = <T extends object>(obj: T): T => {
|
||||||
const raw = isProxy(obj) ? toRaw(obj) : obj;
|
const raw = isProxy(obj) ? toRaw(obj) : obj;
|
||||||
|
|
||||||
// Use structuredClone if available (handles Dates, Maps, Sets, circulars)
|
// Use structuredClone if available (handles Dates, Maps, Sets, circulars)
|
||||||
if (typeof (globalThis as any).structuredClone === "function") {
|
if (typeof globalThis.structuredClone === "function") {
|
||||||
return (globalThis as any).structuredClone(raw);
|
return globalThis.structuredClone(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback for older environments (loses Dates, Sets, Maps)
|
// Fallback for older environments (loses Dates, Sets, Maps)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user