refactor(timesheet): UI overhaul, better separation of concerns with components, minor chatbot changes
|
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;
|
background: #fd4b2d !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-table tbody tr:hover {
|
.q-table tbody tr:hover > td {
|
||||||
background: #00ff260c;
|
background-color: var(--q-accent2) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.body--dark {
|
body.body--dark {
|
||||||
|
|
@ -71,6 +71,6 @@ input[type=number] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-field--dark .q-field__control:hover::before, .q-field--outlined .q-field__control:hover::before {
|
.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;
|
border-width: 2px;
|
||||||
}
|
}
|
||||||
|
|
@ -15,15 +15,13 @@
|
||||||
$primary : #30303A;
|
$primary : #30303A;
|
||||||
$secondary : #DAE0E7;
|
$secondary : #DAE0E7;
|
||||||
$accent : #0c9a3b;
|
$accent : #0c9a3b;
|
||||||
$accent2 : #0a7d32;
|
|
||||||
|
|
||||||
$dark-shadow-color : #000000;
|
$dark-shadow-color : #000;
|
||||||
|
|
||||||
$elevation-dark-umbra : rgba($dark-shadow-color, 1);
|
$elevation-dark-umbra : rgba($dark-shadow-color, .2);
|
||||||
$elevation-dark-penumbra : rgba($dark-shadow-color, 0.75);
|
$elevation-dark-penumbra : rgba($dark-shadow-color, .14);
|
||||||
$elevation-dark-ambient : rgba($dark-shadow-color, 0.53);
|
$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);
|
$layout-shadow-dark : 0 0 5px 5px rgba($dark-shadow-color, 0.5);
|
||||||
|
|
||||||
$input-text-color : #455A64;
|
$input-text-color : #455A64;
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,7 @@ export default {
|
||||||
name: "name",
|
name: "name",
|
||||||
lock: "",
|
lock: "",
|
||||||
unlock: "",
|
unlock: "",
|
||||||
|
today: "today",
|
||||||
},
|
},
|
||||||
misc: {
|
misc: {
|
||||||
or: "or",
|
or: "or",
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,7 @@ export default {
|
||||||
name: "nom",
|
name: "nom",
|
||||||
lock: "verrouiller",
|
lock: "verrouiller",
|
||||||
unlock: "déverrouiller",
|
unlock: "déverrouiller",
|
||||||
|
today: "aujourd'hui",
|
||||||
},
|
},
|
||||||
misc: {
|
misc: {
|
||||||
or: "ou",
|
or: "ou",
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,12 @@
|
||||||
import ChatbotDrawer from 'src/modules/chatbot/components/chatbot-drawer.vue';
|
import ChatbotDrawer from 'src/modules/chatbot/components/chatbot-drawer.vue';
|
||||||
|
|
||||||
import { onMounted, watch, ref } from 'vue';
|
import { onMounted, watch, ref } from 'vue';
|
||||||
|
import { setCssVar } from 'quasar';
|
||||||
import { RouterView } from 'vue-router';
|
import { RouterView } from 'vue-router';
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
|
|
||||||
|
setCssVar('accent2', '#36c45a44');
|
||||||
const ui_store = useUiStore();
|
const ui_store = useUiStore();
|
||||||
const auth_store = useAuthStore();
|
const auth_store = useAuthStore();
|
||||||
const userPreferences = ref(ui_store.userPreferences);
|
const userPreferences = ref(ui_store.userPreferences);
|
||||||
|
|
@ -47,3 +48,13 @@
|
||||||
<FooterBar v-if="!$q.platform.is.mobile" />
|
<FooterBar v-if="!$q.platform.is.mobile" />
|
||||||
</q-layout>
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="css">
|
||||||
|
.text-accent2 {
|
||||||
|
color: '#36c45a44' !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-accent2 {
|
||||||
|
background-color: '#36c45a44' !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
const chatbot_store = useChatbotStore();
|
const chatbot_store = useChatbotStore();
|
||||||
|
|
||||||
const text = ref('');
|
const text = ref('');
|
||||||
const is_showing_right_drawer = ref(true);
|
const isShowingRightDrawer = ref(true);
|
||||||
const drawer_width = ref(85);
|
const drawer_width = ref(85);
|
||||||
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-drawer
|
<q-drawer
|
||||||
v-model="is_showing_right_drawer"
|
v-model="isShowingRightDrawer"
|
||||||
overlay
|
overlay
|
||||||
persistent
|
persistent
|
||||||
:width="drawer_width"
|
:width="drawer_width"
|
||||||
|
|
@ -82,27 +82,42 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else
|
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
|
<q-btn
|
||||||
dense
|
dense
|
||||||
round
|
round
|
||||||
icon="las la-robot"
|
icon="las la-robot"
|
||||||
color="accent"
|
color="accent"
|
||||||
size="2em"
|
size="2em"
|
||||||
class="shadow-5"
|
|
||||||
style="pointer-events: auto;"
|
style="pointer-events: auto;"
|
||||||
@click="chatbot_store.is_showing_chatbot = true"
|
@click="chatbot_store.is_showing_chatbot = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</q-drawer>
|
</q-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="css">
|
||||||
:deep(.q-drawer) {
|
:deep(.q-drawer) {
|
||||||
background: rgba(0, 0, 0, 0);
|
background: rgba(0, 0, 0, 0);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
pointer-events: none;
|
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>
|
</style>
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
>
|
>
|
||||||
const { route = "" } = defineProps<{
|
const { route = "" } = defineProps<{
|
||||||
iconImageSource: string,
|
iconImageSource: string,
|
||||||
bgImageSource: string,
|
|
||||||
name: string,
|
name: string,
|
||||||
route?: string,
|
route?: string,
|
||||||
}>();
|
}>();
|
||||||
|
|
@ -16,36 +15,21 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="full-width cursor-pointer bg-dark shadow-2 rounded-15 q-pa-xs position-relative"
|
class="full-width cursor-pointer rounded-50 bg-accent text-white link-btn"
|
||||||
style="border: solid 1px var(--q-accent);"
|
|
||||||
@click="onClickExternalShortcut"
|
@click="onClickExternalShortcut"
|
||||||
>
|
>
|
||||||
<span
|
<div class="row items-center q-px-lg q-py-xs rounded-50">
|
||||||
v-if="$q.platform.is.mobile"
|
<span class="col text-uppercase text-bold">
|
||||||
class="col text-uppercase text-bold text-accent absolute"
|
|
||||||
style="transform: translate(20px, -20px);"
|
|
||||||
>
|
|
||||||
{{ name }}
|
{{ name }}
|
||||||
</span>
|
</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
|
<q-icon
|
||||||
round
|
round
|
||||||
color="dark"
|
color="dark"
|
||||||
size="md"
|
size="md"
|
||||||
:name="`img:${iconImageSource}`"
|
:name="iconImageSource"
|
||||||
class="col-auto q-pr-md"
|
class="col-auto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
|
||||||
v-if="!$q.platform.is.mobile"
|
|
||||||
class="col text-uppercase text-bold"
|
|
||||||
>
|
|
||||||
{{ name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -54,11 +38,17 @@
|
||||||
scoped
|
scoped
|
||||||
lang="css"
|
lang="css"
|
||||||
>
|
>
|
||||||
.link-card {
|
.link-btn {
|
||||||
background-blend-mode: multiply;
|
box-shadow: 0 6px rgb(4, 77, 4);
|
||||||
background-position: bottom right;
|
transform: translateY(-6px);
|
||||||
background-repeat: no-repeat;
|
}
|
||||||
background-color: var(--q-dark);
|
|
||||||
background-size: contain;
|
.link-btn:hover {
|
||||||
|
background-color: var(--q-accent2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-btn:active {
|
||||||
|
box-shadow: 0 2px rgb(4, 77, 4);
|
||||||
|
transform: translateY(2px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -28,28 +28,34 @@
|
||||||
:autoplay="autoplayTimer"
|
:autoplay="autoplayTimer"
|
||||||
control-color="accent"
|
control-color="accent"
|
||||||
control-type="outline"
|
control-type="outline"
|
||||||
class="bg-dark fit rounded-15 shadow-18"
|
class="bg-dark rounded-15 fit shadow-18"
|
||||||
@mouseenter="onCarouselMouseEvent('enter')"
|
@mouseenter="onCarouselMouseEvent('enter')"
|
||||||
@mouseleave="onCarouselMouseEvent('exit')"
|
@mouseleave="onCarouselMouseEvent('exit')"
|
||||||
>
|
>
|
||||||
<!-- welcome slide -->
|
<!-- welcome slide -->
|
||||||
<q-carousel-slide
|
<q-carousel-slide
|
||||||
name="welcome"
|
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
|
<q-img
|
||||||
src="src/assets/targo_building.png"
|
src="src/assets/targo_building.png"
|
||||||
position="50% 25%"
|
position="50% 25%"
|
||||||
fit="cover"
|
fit="cover"
|
||||||
class="col-9"
|
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') }}
|
{{ $t('dashboard.carousel.welcome_title') }}
|
||||||
</div>
|
</div>
|
||||||
</q-img>
|
</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>
|
<span class="col-auto text-center">{{ $t('dashboard.carousel.welcome_message') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -61,19 +67,25 @@
|
||||||
class="q-pa-none cursor-pointer"
|
class="q-pa-none cursor-pointer"
|
||||||
@click="$router.push(RouteNames.HELP)"
|
@click="$router.push(RouteNames.HELP)"
|
||||||
>
|
>
|
||||||
<div class="column fit">
|
<div
|
||||||
|
class="column fit"
|
||||||
|
:class="$q.platform.is.mobile ? 'no-wrap' : ''"
|
||||||
|
>
|
||||||
<q-img
|
<q-img
|
||||||
src="src/assets/targo_help_banner.png"
|
src="src/assets/targo_help_banner.png"
|
||||||
position="50% 25%"
|
position="50% 25%"
|
||||||
fit="none"
|
fit="none"
|
||||||
class="col-9"
|
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') }}
|
{{ $t('dashboard.carousel.help_title') }}
|
||||||
</div>
|
</div>
|
||||||
</q-img>
|
</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>
|
<span class="col-auto text-center">{{ $t('dashboard.carousel.help_message') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,6 @@
|
||||||
|
|
||||||
// ========== methods ========================================
|
// ========== methods ========================================
|
||||||
|
|
||||||
// const timesheetRows = computed(() => timesheetStore.timesheets);
|
|
||||||
|
|
||||||
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
|
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
|
||||||
uiStore.focusNextComponent = true;
|
uiStore.focusNextComponent = true;
|
||||||
const newShift = new Shift;
|
const newShift = new Shift;
|
||||||
|
|
@ -52,14 +50,6 @@
|
||||||
day_shifts.push(newShift);
|
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) => {
|
const getDayApproval = (day: TimesheetDay) => {
|
||||||
if (day.shifts.length < 1) return false;
|
if (day.shifts.length < 1) return false;
|
||||||
return day.shifts.every(shift => shift.is_approved === true);
|
return day.shifts.every(shift => shift.is_approved === true);
|
||||||
|
|
@ -101,7 +91,7 @@
|
||||||
:class="$q.platform.is.mobile ? 'column no-wrap q-pb-lg' : 'row'"
|
:class="$q.platform.is.mobile ? 'column no-wrap q-pb-lg' : 'row'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="timesheet, timesheet_index of timesheetStore.timesheets"
|
v-for="timesheet of timesheetStore.timesheets"
|
||||||
:key="timesheet.timesheet_id"
|
:key="timesheet.timesheet_id"
|
||||||
class="no-wrap"
|
class="no-wrap"
|
||||||
:class="$q.platform.is.mobile ? 'col-auto column' : 'col column fit items-center'"
|
:class="$q.platform.is.mobile ? 'col-auto column' : 'col column fit items-center'"
|
||||||
|
|
@ -182,13 +172,10 @@
|
||||||
class="q-pa-none transparent"
|
class="q-pa-none transparent"
|
||||||
>
|
>
|
||||||
<ShiftListDay
|
<ShiftListDay
|
||||||
outlined
|
v-model="timesheet.days[day_index]!"
|
||||||
:timesheet-id="timesheet.timesheet_id"
|
:timesheet-id="timesheet.timesheet_id"
|
||||||
:week-day-index="day_index"
|
:week-day-index="day_index"
|
||||||
:animation-delay-multiplier="day_index"
|
:timesheet-approved="timesheet.is_approved"
|
||||||
:approved="(getDayApproval(day) || timesheet.is_approved)"
|
|
||||||
:day="day"
|
|
||||||
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
|
|
@ -226,13 +213,11 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ShiftListDay
|
<ShiftListDay
|
||||||
|
v-model="timesheet.days[day_index]!"
|
||||||
:timesheet-id="timesheet.timesheet_id"
|
:timesheet-id="timesheet.timesheet_id"
|
||||||
:week-day-index="day_index"
|
:week-day-index="day_index"
|
||||||
:day="day"
|
:approved="timesheet.is_approved"
|
||||||
:holiday="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date)"
|
|
||||||
:approved="getDayApproval(day) || timesheet.is_approved"
|
|
||||||
class="col"
|
class="col"
|
||||||
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
displayDate: string;
|
displayDate: string;
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
approved?: boolean;
|
approved?: boolean;
|
||||||
|
today?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const date_font_size = computed(() => dense ? '1.5em' : '2.5em');
|
const date_font_size = computed(() => dense ? '1.5em' : '2.5em');
|
||||||
|
|
@ -26,6 +27,14 @@
|
||||||
class="column flex-center rounded-10 text-center self-center bg-transparent"
|
class="column flex-center rounded-10 text-center self-center bg-transparent"
|
||||||
:style="date_box_size"
|
:style="date_box_size"
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
v-if="today"
|
||||||
|
class="absolute-top-left text-uppercase text-weight-bolder q-pt-xs bordered-text"
|
||||||
|
style="transform: translate(20px, -8px);"
|
||||||
|
>
|
||||||
|
{{ $t('shared.label.today') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-if="!dense"
|
v-if="!dense"
|
||||||
class="col-auto text-uppercase text-weight-bold"
|
class="col-auto text-uppercase text-weight-bold"
|
||||||
|
|
@ -34,13 +43,15 @@
|
||||||
>
|
>
|
||||||
{{ $d(display_date, { weekday: $q.platform.is.mobile ? 'short' : 'long' }) }}
|
{{ $d(display_date, { weekday: $q.platform.is.mobile ? 'short' : 'long' }) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="col-auto text-weight-bolder"
|
class="col-auto text-weight-bolder"
|
||||||
:class="approved ? 'text-white' : ''"
|
:class="today ? 'text-info' : (approved ? 'text-white' : '')"
|
||||||
:style="'font-size: ' + date_font_size + '; line-height: 90% !important;'"
|
:style="'font-size: ' + date_font_size + '; line-height: 90% !important;'"
|
||||||
>
|
>
|
||||||
{{ display_date.getDate() }}
|
{{ display_date.getDate() }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="col-auto text-uppercase text-weight-bold"
|
class="col-auto text-uppercase text-weight-bold"
|
||||||
:class="approved ? 'text-white' : ''"
|
:class="approved ? 'text-white' : ''"
|
||||||
|
|
@ -50,3 +61,13 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style
|
||||||
|
scoped
|
||||||
|
lang="css"
|
||||||
|
>
|
||||||
|
.bordered-text {
|
||||||
|
text-shadow: 2px 0 var(--q-dark), -2px 0 var(--q-dark), 0 2px var(--q-dark), 0 -2px var(--q-dark),
|
||||||
|
1px 1px var(--q-dark), -1px -1px var(--q-dark), 1px -1px var(--q-dark), -1px 1px var(--q-dark);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -3,73 +3,129 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import ShiftListDayRow from 'src/modules/timesheets/components/shift-list-day-row.vue';
|
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 { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||||
import { isShiftOverlap } from 'src/modules/timesheets/utils/shift.util';
|
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';
|
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;
|
timesheetId: number;
|
||||||
weekDayIndex: number;
|
weekDayIndex: number;
|
||||||
day: TimesheetDay;
|
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
approved?: boolean;
|
timesheetApproved?: boolean;
|
||||||
holiday?: boolean;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const { locale } = useI18n();
|
||||||
'deleteUnsavedShift': [void];
|
const uiStore = useUiStore();
|
||||||
}>();
|
const shiftApi = useShiftApi();
|
||||||
|
const timesheetApi = useTimesheetApi();
|
||||||
const shift_api = useShiftApi();
|
const timesheetStore = useTimesheetStore();
|
||||||
const timesheet_api = useTimesheetApi();
|
const presetMouseover = ref(false);
|
||||||
const timesheet_store = useTimesheetStore();
|
const shiftErrorMessage = ref<string | undefined>();
|
||||||
const preset_mouseover = ref(false);
|
|
||||||
const shift_error_message = ref<string | undefined>();
|
|
||||||
const employeeEmail = inject<string>('employeeEmail');
|
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 ==================
|
// ================== Methods ==================
|
||||||
|
|
||||||
const deleteCurrentShift = async (shift: Shift) => {
|
const addNewShift = () => {
|
||||||
if (shift.id <= 0) {
|
uiStore.focusNextComponent = true;
|
||||||
shift.id = 0;
|
const newShift = new Shift(day.value.date);
|
||||||
emit('deleteUnsavedShift');
|
newShift.timesheet_id = timesheetId;
|
||||||
} else {
|
day.value.shifts.push(newShift);
|
||||||
await shift_api.deleteShiftById(shift.id, employeeEmail);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
onTimeFieldBlur();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTimeFieldBlur = () => {
|
const onTimeFieldBlur = () => {
|
||||||
const is_error = isShiftOverlap(day.shifts);
|
const is_error = isShiftOverlap(day.value.shifts);
|
||||||
day.shifts.map(shift => shift.has_error = is_error);
|
day.value.shifts.map(shift => shift.has_error = is_error);
|
||||||
if (is_error)
|
if (is_error)
|
||||||
shift_error_message.value = 'timesheet.errors.SHIFT_OVERLAP_SHORT';
|
shiftErrorMessage.value = 'timesheet.errors.SHIFT_OVERLAP_SHORT';
|
||||||
else
|
else
|
||||||
shift_error_message.value = undefined;
|
shiftErrorMessage.value = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickApplyDailyPreset = async () => {
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="row fit rounded-10 ellipsis no-wrap">
|
||||||
|
<!-- optional label indicating which holiday if today is a holiday -->
|
||||||
|
<span
|
||||||
|
v-if="isHoliday"
|
||||||
|
class="absolute-top-left text-uppercase text-weight-bolder text-purple-5"
|
||||||
|
style="transform: translate(25px, -7px);"
|
||||||
|
>
|
||||||
|
{{ getHolidayName(day.date) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="column justify-center q-py-xs"
|
class="col"
|
||||||
:class="approved ? '' : ''"
|
:class="isToday ? 'z-top' : 'shadow-12'"
|
||||||
@mouseenter="preset_mouseover = true"
|
:style="isHoliday ? 'border: 2px solid #ab47bc' : ''"
|
||||||
@mouseleave="preset_mouseover = false"
|
>
|
||||||
|
<div
|
||||||
|
class="col row fit"
|
||||||
|
:class="(isDayApproved || timesheetApproved) ? (isHoliday ? 'bg-purple-5' : 'bg-accent') : 'bg-dark'"
|
||||||
|
>
|
||||||
|
<!-- 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"
|
||||||
|
@mouseenter="presetMouseover = true"
|
||||||
|
@mouseleave="presetMouseover = false"
|
||||||
>
|
>
|
||||||
<!-- Button to apply preset to day -->
|
<!-- Button to apply preset to day -->
|
||||||
<transition
|
<transition
|
||||||
|
|
@ -78,7 +134,7 @@
|
||||||
leave-active-class="animated zoomOut fast"
|
leave-active-class="animated zoomOut fast"
|
||||||
>
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="!$q.platform.is.mobile && day.shifts.length < 1 && preset_mouseover && timesheet_store.has_timesheet_preset"
|
v-if="!$q.platform.is.mobile && day.shifts.length < 1 && presetMouseover && timesheetStore.has_timesheet_preset"
|
||||||
:disable="day.shifts.length > 0"
|
:disable="day.shifts.length > 0"
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
|
|
@ -97,33 +153,45 @@
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="shift, shift_index in day.shifts"
|
v-for="shift, shiftIndex in day.shifts"
|
||||||
:key="shift_index"
|
:key="shiftIndex"
|
||||||
class="col-auto"
|
class="col-auto"
|
||||||
>
|
>
|
||||||
<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
|
<ShiftListDayRow
|
||||||
v-else
|
v-model:shift="day.shifts[shiftIndex]!"
|
||||||
v-model:shift="day.shifts[shift_index]!"
|
:holiday="isHoliday"
|
||||||
:holiday="holiday"
|
|
||||||
:current-shifts="day.shifts"
|
:current-shifts="day.shifts"
|
||||||
:is-timesheet-approved="approved"
|
:is-timesheet-approved="timesheetApproved"
|
||||||
:error-message="shift_error_message"
|
:error-message="shiftErrorMessage"
|
||||||
@request-delete="deleteCurrentShift(shift)"
|
@request-delete="deleteCurrentShift(shift.id, shiftIndex)"
|
||||||
@on-time-field-blur="onTimeFieldBlur()"
|
@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"
|
||||||
|
@click="addNewShift"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import ShiftList from 'src/modules/timesheets/components/shift-list.vue';
|
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 { computed, ref } from 'vue';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
@ -66,7 +67,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Else show timesheets if found -->
|
<!-- 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-scroll-area>
|
||||||
|
|
||||||
<q-page-sticky
|
<q-page-sticky
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
class="col row full-width"
|
class="col row full-width"
|
||||||
>
|
>
|
||||||
<span class="col-auto text-uppercase text-caption text-bold text-accent">
|
<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>
|
||||||
|
|
||||||
<span class="col text-right">{{ getHoursMinutesStringFromHoursFloat(hours) }}</span>
|
<span class="col text-right">{{ getHoursMinutesStringFromHoursFloat(hours) }}</span>
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,12 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import ShiftListDay from 'src/modules/timesheets/components/shift-list-day.vue';
|
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 { useQuasar } from 'quasar';
|
||||||
import { ref, computed, watch, onMounted, inject } from 'vue';
|
import { ref, computed, watch, inject, onMounted } from 'vue';
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-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 { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||||
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
import { TimesheetDayDisplay } from 'src/modules/timesheets/models/timesheet.models';
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
// ========== constants ========================================
|
// ========== constants ========================================
|
||||||
|
|
||||||
|
|
@ -25,59 +21,31 @@
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const q = useQuasar();
|
const q = useQuasar();
|
||||||
const { extractDate } = date;
|
|
||||||
const { locale } = useI18n();
|
|
||||||
const uiStore = useUiStore();
|
|
||||||
const timesheetApi = useTimesheetApi();
|
const timesheetApi = useTimesheetApi();
|
||||||
const timesheetStore = useTimesheetStore();
|
const timesheetStore = useTimesheetStore();
|
||||||
|
|
||||||
const mobileAnimationDirection = ref('fadeInLeft');
|
|
||||||
const currentDayComponent = ref<HTMLElement[] | null>(null);
|
const currentDayComponent = ref<HTMLElement[] | null>(null);
|
||||||
const currentDayComponentWatcher = ref(currentDayComponent);
|
const currentDayComponentWatcher = ref(currentDayComponent);
|
||||||
const employeeEmail = inject<string>('employeeEmail');
|
const employeeEmail = inject<string>('employeeEmail');
|
||||||
|
|
||||||
// ========== computed ========================================
|
// ========== 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 ========================================
|
// ========== 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) => {
|
const onClickApplyWeeklyPreset = async (timesheet_id: number) => {
|
||||||
await timesheetApi.applyPreset(timesheet_id, undefined, undefined, employeeEmail);
|
await timesheetApi.applyPreset(timesheet_id, undefined, undefined, employeeEmail);
|
||||||
}
|
}
|
||||||
|
|
@ -95,191 +63,57 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="fit"
|
:key="timesheetStore.is_loading ? 0 : 1"
|
||||||
:class="$q.platform.is.mobile ? 'column no-wrap q-pb-lg' : 'row'"
|
class="fit column no-wrap q-pb-lg"
|
||||||
|
:class="$q.platform.is.mobile ? '' : ''"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="timesheet, timesheet_index of timesheetStore.timesheets"
|
v-if="isShowingWeeklyPresets"
|
||||||
:key="timesheet.timesheet_id"
|
class="row flex-center q-py-xs"
|
||||||
class="no-wrap"
|
|
||||||
:class="$q.platform.is.mobile ? 'col-auto column' : 'col column fit items-center'"
|
|
||||||
>
|
>
|
||||||
<transition
|
<div
|
||||||
appear
|
v-for="timesheet, timesheetIndex in timesheetStore.timesheets"
|
||||||
enter-active-class="animated fadeInDown"
|
:key="timesheetIndex"
|
||||||
leave-active-class="animated fadeOutUp"
|
class="col row flex-center full-width"
|
||||||
>
|
>
|
||||||
<q-btn
|
<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)"
|
:disable="!timesheet.days.every(day => day.shifts.length < 1)"
|
||||||
flat
|
outline
|
||||||
dense
|
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"
|
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>
|
</div>
|
||||||
</transition>
|
</div>
|
||||||
|
|
||||||
<transition-group
|
<transition-group
|
||||||
appear
|
appear
|
||||||
:enter-active-class="`animated ${animationStyle}`"
|
enter-active-class="animated fadeInDown"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="day, day_index in timesheet.days"
|
v-for="row, rowIndex of timesheetRows"
|
||||||
:key="day.date"
|
:key="JSON.stringify(row)"
|
||||||
:ref="getMobileDayRef(day.date)"
|
class="col-auto row items-stretch"
|
||||||
class="col-auto row q-pa-sm full-width relative-position"
|
:style="`animation-delay: ${rowIndex / 10}s;`"
|
||||||
: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
|
<div
|
||||||
v-if="$q.platform.is.mobile && ($q.screen.width < $q.screen.height)"
|
v-for="day, dayIndex in row"
|
||||||
class="col-auto full-width q-px-md q-py-sm"
|
:key="day.day.date"
|
||||||
>
|
class="col row items-stretch q-pa-sm relative-position"
|
||||||
<q-card
|
:class="day.day.date === CURRENT_DATE_STRING ? 'bg-info rounded-15 shadow-6' : ''"
|
||||||
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
|
<ShiftListDay
|
||||||
outlined
|
v-model="row[dayIndex]!.day"
|
||||||
:timesheet-id="timesheet.timesheet_id"
|
:week-day-index="dayIndex"
|
||||||
:week-day-index="day_index"
|
:timesheet-id="day.timesheetId"
|
||||||
:animation-delay-multiplier="day_index"
|
:timesheet-approved="day.isTimesheetApproved"
|
||||||
: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>
|
||||||
</div>
|
</div>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</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>
|
|
||||||
|
|
@ -27,10 +27,10 @@ export class Shift {
|
||||||
is_remote: boolean;
|
is_remote: boolean;
|
||||||
has_error: boolean;
|
has_error: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor(date?: string) {
|
||||||
this.id = -1;
|
this.id = -1;
|
||||||
this.timesheet_id = -1;
|
this.timesheet_id = -1;
|
||||||
this.date = '';
|
this.date = date ?? '';
|
||||||
this.type = 'REGULAR';
|
this.type = 'REGULAR';
|
||||||
this.start_time = '';
|
this.start_time = '';
|
||||||
this.end_time = '';
|
this.end_time = '';
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,14 @@ export interface TotalExpenses {
|
||||||
mileage: number;
|
mileage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimesheetDayDisplay extends TimesheetDay {
|
export class TimesheetDayDisplay {
|
||||||
timesheet_id: number;
|
timesheetId: number;
|
||||||
i18WeekdayKey: string;
|
isTimesheetApproved: boolean;
|
||||||
|
day: TimesheetDay;
|
||||||
|
|
||||||
|
constructor(timesheet: Timesheet, day: TimesheetDay) {
|
||||||
|
this.timesheetId = timesheet.timesheet_id;
|
||||||
|
this.isTimesheetApproved = timesheet.is_approved;
|
||||||
|
this.day = day;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -12,67 +12,57 @@
|
||||||
class="q-pa-md justify-center items-stretch bg-secondary"
|
class="q-pa-md justify-center items-stretch bg-secondary"
|
||||||
:class="$q.platform.is.mobile ? 'column' : 'row'"
|
:class="$q.platform.is.mobile ? 'column' : 'row'"
|
||||||
>
|
>
|
||||||
<!-- center column -->
|
<!-- left column -->
|
||||||
<div class="column col items-center q-pa-md">
|
<div class="column col items-center q-pa-md">
|
||||||
<div class="col-8 fit q-py-md">
|
<div class="col q-py-md">
|
||||||
<MainCarousel />
|
<MainCarousel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- right column -->
|
<!-- right column -->
|
||||||
<div class="column col-lg-4 items-center" :class="$q.platform.is.mobile ? 'q-px-md' : 'q-px-xl'">
|
<div
|
||||||
<span
|
class="column col-xs-12 col-md-4 col-lg-3 items-center"
|
||||||
v-if="!$q.platform.is.mobile"
|
:class="$q.platform.is.mobile ? 'q-px-md' : 'q-px-xl'"
|
||||||
class="col-auto text-uppercase text-weight-bold self-start q-px-md q-pt-lg"
|
|
||||||
>
|
>
|
||||||
|
<span class="col-auto text-uppercase text-weight-bold self-start q-px-md q-pt-lg">
|
||||||
{{ $t('dashboard.useful_links') }}
|
{{ $t('dashboard.useful_links') }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="col-auto full-width"
|
<div class="col-auto full-width q-py-sm">
|
||||||
:class="$q.platform.is.mobile ? 'q-py-md' : 'q-py-sm'">
|
|
||||||
<ShortcutCard
|
<ShortcutCard
|
||||||
icon-image-source="src/assets/links/logo_gmail.png"
|
icon-image-source="img:src/assets/links/logo_gmail.png"
|
||||||
bg-image-source="src/assets/links/google_bg.png"
|
|
||||||
name="Messagerie"
|
name="Messagerie"
|
||||||
route="https://mail.google.com/mail/u/0/#inbox"
|
route="https://mail.google.com/mail/u/0/#inbox"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto full-width"
|
<div class="col-auto full-width q-py-sm">
|
||||||
:class="$q.platform.is.mobile ? 'q-py-md' : 'q-py-sm'">
|
|
||||||
<ShortcutCard
|
<ShortcutCard
|
||||||
icon-image-source="src/assets/links/facturation-transparent.png"
|
icon-image-source="img:src/assets/links/facturation-transparent.png"
|
||||||
bg-image-source="src/assets/links/facturation_bg.png"
|
|
||||||
name="Facturation"
|
name="Facturation"
|
||||||
route="https://facturation.targo.ca/facturation/accueil.php?menu=ticket_open"
|
route="https://facturation.targo.ca/facturation/accueil.php?menu=ticket_open"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto full-width"
|
<div class="col-auto full-width q-py-sm">
|
||||||
:class="$q.platform.is.mobile ? 'q-py-md' : 'q-py-sm'">
|
|
||||||
<ShortcutCard
|
<ShortcutCard
|
||||||
icon-image-source="src/assets/links/map-icon.png"
|
icon-image-source="location_on"
|
||||||
bg-image-source="src/assets/links/map_targo_banner.png"
|
|
||||||
name="Map Targo"
|
name="Map Targo"
|
||||||
route="https://map.targointernet.com/infrastructure/map"
|
route="https://map.targointernet.com/infrastructure/map"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto full-width"
|
<div class="col-auto full-width q-py-sm">
|
||||||
:class="$q.platform.is.mobile ? 'q-py-md' : 'q-py-sm'">
|
|
||||||
<ShortcutCard
|
<ShortcutCard
|
||||||
icon-image-source="src/assets/links/hydroQC_icon.png"
|
icon-image-source="img:src/assets/links/hydroQC_icon.png"
|
||||||
bg-image-source="src/assets/links/hydroQC_bg.png"
|
|
||||||
name="Info Pannes"
|
name="Info Pannes"
|
||||||
route="https://infopannes.solutions.hydroquebec.com/info-pannes/pannes/pannes-en-cours"
|
route="https://infopannes.solutions.hydroquebec.com/info-pannes/pannes/pannes-en-cours"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto full-width"
|
<div class="col-auto full-width q-py-sm">
|
||||||
:class="$q.platform.is.mobile ? 'q-py-md' : 'q-py-sm'">
|
|
||||||
<ShortcutCard
|
<ShortcutCard
|
||||||
icon-image-source="src/assets/links/intranet_logo.png"
|
icon-image-source="language"
|
||||||
bg-image-source="src/assets/links/intranet_targo_bg.png"
|
|
||||||
name="Intranet"
|
name="Intranet"
|
||||||
route="https://intranet.facturation.targo.ca/"
|
route="https://intranet.facturation.targo.ca/"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||