feat(docker): Add/Correct Dockerfile for remote Docker Lab deployment
This commit is contained in:
parent
33061ef2ab
commit
6c6cecbe7d
23
Dockerfile
Normal file
23
Dockerfile
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# targo-frontend
|
||||
FROM node:22
|
||||
|
||||
# Set working directory inside container
|
||||
WORKDIR /app
|
||||
|
||||
# Set environment variables
|
||||
ENV VITE_TARGO_BACKEND_URL="http://targo-backend:3000"
|
||||
|
||||
# Copy package.json & package-lock.json first (for caching)
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy the rest of the code
|
||||
COPY . .
|
||||
|
||||
# Expose Quasar dev port
|
||||
EXPOSE 9000
|
||||
|
||||
# Default command
|
||||
CMD ["quasar", "dev"]
|
||||
|
|
@ -15,7 +15,7 @@ declare module 'vue' {
|
|||
// "export default () => {}" function below (which runs individually
|
||||
// for each client)
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_TARGO_BACKEND_AUTH_URL,
|
||||
baseURL: import.meta.env.VITE_TARGO_BACKEND_URL,
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// app global css in SCSS form
|
||||
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100) {
|
||||
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100, 200) {
|
||||
.rounded-#{$size} {
|
||||
border-radius: #{$size}px !important;
|
||||
}
|
||||
|
|
@ -36,4 +36,9 @@ body.body--dark {
|
|||
|
||||
.shift-highlight {
|
||||
background: #0195462a;
|
||||
}
|
||||
|
||||
.frosted-glass {
|
||||
background-color: #FFFA !important;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
|
@ -27,6 +27,8 @@ $layout-shadow-dark : 0 0 10px 5px rgba($dark-shadow-color, 0.5);
|
|||
|
||||
$input-text-color : #455A64;
|
||||
$input-autofill-color : #AAD5C4;
|
||||
$field-dense-label-top : 5px !default;
|
||||
$field-dense-label-font-size : 16px !default;
|
||||
|
||||
|
||||
$dark : #42444b;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
<template>
|
||||
<q-drawer
|
||||
v-model="ui_store.isRightDrawerOpen"
|
||||
v-model="ui_store.is_left_drawer_open"
|
||||
persistent
|
||||
mini-to-overlay
|
||||
elevated
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useAuthApi } from 'src/modules/auth/composables/use-auth-api';
|
||||
|
||||
|
|
@ -6,16 +9,20 @@
|
|||
|
||||
const email = defineModel<string>('email', { default: '', });
|
||||
const is_remembered = ref<boolean>(false);
|
||||
const is_employee_email = computed( () => email.value.includes('@targ'));
|
||||
const is_employee_email = computed(() => email.value.includes('@targ'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card class="rounded-15">
|
||||
<q-card class="rounded-15 shadow-10">
|
||||
<q-card-section class="text-center bg-primary q-pa-lg">
|
||||
<q-img src="/src/assets/logo-targo-white.svg" ratio="4.6" fit="contain" />
|
||||
<q-img
|
||||
src="/src/assets/logo-targo-white.svg"
|
||||
ratio="4.6"
|
||||
fit="contain"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<div class="q-pt-sm q-px-xl q-pb-lg">
|
||||
<div class="q-pt-sm q-px-xl q-pb-lg ">
|
||||
<q-card-section class="text-center text-uppercase">
|
||||
<div class="text-h6 text-weight-bold">
|
||||
{{ $t('login.page_header') }}
|
||||
|
|
@ -23,34 +30,40 @@
|
|||
</q-card-section>
|
||||
|
||||
<q-form @submit="auth_api.login">
|
||||
<q-input
|
||||
v-model="email"
|
||||
dense
|
||||
outlined
|
||||
label-color="primary"
|
||||
:label="$t('login.email')"
|
||||
/>
|
||||
<q-input
|
||||
v-model="email"
|
||||
dense
|
||||
outlined
|
||||
label-color="primary"
|
||||
class="rounded-5 inset-shadow bg-blue-grey-1"
|
||||
label-slot
|
||||
input-class="text-weight-medium text-h6"
|
||||
>
|
||||
<template #label>
|
||||
<span class="text-weight-bolder text-uppercase text-overline"> {{ $t('login.email') }} </span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-card-section class="q-ma-none q-pa-none text-uppercase text-caption text-weight-medium">
|
||||
<q-toggle
|
||||
v-model="is_remembered"
|
||||
color="primary"
|
||||
:label="$t('login.button.remember_me')"
|
||||
<q-toggle
|
||||
v-model="is_remembered"
|
||||
color="primary"
|
||||
:label="$t('login.button.remember_me')"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
|
||||
<q-card-actions>
|
||||
<q-btn
|
||||
push
|
||||
rounded
|
||||
disabled
|
||||
type="submit"
|
||||
color="primary"
|
||||
:label="$t('login.button.connect')"
|
||||
class="full-width"
|
||||
<q-btn
|
||||
push
|
||||
rounded
|
||||
disabled
|
||||
type="submit"
|
||||
color="primary"
|
||||
:label="$t('login.button.connect')"
|
||||
class="full-width"
|
||||
/>
|
||||
</q-card-actions>
|
||||
|
||||
|
||||
<!-- A implémenter plus tard sans doute, pour les clients. A revoir avec Authentik API pour création de users -->
|
||||
<!-- <q-card-section class="text-center q-pa-none q-mt-none">
|
||||
<RouterLink disabled class="text-primary" to="/signup">{{ $t('loginPage.signUp') }}</RouterLink>
|
||||
|
|
@ -58,39 +71,49 @@
|
|||
</q-form>
|
||||
|
||||
<q-card-section class="row q-pt-sm">
|
||||
<q-separator color="primary" class="col self-center"/>
|
||||
<span class="col text-primary text-weight-bolder text-center text-uppercase self-center">{{ $t('shared.misc.or') }}</span>
|
||||
<q-separator color="primary" class="col self-center"/>
|
||||
<q-separator
|
||||
color="primary"
|
||||
class="col self-center"
|
||||
/>
|
||||
<span class="col text-primary text-weight-bolder text-center text-uppercase self-center">{{
|
||||
$t('shared.misc.or') }}</span>
|
||||
<q-separator
|
||||
color="primary"
|
||||
class="col self-center"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="column q-px-sm q-pt-none">
|
||||
<q-btn
|
||||
rounded
|
||||
push
|
||||
disabled
|
||||
color="fb-blue"
|
||||
<q-btn
|
||||
rounded
|
||||
push
|
||||
disabled
|
||||
color="fb-blue"
|
||||
icon="img:src/assets/Facebook-f_Logo-White-Logo.wine.svg"
|
||||
:label="$t('login.button.facebook')"
|
||||
class="full-width row q-mb-sm"
|
||||
:label="$t('login.button.facebook')"
|
||||
class="full-width row q-mb-sm"
|
||||
>
|
||||
<q-tooltip anchor="top middle" class="bg-primary">{{$t('login.tooltip.coming_soon')}}</q-tooltip>
|
||||
<q-tooltip
|
||||
anchor="top middle"
|
||||
class="bg-primary"
|
||||
>{{ $t('login.tooltip.coming_soon') }}</q-tooltip>
|
||||
</q-btn>
|
||||
<q-slide-transition>
|
||||
<div v-if="is_employee_email">
|
||||
<transition
|
||||
slow
|
||||
enter-active-class="animated zoomIn"
|
||||
<transition
|
||||
slow
|
||||
enter-active-class="animated zoomIn"
|
||||
leave-active-class="animated zoomOut"
|
||||
>
|
||||
<q-btn
|
||||
push
|
||||
rounded
|
||||
color="primary"
|
||||
icon="img:src/assets/logo-targo-simple.svg"
|
||||
:label="$t('login.button.employee')"
|
||||
class="full-width row"
|
||||
@click="auth_api.oidcLogin"
|
||||
/>
|
||||
<q-btn
|
||||
push
|
||||
rounded
|
||||
color="primary"
|
||||
icon="img:src/assets/logo-targo-simple.svg"
|
||||
:label="$t('login.button.employee')"
|
||||
class="full-width row"
|
||||
@click="auth_api.oidcLogin"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
</q-slide-transition>
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
defineProps<{
|
||||
isLoading: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card
|
||||
v-if="isLoading"
|
||||
flat
|
||||
class="column flex-center rounded-10"
|
||||
style="width: 20vw !important; height: 20vh !important;"
|
||||
>
|
||||
<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.label.loading') }}
|
||||
</div>
|
||||
</q-card>
|
||||
</template>
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
>
|
||||
<q-badge
|
||||
v-if="expense.is_approved"
|
||||
class="absolute z-top rounded-20 bg-white q-pa-none"
|
||||
class="absolute z-top rounded-20 bg-dark q-pa-none"
|
||||
style="transform: translate(-15px, -15px);"
|
||||
>
|
||||
<q-icon
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
{ type: 'REGULAR', color: 'secondary', label_type: 'timesheet.shift.types.REGULAR' },
|
||||
{ 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' },
|
||||
|
|
|
|||
|
|
@ -2,151 +2,196 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { ref } from 'vue';
|
||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
/* eslint-disable*/
|
||||
import { onMounted, ref, useTemplateRef } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { QSelect } from 'quasar';
|
||||
import { Shift, ShiftType } from 'src/modules/timesheets/models/shift.models';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
|
||||
const { shift, dense = false } = defineProps<{
|
||||
shift: Shift;
|
||||
const { t } = useI18n();
|
||||
const ui_store = useUiStore();
|
||||
|
||||
const shift = defineModel<Shift>('shift', { required: true });
|
||||
|
||||
const { dense = false } = defineProps<{
|
||||
dense?: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
'save-comment': [comment: string, shift: Shift];
|
||||
'request-update': [shift: Shift];
|
||||
'saveComment': [comment: string, shift_id: number];
|
||||
'requestUpdate': [shift_id: number];
|
||||
'requestDelete': [void];
|
||||
}>();
|
||||
|
||||
const shift_start = ref(shift.start_time);
|
||||
const shift_end = ref(shift.end_time);
|
||||
const time_picker_model = ref('');
|
||||
const is_showing_time_picker = ref(false);
|
||||
const select_ref = useTemplateRef<QSelect>('select');
|
||||
|
||||
|
||||
|
||||
const options: { label: string, value: ShiftType, icon: string, icon_color: string }[] = [
|
||||
{ label: t('timesheet.shift.types.REGULAR'), value: 'REGULAR', icon: 'wb_sunny', icon_color: '' },
|
||||
{ label: t('timesheet.shift.types.EVENING'), value: 'EVENING', icon: 'bedtime', icon_color: 'indigo-8' },
|
||||
{ label: t('timesheet.shift.types.EMERGENCY'), value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-8' },
|
||||
{ label: t('timesheet.shift.types.VACATION'), value: 'VACATION', icon: 'beach_access', icon_color: 'yellow-8' },
|
||||
{ label: t('timesheet.shift.types.HOLIDAY'), value: 'HOLIDAY', icon: 'forest', icon_color: 'green-8' },
|
||||
{ label: t('timesheet.shift.types.SICK'), value: 'SICK', icon: 'medication_liquid', icon_color: 'cyan-8' },
|
||||
];
|
||||
|
||||
const shift_type_selected = ref(options.find(option => option.value == shift.value.type));
|
||||
|
||||
const showTimePicker = (time: string) => {
|
||||
is_showing_time_picker.value = true;
|
||||
time_picker_model.value = time;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (ui_store.focus_next_component) {
|
||||
select_ref.value?.focus();
|
||||
select_ref.value?.showPopup();
|
||||
ui_store.focus_next_component = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card-section
|
||||
horizontal
|
||||
class="q-py-none q-mx-md text-uppercase text-center items-center rounded-10"
|
||||
style="line-height: 1;"
|
||||
<div
|
||||
v-if="shift.shift_id !== 0"
|
||||
class="col row flex-center text-uppercase rounded-10"
|
||||
>
|
||||
<!-- time-picker for mobile timesheet -->
|
||||
<q-dialog
|
||||
v-model="is_showing_time_picker"
|
||||
class="z-max"
|
||||
<!-- shift type -->
|
||||
<q-select
|
||||
ref="select"
|
||||
v-model="shift_type_selected"
|
||||
standout="bg-blue-grey-9"
|
||||
dense
|
||||
options-dense
|
||||
hide-dropdown-icon
|
||||
:menu-offset="[0, 10]"
|
||||
:options="options"
|
||||
class="rounded-5 q-mx-xs shadow-1"
|
||||
:class="ui_store.is_mobile_mode ? 'col-auto' : 'col'"
|
||||
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||
popup-content-style="border: 2px solid var(--q-primary)"
|
||||
>
|
||||
<q-time
|
||||
v-model="time_picker_model"
|
||||
format24h
|
||||
color="primary"
|
||||
/>
|
||||
</q-dialog>
|
||||
|
||||
<div class="col row">
|
||||
<!-- punch-in timestamp -->
|
||||
<q-input
|
||||
dense
|
||||
standout="bg-blue-grey-9"
|
||||
label-slot
|
||||
v-model="shift_start"
|
||||
class="col q-pa-none q-my-xs"
|
||||
input-class="text-weight-bold text-h6"
|
||||
label-color="primary"
|
||||
mask="## : ##"
|
||||
lazy-rules
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.in') }}</span>
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
icon="access_time"
|
||||
color="primary"
|
||||
class="q-ma-none"
|
||||
@click.stop="showTimePicker(shift_start)"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<!-- arrows pointing to punch-out timestamps -->
|
||||
<q-card-section
|
||||
horizontal
|
||||
class="col-auto items-center justify-center q-mx-sm"
|
||||
>
|
||||
<template #selected-item="scope">
|
||||
<div
|
||||
v-for="icon_data, index in [
|
||||
{ transform: 'transform: translateX(5px);', color: 'accent' },
|
||||
{ transform: 'transform: translateX(-5px);', color: 'primary' }]"
|
||||
:key="index"
|
||||
class="row text-weight-bold q-ma-none q-pa-none no-wrap ellipsis"
|
||||
:class="ui_store.is_mobile_mode ? 'items-center' : 'flex-center'"
|
||||
:tabindex="scope.tabindex"
|
||||
>
|
||||
<q-icon
|
||||
v-if="shift.type && !dense"
|
||||
name="double_arrow"
|
||||
:color="icon_data.color"
|
||||
:size="dense ? '16px' : '24px'"
|
||||
:style="icon_data.transform"
|
||||
:name="scope.opt.icon"
|
||||
:color="scope.opt.icon_color"
|
||||
size="sm"
|
||||
class="col-auto q-mx-xs"
|
||||
/>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="text-primary"
|
||||
> > </span>
|
||||
v-if="$q.screen.gt.md"
|
||||
style="line-height: 0.9em;"
|
||||
class="col ellipsis"
|
||||
>{{ scope.opt.label }}</span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<!-- punch-out timestamps -->
|
||||
<q-input
|
||||
dense
|
||||
standout="bg-blue-grey-9"
|
||||
label-slot
|
||||
v-model="shift_end"
|
||||
class="col q-pa-none q-my-xs"
|
||||
input-class="text-weight-bold text-h6"
|
||||
label-color="primary"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.out') }}</span>
|
||||
</template>
|
||||
<!-- punch-in timestamp -->
|
||||
<q-input
|
||||
v-model="shift.start_time"
|
||||
dense
|
||||
type="time"
|
||||
standout="bg-blue-grey-9"
|
||||
label-slot
|
||||
label-color="primary"
|
||||
input-class="text-weight-medium"
|
||||
input-style="font-size: 1.2em;"
|
||||
class="col q-mx-xs"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.in') }}</span>
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
icon="access_time"
|
||||
color="primary"
|
||||
@click="showTimePicker(shift_end)"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<template #append>
|
||||
<q-btn
|
||||
v-if="ui_store.is_mobile_mode"
|
||||
dense
|
||||
flat
|
||||
icon="access_time"
|
||||
color="primary"
|
||||
@click.stop="showTimePicker(shift.start_time)"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-card-section class="col-grow q-pa-none no-wrap q-mx-xs">
|
||||
<!-- comment btn -->
|
||||
<!-- punch-out timestamps -->
|
||||
<q-input
|
||||
v-model="shift.end_time"
|
||||
dense
|
||||
type="time"
|
||||
standout="bg-blue-grey-9"
|
||||
label-slot
|
||||
label-color="primary"
|
||||
input-class="text-weight-medium"
|
||||
input-style="font-size: 1.2em;"
|
||||
class="col q-mx-xs"
|
||||
lazy-rules
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.out') }}</span>
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<q-btn
|
||||
v-if="ui_store.is_mobile_mode"
|
||||
dense
|
||||
flat
|
||||
icon="access_time"
|
||||
color="primary"
|
||||
@click="showTimePicker(shift.end_time)"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<!-- comment and delete buttons -->
|
||||
<div
|
||||
v-if="$q.screen.gt.sm"
|
||||
class="col-auto"
|
||||
>
|
||||
<q-icon
|
||||
v-if="shift.type && dense"
|
||||
:name="shift.comment ? 'comment' : ''"
|
||||
color="primary"
|
||||
:size="dense ? 'xs' : 'sm'"
|
||||
class="q-pa-none q-mr-xs"
|
||||
class="col-auto q-pa-none q-mr-xs"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
v-else
|
||||
flat
|
||||
dense
|
||||
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
||||
:text-color="shift.comment ? 'primary' : 'grey-8'"
|
||||
class="col-auto q-ma-none q-pl-md full-height"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
round
|
||||
unelevated
|
||||
tabindex="-1"
|
||||
icon="cancel"
|
||||
color="negative"
|
||||
class="q-pa-none q-mr-xs"
|
||||
@click="$emit('requestDelete')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -2,103 +2,123 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
/* eslint-disable */
|
||||
import { date } from 'quasar';
|
||||
import { computed } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import ShiftListRow from 'src/modules/timesheets/components/shift-list-row.vue';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { type Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
import { computed } from 'vue';
|
||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||
import { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
|
||||
const q = useQuasar();
|
||||
const ui_store = useUiStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const shift_api = useShiftApi();
|
||||
|
||||
const { dense = false } = defineProps<{
|
||||
dense?: boolean;
|
||||
}>();
|
||||
|
||||
const is_mobile = computed(() => q.screen.lt.md);
|
||||
const date_font_size = computed(() => dense ? '1.5em' : '2.5em');
|
||||
const weekday_font_size = computed(() => dense ? '0.55em;' : '0.7em;');
|
||||
const date_box_size = computed(() => dense ? 'width: 50px;' : 'width: 75px;');
|
||||
const date_box_size = computed(() => dense || is_mobile.value ? 'width: 40px; height: 75px;' : 'width: 75px; height: 75px;');
|
||||
|
||||
|
||||
|
||||
// const get_date_from_short = (short_date: string): Date => {
|
||||
// if (timesheet_store.pay_period === undefined) return new Date();
|
||||
// return new Date(timesheet_store.pay_period.pay_year.toString() + '/' + short_date);
|
||||
// };
|
||||
|
||||
// const to_iso_date = (short_date: string): string => {
|
||||
// return date.formatDate(get_date_from_short(short_date), 'YYYY-MM-DD');
|
||||
// };
|
||||
|
||||
const shifts_or_placeholder = (shifts: Shift[]): Shift[] => {
|
||||
return shifts.length > 0 ? shifts : [];
|
||||
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
|
||||
ui_store.focus_next_component = true;
|
||||
const new_shift = new Shift;
|
||||
new_shift.date = date;
|
||||
new_shift.timesheet_id = timesheet_id;
|
||||
day_shifts.push(new_shift);
|
||||
};
|
||||
|
||||
const deleteCurrentShift = async (shift: Shift) => {
|
||||
console.log('shift to delete: ', shift);
|
||||
if (shift.shift_id < 0) {
|
||||
shift.shift_id = 0;
|
||||
return;
|
||||
}
|
||||
await shift_api.deleteShiftById(shift.shift_id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-for="week, index in timesheet_store.timesheets"
|
||||
:key="index"
|
||||
class="column col q-mx-xs"
|
||||
>
|
||||
<div class="col row shadow-2 rounded-10 q-my-xs">
|
||||
|
||||
<q-card
|
||||
v-for="day, day_index in week.days"
|
||||
:key="day_index + index"
|
||||
class="row col items-center no-shadow"
|
||||
style="border-radius: 10px 0 0 10px;"
|
||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||
<div
|
||||
v-for="timesheet in timesheet_store.timesheets"
|
||||
:key="timesheet.timesheet_id"
|
||||
class="col column"
|
||||
>
|
||||
<div
|
||||
v-for="day in timesheet.days"
|
||||
:key="day.date"
|
||||
class="col-auto row shadow-2 rounded-10 q-ma-xs"
|
||||
>
|
||||
|
||||
<!-- Dates column -->
|
||||
<q-card-section class="col-auto q-pa-none text-white">
|
||||
<div
|
||||
class="col row bg-dark"
|
||||
style="border-radius: 10px 0 0 10px;"
|
||||
>
|
||||
<!-- Dates column -->
|
||||
<div
|
||||
class="bg-primary rounded-10 q-pa-xs text-center q-ml-sm"
|
||||
class="col-auto column flex-center bg-primary rounded-10 text-center q-ma-sm self-center"
|
||||
:class="$q.screen.lt.md ? '' : ''"
|
||||
:style="date_box_size"
|
||||
>
|
||||
<q-item-label
|
||||
<span
|
||||
v-if="!dense"
|
||||
class="col-auto text-uppercase text-white"
|
||||
:style="'font-size: ' + weekday_font_size"
|
||||
class="text-uppercase"
|
||||
>{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), { weekday: $q.screen.lt.md ? 'short' : 'long' })
|
||||
}}</q-item-label>
|
||||
<q-item-label
|
||||
class="text-weight-bolder"
|
||||
>
|
||||
{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), {
|
||||
weekday: $q.screen.lt.md ? 'short' :
|
||||
'long'
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
class="col-auto text-weight-bolder text-grey-1"
|
||||
:style="'font-size: ' + date_font_size + '; line-height: 90% !important;'"
|
||||
>{{ date.extractDate(day.date, 'YYYY-MM-DD').getDate() }}</q-item-label>
|
||||
<q-item-label
|
||||
>
|
||||
{{ date.extractDate(day.date, 'YYYY-MM-DD').getDate() }}
|
||||
</span>
|
||||
<span
|
||||
class="col-auto text-uppercase text-white"
|
||||
:style="'font-size: ' + weekday_font_size"
|
||||
class="text-uppercase"
|
||||
>{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), { month: $q.screen.lt.md ? 'short' : 'long' })
|
||||
}}</q-item-label>
|
||||
>
|
||||
{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), {
|
||||
month: $q.screen.lt.md ? 'short' : 'long'
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<!-- List of shifts column -->
|
||||
<q-card-section class="col column q-pa-none full-height">
|
||||
<div
|
||||
v-if="day.shifts.length > 0"
|
||||
class="col-grow column justify-center"
|
||||
>
|
||||
|
||||
<!-- List of shifts column -->
|
||||
<div class="col column">
|
||||
<ShiftListRow
|
||||
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
||||
v-for="shift, shift_index in day.shifts"
|
||||
:key="shift_index"
|
||||
class="col"
|
||||
v-model:shift="day.shifts[shift_index]!"
|
||||
:dense="dense"
|
||||
:shift="shift"
|
||||
@request-update=""
|
||||
@request-delete="deleteCurrentShift(shift)"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
unelevated
|
||||
class="col-auto bg-primary"
|
||||
icon="more_time"
|
||||
size="lg"
|
||||
text-color="white"
|
||||
style="border-radius: 0 10px 10px 0;"
|
||||
/>
|
||||
<div class="col-auto self-stretch">
|
||||
<q-btn
|
||||
unelevated
|
||||
icon="more_time"
|
||||
:size="$q.screen.lt.md ? 'md' : 'lg'"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
class="full-height"
|
||||
:class="$q.screen.lt.md ? 'q-px-xs' : ''"
|
||||
style="border-radius: 0 10px 10px 0;"
|
||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -2,17 +2,18 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import GenericLoader from 'src/modules/shared/components/generic-loader.vue';
|
||||
import ShiftList from 'src/modules/timesheets/components/shift-list.vue';
|
||||
import ExpenseDialog from 'src/modules/timesheets/components/expense-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 ShiftListLegend from 'src/modules/timesheets/components/shift-list-legend.vue';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
import { provide } from 'vue';
|
||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||
|
||||
const { open } = useExpensesStore();
|
||||
const shift_api = useShiftApi();
|
||||
|
||||
const { employeeEmail, dense = false } = defineProps<{
|
||||
employeeEmail: string;
|
||||
|
|
@ -27,21 +28,28 @@
|
|||
|
||||
<template>
|
||||
<div class="column flex-center full-width">
|
||||
<GenericLoader
|
||||
:is-loading="timesheet_store.is_loading"
|
||||
class="col-auto text-center"
|
||||
/>
|
||||
|
||||
<q-dialog
|
||||
v-model="timesheet_store.is_loading"
|
||||
transition-show="jump-down"
|
||||
transition-hide="jump-down"
|
||||
>
|
||||
<q-card class="q-pa-xl rounded-200 bg-white frosted-glass">
|
||||
<q-spinner-radio
|
||||
color="primary"
|
||||
size="20vh"
|
||||
/>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-card
|
||||
v-if="!timesheet_store.is_loading"
|
||||
flat
|
||||
class="transparent full-width"
|
||||
>
|
||||
|
||||
<q-card-section
|
||||
v-if="!dense"
|
||||
:horizontal="$q.screen.gt.sm"
|
||||
class="q-px-md items-center"
|
||||
class="q-px-md items-center q-mb-md"
|
||||
:class="$q.screen.lt.md ? 'column' : ''"
|
||||
>
|
||||
<!-- navigation btn -->
|
||||
|
|
@ -65,9 +73,21 @@
|
|||
/>
|
||||
|
||||
<!-- shift's colored legend -->
|
||||
<ShiftListLegend :is-loading="false" />
|
||||
<!-- <ShiftListLegend :is-loading="false" /> -->
|
||||
|
||||
<q-space />
|
||||
<!-- save timesheet changes button -->
|
||||
<q-btn
|
||||
v-if="$q.screen.gt.sm"
|
||||
push
|
||||
rounded
|
||||
:disable="timesheet_store.is_loading"
|
||||
color="primary"
|
||||
icon="upload"
|
||||
:label="$t('shared.label.save')"
|
||||
class="q-mr-md"
|
||||
@click="shift_api.saveShiftChanges"
|
||||
/>
|
||||
|
||||
<!-- desktop expenses button -->
|
||||
<q-btn
|
||||
|
|
@ -82,14 +102,8 @@
|
|||
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section
|
||||
:horizontal="$q.screen.gt.sm"
|
||||
class="bg-secondary q-pa-sm rounded-10"
|
||||
>
|
||||
<ShiftList :dense="dense" />
|
||||
</q-card-section>
|
||||
<ShiftList :dense="dense" />
|
||||
</q-card>
|
||||
|
||||
<ExpenseDialog />
|
||||
</div>
|
||||
</template>
|
||||
39
src/modules/timesheets/composables/use-shift-api.ts
Normal file
39
src/modules/timesheets/composables/use-shift-api.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { useAuthStore } from "src/stores/auth-store";
|
||||
import { useShiftStore } from "src/stores/shift-store";
|
||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||
|
||||
export const useShiftApi = () => {
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const shift_store = useShiftStore();
|
||||
const auth_store = useAuthStore();
|
||||
|
||||
const deleteShiftById = async (shift_id: number) => {
|
||||
timesheet_store.is_loading = true;
|
||||
const success = await shift_store.deleteShiftById(shift_id);
|
||||
|
||||
if (success) {
|
||||
await timesheet_store.getTimesheetsByEmployeeEmail(auth_store.user?.email ?? '');
|
||||
}
|
||||
|
||||
timesheet_store.is_loading = false;
|
||||
};
|
||||
|
||||
const saveShiftChanges = async () => {
|
||||
timesheet_store.is_loading = true;
|
||||
const create_success = await shift_store.createNewShifts();
|
||||
|
||||
if (create_success) {
|
||||
const update_success = await shift_store.updateShifts();
|
||||
|
||||
if (update_success) {
|
||||
await timesheet_store.getTimesheetsByEmployeeEmail(auth_store.user?.email ?? '')
|
||||
}
|
||||
}
|
||||
timesheet_store.is_loading = false;
|
||||
}
|
||||
|
||||
return {
|
||||
deleteShiftById,
|
||||
saveShiftChanges,
|
||||
};
|
||||
}
|
||||
|
|
@ -6,21 +6,29 @@ export const useTimesheetApi = () => {
|
|||
const auth_store = useAuthStore();
|
||||
|
||||
const getTimesheetsByDate = async (date_string: string, employee_email?: string) => {
|
||||
timesheet_store.is_loading = true;
|
||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
|
||||
|
||||
if (success) {
|
||||
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email ?? auth_store.user?.email ?? '');
|
||||
timesheet_store.is_loading = false;
|
||||
}
|
||||
|
||||
timesheet_store.is_loading = false;
|
||||
}
|
||||
|
||||
const getTimesheetsByCurrentPayPeriod = async (employee_email?: string) => {
|
||||
if (timesheet_store.pay_period === undefined) return false;
|
||||
|
||||
|
||||
timesheet_store.is_loading = true;
|
||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(timesheet_store.pay_period.pay_year, timesheet_store.pay_period.pay_period_no );
|
||||
|
||||
if (success) {
|
||||
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email ?? auth_store.user?.email ?? '');
|
||||
timesheet_store.is_loading = false;
|
||||
}
|
||||
|
||||
timesheet_store.is_loading = false;
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,29 +1,46 @@
|
|||
export const SHIFT_TYPES: ShiftType[] = [
|
||||
'REGULAR',
|
||||
'EVENING',
|
||||
'REGULAR',
|
||||
'EVENING',
|
||||
'EMERGENCY',
|
||||
'OVERTIME',
|
||||
'HOLIDAY',
|
||||
'VACATION',
|
||||
'HOLIDAY',
|
||||
'VACATION',
|
||||
'SICK'
|
||||
];
|
||||
|
||||
export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'OVERTIME' | 'HOLIDAY' | 'VACATION' | 'SICK' ;
|
||||
export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'HOLIDAY' | 'VACATION' | 'SICK';
|
||||
|
||||
export type ShiftLegendItem = {
|
||||
type: ShiftType;
|
||||
color: string;
|
||||
label_type: string;
|
||||
type: ShiftType;
|
||||
color: string;
|
||||
label_type: string;
|
||||
text_color?: string;
|
||||
};
|
||||
|
||||
export interface Shift {
|
||||
id: number;
|
||||
date: string; //YYYY-MM-DD
|
||||
type: ShiftType;
|
||||
start_time: string; //HH:mm:ss
|
||||
end_time: string; //HH:mm:ss
|
||||
comment: string | undefined;
|
||||
export class Shift {
|
||||
shift_id: number;
|
||||
timesheet_id: number;
|
||||
date: string; //YYYY-MM-DD
|
||||
type: ShiftType;
|
||||
start_time: string; //HH:mm:ss
|
||||
end_time: string; //HH:mm:ss
|
||||
comment: string | undefined;
|
||||
is_approved: boolean;
|
||||
is_remote: boolean;
|
||||
is_remote: boolean;
|
||||
|
||||
constructor() {
|
||||
this.shift_id = -1;
|
||||
this.timesheet_id = -1;
|
||||
this.date = '';
|
||||
this.type = 'REGULAR';
|
||||
this.start_time = '';
|
||||
this.end_time = '';
|
||||
this.comment = undefined;
|
||||
this.is_approved = false;
|
||||
this.is_remote = false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface NewShift {
|
||||
timesheet_id: number;
|
||||
shifts: Shift[];
|
||||
}
|
||||
|
|
@ -4,8 +4,13 @@ import type { Expense } from "src/modules/timesheets/models/expense.models";
|
|||
export const TIME_FORMAT_PATTERN = /^(\d{2}:\d{2})?$/;
|
||||
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
export interface TimesheetResponse {
|
||||
employee_full_name: string;
|
||||
timesheets: Timesheet[];
|
||||
}
|
||||
|
||||
export interface Timesheet {
|
||||
id: number;
|
||||
timesheet_id: number;
|
||||
is_approved: boolean;
|
||||
weekly_hours: TotalHours;
|
||||
weekly_expenses: TotalExpenses;
|
||||
|
|
@ -36,77 +41,77 @@ export interface TotalExpenses {
|
|||
mileage: number;
|
||||
}
|
||||
|
||||
export const test_timesheets: Timesheet[] = [
|
||||
{
|
||||
id: 1,
|
||||
is_approved: false,
|
||||
weekly_hours: { regular: 8, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0, absent: 0 },
|
||||
weekly_expenses: { expenses: 15.5, mileage: 0 },
|
||||
days: [
|
||||
{
|
||||
date: '2025-10-18',
|
||||
daily_hours: { regular: 8, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0, absent: 0 },
|
||||
daily_expenses: { expenses: 15.5, mileage: 0 },
|
||||
shifts: [
|
||||
{ id: 101, date: '2025-01-06', type: 'REGULAR', start_time: '08:00', end_time: '12:00', comment: 'blah', is_approved: false, is_remote: false, },
|
||||
{ id: 102, date: '2025-01-06', type: 'REGULAR', start_time: '13:00', end_time: '17:00', comment: undefined, is_approved: false, is_remote: false, },
|
||||
],
|
||||
expenses: [
|
||||
{ id: 201, date: '2025-01-06', type: 'EXPENSES', amount: 15.5, comment: 'Lunch receipt', is_approved: false, },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
is_approved: true,
|
||||
weekly_hours: {
|
||||
regular: 0,
|
||||
evening: 0,
|
||||
emergency: 0,
|
||||
overtime: 8,
|
||||
vacation: 0,
|
||||
holiday: 0,
|
||||
sick: 0,
|
||||
absent: 0,
|
||||
},
|
||||
weekly_expenses: {
|
||||
expenses: 0,
|
||||
mileage: 32.4,
|
||||
},
|
||||
days: [
|
||||
{
|
||||
date: '2025-10-27',
|
||||
daily_hours: {
|
||||
regular: 0,
|
||||
evening: 0,
|
||||
emergency: 0,
|
||||
overtime: 8,
|
||||
vacation: 0,
|
||||
holiday: 0,
|
||||
sick: 0,
|
||||
absent: 0,
|
||||
},
|
||||
daily_expenses: {
|
||||
expenses: 0,
|
||||
mileage: 32.4,
|
||||
},
|
||||
shifts: [
|
||||
{ id: 101, date: '2025-10-27', type: 'REGULAR', start_time: '08:00', end_time: '12:00', comment: undefined, is_approved: false, is_remote: false, },
|
||||
{ id: 102, date: '2025-10-27', type: 'REGULAR', start_time: '13:00', end_time: '17:00', comment: undefined, is_approved: false, is_remote: false, },
|
||||
],
|
||||
expenses: [
|
||||
{
|
||||
id: 202,
|
||||
date: '2025-10-27',
|
||||
type: 'MILEAGE',
|
||||
amount: 0,
|
||||
mileage: 32.4,
|
||||
comment: 'Travel to client site',
|
||||
is_approved: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
// export const test_timesheets: Timesheet[] = [
|
||||
// {
|
||||
// timehsid: 1,
|
||||
// is_approved: false,
|
||||
// weekly_hours: { regular: 8, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0, absent: 0 },
|
||||
// weekly_expenses: { expenses: 15.5, mileage: 0 },
|
||||
// days: [
|
||||
// {
|
||||
// date: '2025-10-18',
|
||||
// daily_hours: { regular: 8, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0, absent: 0 },
|
||||
// daily_expenses: { expenses: 15.5, mileage: 0 },
|
||||
// shifts: [
|
||||
// { id: 101, date: '2025-01-06', type: 'REGULAR', start_time: '08:00', end_time: '12:00', comment: 'blah', is_approved: false, is_remote: false, },
|
||||
// { id: 102, date: '2025-01-06', type: 'REGULAR', start_time: '13:00', end_time: '17:00', comment: undefined, is_approved: false, is_remote: false, },
|
||||
// ],
|
||||
// expenses: [
|
||||
// { id: 201, date: '2025-01-06', type: 'EXPENSES', amount: 15.5, comment: 'Lunch receipt', is_approved: false, },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// id: 2,
|
||||
// is_approved: true,
|
||||
// weekly_hours: {
|
||||
// regular: 0,
|
||||
// evening: 0,
|
||||
// emergency: 0,
|
||||
// overtime: 8,
|
||||
// vacation: 0,
|
||||
// holiday: 0,
|
||||
// sick: 0,
|
||||
// absent: 0,
|
||||
// },
|
||||
// weekly_expenses: {
|
||||
// expenses: 0,
|
||||
// mileage: 32.4,
|
||||
// },
|
||||
// days: [
|
||||
// {
|
||||
// date: '2025-10-27',
|
||||
// daily_hours: {
|
||||
// regular: 0,
|
||||
// evening: 0,
|
||||
// emergency: 0,
|
||||
// overtime: 8,
|
||||
// vacation: 0,
|
||||
// holiday: 0,
|
||||
// sick: 0,
|
||||
// absent: 0,
|
||||
// },
|
||||
// daily_expenses: {
|
||||
// expenses: 0,
|
||||
// mileage: 32.4,
|
||||
// },
|
||||
// shifts: [
|
||||
// { id: 101, date: '2025-10-27', type: 'REGULAR', start_time: '08:00', end_time: '12:00', comment: undefined, is_approved: false, is_remote: false, },
|
||||
// { id: 102, date: '2025-10-27', type: 'REGULAR', start_time: '13:00', end_time: '17:00', comment: undefined, is_approved: false, is_remote: false, },
|
||||
// ],
|
||||
// expenses: [
|
||||
// {
|
||||
// id: 202,
|
||||
// date: '2025-10-27',
|
||||
// type: 'MILEAGE',
|
||||
// amount: 0,
|
||||
// mileage: 32.4,
|
||||
// comment: 'Travel to client site',
|
||||
// is_approved: true,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ];
|
||||
|
|
|
|||
13
src/modules/timesheets/services/expense-service.ts
Normal file
13
src/modules/timesheets/services/expense-service.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { api } from "src/boot/axios";
|
||||
|
||||
export const ExpenseService = {
|
||||
getExpensesByTimesheetId: async (timesheet_id: number) => {
|
||||
const response = await api.get(`timesheet/${timesheet_id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
upsertOrDeleteExpenseById: async (expense_id: number) => {
|
||||
const response = await api.post(`epxense/${expense_id}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
24
src/modules/timesheets/services/shift-service.ts
Normal file
24
src/modules/timesheets/services/shift-service.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/* eslint-disable */
|
||||
import { api } from "src/boot/axios";
|
||||
import type { Shift } from "src/modules/timesheets/models/shift.models";
|
||||
|
||||
export const ShiftService = {
|
||||
deleteShiftById: async (shift_id: number) => {
|
||||
const response = await api.delete(`/shift/${shift_id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
createNewShifts: async (new_shifts: Shift[]) => {
|
||||
// const response = await api.post(`/shift/`, { dtos: new_shifts });
|
||||
// return response;
|
||||
console.log('create shift payload: ', new_shifts);
|
||||
return {status: 200};
|
||||
},
|
||||
|
||||
updateShifts: async (existing_shifts: Shift[]) => {
|
||||
// const response = await api.patch(`/shift/`, { dtos: existing_shifts });
|
||||
// return response;
|
||||
console.log('update shift payload: ', existing_shifts);
|
||||
return {status: 200};
|
||||
}
|
||||
};
|
||||
|
|
@ -1,11 +1,7 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import { api } from "src/boot/axios";
|
||||
import type { PayPeriod } from "src/modules/shared/models/pay-period.models";
|
||||
import type { Timesheet } from "src/modules/timesheets/models/timesheet.models";
|
||||
import type { TimesheetResponse } from "src/modules/timesheets/models/timesheet.models";
|
||||
import type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||
import { Shift } from "src/modules/timesheets/models/shift.models";
|
||||
|
||||
export const timesheetService = {
|
||||
getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => {
|
||||
|
|
@ -23,28 +19,8 @@ export const timesheetService = {
|
|||
return response.data;
|
||||
},
|
||||
|
||||
getTimesheetsByPayPeriodAndEmployeeEmail: async (employee_email: string, year: number, period_number: number): Promise<Timesheet[]> => {
|
||||
getTimesheetsByPayPeriodAndEmployeeEmail: async (employee_email: string, year: number, period_number: number): Promise<TimesheetResponse> => {
|
||||
const response = await api.get('timesheets', { params: { employee_email, year, period_number } });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getExpensesByTimesheetId: async (timesheet_id: number): Promise<Expense[]> => {
|
||||
const response = await api.get(`/expenses/list/${timesheet_id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
upsertShiftsByEmployeeEmailAndAction: async (email: string, payload: Shift[]): Promise<Timesheet[]> => {
|
||||
const response = await api.put(`/shifts/upsert/${email}`, payload);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteShiftsByEmployeeEmailAndDate: async (email: string, date: string, payload: Shift[]) => {
|
||||
const response = await api.delete(`/shifts/delete/${email}/${date}`, { data: payload });
|
||||
return response;
|
||||
},
|
||||
|
||||
upsertOrDeleteExpenseByEmailAndExpenseId: async (email: string, expense_id: number): Promise<Timesheet[]> => {
|
||||
const response = await api.put(`/expenses/upsert/${email}`, expense_id);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
|
@ -33,13 +33,10 @@
|
|||
/>
|
||||
|
||||
<div
|
||||
class="col column items-center"
|
||||
class="col"
|
||||
:style="$q.screen.gt.sm ? 'width: 90vw' : ''"
|
||||
>
|
||||
<TimesheetWrapper
|
||||
class="col-auto"
|
||||
:employee-email="user?.email ?? ''"
|
||||
/>
|
||||
<TimesheetWrapper :employee-email="user?.email ?? ''" />
|
||||
</div>
|
||||
|
||||
</q-page>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
void handleAuthMessage(event);
|
||||
});
|
||||
|
||||
const oidc_popup = window.open('http://localhost:3000/auth/v1/login', 'authPopup', 'width=600,height=800');
|
||||
const oidc_popup = window.open(`${import.meta.env.VITE_TARGO_BACKEND_AUTH_URL}auth/v1/login`, 'authPopup', 'width=600,height=800');
|
||||
|
||||
if (!oidc_popup)
|
||||
Notify.create({
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
|
||||
import { ExpensesApiError, type GenericApiError } from "src/modules/timesheets/models/expense-validation.models";
|
||||
import { empty_expense, test_expenses, type Expense } from "src/modules/timesheets/models/expense.models";
|
||||
import { ExpenseService } from "src/modules/timesheets/services/expense-service";
|
||||
|
||||
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
error.value = null;
|
||||
|
||||
try {
|
||||
const expenses = await timesheetService.getExpensesByTimesheetId(timesheet_id);
|
||||
const expenses = await ExpenseService.getExpensesByTimesheetId(timesheet_id);
|
||||
pay_period_expenses.value = expenses;
|
||||
} catch (err: unknown) {
|
||||
if (typeof err === 'object') {
|
||||
|
|
@ -62,15 +62,12 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
}
|
||||
};
|
||||
|
||||
const upsertOrDeleteExpensesById = async (employee_email: string, date: string, expense_id: number): Promise<void> => {
|
||||
const upsertOrDeleteExpensesById = async (expense_id: number): Promise<void> => {
|
||||
is_loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
await timesheetService.upsertOrDeleteExpenseByEmailAndExpenseId(
|
||||
encodeURIComponent(employee_email),
|
||||
expense_id,
|
||||
);
|
||||
await ExpenseService.upsertOrDeleteExpenseById(expense_id);
|
||||
// TODO: Save response data into proper ref
|
||||
} catch (err) {
|
||||
// setErrorFrom(err);
|
||||
|
|
|
|||
71
src/stores/shift-store.ts
Normal file
71
src/stores/shift-store.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { ShiftService } from "src/modules/timesheets/services/shift-service";
|
||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||
import { Notify } from "quasar";
|
||||
|
||||
export const useShiftStore = defineStore('shift_store', () => {
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const shift_error = ref();
|
||||
|
||||
const deleteShiftById = async (shift_id: number): Promise<boolean> => {
|
||||
try {
|
||||
await ShiftService.deleteShiftById(shift_id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('DEV ERROR || error while deleting shift: ', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const createNewShifts = async (): Promise<boolean> => {
|
||||
if (timesheet_store.timesheets === undefined) return false;
|
||||
|
||||
try {
|
||||
const new_shifts = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.shifts).filter(shift => shift.shift_id < 0);
|
||||
|
||||
if (new_shifts?.length > 0) {
|
||||
const response = await ShiftService.createNewShifts(new_shifts);
|
||||
if (response.status <= 200) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('No new shifts to save');
|
||||
Notify.create('no new shifts to save')
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error creating new shifts: ', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateShifts = async (): Promise<boolean> => {
|
||||
if (timesheet_store.timesheets === undefined) return false;
|
||||
|
||||
try {
|
||||
const existing_shifts = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.shifts).filter(shift => shift.shift_id > 0);
|
||||
|
||||
if (existing_shifts?.length > 0) {
|
||||
const response = await ShiftService.updateShifts(existing_shifts);
|
||||
if (response.status <= 200) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('No shifts to update');
|
||||
Notify.create('no shifts to update')
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error updating shifts: ', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
shift_error,
|
||||
deleteShiftById,
|
||||
createNewShifts,
|
||||
updateShifts,
|
||||
}
|
||||
})
|
||||
|
|
@ -1,28 +1,24 @@
|
|||
import { ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import { withLoading } from 'src/utils/store-helpers';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/timesheet-approval-service';
|
||||
import { timesheetService } from 'src/modules/timesheets/services/timesheet-service';
|
||||
import type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||
import type { PayPeriod } from 'src/modules/shared/models/pay-period.models';
|
||||
import { test_timesheets, type Timesheet } from 'src/modules/timesheets/models/timesheet.models';
|
||||
import type { Timesheet } from 'src/modules/timesheets/models/timesheet.models';
|
||||
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
||||
|
||||
const auth_store = useAuthStore();
|
||||
|
||||
export const useTimesheetStore = defineStore('timesheet', () => {
|
||||
const auth_store = useAuthStore();
|
||||
const is_loading = ref<boolean>(false);
|
||||
const pay_period = ref<PayPeriod>();
|
||||
const pay_period_overviews = ref<TimesheetOverview[]>([]);
|
||||
const current_pay_period_overview = ref<TimesheetOverview>();
|
||||
const timesheets = ref<Timesheet[]>(test_timesheets);
|
||||
const timesheets = ref<Timesheet[]>();
|
||||
const pay_period_report = ref();
|
||||
const is_calendar_limit = computed(() => (pay_period.value?.pay_year === 2024 && pay_period.value?.pay_period_no <= 1) ?? false);
|
||||
|
||||
const getPayPeriodByDateOrYearAndNumber = async (date_or_year: string | number, period_number?: number): Promise<boolean> => {
|
||||
is_loading.value = true;
|
||||
|
||||
try {
|
||||
if (typeof date_or_year === 'string') {
|
||||
pay_period.value = await timesheetService.getPayPeriodByDate(date_or_year);
|
||||
|
|
@ -31,7 +27,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
pay_period.value = await timesheetService.getPayPeriodByYearAndPeriodNumber(date_or_year, period_number);
|
||||
}
|
||||
else pay_period.value = undefined;
|
||||
is_loading.value = false;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
|
@ -39,7 +34,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
pay_period.value = undefined;
|
||||
pay_period_overviews.value = [];
|
||||
//TODO: More in-depth error-handling here
|
||||
is_loading.value = false;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -59,49 +53,46 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
pay_period_overviews.value = [];
|
||||
// TODO: More in-depth error-handling here
|
||||
is_loading.value = false;
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getTimesheetsByEmployeeEmail = async (employee_email: string) => {
|
||||
is_loading.value = true;
|
||||
|
||||
if (pay_period.value === undefined) return;
|
||||
try {
|
||||
const response = await timesheetService.getTimesheetsByPayPeriodAndEmployeeEmail(employee_email, pay_period.value.pay_year, pay_period.value.pay_period_no);
|
||||
timesheets.value = response;
|
||||
timesheets.value = response.timesheets;
|
||||
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
|
||||
timesheets.value = [];
|
||||
is_loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getPayPeriodReportByYearAndPeriodNumber = async (year: number, period_number: number, report_filters?: TimesheetApprovalCSVReportFilters) => {
|
||||
return withLoading(is_loading.value, async () => {
|
||||
try {
|
||||
const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber(
|
||||
year,
|
||||
period_number,
|
||||
report_filters
|
||||
);
|
||||
pay_period_report.value = response;
|
||||
try {
|
||||
const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber(
|
||||
year,
|
||||
period_number,
|
||||
report_filters
|
||||
);
|
||||
pay_period_report.value = response;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('There was an error retrieving the report CSV: ', error);
|
||||
// TODO: More in-depth error-handling here
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('There was an error retrieving the report CSV: ', error);
|
||||
// TODO: More in-depth error-handling here
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
is_loading,
|
||||
is_calendar_limit,
|
||||
pay_period,
|
||||
pay_period_overviews,
|
||||
current_pay_period_overview,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,22 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
|
||||
export const useUiStore = defineStore('ui', () => {
|
||||
const isRightDrawerOpen = ref(true);
|
||||
|
||||
const q = useQuasar();
|
||||
const is_left_drawer_open = ref(true);
|
||||
const focus_next_component = ref(false);
|
||||
const is_mobile_mode = computed(() => q.screen.lt.md);
|
||||
|
||||
const toggleRightDrawer = () => {
|
||||
isRightDrawerOpen.value = !isRightDrawerOpen.value;
|
||||
is_left_drawer_open.value = !is_left_drawer_open.value;
|
||||
}
|
||||
|
||||
return { isRightDrawerOpen, toggleRightDrawer };
|
||||
return {
|
||||
is_mobile_mode,
|
||||
focus_next_component,
|
||||
is_left_drawer_open,
|
||||
toggleRightDrawer
|
||||
};
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user