Merge pull request 'release/nicolas/v1.3' (#95) from release/nicolas/v1.3 into main
Reviewed-on: Targo/targo_frontend#95
BIN
src/assets/circle.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 1.7 KiB |
|
|
@ -19,8 +19,8 @@
|
|||
background: #fd4b2d !important;
|
||||
}
|
||||
|
||||
.q-table tbody tr:hover {
|
||||
background: #00ff260c;
|
||||
.q-table tbody tr:hover > td {
|
||||
background-color: var(--q-accent2) !important;
|
||||
}
|
||||
|
||||
body.body--dark {
|
||||
|
|
@ -42,6 +42,14 @@ body.body--dark {
|
|||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.text-accent2 {
|
||||
color: #95f0a1B0;
|
||||
}
|
||||
|
||||
.bg-accent2 {
|
||||
background-color: #95f0a1B0;
|
||||
}
|
||||
|
||||
.q-btn--push::before {
|
||||
border-bottom: 4px solid rgba(0,0,0, 0.25);
|
||||
}
|
||||
|
|
@ -71,6 +79,16 @@ input[type=number] {
|
|||
}
|
||||
|
||||
.q-field--dark .q-field__control:hover::before, .q-field--outlined .q-field__control:hover::before {
|
||||
border-color: var(--q-accent);
|
||||
border-color: var(--q-accent2);
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.text-border-white {
|
||||
text-shadow: 2px 0 #fff, -2px 0 #fff, 0 2px #fff, 0 -2px #fff,
|
||||
1px 1px #fff, -1px -1px #fff, 1px -1px #fff, -1px 1px #fff;
|
||||
}
|
||||
|
||||
.text-border-dark {
|
||||
text-shadow: 2px 0 var(--q-primary), -2px 0 var(--q-primary), 0 2px var(--q-primary), 0 -2px var(--q-primary),
|
||||
1px 1px var(--q-primary), -1px -1px var(--q-primary), 1px -1px var(--q-primary), -1px 1px var(--q-primary);
|
||||
}
|
||||
|
|
@ -15,15 +15,13 @@
|
|||
$primary : #30303A;
|
||||
$secondary : #DAE0E7;
|
||||
$accent : #0c9a3b;
|
||||
$accent2 : #0a7d32;
|
||||
|
||||
$dark-shadow-color : #000000;
|
||||
$dark-shadow-color : #000;
|
||||
|
||||
$elevation-dark-umbra : rgba($dark-shadow-color, 1);
|
||||
$elevation-dark-penumbra : rgba($dark-shadow-color, 0.75);
|
||||
$elevation-dark-ambient : rgba($dark-shadow-color, 0.53);
|
||||
$elevation-dark-umbra : rgba($dark-shadow-color, .2);
|
||||
$elevation-dark-penumbra : rgba($dark-shadow-color, .14);
|
||||
$elevation-dark-ambient : rgba($dark-shadow-color, .12);
|
||||
|
||||
$dark-shadow-2 : 2px 3px $elevation-dark-umbra, 2px 3px 6px $elevation-dark-penumbra, 2px 3px 14px $elevation-dark-ambient;
|
||||
$layout-shadow-dark : 0 0 5px 5px rgba($dark-shadow-color, 0.5);
|
||||
|
||||
$input-text-color : #455A64;
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ export default {
|
|||
name: "name",
|
||||
lock: "",
|
||||
unlock: "",
|
||||
today: "today",
|
||||
},
|
||||
misc: {
|
||||
or: "or",
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ export default {
|
|||
name: "nom",
|
||||
lock: "verrouiller",
|
||||
unlock: "déverrouiller",
|
||||
today: "aujourd'hui",
|
||||
},
|
||||
misc: {
|
||||
or: "ou",
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@
|
|||
import ChatbotDrawer from 'src/modules/chatbot/components/chatbot-drawer.vue';
|
||||
|
||||
import { onMounted, watch, ref } from 'vue';
|
||||
import { setCssVar } from 'quasar';
|
||||
import { RouterView } from 'vue-router';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
|
||||
|
||||
setCssVar('accent2', '#95f0a1B0');
|
||||
const ui_store = useUiStore();
|
||||
const auth_store = useAuthStore();
|
||||
const userPreferences = ref(ui_store.userPreferences);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
const chatbot_store = useChatbotStore();
|
||||
|
||||
const text = ref('');
|
||||
const is_showing_right_drawer = ref(true);
|
||||
const isShowingRightDrawer = ref(true);
|
||||
const drawer_width = ref(85);
|
||||
|
||||
const handleSend = async () => {
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
<template>
|
||||
<q-drawer
|
||||
v-model="is_showing_right_drawer"
|
||||
v-model="isShowingRightDrawer"
|
||||
overlay
|
||||
persistent
|
||||
:width="drawer_width"
|
||||
|
|
@ -82,27 +82,42 @@
|
|||
|
||||
<div
|
||||
v-else
|
||||
class="row col-auto q-pa-sm self-end"
|
||||
class="row col-auto q-pa-xs self-end bg-secondary rounded-50"
|
||||
>
|
||||
<div class="bg-primary q-pa-xs rounded-50 chatbot-button">
|
||||
<q-btn
|
||||
dense
|
||||
round
|
||||
icon="las la-robot"
|
||||
color="accent"
|
||||
size="2em"
|
||||
class="shadow-5"
|
||||
style="pointer-events: auto;"
|
||||
@click="chatbot_store.is_showing_chatbot = true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</q-drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="css">
|
||||
:deep(.q-drawer) {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.chatbot-button {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 8px black;
|
||||
}
|
||||
|
||||
.chatbot-button:active {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 2px black;
|
||||
}
|
||||
|
||||
.chatbot-button:hover > :first-child {
|
||||
background-color: var(--q-info) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
>
|
||||
const { route = "" } = defineProps<{
|
||||
iconImageSource: string,
|
||||
bgImageSource: string,
|
||||
name: string,
|
||||
route?: string,
|
||||
}>();
|
||||
|
|
@ -16,36 +15,21 @@
|
|||
|
||||
<template>
|
||||
<div
|
||||
class="full-width cursor-pointer bg-dark shadow-2 rounded-15 q-pa-xs position-relative"
|
||||
style="border: solid 1px var(--q-accent);"
|
||||
class="full-width cursor-pointer rounded-50 link-btn shadow-4"
|
||||
@click="onClickExternalShortcut"
|
||||
>
|
||||
<span
|
||||
v-if="$q.platform.is.mobile"
|
||||
class="col text-uppercase text-bold text-accent absolute"
|
||||
style="transform: translate(20px, -20px);"
|
||||
>
|
||||
<div class="row items-center q-px-lg q-py-xs rounded-50">
|
||||
<span class="col text-uppercase text-bold">
|
||||
{{ name }}
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="row items-center q-px-lg q-py-sm link-card rounded-10 inset-shadow"
|
||||
:style="`background-image: url(${bgImageSource}); background-size: ${$q.platform.is.mobile ? 'cover' : 'contain'};`"
|
||||
>
|
||||
<q-icon
|
||||
round
|
||||
color="dark"
|
||||
color="accent"
|
||||
size="md"
|
||||
:name="`img:${iconImageSource}`"
|
||||
class="col-auto q-pr-md"
|
||||
:name="iconImageSource"
|
||||
class="col-auto"
|
||||
/>
|
||||
|
||||
<span
|
||||
v-if="!$q.platform.is.mobile"
|
||||
class="col text-uppercase text-bold"
|
||||
>
|
||||
{{ name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -54,11 +38,12 @@
|
|||
scoped
|
||||
lang="css"
|
||||
>
|
||||
.link-card {
|
||||
background-blend-mode: multiply;
|
||||
background-position: bottom right;
|
||||
background-repeat: no-repeat;
|
||||
.link-btn {
|
||||
background-color: var(--q-dark);
|
||||
background-size: contain;
|
||||
border: 2px solid var(--q-accent);
|
||||
}
|
||||
|
||||
.link-btn:hover {
|
||||
background-color: var(--q-accent2);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -28,28 +28,34 @@
|
|||
:autoplay="autoplayTimer"
|
||||
control-color="accent"
|
||||
control-type="outline"
|
||||
class="bg-dark fit rounded-15 shadow-18"
|
||||
class="bg-dark rounded-15 fit shadow-18"
|
||||
@mouseenter="onCarouselMouseEvent('enter')"
|
||||
@mouseleave="onCarouselMouseEvent('exit')"
|
||||
>
|
||||
<!-- welcome slide -->
|
||||
<q-carousel-slide
|
||||
name="welcome"
|
||||
class="q-pa-none fit"
|
||||
class="q-pa-none"
|
||||
>
|
||||
<div
|
||||
class="column fit"
|
||||
:class="$q.platform.is.mobile ? 'no-wrap' : ''"
|
||||
>
|
||||
<div class="column fit">
|
||||
<q-img
|
||||
src="src/assets/targo_building.png"
|
||||
position="50% 25%"
|
||||
fit="cover"
|
||||
class="col-9"
|
||||
>
|
||||
<div class="absolute-bottom text-h6 text-uppercase text-weight-light">
|
||||
<div class="absolute-bottom text-h5 text-uppercase text-weight-light">
|
||||
{{ $t('dashboard.carousel.welcome_title') }}
|
||||
</div>
|
||||
</q-img>
|
||||
|
||||
<div class="col column flex-center q-px-md text-h5 text-weight-light">
|
||||
<div
|
||||
class="col column flex-center q-px-md text-weight-light"
|
||||
:class="$q.platform.is.mobile ? 'text-h6' : 'text-h5'"
|
||||
>
|
||||
<span class="col-auto text-center">{{ $t('dashboard.carousel.welcome_message') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -61,19 +67,25 @@
|
|||
class="q-pa-none cursor-pointer"
|
||||
@click="$router.push(RouteNames.HELP)"
|
||||
>
|
||||
<div class="column fit">
|
||||
<div
|
||||
class="column fit"
|
||||
:class="$q.platform.is.mobile ? 'no-wrap' : ''"
|
||||
>
|
||||
<q-img
|
||||
src="src/assets/targo_help_banner.png"
|
||||
position="50% 25%"
|
||||
fit="none"
|
||||
class="col-9"
|
||||
>
|
||||
<div class="absolute-bottom text-h6 text-uppercase text-weight-light">
|
||||
<div class="absolute-bottom text-h5 text-uppercase text-weight-light">
|
||||
{{ $t('dashboard.carousel.help_title') }}
|
||||
</div>
|
||||
</q-img>
|
||||
|
||||
<div class="col column flex-center q-px-md text-h5 text-weight-light">
|
||||
<div
|
||||
class="col column flex-center q-px-md text-weight-light"
|
||||
:class="$q.platform.is.mobile ? 'text-h6' : 'text-h5'"
|
||||
>
|
||||
<span class="col-auto text-center">{{ $t('dashboard.carousel.help_message') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,34 +2,62 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { colors, getCssVar } from 'quasar';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const model = defineModel<string | number | null | undefined>({ required: true });
|
||||
const is_date_picker_open = defineModel<boolean>('isDatePickerOpen', { default: false });
|
||||
|
||||
defineProps<{
|
||||
label?: string | undefined;
|
||||
requiresDatePicker?: boolean | undefined;
|
||||
maxLength?: number;
|
||||
noTopPadding?: boolean;
|
||||
backgroundColor?: 'bg-secondary' | 'bg-dark';
|
||||
backgroundColor?: 'secondary' | 'dark' | 'white' | undefined;
|
||||
inputTextColor?: string | undefined;
|
||||
appendContent?: string | number;
|
||||
autoFocus?: boolean;
|
||||
error?: boolean;
|
||||
dense?: boolean;
|
||||
readonly?: boolean;
|
||||
textAlign?: 'left' | 'right';
|
||||
}>();
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
'focus': [void];
|
||||
'blur': [void];
|
||||
}>();
|
||||
|
||||
const isDatePickerOpen = defineModel<boolean>('isDatePickerOpen', { default: false });
|
||||
|
||||
const bgLightGrey = computed(() => {
|
||||
const secondary = getCssVar('secondary');
|
||||
if (secondary === null) return;
|
||||
|
||||
return colors.lighten(secondary, 50);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="col q-px-sm"
|
||||
:class="noTopPadding ? '' : 'q-pt-md'"
|
||||
>
|
||||
<transition
|
||||
enter-active-class="animated shakeX"
|
||||
:duration="{ enter: 200, leave: 0 }"
|
||||
mode="out-in"
|
||||
>
|
||||
<q-input
|
||||
v-model="model"
|
||||
v-bind="$attrs"
|
||||
dense
|
||||
:key="error ? 1 : 2"
|
||||
:dense="dense"
|
||||
:autofocus="autoFocus"
|
||||
:readonly="readonly"
|
||||
borderless
|
||||
color="accent"
|
||||
label-color="white"
|
||||
|
|
@ -37,15 +65,20 @@
|
|||
label-slot
|
||||
no-error-icon
|
||||
hide-bottom-space
|
||||
:error="error"
|
||||
:maxlength="maxLength"
|
||||
class="q-px-md rounded-5 inset-shadow"
|
||||
:class="$q.dark.isActive ? 'bg-primary' : (backgroundColor ?? 'bg-secondary')"
|
||||
:style="`border: 1px solid var(${$q.dark.isActive ? '--q-secondary' : '--q-primary'});`"
|
||||
:class="backgroundColor ? `bg-${backgroundColor ?? 'secondary'}` : ($q.dark.isActive ? 'bg-primary' : '')"
|
||||
:style="`border: 1px solid ${error ? getCssVar('negative') : colors.getPaletteColor($q.dark.isActive ? 'black' : 'blue-grey-4')}; ${(backgroundColor || $q.dark.isActive) ? '' : `background-color: ${bgLightGrey}`}`"
|
||||
:input-class="`text-${inputTextColor} text-${textAlign}`"
|
||||
input-style="font-size: 1.3em; font-weight: 500;"
|
||||
@focus="$emit('focus')"
|
||||
@blur="$emit('blur')"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bold text-uppercase q-px-md"
|
||||
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-primary'"
|
||||
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-blue-grey-7'"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
|
|
@ -62,17 +95,17 @@
|
|||
size="lg"
|
||||
icon="calendar_month"
|
||||
color="accent"
|
||||
@click="is_date_picker_open = true"
|
||||
@click="isDatePickerOpen = true"
|
||||
>
|
||||
<q-dialog
|
||||
v-model="is_date_picker_open"
|
||||
v-model="isDatePickerOpen"
|
||||
backdrop-filter="none"
|
||||
>
|
||||
<q-date
|
||||
v-model="model"
|
||||
mask="YYYY-MM-DD"
|
||||
color="accent"
|
||||
@update:model-value="is_date_picker_open = false"
|
||||
@update:model-value="isDatePickerOpen = false"
|
||||
/>
|
||||
</q-dialog>
|
||||
</q-btn>
|
||||
|
|
@ -80,13 +113,14 @@
|
|||
|
||||
<div
|
||||
v-if="!!appendContent"
|
||||
class="self-end text-uppercase text-bold text-accent"
|
||||
style="font-size: 0.8em;"
|
||||
class="self-end text-uppercase text-weight-medium text-accent"
|
||||
style="font-size: 0.75em;"
|
||||
>
|
||||
{{ appendContent }}
|
||||
</div>
|
||||
</template>
|
||||
</q-input>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,26 @@
|
|||
color="accent"
|
||||
class="col-auto q-px-md"
|
||||
>
|
||||
<q-tooltip
|
||||
v-if="shift.comment && shift.comment.length > 0"
|
||||
anchor="top middle"
|
||||
self="center middle"
|
||||
:offset="[0, 20]"
|
||||
class="bg-dark shadow-24"
|
||||
:class="$q.dark.isActive ? 'text-white' : 'text-primary'"
|
||||
style="border: 1px solid var(--q-accent)"
|
||||
>
|
||||
<div class="row">
|
||||
<span
|
||||
class="text-uppercase text-bold text-accent q-pr-xs"
|
||||
style="font-size: 1.2em;"
|
||||
>
|
||||
{{ $t('timesheet.expense.employee_comment') }}:
|
||||
</span>
|
||||
<span style="font-size: 1.2em;">{{ shift.comment }}</span>
|
||||
</div>
|
||||
</q-tooltip>
|
||||
|
||||
<q-badge
|
||||
v-if="hasComment"
|
||||
rounded
|
||||
|
|
@ -87,8 +107,28 @@
|
|||
clickable
|
||||
@click="onClickViewComments"
|
||||
>
|
||||
<q-tooltip
|
||||
v-if="shift.comment && shift.comment.length > 0"
|
||||
anchor="top middle"
|
||||
self="center middle"
|
||||
:offset="[0, 20]"
|
||||
class="bg-dark shadow-24"
|
||||
:class="$q.dark.isActive ? 'text-white' : 'text-primary'"
|
||||
style="border: 1px solid var(--q-accent)"
|
||||
>
|
||||
<div class="row">
|
||||
<span
|
||||
class="text-uppercase text-bold text-accent q-pr-xs"
|
||||
style="font-size: 1.2em;"
|
||||
>
|
||||
{{ $t('timesheet.expense.employee_comment') }}:
|
||||
</span>
|
||||
<span style="font-size: 1.2em;">{{ shift.comment }}</span>
|
||||
</div>
|
||||
</q-tooltip>
|
||||
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="las la-power-off" />
|
||||
<q-avatar icon="las la-comment" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@
|
|||
|
||||
// ========== constants ========================================
|
||||
|
||||
const WARNING_COLUMNS: OverviewColumns[] = ['EVENING', 'HOLIDAY', 'VACATION', 'SICK']
|
||||
const NEGATIVE_COLUMNS: OverviewColumns[] = ['OVERTIME', 'EMERGENCY']
|
||||
const WARNING_COLUMNS: OverviewColumns[] = ['EVENING']
|
||||
const NEGATIVE_COLUMNS: OverviewColumns[] = ['EMERGENCY', 'OVERTIME']
|
||||
const TIME_COLUMNS: OverviewColumns[] = ['REGULAR', 'EVENING', 'EMERGENCY', 'OVERTIME', 'HOLIDAY', 'VACATION', 'SICK'];
|
||||
const VISIBLE_COLUMNS = ref<OverviewColumns[]>([
|
||||
'employee_first_name',
|
||||
|
|
@ -105,10 +105,14 @@
|
|||
|
||||
const getListViewTimeCss = (column_name: OverviewColumns, value: number): { classes: string, style: string } => {
|
||||
if (WARNING_COLUMNS.includes(column_name) && value > 0)
|
||||
return { classes: 'bg-warning text-white text-bold rounded-5', style: '' };
|
||||
return { classes: 'bg-warning text-bold text-primary rounded-5', style: '' };
|
||||
|
||||
if (NEGATIVE_COLUMNS.includes(column_name) && value > 0) {
|
||||
if ((column_name === 'OVERTIME') && value < 4)
|
||||
return { classes: 'bg-warning text-bold text-primary rounded-5', style: '' };
|
||||
|
||||
if (NEGATIVE_COLUMNS.includes(column_name) && value > 0)
|
||||
return { classes: 'bg-negative text-white text-bold rounded-5', style: '' };
|
||||
}
|
||||
|
||||
return { classes: '', style: '' }
|
||||
}
|
||||
|
|
@ -253,13 +257,17 @@
|
|||
<!-- any other fields, though time fields will have their own conditional class to highlight abnormalities -->
|
||||
<div
|
||||
v-else
|
||||
class="q-px-xs"
|
||||
:class="getListViewTimeCss(props.col.name, props.value).classes"
|
||||
class="q-px-xs row"
|
||||
>
|
||||
<div
|
||||
class="col-auto q-px-sm"
|
||||
:class="getListViewTimeCss(props.col.name, props.value)?.classes"
|
||||
>
|
||||
{{ TIME_COLUMNS.includes(props.col.name) ?
|
||||
getHoursMinutesStringFromHoursFloat(props.value) : props.value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</q-td>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -4,23 +4,25 @@
|
|||
>
|
||||
import TargoInput from 'src/modules/shared/components/targo-input.vue';
|
||||
|
||||
import { colors } from 'quasar';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||
import { getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||
import { Expense, type ExpenseOption, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||
import { type ExpenseOption, type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||
|
||||
// ================= state ======================
|
||||
|
||||
const COMMENT_MAX_LENGTH = 280;
|
||||
|
||||
const expense = defineModel<Expense>({ default: new Expense(new Date().toISOString().slice(0, 10)) })
|
||||
const file = defineModel<File>('file');
|
||||
const { email } = defineProps<{
|
||||
const { expenseType, email } = defineProps<{
|
||||
expenseType?: ExpenseType;
|
||||
email?: string | undefined;
|
||||
mode?: 'normal' | 'approval';
|
||||
refreshKey?: number;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
'clickSave': [void];
|
||||
|
|
@ -30,8 +32,9 @@
|
|||
const timesheetStore = useTimesheetStore();
|
||||
const expenseStore = useExpensesStore();
|
||||
const expensesApi = useExpensesApi();
|
||||
const rules = useExpenseRules();
|
||||
const isNavigatorOpen = ref(false);
|
||||
const rules = useExpenseRules(t);
|
||||
const isHoveringDisabledSave = ref(false);
|
||||
|
||||
|
||||
const expenseOptions: ExpenseOption[] = [
|
||||
|
|
@ -47,8 +50,11 @@
|
|||
const period_start_date = computed(() => timesheetStore.pay_period?.period_start.replaceAll('-', '/') ?? '');
|
||||
const period_end_date = computed(() => timesheetStore.pay_period?.period_end.replaceAll('-', '/') ?? '');
|
||||
const isSaveDisabled = computed(() =>
|
||||
JSON.stringify(expenseStore.current_expense) === JSON.stringify(expenseStore.initial_expense)
|
||||
(JSON.stringify(expenseStore.current_expense) === JSON.stringify(expenseStore.initial_expense)) ||
|
||||
(!expenseStore.current_expense.amount && !expenseStore.current_expense.mileage) ||
|
||||
expenseStore.current_expense.comment.length < 1
|
||||
);
|
||||
const isTypeError = computed(() => isHoveringDisabledSave.value && !rules.typeRequired(expenseStore.current_expense.type))
|
||||
|
||||
// ==================== method =======================
|
||||
|
||||
|
|
@ -73,11 +79,23 @@
|
|||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (expense.value)
|
||||
expenseSelected.value = expenseOptions.find(expense_option => expense_option.value === expense.value.type);
|
||||
const resetAmounts = (resetAmount?: number) => {
|
||||
expenseStore.current_expense.amount = resetAmount ?? null;
|
||||
expenseStore.current_expense.mileage = resetAmount ?? null;
|
||||
}
|
||||
|
||||
const onAmountBlur = () => {
|
||||
if (expenseStore.current_expense.type === 'MILEAGE' && expenseStore.current_expense.mileage)
|
||||
expenseStore.current_expense.amount = null;
|
||||
else if (expenseStore.current_expense.type !== 'MILEAGE' && expenseStore.current_expense.amount)
|
||||
expenseStore.current_expense.mileage = null;
|
||||
else
|
||||
expenseSelected.value = expenseOptions[0];
|
||||
resetAmounts(0);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (expenseType)
|
||||
expenseSelected.value = expenseOptions.find(opt => opt.value === expenseType);
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -121,16 +139,27 @@
|
|||
<TargoInput
|
||||
v-model="expenseStore.current_expense.date"
|
||||
no-top-padding
|
||||
dense
|
||||
readonly
|
||||
:label="$t('timesheet.expense.date')"
|
||||
background-color="bg-dark"
|
||||
background-color="dark"
|
||||
class="col"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- expenses type selection -->
|
||||
<div class="col">
|
||||
<div
|
||||
class="col"
|
||||
:key="refreshKey ?? 0"
|
||||
>
|
||||
<transition
|
||||
enter-active-class="animated shakeX"
|
||||
:duration="{ enter: 200, leave: 0 }"
|
||||
mode="out-in"
|
||||
>
|
||||
<q-select
|
||||
v-model="expenseSelected"
|
||||
:key="isTypeError ? 1 : 2"
|
||||
dense
|
||||
borderless
|
||||
color="accent"
|
||||
|
|
@ -150,14 +179,14 @@
|
|||
menu-anchor="bottom middle"
|
||||
menu-self="top middle"
|
||||
:menu-offset="[0, 5]"
|
||||
:style="`border: 1px solid var(${$q.dark.isActive ? '--q-secondary' : '--q-primary'});`"
|
||||
:rules="[rules.typeRequired]"
|
||||
:style="`border: 1px solid ${isTypeError ? 'var(--q-negative)' : ($q.dark.isActive ? 'var(--q-secondary)' : colors.getPaletteColor('blue-grey-4'))};`"
|
||||
:error="isTypeError"
|
||||
@update:model-value="option => expenseStore.current_expense.type = option.value"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-medium text-uppercase q-px-sm no-pointer-events"
|
||||
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-primary'"
|
||||
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-blue-grey-7'"
|
||||
>
|
||||
{{ $t('timesheet.expense.type') }}
|
||||
</span>
|
||||
|
|
@ -180,6 +209,7 @@
|
|||
</div>
|
||||
</template>
|
||||
</q-select>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- amount input -->
|
||||
|
|
@ -188,22 +218,32 @@
|
|||
v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenseStore.current_expense?.type ?? 'EXPENSES')"
|
||||
v-model.number="expenseStore.current_expense.amount"
|
||||
no-top-padding
|
||||
background-color="bg-dark"
|
||||
dense
|
||||
background-color="dark"
|
||||
type="number"
|
||||
input-class="text-right"
|
||||
append-content=" $"
|
||||
:label="$t('timesheet.expense.amount')"
|
||||
text-align="right"
|
||||
:error="isHoveringDisabledSave && !rules.amountRequired(expenseStore.current_expense.amount)"
|
||||
@focus="resetAmounts()"
|
||||
@blur="onAmountBlur()"
|
||||
/>
|
||||
|
||||
<TargoInput
|
||||
v-else
|
||||
v-model.number="expenseStore.current_expense.mileage"
|
||||
no-top-padding
|
||||
background-color="bg-dark"
|
||||
dense
|
||||
background-color="dark"
|
||||
type="number"
|
||||
input-class="text-right"
|
||||
append-content=" km"
|
||||
:label="$t('timesheet.expense.mileage')"
|
||||
text-align="right"
|
||||
:error="isHoveringDisabledSave && !rules.mileageRequired(expenseStore.current_expense.mileage)"
|
||||
@focus="resetAmounts()"
|
||||
@blur="onAmountBlur()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -213,9 +253,11 @@
|
|||
<div class="col">
|
||||
<TargoInput
|
||||
v-model="expenseStore.current_expense.comment"
|
||||
dense
|
||||
no-top-padding
|
||||
background-color="bg-dark"
|
||||
background-color="dark"
|
||||
:max-length="COMMENT_MAX_LENGTH"
|
||||
:error="isHoveringDisabledSave && !rules.commentRequired(expenseStore.current_expense.comment)"
|
||||
:label="$t('timesheet.expense.employee_comment')"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -226,8 +268,9 @@
|
|||
>
|
||||
<TargoInput
|
||||
v-model="expenseStore.current_expense.supervisor_comment"
|
||||
dense
|
||||
no-top-padding
|
||||
background-color="bg-dark"
|
||||
background-color="dark"
|
||||
:max-length="COMMENT_MAX_LENGTH"
|
||||
:label="$t('timesheet.expense.supervisor_comment')"
|
||||
/>
|
||||
|
|
@ -247,7 +290,7 @@
|
|||
accept="image/*"
|
||||
class="q-px-md rounded-5 inset-shadow"
|
||||
:class="$q.dark.isActive ? 'bg-primary' : 'bg-dark'"
|
||||
:style="`border: 1px solid var(${$q.dark.isActive ? '--q-secondary' : '--q-primary'});`"
|
||||
:style="`border: 1px solid ${$q.dark.isActive ? 'var(--q-secondary)' : colors.getPaletteColor('blue-grey-4')};`"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon
|
||||
|
|
@ -260,7 +303,7 @@
|
|||
<template #label>
|
||||
<span
|
||||
class="text-weight-medium text-uppercase q-px-sm no-pointer-events"
|
||||
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-primary'"
|
||||
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-blue-grey-7'"
|
||||
>
|
||||
{{ $t('timesheet.expense.hints.attach_file') }}
|
||||
</span>
|
||||
|
|
@ -273,16 +316,24 @@
|
|||
<div class="col row full-width items-center">
|
||||
<q-space />
|
||||
|
||||
<transition
|
||||
enter-active-class="animated rubberBand fast"
|
||||
mode="out-in"
|
||||
>
|
||||
<q-btn
|
||||
:key="isSaveDisabled ? 1 : 0"
|
||||
push
|
||||
:disable="isSaveDisabled"
|
||||
:color="isSaveDisabled ? 'grey-5' : 'accent'"
|
||||
:icon="expenseStore.mode === 'update' ? 'save' : 'upload'"
|
||||
:label="expenseStore.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
|
||||
class="q-px-sm "
|
||||
:class="expenseStore.mode === 'create' ? 'q-mr-lg q-mb-md' : 'q-mb-sm q-ml-lg'"
|
||||
class="q-px-xl"
|
||||
:class="expenseStore.mode === 'create' ? 'q-mr-lg q-my-md' : 'q-mb-sm q-ml-lg'"
|
||||
type="submit"
|
||||
@mouseenter="isHoveringDisabledSave = isSaveDisabled"
|
||||
@mouseleave="isHoveringDisabledSave = false"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -134,7 +134,11 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<q-separator vertical spaced class="q-my-xs"/>
|
||||
<q-separator
|
||||
vertical
|
||||
spaced
|
||||
class="q-my-xs"
|
||||
/>
|
||||
|
||||
<!-- comments section -->
|
||||
<div class="col column">
|
||||
|
|
@ -241,7 +245,8 @@
|
|||
</template>
|
||||
|
||||
<ExpenseDialogForm
|
||||
v-model="expense"
|
||||
:key="isShowingUpdateForm ? 1 : 2"
|
||||
:expense-type="expense.type"
|
||||
:email="getEmployeeEmail()"
|
||||
:mode="mode"
|
||||
@click-save="hideUpdateForm"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { ref } from 'vue';
|
|||
const onClickExpenseCreate = () => {
|
||||
expense_store.mode = 'create';
|
||||
expense_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||
refreshKey.value += 1
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ import { ref } from 'vue';
|
|||
<q-card-section class="q-pa-none">
|
||||
<ExpenseDialogHeader />
|
||||
|
||||
<ExpenseDialogList :key="refreshKey + 1" />
|
||||
<ExpenseDialogList />
|
||||
|
||||
<q-expansion-item
|
||||
v-if="!isApproved"
|
||||
|
|
@ -57,8 +58,7 @@ import { ref } from 'vue';
|
|||
hide-expand-icon
|
||||
:dense="!$q.platform.is.mobile"
|
||||
group="expenses"
|
||||
@show="onClickExpenseCreate()"
|
||||
@after-hide="refreshKey += 1"
|
||||
@before-show="onClickExpenseCreate()"
|
||||
header-class="bg-accent text-white"
|
||||
>
|
||||
<template #header>
|
||||
|
|
@ -77,10 +77,8 @@ import { ref } from 'vue';
|
|||
</template>
|
||||
|
||||
<ExpenseDialogFormMobile v-if="$q.platform.is.mobile" />
|
||||
<ExpenseDialogForm
|
||||
v-else
|
||||
:key="refreshKey"
|
||||
/>
|
||||
|
||||
<ExpenseDialogForm v-else :key="refreshKey" :refresh-key="refreshKey" />
|
||||
</q-expansion-item>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
const is_navigator_open = ref(false);
|
||||
const is_showing_comment_dialog_mobile = ref(false);
|
||||
|
||||
const rules = useExpenseRules(t);
|
||||
const rules = useExpenseRules();
|
||||
|
||||
const period_start_date = computed(() => timesheet_store.pay_period?.period_start.replaceAll('-', '/') ?? '');
|
||||
const period_end_date = computed(() => timesheet_store.pay_period?.period_end.replaceAll('-', '/') ?? '');
|
||||
|
|
|
|||
|
|
@ -102,23 +102,18 @@
|
|||
<div class="column col q-px-sm q-py-xs">
|
||||
<!-- date label and delete button -->
|
||||
<div class="col-auto row items-center q-pl-xs">
|
||||
<q-icon
|
||||
name="calendar_month"
|
||||
size="sm"
|
||||
class="col-auto"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="col text-uppercase text-weight-light full-width q-pl-sm text-h6"
|
||||
class="col text-uppercase text-bold text-h5 full-width q-pl-sm"
|
||||
:class="approved_class"
|
||||
>
|
||||
{{ $d(
|
||||
date.extractDate(expense.date, 'YYYY-MM-DD'),
|
||||
{ month: 'long', day: 'numeric' }
|
||||
{ month: 'long', day: 'numeric', year: 'numeric' }
|
||||
) }}
|
||||
</span>
|
||||
|
||||
<q-btn
|
||||
v-if="!expense.is_approved"
|
||||
flat
|
||||
dense
|
||||
icon="las la-trash"
|
||||
|
|
@ -130,7 +125,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div class="col row full-width items-center q-px-xs">
|
||||
<div class="col row full-width items-center q-px-xs no-wrap">
|
||||
<!-- avatar type icon section -->
|
||||
<q-icon
|
||||
:name="getExpenseIcon(expense.type)"
|
||||
|
|
@ -139,28 +134,30 @@
|
|||
/>
|
||||
|
||||
<!-- amount or mileage section -->
|
||||
<div class="col text-weight-bold text-h6">
|
||||
<div class="col text-h6">
|
||||
<q-item-label v-if="expense.type === 'MILEAGE'">
|
||||
{{ expense.mileage?.toFixed(1) }} km
|
||||
</q-item-label>
|
||||
<q-item-label v-else>
|
||||
$ {{ expense.amount.toFixed(2) }}
|
||||
$ {{ expense.amount?.toFixed(2) }}
|
||||
</q-item-label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- attachment file -->
|
||||
<div class="col-auto q-pa-xs full-width">
|
||||
<div class="col-6 q-pa-xs">
|
||||
<q-btn
|
||||
push
|
||||
:disable="!expense.attachment_key"
|
||||
:color="expense.is_approved ? 'white' : 'accent'"
|
||||
:text-color="expense.is_approved ? 'accent' : 'white'"
|
||||
icon="las la-paperclip"
|
||||
:label="expense.attachment_name ?? `( ${$t('shared.label.empty')} )`"
|
||||
class="full-width text-lowercase q-mx-sm q-px-sm q-pb-sm inset-shadow"
|
||||
:label="expense.attachment_name ?? $t('timesheet.expense.no_attachment')"
|
||||
class="full-width text-lowercase q-mx-sm q-px-sm q-pb-sm text-caption"
|
||||
:style="expense.attachment_key ? '' : 'filter: grayscale(1); font-style: italic;'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-icon
|
||||
v-if="expense.is_approved"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import ShiftListDayRowMobile from 'src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue';
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { date } from 'quasar';
|
||||
import { computed } from 'vue';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
||||
// import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||
|
||||
const { locale } = useI18n();
|
||||
const uiStore = useUiStore();
|
||||
const timesheetStore = useTimesheetStore();
|
||||
|
||||
const day = defineModel<TimesheetDay>({ required: true });
|
||||
|
||||
const { isTimesheetApproved = false } = defineProps<{
|
||||
timesheetId: number;
|
||||
isTimesheetApproved?: boolean;
|
||||
}>();
|
||||
|
||||
const isDayApproved = computed(() => day.value.shifts.every(shift => shift.is_approved) && day.value.shifts.length > 1);
|
||||
|
||||
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
|
||||
uiStore.focusNextComponent = true;
|
||||
const newShift = new Shift;
|
||||
newShift.date = date;
|
||||
newShift.timesheet_id = timesheet_id;
|
||||
day_shifts.push(newShift);
|
||||
};
|
||||
|
||||
const getHolidayName = (date: string) => {
|
||||
const holiday = timesheetStore.federal_holidays.find(holiday => holiday.date === date);
|
||||
if (!holiday) return;
|
||||
|
||||
if (locale.value === 'fr-FR')
|
||||
return holiday.nameFr;
|
||||
|
||||
else if (locale.value === 'en-CA')
|
||||
return holiday.nameEn;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row q-pa-sm full-width relative-position">
|
||||
|
||||
<!-- optional label indicating which holiday if today is a holiday -->
|
||||
<span
|
||||
v-if="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date)"
|
||||
class="absolute-top-left text-uppercase text-weight-bolder text-purple-5"
|
||||
style="transform: translate(25px, -7px);"
|
||||
>
|
||||
{{ getHolidayName(day.date) }}
|
||||
</span>
|
||||
|
||||
<!-- mobile version in portrait mode -->
|
||||
<div
|
||||
v-if="$q.platform.is.mobile && ($q.screen.width < $q.screen.height)"
|
||||
class="col-auto full-width q-px-md q-py-sm"
|
||||
>
|
||||
<div
|
||||
class="shadow-12 rounded-10"
|
||||
:class="(isDayApproved || isTimesheetApproved) ? 'bg-accent' : 'bg-dark'"
|
||||
>
|
||||
<div
|
||||
class="text-weight-bolder text-uppercase text-h6 q-py-sm text-center relative-position"
|
||||
:class="(isDayApproved || isTimesheetApproved) ? 'bg-accent rounded-10' : 'bg-primary'"
|
||||
style="line-height: 1em; border-radius: 10px 10px 0 0;"
|
||||
>
|
||||
<span class="text-white">
|
||||
{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), {
|
||||
weekday: 'long', day: 'numeric', month: 'long'
|
||||
}) }}
|
||||
</span>
|
||||
|
||||
<q-icon
|
||||
v-if="(isDayApproved || isTimesheetApproved)"
|
||||
name="verified"
|
||||
size="3em"
|
||||
color="white"
|
||||
class="absolute-top-left z-top"
|
||||
style="top: -0.2em; left: 0px;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="day.shifts.filter(shift => shift.id !== 0).length > 0"
|
||||
class="q-pa-none transparent"
|
||||
>
|
||||
<div
|
||||
v-for="_shift, shiftIndex in day.shifts"
|
||||
:key="shiftIndex"
|
||||
>
|
||||
<ShiftListDayRowMobile
|
||||
v-model:shift="day.shifts[shiftIndex]!"
|
||||
:current-shifts="day.shifts"
|
||||
:has-shift-after="shiftIndex < day.shifts.length - 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-pa-none">
|
||||
<q-btn
|
||||
v-if="!(isDayApproved || isTimesheetApproved)"
|
||||
square
|
||||
dense
|
||||
size="xl"
|
||||
color="accent"
|
||||
icon="more_time"
|
||||
class="full-width"
|
||||
style="border-radius: 0 0 10px 10px;"
|
||||
@click="addNewShift(day.shifts, day.date, timesheetId)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -2,34 +2,35 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import TargoInput from 'src/modules/shared/components/targo-input.vue';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { QSelect, QInput } from 'quasar';
|
||||
import { colors, getCssVar, QSelect } from 'quasar';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { getCurrentDailyMinutesWorked, getTimeStringFromMinutes, SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||
import { getCurrentDailyMinutesWorked, getShiftOptions, getTimeStringFromMinutes, SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||
import type { Shift, ShiftOption, ShiftType } from 'src/modules/timesheets/models/shift.models';
|
||||
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
|
||||
|
||||
// ========== state ========================================
|
||||
|
||||
const SHIFT_TYPES_WITH_PREDEFINED_TIMES: ShiftType[] = ['HOLIDAY', 'SICK', 'VACATION'];
|
||||
const COMMENT_LENGTH_MAX = 280;
|
||||
// const COMMENT_LENGTH_MAX = 280;
|
||||
|
||||
const shift = defineModel<Shift>('shift', { required: true });
|
||||
|
||||
const {
|
||||
dense = false,
|
||||
currentShifts,
|
||||
hasShiftAfter = false,
|
||||
isTimesheetApproved = false,
|
||||
errorMessage = undefined,
|
||||
expectedDailyHours = 8,
|
||||
currentShifts,
|
||||
} = defineProps<{
|
||||
dense?: boolean;
|
||||
currentShifts: Shift[];
|
||||
hasShiftAfter?: boolean;
|
||||
isTimesheetApproved?: boolean;
|
||||
errorMessage?: string | undefined;
|
||||
expectedDailyHours?: number;
|
||||
currentShifts: Shift[];
|
||||
isHoliday?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -40,7 +41,7 @@
|
|||
const uiStore = useUiStore();
|
||||
const shiftTypeSelected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
||||
const selectRef = ref<QSelect | null>(null);
|
||||
const isShowingCommentPopup = ref(false);
|
||||
// const isShowingCommentPopup = ref(false);
|
||||
const errorMessageRow = ref('');
|
||||
const isShowingPredefinedTime = ref(shift.value.type === 'HOLIDAY');
|
||||
const predefinedHoursString = ref('');
|
||||
|
|
@ -48,7 +49,9 @@
|
|||
|
||||
// ========== computed ========================================
|
||||
|
||||
const comment_length = computed(() => shift.value.comment?.length ?? 0);
|
||||
const commentLength = computed(() => shift.value.comment?.length ?? 0);
|
||||
const isApproved = computed(() => isTimesheetApproved || shift.value.is_approved);
|
||||
const hasPTO = computed(() => currentShifts.some(shift => SHIFT_TYPES_WITH_PREDEFINED_TIMES.includes(shift.type)));
|
||||
|
||||
// ========== methods =========================================
|
||||
|
||||
|
|
@ -71,12 +74,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
const getCommentCounterColor = (comment_length: number) => {
|
||||
if (comment_length < 200) return 'primary';
|
||||
if (comment_length < 250) return 'warning';
|
||||
return 'negative';
|
||||
};
|
||||
|
||||
const onShiftTypeChange = (option: ShiftOption) => {
|
||||
shift.value.type = option.value;
|
||||
|
||||
|
|
@ -114,145 +111,61 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row q-px-xs">
|
||||
<div class="column">
|
||||
<div class="row q-pa-sm">
|
||||
<div class="col column">
|
||||
<div class="col row items-center text-uppercase q-px-xs rounded-5">
|
||||
<!-- comment button -->
|
||||
<q-btn
|
||||
v-if="!dense"
|
||||
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
||||
:text-color="shift.comment ? ((shift.is_approved && isTimesheetApproved) ? 'white' : 'accent') : 'grey-5'"
|
||||
class="col-auto full-height q-mx-xs rounded-5 shadow-1"
|
||||
@click="isShowingCommentPopup = true"
|
||||
>
|
||||
<q-dialog v-model="isShowingCommentPopup">
|
||||
<q-input
|
||||
color="white"
|
||||
v-model="shift.comment"
|
||||
dense
|
||||
:readonly="(shift.is_approved || isTimesheetApproved)"
|
||||
autofocus
|
||||
counter
|
||||
bottom-slots
|
||||
stack-label
|
||||
:label="$t('timesheet.shift.fields.header_comment')"
|
||||
:maxlength="COMMENT_LENGTH_MAX"
|
||||
:class="(shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed' : ''"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="edit" />
|
||||
</template>
|
||||
|
||||
<template #counter>
|
||||
<div class="row flex-center">
|
||||
<q-space />
|
||||
<q-knob
|
||||
v-model="comment_length"
|
||||
readonly
|
||||
:max="COMMENT_LENGTH_MAX"
|
||||
size="1.6em"
|
||||
:thickness="0.4"
|
||||
:color="getCommentCounterColor(comment_length)"
|
||||
track-color="grey-4"
|
||||
class="col-auto q-mr-xs"
|
||||
/>
|
||||
<span
|
||||
:class="'col-auto text-weight-bolder text-' + getCommentCounterColor(comment_length)"
|
||||
>{{ 280 - comment_length }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-dialog>
|
||||
</q-btn>
|
||||
|
||||
<div class="row justify-center q-pb-xs q-px-sm full-width">
|
||||
<!-- shift type -->
|
||||
<q-select
|
||||
ref="select"
|
||||
ref="selectRef"
|
||||
v-model="shiftTypeSelected"
|
||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||
dense
|
||||
:borderless="(shift.is_approved && isTimesheetApproved)"
|
||||
:readonly="(shift.is_approved && isTimesheetApproved)"
|
||||
options-dense
|
||||
borderless
|
||||
color="accent"
|
||||
label-color="white"
|
||||
stack-label
|
||||
label-slot
|
||||
hide-dropdown-icon
|
||||
:menu-offset="[0, 10]"
|
||||
:readonly="isApproved"
|
||||
:options="getShiftOptions(hasPTO, currentShifts.length > 1)"
|
||||
lazy-rules
|
||||
no-error-icon
|
||||
hide-bottom-space
|
||||
options-selected-class="text-white text-bold bg-accent"
|
||||
class="col q-px-md rounded-5 inset-shadow text-uppercase"
|
||||
:class="isApproved ? 'bg-white' : ($q.dark.isActive ? 'bg-primary' : 'bg-secondary')"
|
||||
popup-content-class="text-uppercase text-weight-medium rounded-5 shadow-12 z-top"
|
||||
popup-content-style="border: 1px solid var(--q-primary)"
|
||||
menu-anchor="bottom middle"
|
||||
menu-self="top middle"
|
||||
:options="SHIFT_OPTIONS"
|
||||
class="col rounded-5 bg-dark"
|
||||
:class="!shift.is_approved && !isTimesheetApproved ? '' : 'inset-shadow'"
|
||||
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||
popup-content-style="border: 2px solid var(--q-accent)"
|
||||
:menu-offset="[0, 5]"
|
||||
:style="`border: 1px solid ${$q.dark.isActive ? 'var(--q-secondary)' : colors.getPaletteColor('blue-grey-4')}; background-color: ${colors.lighten(getCssVar('secondary')!, 50)}`"
|
||||
@blur="onBlurShiftTypeSelect"
|
||||
@update:model-value="onShiftTypeChange"
|
||||
>
|
||||
<template #selected-item="scope">
|
||||
<div
|
||||
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis fit"
|
||||
class="row items-center text-weight-bold q-pt-sm no-wrap ellipsis"
|
||||
:tabindex="scope.tabindex"
|
||||
>
|
||||
<q-icon
|
||||
:name="scope.opt.icon"
|
||||
:color="scope.opt.icon_color"
|
||||
:color="shift.is_approved ? 'accent' : scope.opt.icon_color"
|
||||
size="sm"
|
||||
class="col-auto"
|
||||
:class="shift.is_approved ? 'q-mx-xs' : 'q-mr-xs'"
|
||||
/>
|
||||
|
||||
<span
|
||||
style="line-height: 1.2em;"
|
||||
class="col-auto ellipsis"
|
||||
:class="!shift.is_approved ? '' : 'text-white'"
|
||||
style="font-size: 1.3em;"
|
||||
class="col ellipsis"
|
||||
:class="shift.is_approved ? 'text-accent' : ''"
|
||||
>
|
||||
{{ $t(scope.opt.label) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #after>
|
||||
<q-icon
|
||||
v-if="shift.is_approved"
|
||||
:name="shift.is_remote ? 'las la-laptop' : 'las la-building'"
|
||||
size="1.2em"
|
||||
color="white"
|
||||
class="q-mr-sm"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="top middle"
|
||||
self="bottom middle"
|
||||
:offset="[0, 10]"
|
||||
:hide-delay="1000"
|
||||
class="text-uppercase text-weight-bold text-white bg-primary"
|
||||
>
|
||||
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
||||
$t('timesheet.shift.types.OFFICE') }}
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
|
||||
<q-toggle
|
||||
v-else
|
||||
v-model="shift.is_remote"
|
||||
:disable="shift.is_approved"
|
||||
dense
|
||||
keep-color
|
||||
size="3em"
|
||||
color="accent"
|
||||
icon="las la-building"
|
||||
checked-icon="las la-laptop"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="top middle"
|
||||
self="bottom middle"
|
||||
:offset="[0, 10]"
|
||||
:hide-delay="1000"
|
||||
class="text-uppercase text-weight-medium text-white bg-accent"
|
||||
>
|
||||
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
||||
$t('timesheet.shift.types.OFFICE') }}
|
||||
</q-tooltip>
|
||||
</q-toggle>
|
||||
</template>
|
||||
|
||||
<template #option="scope">
|
||||
<q-item
|
||||
clickable
|
||||
|
|
@ -267,6 +180,58 @@
|
|||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<!-- work-from-home toggle -->
|
||||
<template #after>
|
||||
<q-icon
|
||||
v-if="shift.is_approved"
|
||||
:name="shift.is_remote ? 'las la-laptop' : 'las la-building'"
|
||||
size="1.2em"
|
||||
color="accent"
|
||||
class="q-mr-sm"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="top middle"
|
||||
self="bottom middle"
|
||||
:offset="[0, 10]"
|
||||
class="text-uppercase text-weight-bold text-white bg-primary"
|
||||
>
|
||||
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
||||
$t('timesheet.shift.types.OFFICE') }}
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
|
||||
<q-toggle
|
||||
v-else
|
||||
v-model="shift.is_remote"
|
||||
:disable="shift.is_approved"
|
||||
dense
|
||||
keep-color
|
||||
size="3em"
|
||||
:color="isHoliday ? 'purple-5' : 'accent'"
|
||||
icon="las la-building"
|
||||
checked-icon="las la-laptop"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="top middle"
|
||||
self="bottom middle"
|
||||
:offset="[0, 10]"
|
||||
class="text-uppercase text-weight-medium text-white bg-accent"
|
||||
>
|
||||
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
||||
$t('timesheet.shift.types.OFFICE') }}
|
||||
</q-tooltip>
|
||||
</q-toggle>
|
||||
</template>
|
||||
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-medium text-uppercase q-px-sm no-pointer-events"
|
||||
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-blue-grey-7'"
|
||||
>
|
||||
{{ $t('timesheet.shift.types.label') }}
|
||||
</span>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
|
|
@ -287,93 +252,75 @@
|
|||
|
||||
<div
|
||||
v-else
|
||||
class="col row items-start text-uppercase rounded-5 q-pa-xs"
|
||||
class="col row items-start text-uppercase rounded-5 q-pt-sm"
|
||||
>
|
||||
<!-- punch in field -->
|
||||
<div class="col q-pr-xs">
|
||||
<q-input
|
||||
<div class="col">
|
||||
<TargoInput
|
||||
v-model="shift.start_time"
|
||||
no-top-padding
|
||||
dense
|
||||
:borderless="(shift.is_approved && isTimesheetApproved)"
|
||||
:readonly="(shift.is_approved && isTimesheetApproved)"
|
||||
type="time"
|
||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||
label-slot
|
||||
lazy-rules
|
||||
no-error-icon
|
||||
hide-bottom-space
|
||||
:readonly="isApproved"
|
||||
:background-color="isApproved ? 'white' : undefined"
|
||||
:input-text-color="isApproved ? 'accent' : ''"
|
||||
:label="$t('shared.misc.in')"
|
||||
:error="shift.has_error"
|
||||
:error-message="errorMessage || errorMessageRow !== '' ? $t(errorMessage ?? errorMessageRow) : ''"
|
||||
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
||||
class="rounded-5 bg-dark"
|
||||
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (!shift.is_approved && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')"
|
||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
||||
input-style="font-size: 1.2em;"
|
||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||
@blur="onTimeFieldBlur(shift.start_time)"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
:class="shift.is_approved ? ' q-ml-md' : ''"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.in') }}</span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- punch out field -->
|
||||
<div class="col">
|
||||
<q-input
|
||||
<TargoInput
|
||||
v-model="shift.end_time"
|
||||
standout
|
||||
no-top-padding
|
||||
dense
|
||||
:borderless="(shift.is_approved && isTimesheetApproved)"
|
||||
:readonly="(shift.is_approved && isTimesheetApproved)"
|
||||
type="time"
|
||||
label-slot
|
||||
no-error-icon
|
||||
hide-bottom-space
|
||||
:readonly="isApproved"
|
||||
:background-color="isApproved ? 'white' : undefined"
|
||||
:input-text-color="isApproved ? 'accent' : ''"
|
||||
:label="$t('shared.misc.out')"
|
||||
:error="shift.has_error"
|
||||
:error-message="errorMessage || errorMessageRow !== '' ? $t(errorMessage ?? errorMessageRow) : ''"
|
||||
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
||||
input-style="font-size: 1.2em;"
|
||||
class="rounded-5 bg-dark"
|
||||
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (shift.is_approved ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : (isTimesheetApproved ? 'inset-shadow' : ''))"
|
||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||
@blur="onTimeFieldBlur(shift.end_time)"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
:class="shift.is_approved ? ' q-ml-md' : ''"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.out') }}</span>
|
||||
</template>
|
||||
</q-input>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto q-pt-md">
|
||||
<TargoInput
|
||||
v-model="shift.comment"
|
||||
no-top-padding
|
||||
dense
|
||||
:readonly="isApproved"
|
||||
:background-color="isApproved ? 'white' : undefined"
|
||||
:input-text-color="isApproved ? 'accent' : ''"
|
||||
:label="$t('timesheet.expense.employee_comment')"
|
||||
:append-content="isApproved ? '' : `${commentLength ?? 0}/280`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
v-if="!shift.is_approved"
|
||||
flat
|
||||
outline
|
||||
dense
|
||||
color="negative"
|
||||
icon="las la-trash"
|
||||
size="lg"
|
||||
class="full-height"
|
||||
class="full-height rounded-5"
|
||||
@click="$emit('requestDelete')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator
|
||||
v-if="hasShiftAfter"
|
||||
spaced
|
||||
class="q-mx-md col-12"
|
||||
size="2px"
|
||||
:color="isApproved ? 'accent2' : 'accent'"
|
||||
class="q-mx-lg"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -390,4 +337,13 @@
|
|||
padding-top: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.q-field--float .q-field__label) {
|
||||
transform: translate(-17px, -60%) scale(0.75) !important;
|
||||
border-radius: 10px 10px 10px 0px;
|
||||
}
|
||||
|
||||
:deep(.q-field--auto-height.q-field--labeled .q-field__control-container) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import ShiftListDayMobile from 'src/modules/timesheets/components/mobile/shift-list-day-mobile.vue';
|
||||
|
||||
|
||||
import { useQuasar } from 'quasar';
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
|
||||
// ========== constants ========================================
|
||||
|
||||
const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10);
|
||||
|
||||
// ========== state ========================================
|
||||
|
||||
const emit = defineEmits<{
|
||||
'onCurrentDayComponentFound': [component: HTMLElement | undefined];
|
||||
}>();
|
||||
|
||||
const q = useQuasar();
|
||||
const timesheetStore = useTimesheetStore();
|
||||
|
||||
const mobileAnimationDirection = ref('fadeInLeft');
|
||||
const currentDayComponent = ref<HTMLElement[] | null>(null);
|
||||
const currentDayComponentWatcher = ref(currentDayComponent);
|
||||
|
||||
// ========== computed ========================================
|
||||
|
||||
const animationStyle = computed(() => q.platform.is.mobile ? mobileAnimationDirection.value : 'fadeInDown');
|
||||
|
||||
// ========== methods ========================================
|
||||
|
||||
const getMobileDayRef = (iso_date_string: string): string => {
|
||||
return iso_date_string === CURRENT_DATE_STRING ? 'currentDayComponent' : '';
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await timesheetStore.getCurrentFederalHolidays();
|
||||
});
|
||||
|
||||
watch(currentDayComponentWatcher, () => {
|
||||
if (currentDayComponent.value && q.platform.is.mobile) {
|
||||
emit('onCurrentDayComponentFound', currentDayComponent.value[0])
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fit column no-wrap q-pb-lg">
|
||||
<div
|
||||
v-for="timesheet of timesheetStore.timesheets"
|
||||
:key="timesheet.timesheet_id"
|
||||
class="col-auto column no-wrap"
|
||||
>
|
||||
<transition-group
|
||||
appear
|
||||
:enter-active-class="`animated ${animationStyle}`"
|
||||
>
|
||||
<div
|
||||
v-for="day, dayIndex in timesheet.days"
|
||||
:key="day.date"
|
||||
:ref="getMobileDayRef(day.date)"
|
||||
class="col-auto row q-pa-sm full-width relative-position"
|
||||
:style="`animation-delay: ${dayIndex / 15}s;`"
|
||||
>
|
||||
<ShiftListDayMobile
|
||||
v-model="timesheet.days[dayIndex]!"
|
||||
:timesheet-id="timesheet.timesheet_id"
|
||||
:is-timesheet-approved="timesheet.is_approved"
|
||||
/>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style
|
||||
scoped
|
||||
lang="scss"
|
||||
>
|
||||
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100, 200) {
|
||||
.mobile-rounded-#{$size} {
|
||||
border-radius: #{$size}px !important;
|
||||
}
|
||||
|
||||
.mobile-rounded-#{$size}>div:first-child {
|
||||
border-radius: #{$size}px #{$size}px 0 0 !important;
|
||||
}
|
||||
|
||||
.mobile-rounded-#{$size}>div:last-child {
|
||||
border-radius: 0 0 #{$size}px #{$size}px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import ShiftListDay from 'src/modules/timesheets/components/shift-list-day.vue';
|
||||
import ShiftListDateWidget from 'src/modules/timesheets/components/shift-list-date-widget.vue';
|
||||
|
||||
import { date, useQuasar } from 'quasar';
|
||||
import { ref, computed, watch, onMounted, inject } from 'vue';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
// ========== constants ========================================
|
||||
|
||||
const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10);
|
||||
|
||||
// ========== state ========================================
|
||||
|
||||
const emit = defineEmits<{
|
||||
'onCurrentDayComponentFound': [component: HTMLElement | undefined];
|
||||
}>();
|
||||
|
||||
const q = useQuasar();
|
||||
const { extractDate } = date;
|
||||
const { locale } = useI18n();
|
||||
const uiStore = useUiStore();
|
||||
const timesheetApi = useTimesheetApi();
|
||||
const timesheetStore = useTimesheetStore();
|
||||
|
||||
const mobileAnimationDirection = ref('fadeInLeft');
|
||||
const currentDayComponent = ref<HTMLElement[] | null>(null);
|
||||
const currentDayComponentWatcher = ref(currentDayComponent);
|
||||
const employeeEmail = inject<string>('employeeEmail');
|
||||
|
||||
// ========== computed ========================================
|
||||
|
||||
const animationStyle = computed(() => q.platform.is.mobile ? mobileAnimationDirection.value : 'fadeInDown');
|
||||
|
||||
// ========== methods ========================================
|
||||
|
||||
// const timesheetRows = computed(() => timesheetStore.timesheets);
|
||||
|
||||
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
|
||||
uiStore.focusNextComponent = true;
|
||||
const newShift = new Shift;
|
||||
newShift.date = date;
|
||||
newShift.timesheet_id = timesheet_id;
|
||||
day_shifts.push(newShift);
|
||||
};
|
||||
|
||||
const deleteUnsavedShift = (timesheet_index: number, day_index: number) => {
|
||||
if (timesheetStore.timesheets !== undefined) {
|
||||
const day = timesheetStore.timesheets[timesheet_index]!.days[day_index]!;
|
||||
const shifts_without_deleted_shift = day.shifts.filter(shift => shift.id !== 0);
|
||||
day.shifts = shifts_without_deleted_shift;
|
||||
}
|
||||
};
|
||||
|
||||
const getDayApproval = (day: TimesheetDay) => {
|
||||
if (day.shifts.length < 1) return false;
|
||||
return day.shifts.every(shift => shift.is_approved === true);
|
||||
};
|
||||
|
||||
const getMobileDayRef = (iso_date_string: string): string => {
|
||||
return iso_date_string === CURRENT_DATE_STRING ? 'currentDayComponent' : '';
|
||||
};
|
||||
|
||||
const getHolidayName = (date: string) => {
|
||||
const holiday = timesheetStore.federal_holidays.find(holiday => holiday.date === date);
|
||||
if (!holiday) return;
|
||||
|
||||
if (locale.value === 'fr-FR')
|
||||
return holiday.nameFr;
|
||||
|
||||
else if (locale.value === 'en-CA')
|
||||
return holiday.nameEn;
|
||||
};
|
||||
|
||||
const onClickApplyWeeklyPreset = async (timesheet_id: number) => {
|
||||
await timesheetApi.applyPreset(timesheet_id, undefined, undefined, employeeEmail);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await timesheetStore.getCurrentFederalHolidays();
|
||||
});
|
||||
|
||||
watch(currentDayComponentWatcher, () => {
|
||||
if (currentDayComponent.value && q.platform.is.mobile) {
|
||||
emit('onCurrentDayComponentFound', currentDayComponent.value[0])
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="fit"
|
||||
:class="$q.platform.is.mobile ? 'column no-wrap q-pb-lg' : 'row'"
|
||||
>
|
||||
<div
|
||||
v-for="timesheet, timesheet_index of timesheetStore.timesheets"
|
||||
:key="timesheet.timesheet_id"
|
||||
class="no-wrap"
|
||||
:class="$q.platform.is.mobile ? 'col-auto column' : 'col column fit items-center'"
|
||||
>
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated fadeInDown"
|
||||
leave-active-class="animated fadeOutUp"
|
||||
>
|
||||
<q-btn
|
||||
v-if="!$q.platform.is.mobile && timesheet.days.every(day => day.shifts.length < 1) && timesheetStore.has_timesheet_preset"
|
||||
:disable="!timesheet.days.every(day => day.shifts.length < 1)"
|
||||
flat
|
||||
dense
|
||||
:label="$t('timesheet.apply_preset_week')"
|
||||
class="col-auto text-uppercase text-weight-bold text-accent q-mx-lg q-py-none rounded-5"
|
||||
@click="onClickApplyWeeklyPreset(timesheet.timesheet_id)"
|
||||
>
|
||||
<q-icon
|
||||
name="las la-calendar-week"
|
||||
color="accent"
|
||||
size="md"
|
||||
/>
|
||||
</q-btn>
|
||||
</transition>
|
||||
|
||||
<transition-group
|
||||
appear
|
||||
:enter-active-class="`animated ${animationStyle}`"
|
||||
>
|
||||
<div
|
||||
v-for="day, day_index in timesheet.days"
|
||||
:key="day.date"
|
||||
:ref="getMobileDayRef(day.date)"
|
||||
class="col-auto row q-pa-sm full-width relative-position"
|
||||
:style="`animation-delay: ${day_index / 15}s;`"
|
||||
>
|
||||
<!-- optional label indicating which holiday if today is a holiday -->
|
||||
<span
|
||||
v-if="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date)"
|
||||
class="absolute-top-left text-uppercase text-weight-bolder text-purple-5"
|
||||
style="transform: translate(25px, -7px);"
|
||||
>
|
||||
{{ getHolidayName(day.date) }}
|
||||
</span>
|
||||
|
||||
<!-- mobile version in portrait mode -->
|
||||
<div
|
||||
v-if="$q.platform.is.mobile && ($q.screen.width < $q.screen.height)"
|
||||
class="col-auto full-width q-px-md q-py-sm"
|
||||
>
|
||||
<q-card
|
||||
class="shadow-12"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent rounded-10' : 'bg-dark mobile-rounded-10'"
|
||||
>
|
||||
<q-card-section
|
||||
class="text-weight-bolder text-uppercase text-h6 q-py-sm text-center relative-position"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent text-white' : 'bg-primary text-white'"
|
||||
style="line-height: 1em;"
|
||||
>
|
||||
<span> {{ $d(extractDate(day.date, 'YYYY-MM-DD'), {
|
||||
weekday: 'long', day: 'numeric', month:
|
||||
'long'
|
||||
}) }}</span>
|
||||
|
||||
<q-icon
|
||||
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
||||
name="verified"
|
||||
size="3em"
|
||||
color="white"
|
||||
class="absolute-top-left z-top"
|
||||
style="top: -0.2em; left: 0px;"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section
|
||||
v-if="day.shifts.filter(shift => shift.id !== 0).length > 0"
|
||||
class="q-pa-none transparent"
|
||||
>
|
||||
<ShiftListDay
|
||||
outlined
|
||||
:timesheet-id="timesheet.timesheet_id"
|
||||
:week-day-index="day_index"
|
||||
:animation-delay-multiplier="day_index"
|
||||
:approved="(getDayApproval(day) || timesheet.is_approved)"
|
||||
:day="day"
|
||||
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-btn
|
||||
v-if="!(getDayApproval(day) || timesheet.is_approved)"
|
||||
square
|
||||
dense
|
||||
size="xl"
|
||||
color="accent"
|
||||
icon="more_time"
|
||||
class="full-width"
|
||||
style="border-radius: 0 0 10px 10px;"
|
||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- desktop version -->
|
||||
<div
|
||||
v-else
|
||||
class="col row full-width rounded-10 ellipsis shadow-10"
|
||||
:style="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'border: 2px solid #ab47bc' : ''"
|
||||
>
|
||||
<div
|
||||
class="col row"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? (timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'bg-purple-5' : 'bg-accent') : 'bg-dark'"
|
||||
>
|
||||
<!-- Date block -->
|
||||
<ShiftListDateWidget
|
||||
:display-date="day.date"
|
||||
:approved="(getDayApproval(day) || timesheet.is_approved)"
|
||||
class="col-auto"
|
||||
/>
|
||||
|
||||
<ShiftListDay
|
||||
:timesheet-id="timesheet.timesheet_id"
|
||||
:week-day-index="day_index"
|
||||
:day="day"
|
||||
:holiday="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date)"
|
||||
:approved="getDayApproval(day) || timesheet.is_approved"
|
||||
class="col"
|
||||
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-auto self-stretch">
|
||||
<q-icon
|
||||
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
||||
name="verified"
|
||||
color="white"
|
||||
size="xl"
|
||||
class="full-height"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? (timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'bg-purple-5' : 'bg-accent') : ''"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
v-else
|
||||
:dense="!$q.platform.is.mobile"
|
||||
square
|
||||
icon="more_time"
|
||||
size="lg"
|
||||
:color="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'purple-5' : 'accent'"
|
||||
text-color="white"
|
||||
class="full-height"
|
||||
:class="$q.platform.is.mobile ? 'q-px-xs' : ''"
|
||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style
|
||||
scoped
|
||||
lang="scss"
|
||||
>
|
||||
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100, 200) {
|
||||
.mobile-rounded-#{$size} {
|
||||
border-radius: #{$size}px !important;
|
||||
}
|
||||
|
||||
.mobile-rounded-#{$size}>div:first-child {
|
||||
border-radius: #{$size}px #{$size}px 0 0 !important;
|
||||
}
|
||||
|
||||
.mobile-rounded-#{$size}>div:last-child {
|
||||
border-radius: 0 0 #{$size}px #{$size}px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
displayDate: string;
|
||||
dense?: boolean;
|
||||
approved?: boolean;
|
||||
today?: boolean;
|
||||
}>();
|
||||
|
||||
const date_font_size = computed(() => dense ? '1.5em' : '2.5em');
|
||||
|
|
@ -23,9 +24,18 @@
|
|||
|
||||
<template>
|
||||
<div
|
||||
class="column flex-center rounded-10 text-center self-center bg-transparent"
|
||||
class="column flex-center rounded-10 text-center self-center bg-transparent relative-position"
|
||||
:style="date_box_size"
|
||||
>
|
||||
<div
|
||||
v-if="today"
|
||||
class="absolute fit q-px-sm q-py-md"
|
||||
>
|
||||
<div class="fit" style="background-image: url('src/assets/circle.png'); background-size: 100% 100%; background-repeat: no-repeat; opacity: 0.65;">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-if="!dense"
|
||||
class="col-auto text-uppercase text-weight-bold"
|
||||
|
|
@ -34,13 +44,15 @@
|
|||
>
|
||||
{{ $d(display_date, { weekday: $q.platform.is.mobile ? 'short' : 'long' }) }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="col-auto text-weight-bolder"
|
||||
:class="approved ? 'text-white' : ''"
|
||||
:class="today ? 'text-accent' : (approved ? 'text-white' : '')"
|
||||
:style="'font-size: ' + date_font_size + '; line-height: 90% !important;'"
|
||||
>
|
||||
{{ display_date.getDate() }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="col-auto text-uppercase text-weight-bold"
|
||||
:class="approved ? 'text-white' : ''"
|
||||
|
|
@ -50,3 +62,14 @@
|
|||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style
|
||||
scoped
|
||||
lang="css"
|
||||
>
|
||||
.bordered-text {
|
||||
text-shadow: 2px 0 white, -2px 0 white, 0 2px white, 0 -2px white,
|
||||
1px 1px white, -1px -1px white, 1px -1px white, -1px 1px white;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,10 +3,10 @@
|
|||
lang="ts"
|
||||
>
|
||||
import DetailsDialogShiftMenu from 'src/modules/timesheet-approval/components/details-dialog-shift-menu.vue';
|
||||
import TargoInput from 'src/modules/shared/components/targo-input.vue';
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { computed, inject, onMounted, ref } from 'vue';
|
||||
import { QSelect, QInput, useQuasar, type QSelectProps } from 'quasar';
|
||||
import { QSelect, useQuasar, colors, getCssVar } from 'quasar';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
import { getCurrentDailyMinutesWorked, getShiftOptions, getTimeStringFromMinutes, SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||
|
|
@ -23,7 +23,6 @@
|
|||
const shift = defineModel<Shift>('shift', { required: true });
|
||||
|
||||
const {
|
||||
errorMessage = undefined,
|
||||
isTimesheetApproved = false,
|
||||
currentShifts,
|
||||
holiday = false,
|
||||
|
|
@ -32,7 +31,7 @@
|
|||
currentShifts: Shift[];
|
||||
expectedDailyHours?: number;
|
||||
isTimesheetApproved?: boolean;
|
||||
errorMessage?: string | undefined;
|
||||
errorTimesheet?: boolean | undefined;
|
||||
holiday?: boolean | undefined;
|
||||
}>();
|
||||
|
||||
|
|
@ -42,7 +41,6 @@
|
|||
}>();
|
||||
|
||||
const q = useQuasar();
|
||||
const { t } = useI18n();
|
||||
const uiStore = useUiStore();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
|
|
@ -59,41 +57,7 @@
|
|||
// ================== Computed ==================
|
||||
|
||||
const hasPTO = computed(() => currentShifts.some(shift => SHIFT_TYPES_WITH_PREDEFINED_TIMES.includes(shift.type)));
|
||||
|
||||
const timeInputProps = computed(() => ({
|
||||
dense: true,
|
||||
borderless: shift.value.is_approved && isTimesheetApproved,
|
||||
readonly: shift.value.is_approved && isTimesheetApproved,
|
||||
standout: q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9',
|
||||
labelSlot: true,
|
||||
lazyRules: true,
|
||||
noErrorIcon: true,
|
||||
hideBottomSpace: true,
|
||||
error: shift.value.has_error,
|
||||
errorMessage: errorMessage ? t(errorMessage) : (shiftErrorMessage.value ? t(shiftErrorMessage.value) : undefined),
|
||||
labelColor: shift.value.is_approved ? 'white' : (holiday ? 'purple-5' : 'accent'),
|
||||
class: `col rounded-5 bg-dark q-mx-xs ${shift.value.id === -2 ? 'bg-negative' : ''} ${shift.value.is_approved || isTimesheetApproved ? 'cursor-not-allowed inset-shadow' : ''}`,
|
||||
inputClass: `text-weight-medium ${shift.value.id === -2 ? 'text-white ' : ' '} ${shift.value.is_approved ? 'text-white cursor-not-allowed q-px-sm' : ''}`,
|
||||
style: shift.value.is_approved ? (holiday ? 'background-color: #7b1fa2 !important' : 'background-color: #0a7d32 !important;') : '',
|
||||
inputStyle: "font-size: 1.2em;"
|
||||
}));
|
||||
|
||||
const shiftTypeSelectProps = computed<Partial<QSelectProps>>(() => ({
|
||||
standout: q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9',
|
||||
dense: true,
|
||||
borderless: shift.value.is_approved && isTimesheetApproved,
|
||||
readonly: shift.value.is_approved && isTimesheetApproved,
|
||||
optionsDense: !q.platform.is.mobile,
|
||||
hideDropdownIcon: true,
|
||||
menuOffset: [0, 10],
|
||||
menuAnchor: "bottom middle",
|
||||
menuSelf: "top middle",
|
||||
options: getShiftOptions(hasPTO.value, currentShifts.length > 1),
|
||||
class: `col rounded-5 q-mx-xs bg-dark ${!shift.value.is_approved && !isTimesheetApproved ? '' : 'inset-shadow'}`,
|
||||
popupContentClass: "text-uppercase text-weight-bold text-center rounded-5",
|
||||
style: shift.value.is_approved ? (holiday ? 'background-color: #7b1fa2 !important' : 'background-color: #0a7d32 !important;') : '',
|
||||
popupContentStyle: "border: 2px solid var(--q-accent)",
|
||||
}));
|
||||
const isApproved = computed(() => isTimesheetApproved || shift.value.is_approved);
|
||||
|
||||
// ================== Methods ==================
|
||||
|
||||
|
|
@ -170,7 +134,7 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="row full-width q-py-xs">
|
||||
<!-- delete shift confirmation dialog -->
|
||||
<q-dialog
|
||||
v-model="is_showing_delete_confirm"
|
||||
|
|
@ -203,34 +167,53 @@
|
|||
</q-dialog>
|
||||
|
||||
<div
|
||||
class="row items-center text-uppercase rounded-5"
|
||||
class="row items-center text-uppercase rounded-5 no-wrap"
|
||||
:class="$q.platform.is.mobile ? 'col q-mb-xs q-px-xs' : 'col-4'"
|
||||
>
|
||||
<!-- shift type -->
|
||||
<q-select
|
||||
ref="selectRef"
|
||||
v-model="shiftTypeSelected"
|
||||
v-bind="shiftTypeSelectProps"
|
||||
dense
|
||||
borderless
|
||||
color="accent"
|
||||
label-color="white"
|
||||
stack-label
|
||||
label-slot
|
||||
hide-dropdown-icon
|
||||
:readonly="isApproved"
|
||||
:options="getShiftOptions(hasPTO, currentShifts.length > 1)"
|
||||
lazy-rules
|
||||
no-error-icon
|
||||
hide-bottom-space
|
||||
options-selected-class="text-white text-bold bg-accent"
|
||||
class="col q-pl-md rounded-5 inset-shadow text-uppercase"
|
||||
:class="isApproved ? 'bg-white' : ($q.dark.isActive ? 'bg-primary' : '')"
|
||||
popup-content-class="text-uppercase text-weight-medium rounded-5 shadow-12 z-top"
|
||||
popup-content-style="border: 1px solid var(--q-primary)"
|
||||
menu-anchor="bottom middle"
|
||||
menu-self="top middle"
|
||||
:menu-offset="[0, 5]"
|
||||
:style="`border: 1px solid ${q.dark.isActive ? 'var(--q-secondary)' : colors.getPaletteColor('blue-grey-4')}; background-color: ${colors.lighten(getCssVar('secondary')!, 50)}`"
|
||||
@blur="onBlurShiftTypeSelect"
|
||||
@update:model-value="onShiftTypeChange"
|
||||
>
|
||||
<template #selected-item="scope">
|
||||
<div
|
||||
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
||||
:class="$q.platform.is.mobile ? 'full-height' : ''"
|
||||
class="row items-center text-weight-bold no-wrap ellipsis"
|
||||
:tabindex="scope.tabindex"
|
||||
>
|
||||
<q-icon
|
||||
:name="scope.opt.icon"
|
||||
:color="shift.is_approved ? 'white' : scope.opt.icon_color"
|
||||
:color="scope.opt.icon_color"
|
||||
size="sm"
|
||||
class="col-auto"
|
||||
:class="shift.is_approved ? 'q-mx-xs' : 'q-mr-xs'"
|
||||
/>
|
||||
|
||||
<span
|
||||
style="line-height: 1.2em;"
|
||||
class="col-auto ellipsis"
|
||||
:class="!shift.is_approved ? '' : 'text-white'"
|
||||
:class="shift.is_approved ? 'text-accent' : ''"
|
||||
>
|
||||
{{ $t(scope.opt.label) }}
|
||||
</span>
|
||||
|
|
@ -278,7 +261,7 @@
|
|||
:disable="shift.is_approved"
|
||||
dense
|
||||
keep-color
|
||||
size="3em"
|
||||
size="2.75em"
|
||||
:color="holiday ? 'purple-5' : 'accent'"
|
||||
icon="las la-building"
|
||||
checked-icon="las la-laptop"
|
||||
|
|
@ -294,6 +277,15 @@
|
|||
</q-tooltip>
|
||||
</q-toggle>
|
||||
</template>
|
||||
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-medium text-uppercase q-px-sm no-pointer-events"
|
||||
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-blue-grey-7'"
|
||||
>
|
||||
{{ $t('timesheet.shift.types.label') }}
|
||||
</span>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
|
|
@ -319,38 +311,32 @@
|
|||
v-else
|
||||
class="col row items-start text-uppercase rounded-5 q-pa-xs"
|
||||
>
|
||||
<q-input
|
||||
ref="start_time"
|
||||
<TargoInput
|
||||
v-model="shift.start_time"
|
||||
v-bind="timeInputProps"
|
||||
no-top-padding
|
||||
dense
|
||||
type="time"
|
||||
:readonly="isApproved"
|
||||
:background-color="isApproved ? 'white' : undefined"
|
||||
:input-text-color="isApproved ? 'accent' : ''"
|
||||
:label="$t('shared.misc.in')"
|
||||
:error="shift.has_error"
|
||||
@blur="onTimeFieldBlur(shift.start_time)"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
:class="shift.is_approved ? ' q-ml-md' : ''"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.in') }}</span>
|
||||
</template>
|
||||
</q-input>
|
||||
/>
|
||||
|
||||
<!-- punch out field -->
|
||||
<q-input
|
||||
ref="end_time"
|
||||
<TargoInput
|
||||
v-model="shift.end_time"
|
||||
v-bind="timeInputProps"
|
||||
no-top-padding
|
||||
dense
|
||||
type="time"
|
||||
:readonly="isApproved"
|
||||
:background-color="isApproved ? 'white' : undefined"
|
||||
:input-text-color="isApproved ? 'accent' : ''"
|
||||
:label="$t('shared.misc.out')"
|
||||
:error="shift.has_error"
|
||||
@blur="onTimeFieldBlur(shift.end_time)"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
:class="shift.is_approved ? ' q-ml-md' : ''"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.out') }}</span>
|
||||
</template>
|
||||
</q-input>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
@ -387,7 +373,7 @@
|
|||
style="border: 3px solid var(--q-accent);"
|
||||
>
|
||||
<q-input
|
||||
color="white"
|
||||
color="accent"
|
||||
v-model="scope.value"
|
||||
dense
|
||||
:readonly="shift.is_approved"
|
||||
|
|
@ -469,4 +455,9 @@ drops down, rather than the standard floating red text only -->
|
|||
padding-top: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.q-field--dense.q-field--float .q-field__label) {
|
||||
transform: translate(-17px, -60%) scale(0.75) !important;
|
||||
border-radius: 10px 10px 10px 0px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,73 +3,130 @@
|
|||
lang="ts"
|
||||
>
|
||||
import ShiftListDayRow from 'src/modules/timesheets/components/shift-list-day-row.vue';
|
||||
import ShiftListDayRowMobile from 'src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue';
|
||||
import ShiftListDateWidget from 'src/modules/timesheets/components/shift-list-date-widget.vue';
|
||||
|
||||
import { inject, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||
import { isShiftOverlap } from 'src/modules/timesheets/utils/shift.util';
|
||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
import { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
||||
|
||||
// ================== State ==================
|
||||
// ========== Constants ========================================
|
||||
|
||||
const { timesheetId, weekDayIndex, day, dense = false, approved = false, holiday = false } = defineProps<{
|
||||
const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10);
|
||||
|
||||
// ========== State ========================================
|
||||
|
||||
const day = defineModel<TimesheetDay>({ required: true });
|
||||
|
||||
const { timesheetId, weekDayIndex, timesheetApproved = false } = defineProps<{
|
||||
timesheetId: number;
|
||||
weekDayIndex: number;
|
||||
day: TimesheetDay;
|
||||
dense?: boolean;
|
||||
approved?: boolean;
|
||||
holiday?: boolean;
|
||||
timesheetApproved?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'deleteUnsavedShift': [void];
|
||||
}>();
|
||||
|
||||
const shift_api = useShiftApi();
|
||||
const timesheet_api = useTimesheetApi();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const preset_mouseover = ref(false);
|
||||
const shift_error_message = ref<string | undefined>();
|
||||
const { locale } = useI18n();
|
||||
const uiStore = useUiStore();
|
||||
const shiftApi = useShiftApi();
|
||||
const timesheetApi = useTimesheetApi();
|
||||
const timesheetStore = useTimesheetStore();
|
||||
const presetMouseover = ref(false);
|
||||
const shiftErrorMessage = ref<string | undefined>();
|
||||
const employeeEmail = inject<string>('employeeEmail');
|
||||
|
||||
// ========== Computed ========================================
|
||||
|
||||
const isDayApproved = computed(() => day.value.shifts.length > 0 && day.value.shifts.every(
|
||||
shift => shift.is_approved === true));
|
||||
|
||||
const isHoliday = computed(() => timesheetStore.federal_holidays.some(
|
||||
holiday => holiday.date === day.value.date));
|
||||
|
||||
const isToday = computed(() => CURRENT_DATE_STRING === day.value.date);
|
||||
|
||||
// ================== Methods ==================
|
||||
|
||||
const deleteCurrentShift = async (shift: Shift) => {
|
||||
if (shift.id <= 0) {
|
||||
shift.id = 0;
|
||||
emit('deleteUnsavedShift');
|
||||
} else {
|
||||
await shift_api.deleteShiftById(shift.id, employeeEmail);
|
||||
}
|
||||
const addNewShift = () => {
|
||||
uiStore.focusNextComponent = true;
|
||||
const newShift = new Shift(day.value.date);
|
||||
newShift.timesheet_id = timesheetId;
|
||||
day.value.shifts.push(newShift);
|
||||
};
|
||||
|
||||
if (day.shifts.length < 2 && shift_error_message.value !== undefined) {
|
||||
const deleteCurrentShift = async (shiftId: number, index: number) => {
|
||||
if (shiftId <= 0)
|
||||
day.value.shifts.splice(index, 1);
|
||||
else
|
||||
await shiftApi.deleteShiftById(shiftId, employeeEmail);
|
||||
|
||||
if (day.value.shifts.length < 2 && shiftErrorMessage.value !== undefined) {
|
||||
onTimeFieldBlur();
|
||||
}
|
||||
};
|
||||
|
||||
const onTimeFieldBlur = () => {
|
||||
const is_error = isShiftOverlap(day.shifts);
|
||||
day.shifts.map(shift => shift.has_error = is_error);
|
||||
const is_error = isShiftOverlap(day.value.shifts);
|
||||
day.value.shifts.map(shift => shift.has_error = is_error);
|
||||
if (is_error)
|
||||
shift_error_message.value = 'timesheet.errors.SHIFT_OVERLAP_SHORT';
|
||||
shiftErrorMessage.value = 'timesheet.errors.SHIFT_OVERLAP_SHORT';
|
||||
else
|
||||
shift_error_message.value = undefined;
|
||||
shiftErrorMessage.value = undefined;
|
||||
}
|
||||
|
||||
const onClickApplyDailyPreset = async () => {
|
||||
await timesheet_api.applyPreset(timesheetId, weekDayIndex, day.date, employeeEmail);
|
||||
await timesheetApi.applyPreset(timesheetId, weekDayIndex, day.value.date, employeeEmail);
|
||||
}
|
||||
|
||||
const getHolidayName = (date: string) => {
|
||||
const holiday = timesheetStore.federal_holidays.find(holiday => holiday.date === date);
|
||||
if (!holiday) return;
|
||||
|
||||
if (locale.value === 'fr-FR')
|
||||
return holiday.nameFr;
|
||||
|
||||
else if (locale.value === 'en-CA')
|
||||
return holiday.nameEn;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row fit rounded-10 ellipsis no-wrap shadow-6">
|
||||
<!-- optional label indicating which holiday if today is a holiday -->
|
||||
<span
|
||||
v-if="isHoliday"
|
||||
class="absolute-top-left text-uppercase text-bold holiday-border text-white"
|
||||
style="transform: translate(25px, -2px);"
|
||||
>
|
||||
{{ getHolidayName(day.date) }}
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="column justify-center q-py-xs"
|
||||
:class="approved ? '' : ''"
|
||||
@mouseenter="preset_mouseover = true"
|
||||
@mouseleave="preset_mouseover = false"
|
||||
class="col rounded-10"
|
||||
:class="isToday ? 'bg-accent' : ''"
|
||||
:style="isToday ? 'padding: 3px; ' : ''"
|
||||
>
|
||||
<div
|
||||
class="col row fit rounded-10"
|
||||
:class="(isDayApproved || timesheetApproved) ? (isHoliday ? 'bg-purple-5' : 'bg-accent') : 'bg-dark'"
|
||||
:style="isHoliday ? 'border: 3px solid #ab47bc' : ''"
|
||||
>
|
||||
<!-- Date block -->
|
||||
<ShiftListDateWidget
|
||||
:display-date="day.date"
|
||||
:approved="isDayApproved || timesheetApproved"
|
||||
:today="isToday"
|
||||
class="col-auto"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="col column justify-center q-py-xs full-width q-my-xs"
|
||||
@mouseenter="presetMouseover = true"
|
||||
@mouseleave="presetMouseover = false"
|
||||
>
|
||||
<!-- Button to apply preset to day -->
|
||||
<transition
|
||||
|
|
@ -78,7 +135,7 @@
|
|||
leave-active-class="animated zoomOut fast"
|
||||
>
|
||||
<q-btn
|
||||
v-if="!$q.platform.is.mobile && day.shifts.length < 1 && preset_mouseover && timesheet_store.has_timesheet_preset"
|
||||
v-if="day.shifts.length < 1 && presetMouseover && timesheetStore.has_timesheet_preset"
|
||||
:disable="day.shifts.length > 0"
|
||||
flat
|
||||
dense
|
||||
|
|
@ -97,33 +154,56 @@
|
|||
</transition>
|
||||
|
||||
<div
|
||||
v-for="shift, shift_index in day.shifts"
|
||||
:key="shift_index"
|
||||
class="col-auto"
|
||||
v-for="shift, shiftIndex in day.shifts"
|
||||
:key="shiftIndex"
|
||||
class="col-auto row"
|
||||
>
|
||||
<ShiftListDayRowMobile
|
||||
v-if="$q.platform.is.mobile"
|
||||
v-model:shift="day.shifts[shift_index]!"
|
||||
:is-timesheet-approved="approved"
|
||||
:error-message="shift_error_message"
|
||||
:dense="dense"
|
||||
:current-shifts="day.shifts"
|
||||
:has-shift-after="shift_index < day.shifts.length - 1"
|
||||
@request-delete="deleteCurrentShift(shift)"
|
||||
@on-time-field-blur="onTimeFieldBlur()"
|
||||
/>
|
||||
|
||||
<ShiftListDayRow
|
||||
v-else
|
||||
v-model:shift="day.shifts[shift_index]!"
|
||||
:holiday="holiday"
|
||||
v-model:shift="day.shifts[shiftIndex]!"
|
||||
:holiday="isHoliday"
|
||||
:current-shifts="day.shifts"
|
||||
:is-timesheet-approved="approved"
|
||||
:error-message="shift_error_message"
|
||||
@request-delete="deleteCurrentShift(shift)"
|
||||
:is-timesheet-approved="timesheetApproved"
|
||||
:error-message="shiftErrorMessage"
|
||||
@request-delete="deleteCurrentShift(shift.id, shiftIndex)"
|
||||
@on-time-field-blur="onTimeFieldBlur()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto self-stretch">
|
||||
<q-icon
|
||||
v-if="(isDayApproved || timesheetApproved)"
|
||||
name="verified"
|
||||
color="white"
|
||||
size="xl"
|
||||
class="full-height"
|
||||
:class="(isDayApproved || timesheetApproved) ? (isHoliday ? 'bg-purple-5' : 'bg-accent') : ''"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
v-else
|
||||
:dense="!$q.platform.is.mobile"
|
||||
square
|
||||
icon="more_time"
|
||||
size="lg"
|
||||
:color="isHoliday ? 'purple-5' : 'accent'"
|
||||
text-color="white"
|
||||
class="full-height"
|
||||
:style="isHoliday ? 'border-radius: 0 6px 6px 0;' : ''"
|
||||
@click="addNewShift"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style
|
||||
scoped
|
||||
lang="scss"
|
||||
>
|
||||
.holiday-border {
|
||||
text-shadow: 2px 0 $purple-5, -2px 0 $purple-5, 0 2px $purple-5, 0 -2px $purple-5,
|
||||
1px 1px $purple-5, -1px -1px $purple-5, 1px -1px $purple-5, -1px 1px $purple-5;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
lang="ts"
|
||||
>
|
||||
import ShiftList from 'src/modules/timesheets/components/shift-list.vue';
|
||||
import ShiftListMobile from 'src/modules/timesheets/components/mobile/shift-list-mobile.vue';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
|
|
@ -66,7 +67,12 @@
|
|||
</div>
|
||||
|
||||
<!-- Else show timesheets if found -->
|
||||
<ShiftList @on-current-day-component-found="onTodayComponentFound" />
|
||||
<ShiftListMobile
|
||||
v-else-if="$q.platform.is.mobile"
|
||||
@on-current-day-component-found="onTodayComponentFound"
|
||||
/>
|
||||
|
||||
<ShiftList v-else />
|
||||
</q-scroll-area>
|
||||
|
||||
<q-page-sticky
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
class="col row full-width"
|
||||
>
|
||||
<span class="col-auto text-uppercase text-caption text-bold text-accent">
|
||||
{{ $t(`timesheet_approvals.table.weekly_hours_${index + 1}`) }}
|
||||
{{ $t(`timesheet_approvals.table.weekly_hours_${index + 1}`) }}:
|
||||
</span>
|
||||
|
||||
<span class="col text-right">{{ getHoursMinutesStringFromHoursFloat(hours) }}</span>
|
||||
|
|
|
|||
|
|
@ -3,20 +3,12 @@
|
|||
lang="ts"
|
||||
>
|
||||
import ShiftListDay from 'src/modules/timesheets/components/shift-list-day.vue';
|
||||
import ShiftListDateWidget from 'src/modules/timesheets/components/shift-list-date-widget.vue';
|
||||
|
||||
import { date, useQuasar } from 'quasar';
|
||||
import { ref, computed, watch, onMounted, inject } from 'vue';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { ref, computed, watch, inject, onMounted } from 'vue';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
// ========== constants ========================================
|
||||
|
||||
const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10);
|
||||
import { TimesheetDayDisplay } from 'src/modules/timesheets/models/timesheet.models';
|
||||
|
||||
// ========== state ========================================
|
||||
|
||||
|
|
@ -25,59 +17,31 @@
|
|||
}>();
|
||||
|
||||
const q = useQuasar();
|
||||
const { extractDate } = date;
|
||||
const { locale } = useI18n();
|
||||
const uiStore = useUiStore();
|
||||
const timesheetApi = useTimesheetApi();
|
||||
const timesheetStore = useTimesheetStore();
|
||||
|
||||
const mobileAnimationDirection = ref('fadeInLeft');
|
||||
const currentDayComponent = ref<HTMLElement[] | null>(null);
|
||||
const currentDayComponentWatcher = ref(currentDayComponent);
|
||||
const employeeEmail = inject<string>('employeeEmail');
|
||||
|
||||
// ========== computed ========================================
|
||||
|
||||
const animationStyle = computed(() => q.platform.is.mobile ? mobileAnimationDirection.value : 'fadeInDown');
|
||||
const timesheetRows = computed(() => {
|
||||
if (timesheetStore.is_loading) return [];
|
||||
const rows: TimesheetDayDisplay[][] = Array.from({ length: 7 }, () => []);
|
||||
timesheetStore.timesheets.flatMap(timesheet => timesheet.days.forEach((day, index) => {
|
||||
rows[index]!.push(new TimesheetDayDisplay(timesheet, day));
|
||||
}));
|
||||
|
||||
return rows;
|
||||
});
|
||||
|
||||
const isShowingWeeklyPresets = computed(() => timesheetStore.timesheets.some(
|
||||
timesheet => timesheet.days.every(day => day.shifts.length < 1)
|
||||
) && timesheetStore.has_timesheet_preset);
|
||||
|
||||
// ========== methods ========================================
|
||||
|
||||
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
|
||||
uiStore.focusNextComponent = true;
|
||||
const newShift = new Shift;
|
||||
newShift.date = date;
|
||||
newShift.timesheet_id = timesheet_id;
|
||||
day_shifts.push(newShift);
|
||||
};
|
||||
|
||||
const deleteUnsavedShift = (timesheet_index: number, day_index: number) => {
|
||||
if (timesheetStore.timesheets !== undefined) {
|
||||
const day = timesheetStore.timesheets[timesheet_index]!.days[day_index]!;
|
||||
const shifts_without_deleted_shift = day.shifts.filter(shift => shift.id !== 0);
|
||||
day.shifts = shifts_without_deleted_shift;
|
||||
}
|
||||
};
|
||||
|
||||
const getDayApproval = (day: TimesheetDay) => {
|
||||
if (day.shifts.length < 1) return false;
|
||||
return day.shifts.every(shift => shift.is_approved === true);
|
||||
};
|
||||
|
||||
const getMobileDayRef = (iso_date_string: string): string => {
|
||||
return iso_date_string === CURRENT_DATE_STRING ? 'currentDayComponent' : '';
|
||||
};
|
||||
|
||||
const getHolidayName = (date: string) => {
|
||||
const holiday = timesheetStore.federal_holidays.find(holiday => holiday.date === date);
|
||||
if (!holiday) return;
|
||||
|
||||
if (locale.value === 'fr-FR')
|
||||
return holiday.nameFr;
|
||||
|
||||
else if (locale.value === 'en-CA')
|
||||
return holiday.nameEn;
|
||||
};
|
||||
|
||||
const onClickApplyWeeklyPreset = async (timesheet_id: number) => {
|
||||
await timesheetApi.applyPreset(timesheet_id, undefined, undefined, employeeEmail);
|
||||
}
|
||||
|
|
@ -95,191 +59,56 @@
|
|||
|
||||
<template>
|
||||
<div
|
||||
class="fit"
|
||||
:class="$q.platform.is.mobile ? 'column no-wrap q-pb-lg' : 'row'"
|
||||
:key="timesheetStore.is_loading ? 0 : 1"
|
||||
class="fit column no-wrap q-pb-lg"
|
||||
:class="$q.platform.is.mobile ? '' : ''"
|
||||
>
|
||||
<div
|
||||
v-for="timesheet, timesheet_index of timesheetStore.timesheets"
|
||||
:key="timesheet.timesheet_id"
|
||||
class="no-wrap"
|
||||
:class="$q.platform.is.mobile ? 'col-auto column' : 'col column fit items-center'"
|
||||
v-if="isShowingWeeklyPresets"
|
||||
class="row flex-center q-py-xs"
|
||||
>
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated fadeInDown"
|
||||
leave-active-class="animated fadeOutUp"
|
||||
<div
|
||||
v-for="timesheet, timesheetIndex in timesheetStore.timesheets"
|
||||
:key="timesheetIndex"
|
||||
class="col row flex-center full-width"
|
||||
>
|
||||
<q-btn
|
||||
v-if="!$q.platform.is.mobile && timesheet.days.every(day => day.shifts.length < 1) && timesheetStore.has_timesheet_preset"
|
||||
:disable="!timesheet.days.every(day => day.shifts.length < 1)"
|
||||
flat
|
||||
outline
|
||||
dense
|
||||
:label="$t('timesheet.apply_preset_week')"
|
||||
class="col-auto text-uppercase text-weight-bold text-accent q-mx-lg q-py-none rounded-5"
|
||||
@click="onClickApplyWeeklyPreset(timesheet.timesheet_id)"
|
||||
>
|
||||
<q-icon
|
||||
name="las la-calendar-week"
|
||||
color="accent"
|
||||
size="md"
|
||||
icon="schedule_send"
|
||||
:label="$t('timesheet.apply_preset_week')"
|
||||
class="text-uppercase text-weight-bold q-px-xl q-py-xs rounded-50"
|
||||
:class="timesheet.days.every(day => day.shifts.length < 1) ? '' : 'invisible'"
|
||||
@click="onClickApplyWeeklyPreset(timesheet.timesheet_id)"
|
||||
/>
|
||||
</q-btn>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition-group
|
||||
appear
|
||||
:enter-active-class="`animated ${animationStyle}`"
|
||||
enter-active-class="animated fadeInDown"
|
||||
>
|
||||
<div
|
||||
v-for="day, day_index in timesheet.days"
|
||||
:key="day.date"
|
||||
:ref="getMobileDayRef(day.date)"
|
||||
class="col-auto row q-pa-sm full-width relative-position"
|
||||
:style="`animation-delay: ${day_index / 15}s;`"
|
||||
v-for="row, rowIndex of timesheetRows"
|
||||
:key="rowIndex"
|
||||
class="col-auto row items-stretch"
|
||||
:style="`animation-delay: ${rowIndex / 10}s;`"
|
||||
>
|
||||
<!-- optional label indicating which holiday if today is a holiday -->
|
||||
<span
|
||||
v-if="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date)"
|
||||
class="absolute-top-left text-uppercase text-weight-bolder text-purple-5"
|
||||
style="transform: translate(25px, -7px);"
|
||||
>
|
||||
{{ getHolidayName(day.date) }}
|
||||
</span>
|
||||
|
||||
<!-- mobile version in portrait mode -->
|
||||
<div
|
||||
v-if="$q.platform.is.mobile && ($q.screen.width < $q.screen.height)"
|
||||
class="col-auto full-width q-px-md q-py-sm"
|
||||
>
|
||||
<q-card
|
||||
class="shadow-12"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent rounded-10' : 'bg-dark mobile-rounded-10'"
|
||||
>
|
||||
<q-card-section
|
||||
class="text-weight-bolder text-uppercase text-h6 q-py-sm text-center relative-position"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent text-white' : 'bg-primary text-white'"
|
||||
style="line-height: 1em;"
|
||||
>
|
||||
<span> {{ $d(extractDate(day.date, 'YYYY-MM-DD'), {
|
||||
weekday: 'long', day: 'numeric', month:
|
||||
'long'
|
||||
}) }}</span>
|
||||
|
||||
<q-icon
|
||||
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
||||
name="verified"
|
||||
size="3em"
|
||||
color="white"
|
||||
class="absolute-top-left z-top"
|
||||
style="top: -0.2em; left: 0px;"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section
|
||||
v-if="day.shifts.filter(shift => shift.id !== 0).length > 0"
|
||||
class="q-pa-none transparent"
|
||||
v-for="day, dayIndex in row"
|
||||
:key="day.day.date"
|
||||
class="col row items-stretch q-pa-xs relative-position"
|
||||
>
|
||||
<ShiftListDay
|
||||
outlined
|
||||
:timesheet-id="timesheet.timesheet_id"
|
||||
:week-day-index="day_index"
|
||||
:animation-delay-multiplier="day_index"
|
||||
:approved="(getDayApproval(day) || timesheet.is_approved)"
|
||||
:day="day"
|
||||
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
||||
v-model="row[dayIndex]!.day"
|
||||
:week-day-index="dayIndex"
|
||||
:timesheet-id="day.timesheetId"
|
||||
:timesheet-approved="day.isTimesheetApproved"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-btn
|
||||
v-if="!(getDayApproval(day) || timesheet.is_approved)"
|
||||
square
|
||||
dense
|
||||
size="xl"
|
||||
color="accent"
|
||||
icon="more_time"
|
||||
class="full-width"
|
||||
style="border-radius: 0 0 10px 10px;"
|
||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- desktop version -->
|
||||
<div
|
||||
v-else
|
||||
class="col row full-width rounded-10 ellipsis shadow-10"
|
||||
:style="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'border: 2px solid #ab47bc' : ''"
|
||||
>
|
||||
<div
|
||||
class="col row"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? (timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'bg-purple-5' : 'bg-accent') : 'bg-dark'"
|
||||
>
|
||||
<!-- Date block -->
|
||||
<ShiftListDateWidget
|
||||
:display-date="day.date"
|
||||
:approved="(getDayApproval(day) || timesheet.is_approved)"
|
||||
class="col-auto"
|
||||
/>
|
||||
|
||||
<ShiftListDay
|
||||
:timesheet-id="timesheet.timesheet_id"
|
||||
:week-day-index="day_index"
|
||||
:day="day"
|
||||
:holiday="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date)"
|
||||
:approved="getDayApproval(day) || timesheet.is_approved"
|
||||
class="col"
|
||||
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-auto self-stretch">
|
||||
<q-icon
|
||||
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
||||
name="verified"
|
||||
color="white"
|
||||
size="xl"
|
||||
class="full-height"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? (timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'bg-purple-5' : 'bg-accent') : ''"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
v-else
|
||||
:dense="!$q.platform.is.mobile"
|
||||
square
|
||||
icon="more_time"
|
||||
size="lg"
|
||||
:color="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'purple-5' : 'accent'"
|
||||
text-color="white"
|
||||
class="full-height"
|
||||
:class="$q.platform.is.mobile ? 'q-px-xs' : ''"
|
||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style
|
||||
scoped
|
||||
lang="scss"
|
||||
>
|
||||
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100, 200) {
|
||||
.mobile-rounded-#{$size} {
|
||||
border-radius: #{$size}px !important;
|
||||
}
|
||||
|
||||
.mobile-rounded-#{$size}>div:first-child {
|
||||
border-radius: #{$size}px #{$size}px 0 0 !important;
|
||||
}
|
||||
|
||||
.mobile-rounded-#{$size}>div:last-child {
|
||||
border-radius: 0 0 #{$size}px #{$size}px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -21,6 +21,9 @@
|
|||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { RouteNames } from 'src/router/router-constants';
|
||||
import { getHoursMinutesBetweenTwoHHmm } from 'src/utils/date-and-time-utils';
|
||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
||||
|
||||
// ================= state ====================
|
||||
|
||||
|
|
@ -39,18 +42,22 @@ import { RouteNames } from 'src/router/router-constants';
|
|||
// ================== computed ====================
|
||||
|
||||
const hasShiftErrors = computed(() => timesheetStore.all_current_shifts.filter(shift => shift.has_error === true).length > 0);
|
||||
|
||||
const isTimesheetsApproved = computed(() => timesheetStore.timesheets.every(timesheet => timesheet.is_approved));
|
||||
|
||||
const weeklyHours = computed(() => timesheetStore.timesheets.map(timesheet =>
|
||||
Object.values(timesheet.weekly_hours).reduce((sum, hoursPerType) => sum += hoursPerType, 0) - timesheet.weekly_hours.sick
|
||||
timesheet.days.reduce((daySum: number, day: TimesheetDay) => {
|
||||
return daySum + day.shifts.reduce((shiftSum: number, shift: Shift) => {
|
||||
if (!shift.end_time || !shift.start_time || shift.type === 'SICK') return shiftSum;
|
||||
|
||||
const time = getHoursMinutesBetweenTwoHHmm(shift.start_time, shift.end_time);
|
||||
return shiftSum + time.hours + Number(time.minutes / 60);
|
||||
}, 0)
|
||||
}, 0)
|
||||
));
|
||||
const totalHours = computed(() => timesheetStore.timesheets.reduce((sum, timesheet) =>
|
||||
sum += timesheet.weekly_hours.regular
|
||||
+ timesheet.weekly_hours.evening
|
||||
+ timesheet.weekly_hours.emergency
|
||||
+ timesheet.weekly_hours.vacation
|
||||
+ timesheet.weekly_hours.holiday
|
||||
+ timesheet.weekly_hours.overtime,
|
||||
|
||||
const totalHours = computed(() => weeklyHours.value.reduce((sum, week) =>
|
||||
sum += week,
|
||||
0 //initial value
|
||||
));
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ export const useExpensesApi = () => {
|
|||
const success = await expenses_store.upsertExpense(expense, employee_email);
|
||||
|
||||
if (success) {
|
||||
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export type ExpenseType = 'PER_DIEM' | 'MILEAGE' | 'EXPENSES' | 'ON_CALL';
|
||||
export type ExpenseType = 'PER_DIEM' | 'MILEAGE' | 'EXPENSES' | 'ON_CALL' | undefined;
|
||||
|
||||
export const EXPENSE_TYPE: ExpenseType[] = ['PER_DIEM', 'MILEAGE', 'EXPENSES', 'ON_CALL',];
|
||||
export const TYPES_WITH_MILEAGE_ONLY: Readonly<ExpenseType[]> = ['MILEAGE'];
|
||||
|
|
@ -9,8 +9,8 @@ export class Expense {
|
|||
timesheet_id: number;
|
||||
date: string; //YYYY-MM-DD
|
||||
type: ExpenseType;
|
||||
amount: number;
|
||||
mileage?: number;
|
||||
amount?: number | null;
|
||||
mileage?: number | null;
|
||||
attachment_name?: string;
|
||||
attachment_key?: string;
|
||||
comment: string;
|
||||
|
|
@ -21,7 +21,7 @@ export class Expense {
|
|||
this.id = -1;
|
||||
this.timesheet_id = -1;
|
||||
this.date = date;
|
||||
this.type = 'EXPENSES';
|
||||
this.type = undefined;
|
||||
this.amount = 0;
|
||||
this.comment = '';
|
||||
this.is_approved = false;
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ export class Shift {
|
|||
is_remote: boolean;
|
||||
has_error: boolean;
|
||||
|
||||
constructor() {
|
||||
constructor(date?: string) {
|
||||
this.id = -1;
|
||||
this.timesheet_id = -1;
|
||||
this.date = '';
|
||||
this.date = date ?? '';
|
||||
this.type = 'REGULAR';
|
||||
this.start_time = '';
|
||||
this.end_time = '';
|
||||
|
|
|
|||
|
|
@ -45,7 +45,14 @@ export interface TotalExpenses {
|
|||
mileage: number;
|
||||
}
|
||||
|
||||
export interface TimesheetDayDisplay extends TimesheetDay {
|
||||
timesheet_id: number;
|
||||
i18WeekdayKey: string;
|
||||
export class TimesheetDayDisplay {
|
||||
timesheetId: number;
|
||||
isTimesheetApproved: boolean;
|
||||
day: TimesheetDay;
|
||||
|
||||
constructor(timesheet: Timesheet, day: TimesheetDay) {
|
||||
this.timesheetId = timesheet.timesheet_id;
|
||||
this.isTimesheetApproved = timesheet.is_approved;
|
||||
this.day = day;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,12 +10,12 @@ export const getExpenseIcon = (type: ExpenseType) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const useExpenseRules = (t: (_key: string) => string) => {
|
||||
export const useExpenseRules = () => {
|
||||
const isPresent = (val: unknown) => val !== undefined && val !== null && val !== '';
|
||||
const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
|
||||
const amountRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.amount_required_for_type');
|
||||
const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.mileage_required_for_type');
|
||||
const commentRequired = (val: unknown) => (typeof val === 'string' ? val.trim().length > 0 : false) || t('timesheet.expense.hints.comment_required');
|
||||
const typeRequired = (val: unknown) => isPresent(val);
|
||||
const amountRequired = (val: number | null | undefined) => isPresent(val) && !!val && val > 0;
|
||||
const mileageRequired = (val: number | null | undefined) => isPresent(val) && !!val && val > 0;
|
||||
const commentRequired = (val: string | null | undefined) => typeof val === 'string' ? val.trim().length > 0 : false;
|
||||
|
||||
return {
|
||||
typeRequired,
|
||||
|
|
|
|||
|
|
@ -12,67 +12,57 @@
|
|||
class="q-pa-md justify-center items-stretch bg-secondary"
|
||||
:class="$q.platform.is.mobile ? 'column' : 'row'"
|
||||
>
|
||||
<!-- center column -->
|
||||
<!-- left column -->
|
||||
<div class="column col items-center q-pa-md">
|
||||
<div class="col-8 fit q-py-md">
|
||||
<div class="col q-py-md">
|
||||
<MainCarousel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- right column -->
|
||||
<div class="column col-lg-4 items-center" :class="$q.platform.is.mobile ? 'q-px-md' : 'q-px-xl'">
|
||||
<span
|
||||
v-if="!$q.platform.is.mobile"
|
||||
class="col-auto text-uppercase text-weight-bold self-start q-px-md q-pt-lg"
|
||||
<div
|
||||
class="column col-xs-12 col-md-4 col-lg-3 items-center"
|
||||
:class="$q.platform.is.mobile ? 'q-px-md' : 'q-px-xl'"
|
||||
>
|
||||
<span class="col-auto text-uppercase text-weight-bold self-start q-px-md q-pt-lg">
|
||||
{{ $t('dashboard.useful_links') }}
|
||||
</span>
|
||||
|
||||
<div class="col-auto full-width"
|
||||
:class="$q.platform.is.mobile ? 'q-py-md' : 'q-py-sm'">
|
||||
<div class="col-auto full-width q-py-sm">
|
||||
<ShortcutCard
|
||||
icon-image-source="src/assets/links/logo_gmail.png"
|
||||
bg-image-source="src/assets/links/google_bg.png"
|
||||
icon-image-source="img:src/assets/links/logo_gmail.png"
|
||||
name="Messagerie"
|
||||
route="https://mail.google.com/mail/u/0/#inbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-auto full-width"
|
||||
:class="$q.platform.is.mobile ? 'q-py-md' : 'q-py-sm'">
|
||||
<div class="col-auto full-width q-py-sm">
|
||||
<ShortcutCard
|
||||
icon-image-source="src/assets/links/facturation-transparent.png"
|
||||
bg-image-source="src/assets/links/facturation_bg.png"
|
||||
icon-image-source="img:src/assets/links/facturation-transparent.png"
|
||||
name="Facturation"
|
||||
route="https://facturation.targo.ca/facturation/accueil.php?menu=ticket_open"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-auto full-width"
|
||||
:class="$q.platform.is.mobile ? 'q-py-md' : 'q-py-sm'">
|
||||
<div class="col-auto full-width q-py-sm">
|
||||
<ShortcutCard
|
||||
icon-image-source="src/assets/links/map-icon.png"
|
||||
bg-image-source="src/assets/links/map_targo_banner.png"
|
||||
icon-image-source="location_on"
|
||||
name="Map Targo"
|
||||
route="https://map.targointernet.com/infrastructure/map"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-auto full-width"
|
||||
:class="$q.platform.is.mobile ? 'q-py-md' : 'q-py-sm'">
|
||||
<div class="col-auto full-width q-py-sm">
|
||||
<ShortcutCard
|
||||
icon-image-source="src/assets/links/hydroQC_icon.png"
|
||||
bg-image-source="src/assets/links/hydroQC_bg.png"
|
||||
icon-image-source="img:src/assets/links/hydroQC_icon.png"
|
||||
name="Info Pannes"
|
||||
route="https://infopannes.solutions.hydroquebec.com/info-pannes/pannes/pannes-en-cours"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-auto full-width"
|
||||
:class="$q.platform.is.mobile ? 'q-py-md' : 'q-py-sm'">
|
||||
<div class="col-auto full-width q-py-sm">
|
||||
<ShortcutCard
|
||||
icon-image-source="src/assets/links/intranet_logo.png"
|
||||
bg-image-source="src/assets/links/intranet_targo_bg.png"
|
||||
icon-image-source="language"
|
||||
name="Intranet"
|
||||
route="https://intranet.facturation.targo.ca/"
|
||||
/>
|
||||
|
|
|
|||