Compare commits
10 Commits
83dd3a4de4
...
528c860a32
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
528c860a32 | ||
| 15e60c9ed2 | |||
|
|
25cd208bfd | ||
|
|
985b8a7564 | ||
| 8a5451a0d6 | |||
|
|
6576177652 | ||
|
|
dcd3a5c188 | ||
| 80c88ae749 | |||
|
|
57334ed118 | ||
|
|
57946dbadd |
|
|
@ -1 +1 @@
|
||||||
VITE_TARGO_BACKEND_URL=PREFIX_BACKEND_URL
|
VITE_TARGO_BACKEND_URL=http://localhost:3000/
|
||||||
|
|
|
||||||
BIN
public/img/circle.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/img/links/facturation-transparent.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/img/links/hydroQC_icon.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
BIN
public/img/links/intranet_logo.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
public/img/links/logo_gmail.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
1
public/img/logo-targo-green.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 326.39 70.35"><defs><style>.cls-1{fill:#019547;}</style></defs><path class="cls-1" d="M25.83,15H8.41a4.14,4.14,0,0,1-3.89-2.71L0,1.18H66.59L62.07,12.27A4.14,4.14,0,0,1,58.18,15H40.76V69.19H25.81Z"/><path class="cls-1" d="M90.74.68h18.55a5.63,5.63,0,0,1,5.63,5.6l.26,62.91H99.55L99.76,54H71.87L66.41,65.51a6.45,6.45,0,0,1-5.84,3.68H49.35L73.53,12.11A18.7,18.7,0,0,1,90.74.68M100,40.73l.07-26h-8a6.75,6.75,0,0,0-6.26,4.18L76.73,40.73Z"/><path class="cls-1" d="M124.51,6.81a5.64,5.64,0,0,1,5.65-5.65H155.6c8.64,0,15.37,2.44,19.81,6.91,3.81,3.78,5.84,9.14,5.84,15.53v.18c0,11-5.92,17.87-14.59,21.1l16.61,24.29h-14a6.51,6.51,0,0,1-5.39-2.87L151.21,47.41H139.44V69.17h-15Zm30.12,27.41c7.28,0,11.45-3.89,11.45-9.62v-.21c0-6.42-4.46-9.7-11.74-9.7H139.46V34.22Z"/><path class="cls-1" d="M184.74,35.37v-.18C184.74,15.85,199.8,0,220.4,0,232.65,0,240,3.31,247.13,9.33l-6.28,7.57a5.18,5.18,0,0,1-6.84,1,23.71,23.71,0,0,0-14.11-4.13c-10.88,0-19.52,9.62-19.52,21.18v.19c0,12.43,8.54,21.57,20.6,21.57a24,24,0,0,0,14.09-4.07V42.91h-8.68A6.41,6.41,0,0,1,220,36.53V30h29.54V59.52a44,44,0,0,1-29,10.78c-21.18,0-35.74-14.8-35.74-34.93"/><path class="cls-1" d="M254.09,35.37v-.18C254.09,15.85,269.33,0,290.33,0s36.06,15.66,36.06,35v.18c0,19.34-15.27,35.19-36.24,35.19s-36.06-15.64-36.06-35m56.63,0v-.18c0-11.67-8.54-21.39-20.6-21.39S269.73,23.34,269.73,35v.18c0,11.67,8.54,21.37,20.6,21.37S310.72,47,310.72,35.37"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
1
public/img/logo-targo-white.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 326.39 70.35"><defs><style>.cls-1{fill:#ffffff;}</style></defs><path class="cls-1" d="M25.83,15H8.41a4.14,4.14,0,0,1-3.89-2.71L0,1.18H66.59L62.07,12.27A4.14,4.14,0,0,1,58.18,15H40.76V69.19H25.81Z"/><path class="cls-1" d="M90.74.68h18.55a5.63,5.63,0,0,1,5.63,5.6l.26,62.91H99.55L99.76,54H71.87L66.41,65.51a6.45,6.45,0,0,1-5.84,3.68H49.35L73.53,12.11A18.7,18.7,0,0,1,90.74.68M100,40.73l.07-26h-8a6.75,6.75,0,0,0-6.26,4.18L76.73,40.73Z"/><path class="cls-1" d="M124.51,6.81a5.64,5.64,0,0,1,5.65-5.65H155.6c8.64,0,15.37,2.44,19.81,6.91,3.81,3.78,5.84,9.14,5.84,15.53v.18c0,11-5.92,17.87-14.59,21.1l16.61,24.29h-14a6.51,6.51,0,0,1-5.39-2.87L151.21,47.41H139.44V69.17h-15Zm30.12,27.41c7.28,0,11.45-3.89,11.45-9.62v-.21c0-6.42-4.46-9.7-11.74-9.7H139.46V34.22Z"/><path class="cls-1" d="M184.74,35.37v-.18C184.74,15.85,199.8,0,220.4,0,232.65,0,240,3.31,247.13,9.33l-6.28,7.57a5.18,5.18,0,0,1-6.84,1,23.71,23.71,0,0,0-14.11-4.13c-10.88,0-19.52,9.62-19.52,21.18v.19c0,12.43,8.54,21.57,20.6,21.57a24,24,0,0,0,14.09-4.07V42.91h-8.68A6.41,6.41,0,0,1,220,36.53V30h29.54V59.52a44,44,0,0,1-29,10.78c-21.18,0-35.74-14.8-35.74-34.93"/><path class="cls-1" d="M254.09,35.37v-.18C254.09,15.85,269.33,0,290.33,0s36.06,15.66,36.06,35v.18c0,19.34-15.27,35.19-36.24,35.19s-36.06-15.64-36.06-35m56.63,0v-.18c0-11.67-8.54-21.39-20.6-21.39S269.73,23.34,269.73,35v.18c0,11.67,8.54,21.37,20.6,21.37S310.72,47,310.72,35.37"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/img/targo-default-avatar.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/img/targo_building.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
public/img/targo_help_banner.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
|
|
@ -57,7 +57,7 @@ export default defineConfig((ctx) => {
|
||||||
|
|
||||||
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
|
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
|
||||||
|
|
||||||
// publicPath: '/',
|
publicPath: '/assets/targo-app/',
|
||||||
// analyze: true,
|
// analyze: true,
|
||||||
// env: {},
|
// env: {},
|
||||||
// rawDefine: {}
|
// rawDefine: {}
|
||||||
|
|
@ -117,7 +117,7 @@ export default defineConfig((ctx) => {
|
||||||
dark: 'auto',
|
dark: 'auto',
|
||||||
},
|
},
|
||||||
|
|
||||||
// iconSet: 'material-icons', // Quasar icon set
|
iconSet: 'material-icons-outlined', // Modern outlined icons
|
||||||
// lang: 'en-US', // Quasar language pack
|
// lang: 'en-US', // Quasar language pack
|
||||||
|
|
||||||
// For special cases outside of where the auto-import strategy can have an impact
|
// For special cases outside of where the auto-import strategy can have an impact
|
||||||
|
|
|
||||||
BIN
src/assets/circle.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/assets/links/facturation-transparent.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/assets/links/facturation_bg.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
src/assets/links/google_bg.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
BIN
src/assets/links/hydroQC_bg.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
src/assets/links/hydroQC_icon.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/links/info-pannes.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
src/assets/links/intranet_logo.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/assets/links/intranet_targo_bg.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
src/assets/links/logo_gmail.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/assets/links/logo_gmail_lockup_default_1x_r5.png
Normal file
|
After Width: | Height: | Size: 1006 B |
BIN
src/assets/links/map-icon.png
Normal file
|
After Width: | Height: | Size: 504 KiB |
BIN
src/assets/links/map_targo_banner.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
155
src/css/app.scss
|
|
@ -2,7 +2,7 @@
|
||||||
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100, 200) {
|
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100, 200) {
|
||||||
.rounded-#{$size} {
|
.rounded-#{$size} {
|
||||||
border-radius: #{$size}px !important;
|
border-radius: #{$size}px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-fb-blue {
|
.text-fb-blue {
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -42,12 +42,21 @@ body.body--dark {
|
||||||
backdrop-filter: blur(5px);
|
backdrop-filter: blur(5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-accent2 {
|
||||||
|
color: #95f0a1B0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-accent2 {
|
||||||
|
background-color: #95f0a1B0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Modern button styling ── */
|
||||||
.q-btn--push::before {
|
.q-btn--push::before {
|
||||||
border-bottom: 4px solid rgba(0,0,0, 0.25);
|
border-bottom: 3px solid rgba(0,0,0, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-btn--push:active {
|
.q-btn--push:active {
|
||||||
transform: translateY(3px);
|
transform: translateY(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-btn--push:active::before {
|
.q-btn--push:active::before {
|
||||||
|
|
@ -67,10 +76,142 @@ input[type=number] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-field--dark .q-field__control::before {
|
.q-field--dark .q-field__control::before {
|
||||||
border-color: #fff3;
|
border-color: #fff2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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-accent);
|
||||||
border-width: 2px;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Modern UI enhancements ── */
|
||||||
|
|
||||||
|
/* Smoother font rendering */
|
||||||
|
body {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Today's row: prominent indicator */
|
||||||
|
.shift-today-glow {
|
||||||
|
box-shadow: 0 0 0 2.5px var(--q-accent), 0 4px 20px rgba(14, 165, 80, 0.2) !important;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Day rows: subtle hover lift */
|
||||||
|
.shift-day-row {
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,0.10) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty day card: dashed border invite */
|
||||||
|
.shift-day-empty {
|
||||||
|
cursor: pointer;
|
||||||
|
.bg-dark {
|
||||||
|
border: 1.5px dashed rgba(255,255,255,0.12) !important;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
&:hover .bg-dark {
|
||||||
|
border-color: var(--q-accent) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cleaner card borders */
|
||||||
|
.q-card {
|
||||||
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.body--dark .q-card {
|
||||||
|
border-color: rgba(255,255,255,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Outlined icon style consistency */
|
||||||
|
.q-icon {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modern header bar */
|
||||||
|
.q-header {
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
.q-toolbar {
|
||||||
|
min-height: 56px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shift type selector: prevent label truncation */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.q-select .q-field__native { min-width: 120px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cleaner input fields */
|
||||||
|
.q-field--outlined .q-field__control {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar: cleaner look */
|
||||||
|
.q-drawer {
|
||||||
|
.q-item {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2px 8px;
|
||||||
|
&.q-router-link--active {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modern scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0,0,0,0.15);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
body.body--dark ::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255,255,255,0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Weekly overview bars */
|
||||||
|
.weekly-bar-fill {
|
||||||
|
transition: width 0.4s ease, background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Approved badge: outlined style */
|
||||||
|
.q-badge {
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover lift utility */
|
||||||
|
.hover-lift {
|
||||||
|
transition: background 0.15s, transform 0.15s;
|
||||||
|
&:hover {
|
||||||
|
background: rgba(14, 165, 80, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip: modern look */
|
||||||
|
.q-tooltip {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,31 @@
|
||||||
// Quasar SCSS (& Sass) Variables
|
// Quasar SCSS (& Sass) Variables
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// To customize the look and feel of this app, you can override
|
// Modernized theme — cleaner, lighter feel with outline aesthetic
|
||||||
// the Sass/SCSS variables found in Quasar's source Sass/SCSS files.
|
|
||||||
|
|
||||||
// Check documentation for full list of Quasar variables
|
$primary : #1e1e2a;
|
||||||
|
$secondary : #eef1f5;
|
||||||
|
$accent : #0ea550;
|
||||||
|
|
||||||
// Your own variables (that are declared here) and Quasar's own
|
$dark-shadow-color : #000;
|
||||||
// ones will be available out of the box in your .vue/.scss/.sass files
|
|
||||||
|
|
||||||
// It's highly recommended to change the default colors
|
$elevation-dark-umbra : rgba($dark-shadow-color, .15);
|
||||||
// to match your app's branding.
|
$elevation-dark-penumbra : rgba($dark-shadow-color, .10);
|
||||||
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
$elevation-dark-ambient : rgba($dark-shadow-color, .08);
|
||||||
|
|
||||||
$primary : #30303A;
|
$layout-shadow-dark : 0 1px 8px rgba($dark-shadow-color, 0.25);
|
||||||
$secondary : #DAE0E7;
|
|
||||||
$accent : #0c9a3b;
|
|
||||||
$accent2 : #0a7d32;
|
|
||||||
|
|
||||||
$dark-shadow-color : #000000;
|
$input-text-color : #37474F;
|
||||||
|
$input-autofill-color : #c8e6d5;
|
||||||
$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);
|
|
||||||
|
|
||||||
$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;
|
|
||||||
$input-autofill-color : #AAD5C4;
|
|
||||||
$field-dense-label-top : 5px !default;
|
$field-dense-label-top : 5px !default;
|
||||||
$field-dense-label-font-size : 16px !default;
|
$field-dense-label-font-size : 14px !default;
|
||||||
|
|
||||||
$button-shadow : 0 0 0 transparent;
|
$button-shadow : 0 0 0 transparent;
|
||||||
|
|
||||||
$dark : #40404C;
|
$dark : #2a2a3a;
|
||||||
$dark-page : #343444;
|
$dark-page : #222233;
|
||||||
|
|
||||||
$positive : #21ba45;
|
$positive : #21ba45;
|
||||||
$negative : #e6364b;
|
$negative : #e6364b;
|
||||||
$info : #6bb9e7;
|
$info : #5ba8d9;
|
||||||
$warning : #e4a944;
|
$warning : #e4a944;
|
||||||
$white : white;
|
$white : white;
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -3,56 +3,95 @@
|
||||||
setup
|
setup
|
||||||
>
|
>
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
|
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-header elevated>
|
<q-header class="app-header">
|
||||||
<q-toolbar class="q-px-sm">
|
<q-toolbar class="q-px-md">
|
||||||
<q-toolbar-title>
|
<!-- Left: menu + logo -->
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
color="white"
|
round
|
||||||
@click="uiStore.toggleRightDrawer"
|
icon="o_menu"
|
||||||
class="q-px-none"
|
color="grey-7"
|
||||||
>
|
@click="uiStore.toggleRightDrawer"
|
||||||
<q-icon
|
|
||||||
name="menu"
|
|
||||||
size="lg"
|
|
||||||
class="q-mr-lg"
|
|
||||||
/>
|
|
||||||
<q-img
|
|
||||||
src="src/assets/logo-targo-white.svg"
|
|
||||||
fit="contain"
|
|
||||||
width="150px"
|
|
||||||
height="30px"
|
|
||||||
/>
|
|
||||||
</q-btn>
|
|
||||||
</q-toolbar-title>
|
|
||||||
|
|
||||||
<q-icon
|
|
||||||
name="las la-user-circle"
|
|
||||||
size="md"
|
|
||||||
class="q-px-sm"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="$q.platform.is.mobile" class="text-uppercase text-bold text-h4 text-accent">
|
<q-img
|
||||||
{{ authStore.user?.first_name.charAt(0) }}{{ authStore.user?.last_name.charAt(0) }}
|
src="img/logo-targo-green.svg"
|
||||||
</div>
|
fit="contain"
|
||||||
|
width="120px"
|
||||||
|
height="28px"
|
||||||
|
class="q-ml-sm cursor-pointer"
|
||||||
|
@click="$router.push('/')"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-if="!$q.platform.is.mobile" class="row items-end">
|
<q-space />
|
||||||
<div class="text-uppercase text-h4 text-weight-medium text-accent q-px-xs">
|
|
||||||
{{ authStore.user?.first_name }}
|
<!-- Right: user info -->
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<q-icon
|
||||||
|
name="o_account_circle"
|
||||||
|
size="sm"
|
||||||
|
color="accent"
|
||||||
|
class="q-mr-sm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="$q.platform.is.mobile" class="user-initials">
|
||||||
|
{{ authStore.user?.first_name?.charAt(0) }}{{ authStore.user?.last_name?.charAt(0) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-uppercase text-h6 text-weight-light q-pr-md">
|
<div v-else class="row items-baseline no-wrap">
|
||||||
{{ authStore.user?.last_name }}
|
<span class="user-first">{{ authStore.user?.first_name }}</span>
|
||||||
|
<span class="user-last">{{ authStore.user?.last_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-toolbar>
|
</q-toolbar>
|
||||||
</q-header>
|
</q-header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-header {
|
||||||
|
background: #fff !important;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.04) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.body--dark) .app-header {
|
||||||
|
background: #1a1a24 !important;
|
||||||
|
border-bottom-color: rgba(255, 255, 255, 0.06);
|
||||||
|
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-initials {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--q-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-first {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--q-accent);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-last {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 400;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #888;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.body--dark) .user-last {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,12 @@
|
||||||
import { useAuthApi } from 'src/modules/auth/composables/use-auth-api';
|
import { useAuthApi } from 'src/modules/auth/composables/use-auth-api';
|
||||||
|
|
||||||
const DRAWER_BUTTONS: { i18n_key: string, icon: string, route: RouteNames, required_module?: UserModuleAccess }[] = [
|
const DRAWER_BUTTONS: { i18n_key: string, icon: string, route: RouteNames, required_module?: UserModuleAccess }[] = [
|
||||||
{ i18n_key: 'nav_bar.home', icon: "home", route: RouteNames.DASHBOARD, required_module: ModuleNames.DASHBOARD },
|
{ i18n_key: 'nav_bar.home', icon: "o_home", route: RouteNames.DASHBOARD, required_module: ModuleNames.DASHBOARD },
|
||||||
{ i18n_key: 'nav_bar.timesheet_approvals', icon: "event_available", route: RouteNames.TIMESHEET_APPROVALS, required_module: ModuleNames.TIMESHEETS_APPROVAL },
|
{ i18n_key: 'nav_bar.timesheet_approvals', icon: "o_event_available", route: RouteNames.TIMESHEET_APPROVALS, required_module: ModuleNames.TIMESHEETS_APPROVAL },
|
||||||
{ i18n_key: 'nav_bar.employee_list', icon: "groups", route: RouteNames.EMPLOYEE_LIST, required_module: ModuleNames.EMPLOYEE_LIST },
|
{ i18n_key: 'nav_bar.employee_list', icon: "o_groups", route: RouteNames.EMPLOYEE_LIST, required_module: ModuleNames.EMPLOYEE_LIST },
|
||||||
{ i18n_key: 'nav_bar.timesheet', icon: "punch_clock", route: RouteNames.TIMESHEET, required_module: ModuleNames.TIMESHEETS },
|
{ i18n_key: 'nav_bar.timesheet', icon: "o_schedule", route: RouteNames.TIMESHEET, required_module: ModuleNames.TIMESHEETS },
|
||||||
{ i18n_key: 'nav_bar.profile', icon: "account_box", route: RouteNames.PROFILE, required_module: ModuleNames.PERSONAL_PROFILE },
|
{ i18n_key: 'nav_bar.profile', icon: "o_person", route: RouteNames.PROFILE, required_module: ModuleNames.PERSONAL_PROFILE },
|
||||||
{ i18n_key: 'nav_bar.help', icon: "contact_support", route: RouteNames.HELP },
|
{ i18n_key: 'nav_bar.help', icon: "o_help_outline", route: RouteNames.HELP },
|
||||||
]
|
]
|
||||||
|
|
||||||
const q = useQuasar();
|
const q = useQuasar();
|
||||||
|
|
@ -67,18 +67,21 @@ import { useAuthApi } from 'src/modules/auth/composables/use-auth-api';
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="button.required_module ? authStore.user?.user_module_access.includes(button.required_module) : true"
|
v-if="button.required_module ? authStore.user?.user_module_access.includes(button.required_module) : true"
|
||||||
class="row items-center full-width q-py-sm cursor-pointer"
|
class="row items-center full-width q-py-sm q-my-xs cursor-pointer rounded-10 q-mx-xs"
|
||||||
:class="$router.currentRoute.value.name === button.route ? ($q.dark.isActive ? 'bg-green-10' : 'bg-green-2') : ''"
|
:class="$router.currentRoute.value.name === button.route
|
||||||
|
? ($q.dark.isActive ? 'bg-green-10' : 'bg-accent text-white')
|
||||||
|
: 'hover-lift'"
|
||||||
|
:style="$router.currentRoute.value.name === button.route ? 'box-shadow: 0 2px 12px rgba(14,165,80,0.3)' : ''"
|
||||||
>
|
>
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="button.icon"
|
:name="button.icon"
|
||||||
color="accent"
|
:color="$router.currentRoute.value.name === button.route ? 'white' : 'accent'"
|
||||||
size="lg"
|
size="md"
|
||||||
class="col-auto q-pl-sm"
|
class="col-auto q-pl-sm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="col text-uppercase text-weight-bold text-h6 q-pl-sm"
|
class="col text-uppercase text-weight-bold text-body1 q-pl-sm"
|
||||||
:class="$q.platform.is.mobile ? '' : 'q-mini-drawer-hide'"
|
:class="$q.platform.is.mobile ? '' : 'q-mini-drawer-hide'"
|
||||||
>
|
>
|
||||||
{{ $t(button.i18n_key) }}
|
{{ $t(button.i18n_key) }}
|
||||||
|
|
@ -93,7 +96,7 @@ import { useAuthApi } from 'src/modules/auth/composables/use-auth-api';
|
||||||
@click="handleLogout"
|
@click="handleLogout"
|
||||||
>
|
>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="exit_to_app"
|
name="o_logout"
|
||||||
color="accent"
|
color="accent"
|
||||||
size="lg"
|
size="lg"
|
||||||
class="col-auto q-pl-sm"
|
class="col-auto q-pl-sm"
|
||||||
|
|
|
||||||
|
|
@ -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', '#95f0a1B0');
|
||||||
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);
|
||||||
|
|
@ -46,4 +47,4 @@
|
||||||
|
|
||||||
<FooterBar v-if="!$q.platform.is.mobile" />
|
<FooterBar v-if="!$q.platform.is.mobile" />
|
||||||
</q-layout>
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -17,8 +17,8 @@
|
||||||
if (is_employee_email.value) return;
|
if (is_employee_email.value) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickEmployeeConnect = () => {
|
const onClickEmployeeConnect = async () => {
|
||||||
auth_api.oidcLogin();
|
await auth_api.oidcLogin();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ export const useAuthApi = () => {
|
||||||
authStore.login();
|
authStore.login();
|
||||||
};
|
};
|
||||||
|
|
||||||
const oidcLogin = () => {
|
const oidcLogin = async () => {
|
||||||
authStore.oidcLogin();
|
await authStore.oidcLogin();
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
>
|
>
|
||||||
<q-btn
|
<div class="bg-primary q-pa-xs rounded-50 chatbot-button">
|
||||||
dense
|
<q-btn
|
||||||
round
|
dense
|
||||||
icon="las la-robot"
|
round
|
||||||
color="accent"
|
icon="las la-robot"
|
||||||
size="2em"
|
color="accent"
|
||||||
class="shadow-5"
|
size="2em"
|
||||||
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>
|
||||||
|
|
@ -2,10 +2,9 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
const { imageSource = "", title = "", description = "", route = "" } = defineProps<{
|
const { route = "" } = defineProps<{
|
||||||
imageSource?: string,
|
iconImageSource: string,
|
||||||
title?: string,
|
name: string,
|
||||||
description?: string,
|
|
||||||
route?: string,
|
route?: string,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
@ -15,28 +14,36 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-card
|
<div
|
||||||
class="shortcut-card cursor-pointer shadow-12"
|
class="full-width cursor-pointer rounded-50 link-btn shadow-4"
|
||||||
@click="onClickExternalShortcut"
|
@click="onClickExternalShortcut"
|
||||||
>
|
>
|
||||||
<q-img
|
<div class="row items-center q-px-lg q-py-xs rounded-50">
|
||||||
:src="imageSource"
|
<span class="col text-uppercase text-bold">
|
||||||
fit="contain"
|
{{ name }}
|
||||||
>
|
</span>
|
||||||
<div class="absolute-bottom text-uppercase text-weight-bolder text-center">{{ title }}</div>
|
|
||||||
</q-img>
|
|
||||||
|
|
||||||
<q-card-section v-if="description">
|
<q-icon
|
||||||
<span>{{ description }}</span>
|
round
|
||||||
</q-card-section>
|
color="accent"
|
||||||
</q-card>
|
size="md"
|
||||||
|
:name="iconImageSource"
|
||||||
|
class="col-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style
|
<style
|
||||||
lang="sass"
|
|
||||||
scoped
|
scoped
|
||||||
|
lang="css"
|
||||||
>
|
>
|
||||||
.shortcut-card
|
.link-btn {
|
||||||
width: 100%
|
background-color: var(--q-dark);
|
||||||
max-width: 250px
|
border: 2px solid var(--q-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-btn:hover {
|
||||||
|
background-color: var(--q-accent2);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -2,10 +2,18 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
|
import { ref } from 'vue';
|
||||||
import { RouteNames } from 'src/router/router-constants';
|
import { RouteNames } from 'src/router/router-constants';
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
const slide = ref<string>('welcome');
|
const slide = ref<string>('welcome');
|
||||||
|
const autoplayTimer = ref(9001);
|
||||||
|
|
||||||
|
const onCarouselMouseEvent = (state: 'enter' | 'exit') => {
|
||||||
|
if (state === 'enter')
|
||||||
|
autoplayTimer.value = 0
|
||||||
|
else
|
||||||
|
autoplayTimer.value = 9001
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -17,29 +25,37 @@ import { ref } from 'vue';
|
||||||
animated
|
animated
|
||||||
infinite
|
infinite
|
||||||
arrows
|
arrows
|
||||||
:autoplay="9001"
|
:autoplay="autoplayTimer"
|
||||||
control-color="accent"
|
control-color="accent"
|
||||||
control-type="outline"
|
control-type="outline"
|
||||||
class="bg-dark full-width rounded-15 shadow-18"
|
class="bg-dark rounded-15 fit shadow-18"
|
||||||
|
@mouseenter="onCarouselMouseEvent('enter')"
|
||||||
|
@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">
|
<div
|
||||||
|
class="column fit"
|
||||||
|
:class="$q.platform.is.mobile ? 'no-wrap' : ''"
|
||||||
|
>
|
||||||
<q-img
|
<q-img
|
||||||
src="src/assets/targo_building.png"
|
src="img/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">
|
<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>
|
||||||
|
|
@ -51,19 +67,25 @@ import { ref } from 'vue';
|
||||||
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="img/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">
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,73 @@
|
||||||
<script
|
<script
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
/* eslint-disable */
|
import { date, useQuasar } from 'quasar';
|
||||||
import { date, useQuasar } from 'quasar';
|
import { computed, onMounted, onUpdated, ref } from 'vue';
|
||||||
import { computed, onMounted, onUpdated, ref } from 'vue';
|
|
||||||
|
const { title: _title, startDate = "", endDate = "" } = defineProps<{
|
||||||
const { title, startDate = "", endDate = "" } = defineProps<{
|
title: string;
|
||||||
title: string;
|
startDate?: string;
|
||||||
startDate?: string;
|
endDate?: string;
|
||||||
endDate?: string;
|
}>();
|
||||||
}>();
|
|
||||||
|
const q = useQuasar();
|
||||||
const q = useQuasar();
|
const emit = defineEmits<{ 'onGetComponentHeight': [value: number] }>();
|
||||||
|
const selfRef = ref<HTMLElement | null>(null);
|
||||||
const emit = defineEmits<{ 'onGetComponentHeight': [value: number] }>();
|
|
||||||
|
const date_format_options = computed(() =>
|
||||||
const selfRef = ref<HTMLElement | null>(null);
|
q.platform.is.mobile
|
||||||
|
? { day: 'numeric', month: 'short', year: 'numeric' }
|
||||||
const date_format_options = computed(() => q.platform.is.mobile ? { day: 'numeric', month: 'short', year: 'numeric' } : { day: 'numeric', month: 'long', year: 'numeric', });
|
: { day: 'numeric', month: 'long', year: 'numeric' }
|
||||||
|
);
|
||||||
onUpdated(() => {
|
|
||||||
if (selfRef.value) {
|
onUpdated(() => { if (selfRef.value) emit('onGetComponentHeight', selfRef.value.offsetHeight); });
|
||||||
emit('onGetComponentHeight', selfRef.value.offsetHeight);
|
onMounted(() => { if (selfRef.value) emit('onGetComponentHeight', selfRef.value.offsetHeight); });
|
||||||
}
|
</script>
|
||||||
});
|
|
||||||
|
<template>
|
||||||
onMounted(() => {
|
<div
|
||||||
if (selfRef.value) {
|
ref="selfRef"
|
||||||
emit('onGetComponentHeight', selfRef.value.offsetHeight);
|
class="column text-center q-pt-md q-pb-xs"
|
||||||
}
|
>
|
||||||
})
|
<transition
|
||||||
</script>
|
enter-active-class="animated fadeInDown"
|
||||||
|
leave-active-class="animated fadeOutDown"
|
||||||
<template>
|
mode="out-in"
|
||||||
<div
|
>
|
||||||
ref="selfRef"
|
<div
|
||||||
class="column text-uppercase text-center text-weight-bolder text-h4 q-pt-md"
|
:key="startDate"
|
||||||
>
|
v-if="startDate.length > 0"
|
||||||
<!-- <span
|
class="col row flex-center full-width q-py-none"
|
||||||
v-if="!$q.platform.is.mobile"
|
>
|
||||||
class="col q-pt-lg"
|
<span class="period-date">
|
||||||
>
|
{{ $d(date.extractDate(startDate, 'YYYY-MM-DD'), date_format_options) }}
|
||||||
{{ $t(title) }}
|
</span>
|
||||||
</span> -->
|
<span class="period-sep">
|
||||||
|
{{ $t('shared.misc.to') }}
|
||||||
<transition
|
</span>
|
||||||
enter-active-class="animated fadeInDown"
|
<span class="period-date">
|
||||||
leave-active-class="animated fadeOutDown"
|
{{ $d(date.extractDate(endDate, 'YYYY-MM-DD'), date_format_options) }}
|
||||||
mode="out-in"
|
</span>
|
||||||
>
|
</div>
|
||||||
<div
|
</transition>
|
||||||
:key="startDate"
|
</div>
|
||||||
v-if="startDate.length > 0"
|
</template>
|
||||||
class="col row flex-center full-width q-py-none q-my-none"
|
|
||||||
:class="$q.platform.is.mobile ? 'q-my-sm' : ''"
|
<style scoped>
|
||||||
>
|
.period-date {
|
||||||
<div class="text-accent text-weight-bold text-h6">
|
font-size: 1.1rem;
|
||||||
{{ $d(date.extractDate(startDate, 'YYYY-MM-DD'), date_format_options) }}
|
font-weight: 700;
|
||||||
</div>
|
text-transform: uppercase;
|
||||||
<div class="text-body2 q-mx-md text-weight-medium">
|
color: var(--q-accent);
|
||||||
{{ $t('shared.misc.to') }}
|
letter-spacing: 0.02em;
|
||||||
</div>
|
}
|
||||||
<div class="text-accent text-weight-bold text-h6">
|
|
||||||
{{ $d(date.extractDate(endDate, 'YYYY-MM-DD'), date_format_options) }}
|
.period-sep {
|
||||||
</div>
|
font-size: 0.8rem;
|
||||||
</div>
|
font-weight: 500;
|
||||||
</transition>
|
color: rgba(128, 128, 128, 0.7);
|
||||||
</div>
|
margin: 0 12px;
|
||||||
</template>
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -45,52 +45,45 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="row">
|
<div class="nav-group row items-center no-wrap">
|
||||||
<!-- navigation to previous week -->
|
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
flat
|
||||||
rounded
|
dense
|
||||||
icon="keyboard_arrow_left"
|
round
|
||||||
|
icon="o_chevron_left"
|
||||||
color="accent"
|
color="accent"
|
||||||
@click="getPreviousPayPeriod"
|
size="md"
|
||||||
:disable="is_previous_pay_period_limit || timesheet_store.is_loading || is_disabled"
|
:disable="is_previous_pay_period_limit || timesheet_store.is_loading || is_disabled"
|
||||||
class="q-mr-sm q-px-sm"
|
@click="getPreviousPayPeriod"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip anchor="top middle" self="center middle" class="bg-primary text-uppercase text-weight-bold">
|
||||||
anchor="top middle"
|
|
||||||
self="center middle"
|
|
||||||
class="bg-primary text-uppercase text-weight-bold"
|
|
||||||
>
|
|
||||||
{{ $t('timesheet.nav_button.previous_week') }}
|
{{ $t('timesheet.nav_button.previous_week') }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
<!-- navigation through calendar date picker -->
|
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
flat
|
||||||
rounded
|
dense
|
||||||
icon="calendar_month"
|
no-caps
|
||||||
|
icon="o_calendar_month"
|
||||||
color="accent"
|
color="accent"
|
||||||
@click="is_showing_calendar_picker = !is_showing_calendar_picker"
|
size="md"
|
||||||
:disable="timesheet_store.is_loading || is_disabled"
|
:disable="timesheet_store.is_loading || is_disabled"
|
||||||
:class="$q.platform.is.mobile ? 'q-px-md' : 'q-px-xl'"
|
class="q-mx-xs"
|
||||||
|
@click="is_showing_calendar_picker = !is_showing_calendar_picker"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip anchor="top middle" self="center middle" class="bg-primary text-uppercase text-weight-bold">
|
||||||
anchor="top middle"
|
|
||||||
self="center middle"
|
|
||||||
class="bg-primary text-uppercase text-weight-bold"
|
|
||||||
>
|
|
||||||
{{ $t('timesheet.nav_button.calendar_date_picker') }}
|
{{ $t('timesheet.nav_button.calendar_date_picker') }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
|
|
||||||
<!-- date picker calendar -->
|
|
||||||
<q-menu
|
<q-menu
|
||||||
v-model="is_showing_calendar_picker"
|
v-model="is_showing_calendar_picker"
|
||||||
no-parent-event
|
no-parent-event
|
||||||
anchor="bottom middle"
|
anchor="bottom middle"
|
||||||
self="top middle"
|
self="top middle"
|
||||||
:offset="[0, 10]"
|
:offset="[0, 8]"
|
||||||
class="shadow-24"
|
class="shadow-24"
|
||||||
|
style="border-radius: 12px; overflow: hidden;"
|
||||||
>
|
>
|
||||||
<q-date
|
<q-date
|
||||||
v-model="calendar_date"
|
v-model="calendar_date"
|
||||||
|
|
@ -104,22 +97,28 @@
|
||||||
</q-menu>
|
</q-menu>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
<!-- navigation to next week -->
|
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
flat
|
||||||
rounded
|
dense
|
||||||
icon="keyboard_arrow_right"
|
round
|
||||||
|
icon="o_chevron_right"
|
||||||
color="accent"
|
color="accent"
|
||||||
@click="getNextPayPeriod"
|
size="md"
|
||||||
:disable="timesheet_store.is_loading || is_disabled"
|
:disable="timesheet_store.is_loading || is_disabled"
|
||||||
class="q-ml-sm q-px-sm"
|
@click="getNextPayPeriod"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip anchor="top middle" self="center middle" class="bg-primary text-uppercase text-weight-bold">
|
||||||
anchor="top middle"
|
{{ $t('timesheet.nav_button.next_week') }}
|
||||||
self="center middle"
|
|
||||||
class="bg-primary text-uppercase text-weight-bold"
|
|
||||||
> {{ $t('timesheet.nav_button.next_week') }}
|
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.nav-group {
|
||||||
|
background: rgba(14, 165, 80, 0.06);
|
||||||
|
border: 1px solid rgba(14, 165, 80, 0.15);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,43 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
|
import { colors, getCssVar } from 'quasar';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const model = defineModel<string | number | null | undefined>({ required: true });
|
const model = defineModel<string | number | null | undefined>({ required: true });
|
||||||
const is_date_picker_open = defineModel<boolean>('isDatePickerOpen', { default: false });
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
label?: string | undefined;
|
label?: string | undefined;
|
||||||
requiresDatePicker?: boolean | undefined;
|
requiresDatePicker?: boolean | undefined;
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
noTopPadding?: boolean;
|
noTopPadding?: boolean;
|
||||||
backgroundColor?: 'bg-secondary' | 'bg-dark';
|
backgroundColor?: 'secondary' | 'dark' | 'white' | undefined;
|
||||||
|
inputTextColor?: string | undefined;
|
||||||
appendContent?: string | number;
|
appendContent?: string | number;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
error?: boolean;
|
||||||
|
dense?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
textAlign?: 'left' | 'right';
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -24,73 +46,87 @@
|
||||||
class="col q-px-sm"
|
class="col q-px-sm"
|
||||||
:class="noTopPadding ? '' : 'q-pt-md'"
|
:class="noTopPadding ? '' : 'q-pt-md'"
|
||||||
>
|
>
|
||||||
<q-input
|
<transition
|
||||||
v-model="model"
|
enter-active-class="animated shakeX"
|
||||||
v-bind="$attrs"
|
:duration="{ enter: 200, leave: 0 }"
|
||||||
dense
|
mode="out-in"
|
||||||
borderless
|
|
||||||
color="accent"
|
|
||||||
label-color="white"
|
|
||||||
stack-label
|
|
||||||
label-slot
|
|
||||||
no-error-icon
|
|
||||||
hide-bottom-space
|
|
||||||
: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'});`"
|
|
||||||
>
|
>
|
||||||
<template #label>
|
<q-input
|
||||||
<span
|
v-model="model"
|
||||||
class="text-weight-bold text-uppercase q-px-md"
|
v-bind="$attrs"
|
||||||
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-primary'"
|
:key="error ? 1 : 2"
|
||||||
>
|
:dense="dense"
|
||||||
{{ label }}
|
:autofocus="autoFocus"
|
||||||
</span>
|
:readonly="readonly"
|
||||||
</template>
|
borderless
|
||||||
|
color="accent"
|
||||||
<template
|
label-color="white"
|
||||||
#append
|
stack-label
|
||||||
v-if="requiresDatePicker || !!appendContent"
|
label-slot
|
||||||
|
no-error-icon
|
||||||
|
hide-bottom-space
|
||||||
|
:error="error"
|
||||||
|
:maxlength="maxLength"
|
||||||
|
class="q-px-md rounded-5 inset-shadow"
|
||||||
|
: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')"
|
||||||
>
|
>
|
||||||
<div v-if="requiresDatePicker">
|
<template #label>
|
||||||
<q-btn
|
<span
|
||||||
flat
|
class="text-weight-bold text-uppercase q-px-md"
|
||||||
dense
|
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-blue-grey-7'"
|
||||||
size="lg"
|
|
||||||
icon="calendar_month"
|
|
||||||
color="accent"
|
|
||||||
@click="is_date_picker_open = true"
|
|
||||||
>
|
>
|
||||||
<q-dialog
|
{{ label }}
|
||||||
v-model="is_date_picker_open"
|
</span>
|
||||||
backdrop-filter="none"
|
</template>
|
||||||
>
|
|
||||||
<q-date
|
|
||||||
v-model="model"
|
|
||||||
mask="YYYY-MM-DD"
|
|
||||||
color="accent"
|
|
||||||
@update:model-value="is_date_picker_open = false"
|
|
||||||
/>
|
|
||||||
</q-dialog>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<template
|
||||||
v-if="!!appendContent"
|
#append
|
||||||
class="self-end text-uppercase text-bold text-accent"
|
v-if="requiresDatePicker || !!appendContent"
|
||||||
style="font-size: 0.8em;"
|
|
||||||
>
|
>
|
||||||
{{ appendContent }}
|
<div v-if="requiresDatePicker">
|
||||||
</div>
|
<q-btn
|
||||||
</template>
|
flat
|
||||||
</q-input>
|
dense
|
||||||
|
size="lg"
|
||||||
|
icon="calendar_month"
|
||||||
|
color="accent"
|
||||||
|
@click="isDatePickerOpen = true"
|
||||||
|
>
|
||||||
|
<q-dialog
|
||||||
|
v-model="isDatePickerOpen"
|
||||||
|
backdrop-filter="none"
|
||||||
|
>
|
||||||
|
<q-date
|
||||||
|
v-model="model"
|
||||||
|
mask="YYYY-MM-DD"
|
||||||
|
color="accent"
|
||||||
|
@update:model-value="isDatePickerOpen = false"
|
||||||
|
/>
|
||||||
|
</q-dialog>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!!appendContent"
|
||||||
|
class="self-end text-uppercase text-weight-medium text-accent"
|
||||||
|
style="font-size: 0.75em;"
|
||||||
|
>
|
||||||
|
{{ appendContent }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
:deep(.q-field--dense.q-field--float .q-field__label) {
|
:deep(.q-field--dense.q-field--float .q-field__label) {
|
||||||
transform: translate(-17px, -60%) scale(0.75);
|
transform: translate(-17px, -60%) scale(0.75);
|
||||||
border-radius: 10px 10px 10px 0px;
|
border-radius: 10px 10px 10px 0px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import TargoInput from 'src/modules/shared/components/targo-input.vue';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||||
|
|
||||||
|
const shift = defineModel<Shift>({ required: true });
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
'clickToggleApproval': [void];
|
||||||
|
'clickDelete': [void];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isCommentDialogOpen = ref(false);
|
||||||
|
|
||||||
|
const approvalOptionState = computed<{ icon: string, label: string }>(() => shift.value.is_approved ?
|
||||||
|
{ icon: 'las la-unlock', label: 'shared.label.unlock' } :
|
||||||
|
{ icon: 'las la-lock', label: 'shared.label.lock' }
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasComment = computed(() => shift.value.comment && shift.value.comment.length > 0)
|
||||||
|
|
||||||
|
const onClickViewComments = () => {
|
||||||
|
isCommentDialogOpen.value = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row full-height flex-center">
|
||||||
|
<q-dialog
|
||||||
|
v-model="isCommentDialogOpen"
|
||||||
|
full-width
|
||||||
|
backdrop-filter="blur(4px)"
|
||||||
|
>
|
||||||
|
<div class="row flex-center full-width">
|
||||||
|
<div class="col-xs-12 col-sm-10 col-md-8 col-lg-6">
|
||||||
|
<TargoInput
|
||||||
|
v-model="shift.comment"
|
||||||
|
auto-focus
|
||||||
|
:label="$t('timesheet.expense.employee_comment')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
icon="more_vert"
|
||||||
|
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
|
||||||
|
floating
|
||||||
|
color="negative"
|
||||||
|
style="transform:translate(-10px, 0px)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-menu
|
||||||
|
auto-close
|
||||||
|
transition-show="jump-down"
|
||||||
|
transition-hide="jump-up"
|
||||||
|
transition-duration="200"
|
||||||
|
>
|
||||||
|
|
||||||
|
<q-list dense>
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
@click="$emit('clickToggleApproval')"
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar :icon="approvalOptionState.icon" />
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section>
|
||||||
|
{{ $t(approvalOptionState.label) }}
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item
|
||||||
|
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-comment" />
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section>
|
||||||
|
<div class="row items-center">
|
||||||
|
<span class="col">{{ $t('timesheet.expense.employee_comment') }}</span>
|
||||||
|
|
||||||
|
<div class="col-auto q-pl-sm">
|
||||||
|
<q-badge
|
||||||
|
v-if="hasComment"
|
||||||
|
rounded
|
||||||
|
color="negative"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
@click="$emit('clickDelete')"
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar
|
||||||
|
icon="las la-trash"
|
||||||
|
text-color="negative"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section>
|
||||||
|
{{ $t('shared.label.remove') }}
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -20,8 +20,8 @@
|
||||||
|
|
||||||
// ========== constants ========================================
|
// ========== constants ========================================
|
||||||
|
|
||||||
const WARNING_COLUMNS: OverviewColumns[] = ['EVENING', 'HOLIDAY', 'VACATION', 'SICK']
|
const WARNING_COLUMNS: OverviewColumns[] = ['EVENING']
|
||||||
const NEGATIVE_COLUMNS: OverviewColumns[] = ['OVERTIME', 'EMERGENCY']
|
const NEGATIVE_COLUMNS: OverviewColumns[] = ['EMERGENCY', 'OVERTIME']
|
||||||
const TIME_COLUMNS: OverviewColumns[] = ['REGULAR', 'EVENING', 'EMERGENCY', 'OVERTIME', 'HOLIDAY', 'VACATION', 'SICK'];
|
const TIME_COLUMNS: OverviewColumns[] = ['REGULAR', 'EVENING', 'EMERGENCY', 'OVERTIME', 'HOLIDAY', 'VACATION', 'SICK'];
|
||||||
const VISIBLE_COLUMNS = ref<OverviewColumns[]>([
|
const VISIBLE_COLUMNS = ref<OverviewColumns[]>([
|
||||||
'employee_first_name',
|
'employee_first_name',
|
||||||
|
|
@ -105,10 +105,14 @@
|
||||||
|
|
||||||
const getListViewTimeCss = (column_name: OverviewColumns, value: number): { classes: string, style: string } => {
|
const getListViewTimeCss = (column_name: OverviewColumns, value: number): { classes: string, style: string } => {
|
||||||
if (WARNING_COLUMNS.includes(column_name) && value > 0)
|
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: 'bg-negative text-white text-bold rounded-5', style: '' };
|
||||||
|
}
|
||||||
|
|
||||||
return { classes: '', style: '' }
|
return { classes: '', style: '' }
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +121,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="full-width">
|
<div class="full-width">
|
||||||
<LoadingOverlay v-model="timesheetStore.is_loading" />
|
<LoadingOverlay v-model="timesheetStore.is_loading" />
|
||||||
|
|
||||||
<q-table
|
<q-table
|
||||||
dense
|
dense
|
||||||
row-key="email"
|
row-key="email"
|
||||||
|
|
@ -253,11 +257,15 @@
|
||||||
<!-- any other fields, though time fields will have their own conditional class to highlight abnormalities -->
|
<!-- any other fields, though time fields will have their own conditional class to highlight abnormalities -->
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="q-px-xs"
|
class="q-px-xs row"
|
||||||
:class="getListViewTimeCss(props.col.name, props.value).classes"
|
|
||||||
>
|
>
|
||||||
{{ TIME_COLUMNS.includes(props.col.name) ?
|
<div
|
||||||
getHoursMinutesStringFromHoursFloat(props.value) : props.value }}
|
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>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,25 @@
|
||||||
>
|
>
|
||||||
import TargoInput from 'src/modules/shared/components/targo-input.vue';
|
import TargoInput from 'src/modules/shared/components/targo-input.vue';
|
||||||
|
|
||||||
|
import { colors } from 'quasar';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||||
import { getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
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 ======================
|
// ================= state ======================
|
||||||
|
|
||||||
const COMMENT_MAX_LENGTH = 280;
|
const COMMENT_MAX_LENGTH = 280;
|
||||||
|
|
||||||
const expense = defineModel<Expense>({ default: new Expense(new Date().toISOString().slice(0, 10)) })
|
|
||||||
const file = defineModel<File>('file');
|
const file = defineModel<File>('file');
|
||||||
const { email } = defineProps<{
|
const { expenseType, email } = defineProps<{
|
||||||
|
expenseType?: ExpenseType;
|
||||||
email?: string | undefined;
|
email?: string | undefined;
|
||||||
mode?: 'normal' | 'approval';
|
mode?: 'normal' | 'approval';
|
||||||
|
refreshKey?: number;
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'clickSave': [void];
|
'clickSave': [void];
|
||||||
|
|
@ -30,8 +32,9 @@
|
||||||
const timesheetStore = useTimesheetStore();
|
const timesheetStore = useTimesheetStore();
|
||||||
const expenseStore = useExpensesStore();
|
const expenseStore = useExpensesStore();
|
||||||
const expensesApi = useExpensesApi();
|
const expensesApi = useExpensesApi();
|
||||||
|
const rules = useExpenseRules();
|
||||||
const isNavigatorOpen = ref(false);
|
const isNavigatorOpen = ref(false);
|
||||||
const rules = useExpenseRules(t);
|
const isHoveringDisabledSave = ref(false);
|
||||||
|
|
||||||
|
|
||||||
const expenseOptions: ExpenseOption[] = [
|
const expenseOptions: ExpenseOption[] = [
|
||||||
|
|
@ -47,8 +50,11 @@
|
||||||
const period_start_date = computed(() => timesheetStore.pay_period?.period_start.replaceAll('-', '/') ?? '');
|
const period_start_date = computed(() => timesheetStore.pay_period?.period_start.replaceAll('-', '/') ?? '');
|
||||||
const period_end_date = computed(() => timesheetStore.pay_period?.period_end.replaceAll('-', '/') ?? '');
|
const period_end_date = computed(() => timesheetStore.pay_period?.period_end.replaceAll('-', '/') ?? '');
|
||||||
const isSaveDisabled = computed(() =>
|
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 =======================
|
// ==================== method =======================
|
||||||
|
|
||||||
|
|
@ -73,11 +79,23 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
const resetAmounts = (resetAmount?: number) => {
|
||||||
if (expense.value)
|
expenseStore.current_expense.amount = resetAmount ?? null;
|
||||||
expenseSelected.value = expenseOptions.find(expense_option => expense_option.value === expense.value.type);
|
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
|
else
|
||||||
expenseSelected.value = expenseOptions[0];
|
resetAmounts(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (expenseType)
|
||||||
|
expenseSelected.value = expenseOptions.find(opt => opt.value === expenseType);
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -121,65 +139,77 @@
|
||||||
<TargoInput
|
<TargoInput
|
||||||
v-model="expenseStore.current_expense.date"
|
v-model="expenseStore.current_expense.date"
|
||||||
no-top-padding
|
no-top-padding
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
:label="$t('timesheet.expense.date')"
|
:label="$t('timesheet.expense.date')"
|
||||||
background-color="bg-dark"
|
background-color="dark"
|
||||||
class="col"
|
class="col"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- expenses type selection -->
|
<!-- expenses type selection -->
|
||||||
<div class="col">
|
<div
|
||||||
<q-select
|
class="col"
|
||||||
v-model="expenseSelected"
|
:key="refreshKey ?? 0"
|
||||||
dense
|
>
|
||||||
borderless
|
<transition
|
||||||
color="accent"
|
enter-active-class="animated shakeX"
|
||||||
label-color="white"
|
:duration="{ enter: 200, leave: 0 }"
|
||||||
stack-label
|
mode="out-in"
|
||||||
label-slot
|
|
||||||
:options="expenseOptions"
|
|
||||||
hide-dropdown-icon
|
|
||||||
lazy-rules
|
|
||||||
no-error-icon
|
|
||||||
hide-bottom-space
|
|
||||||
options-selected-class="text-white text-bold bg-accent"
|
|
||||||
class="q-px-md rounded-5 inset-shadow"
|
|
||||||
:class="$q.dark.isActive ? 'bg-primary' : 'bg-dark'"
|
|
||||||
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 var(${$q.dark.isActive ? '--q-secondary' : '--q-primary'});`"
|
|
||||||
:rules="[rules.typeRequired]"
|
|
||||||
@update:model-value="option => expenseStore.current_expense.type = option.value"
|
|
||||||
>
|
>
|
||||||
<template #label>
|
<q-select
|
||||||
<span
|
v-model="expenseSelected"
|
||||||
class="text-weight-medium text-uppercase q-px-sm no-pointer-events"
|
:key="isTypeError ? 1 : 2"
|
||||||
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-primary'"
|
dense
|
||||||
>
|
borderless
|
||||||
{{ $t('timesheet.expense.type') }}
|
color="accent"
|
||||||
</span>
|
label-color="white"
|
||||||
</template>
|
stack-label
|
||||||
|
label-slot
|
||||||
<template #selected-item="scope">
|
:options="expenseOptions"
|
||||||
<div
|
hide-dropdown-icon
|
||||||
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
lazy-rules
|
||||||
:tabindex="scope.tabindex"
|
no-error-icon
|
||||||
>
|
hide-bottom-space
|
||||||
<q-icon
|
options-selected-class="text-white text-bold bg-accent"
|
||||||
:name="scope.opt.icon"
|
class="q-px-md rounded-5 inset-shadow"
|
||||||
size="xs"
|
:class="$q.dark.isActive ? 'bg-primary' : 'bg-dark'"
|
||||||
class="col-auto q-mx-xs"
|
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 ${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
|
<span
|
||||||
style="line-height: 1em;"
|
class="text-weight-medium text-uppercase q-px-sm no-pointer-events"
|
||||||
class="col-auto ellipsis text-uppercase"
|
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-blue-grey-7'"
|
||||||
>{{ scope.opt.label }}</span>
|
>
|
||||||
</div>
|
{{ $t('timesheet.expense.type') }}
|
||||||
</template>
|
</span>
|
||||||
</q-select>
|
</template>
|
||||||
|
|
||||||
|
<template #selected-item="scope">
|
||||||
|
<div
|
||||||
|
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
||||||
|
:tabindex="scope.tabindex"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
:name="scope.opt.icon"
|
||||||
|
size="xs"
|
||||||
|
class="col-auto q-mx-xs"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style="line-height: 1em;"
|
||||||
|
class="col-auto ellipsis text-uppercase"
|
||||||
|
>{{ scope.opt.label }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- amount input -->
|
<!-- amount input -->
|
||||||
|
|
@ -188,22 +218,32 @@
|
||||||
v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenseStore.current_expense?.type ?? 'EXPENSES')"
|
v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenseStore.current_expense?.type ?? 'EXPENSES')"
|
||||||
v-model.number="expenseStore.current_expense.amount"
|
v-model.number="expenseStore.current_expense.amount"
|
||||||
no-top-padding
|
no-top-padding
|
||||||
background-color="bg-dark"
|
dense
|
||||||
|
background-color="dark"
|
||||||
type="number"
|
type="number"
|
||||||
input-class="text-right"
|
input-class="text-right"
|
||||||
append-content=" $"
|
append-content=" $"
|
||||||
:label="$t('timesheet.expense.amount')"
|
:label="$t('timesheet.expense.amount')"
|
||||||
|
text-align="right"
|
||||||
|
:error="isHoveringDisabledSave && !rules.amountRequired(expenseStore.current_expense.amount)"
|
||||||
|
@focus="resetAmounts()"
|
||||||
|
@blur="onAmountBlur()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TargoInput
|
<TargoInput
|
||||||
v-else
|
v-else
|
||||||
v-model.number="expenseStore.current_expense.mileage"
|
v-model.number="expenseStore.current_expense.mileage"
|
||||||
no-top-padding
|
no-top-padding
|
||||||
background-color="bg-dark"
|
dense
|
||||||
|
background-color="dark"
|
||||||
type="number"
|
type="number"
|
||||||
input-class="text-right"
|
input-class="text-right"
|
||||||
append-content=" km"
|
append-content=" km"
|
||||||
:label="$t('timesheet.expense.mileage')"
|
:label="$t('timesheet.expense.mileage')"
|
||||||
|
text-align="right"
|
||||||
|
:error="isHoveringDisabledSave && !rules.mileageRequired(expenseStore.current_expense.mileage)"
|
||||||
|
@focus="resetAmounts()"
|
||||||
|
@blur="onAmountBlur()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -213,9 +253,11 @@
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<TargoInput
|
<TargoInput
|
||||||
v-model="expenseStore.current_expense.comment"
|
v-model="expenseStore.current_expense.comment"
|
||||||
|
dense
|
||||||
no-top-padding
|
no-top-padding
|
||||||
background-color="bg-dark"
|
background-color="dark"
|
||||||
:max-length="COMMENT_MAX_LENGTH"
|
:max-length="COMMENT_MAX_LENGTH"
|
||||||
|
:error="isHoveringDisabledSave && !rules.commentRequired(expenseStore.current_expense.comment)"
|
||||||
:label="$t('timesheet.expense.employee_comment')"
|
:label="$t('timesheet.expense.employee_comment')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -226,8 +268,9 @@
|
||||||
>
|
>
|
||||||
<TargoInput
|
<TargoInput
|
||||||
v-model="expenseStore.current_expense.supervisor_comment"
|
v-model="expenseStore.current_expense.supervisor_comment"
|
||||||
|
dense
|
||||||
no-top-padding
|
no-top-padding
|
||||||
background-color="bg-dark"
|
background-color="dark"
|
||||||
:max-length="COMMENT_MAX_LENGTH"
|
:max-length="COMMENT_MAX_LENGTH"
|
||||||
:label="$t('timesheet.expense.supervisor_comment')"
|
:label="$t('timesheet.expense.supervisor_comment')"
|
||||||
/>
|
/>
|
||||||
|
|
@ -247,7 +290,7 @@
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
class="q-px-md rounded-5 inset-shadow"
|
class="q-px-md rounded-5 inset-shadow"
|
||||||
:class="$q.dark.isActive ? 'bg-primary' : 'bg-dark'"
|
: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>
|
<template #append>
|
||||||
<q-icon
|
<q-icon
|
||||||
|
|
@ -260,7 +303,7 @@
|
||||||
<template #label>
|
<template #label>
|
||||||
<span
|
<span
|
||||||
class="text-weight-medium text-uppercase q-px-sm no-pointer-events"
|
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') }}
|
{{ $t('timesheet.expense.hints.attach_file') }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -273,16 +316,24 @@
|
||||||
<div class="col row full-width items-center">
|
<div class="col row full-width items-center">
|
||||||
<q-space />
|
<q-space />
|
||||||
|
|
||||||
<q-btn
|
<transition
|
||||||
push
|
enter-active-class="animated rubberBand fast"
|
||||||
:disable="isSaveDisabled"
|
mode="out-in"
|
||||||
:color="isSaveDisabled ? 'grey-5' : 'accent'"
|
>
|
||||||
:icon="expenseStore.mode === 'update' ? 'save' : 'upload'"
|
<q-btn
|
||||||
:label="expenseStore.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
|
:key="isSaveDisabled ? 1 : 0"
|
||||||
class="q-px-sm "
|
push
|
||||||
:class="expenseStore.mode === 'create' ? 'q-mr-lg q-mb-md' : 'q-mb-sm q-ml-lg'"
|
:disable="isSaveDisabled"
|
||||||
type="submit"
|
: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-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>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -292,8 +343,8 @@
|
||||||
scoped
|
scoped
|
||||||
lang="css"
|
lang="css"
|
||||||
>
|
>
|
||||||
:deep(.q-field--dense.q-field--float .q-field__label) {
|
:deep(.q-field--dense.q-field--float .q-field__label) {
|
||||||
transform: translate(-17px, -60%) scale(0.75) !important;
|
transform: translate(-17px, -60%) scale(0.75) !important;
|
||||||
border-radius: 10px 10px 10px 0px;
|
border-radius: 10px 10px 10px 0px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -133,8 +133,12 @@
|
||||||
}) }}
|
}) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-separator vertical spaced class="q-my-xs"/>
|
<q-separator
|
||||||
|
vertical
|
||||||
|
spaced
|
||||||
|
class="q-my-xs"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- comments section -->
|
<!-- comments section -->
|
||||||
<div class="col column">
|
<div class="col column">
|
||||||
|
|
@ -154,7 +158,7 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-separator class="q-mr-md"/>
|
<q-separator class="q-mr-md" />
|
||||||
|
|
||||||
<div class="col row items-center">
|
<div class="col row items-center">
|
||||||
<span
|
<span
|
||||||
|
|
@ -241,7 +245,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ExpenseDialogForm
|
<ExpenseDialogForm
|
||||||
v-model="expense"
|
:key="isShowingUpdateForm ? 1 : 2"
|
||||||
|
:expense-type="expense.type"
|
||||||
:email="getEmployeeEmail()"
|
:email="getEmployeeEmail()"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
@click-save="hideUpdateForm"
|
@click-save="hideUpdateForm"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { Expense } from 'src/modules/timesheets/models/expense.models';
|
import { Expense } from 'src/modules/timesheets/models/expense.models';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const expense_store = useExpensesStore();
|
const expense_store = useExpensesStore();
|
||||||
const refreshKey = ref(0);
|
const refreshKey = ref(0);
|
||||||
|
|
@ -23,6 +23,7 @@ import { ref } from 'vue';
|
||||||
const onClickExpenseCreate = () => {
|
const onClickExpenseCreate = () => {
|
||||||
expense_store.mode = 'create';
|
expense_store.mode = 'create';
|
||||||
expense_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
expense_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
|
refreshKey.value += 1
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -49,7 +50,7 @@ import { ref } from 'vue';
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<ExpenseDialogHeader />
|
<ExpenseDialogHeader />
|
||||||
|
|
||||||
<ExpenseDialogList :key="refreshKey + 1" />
|
<ExpenseDialogList />
|
||||||
|
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
v-if="!isApproved"
|
v-if="!isApproved"
|
||||||
|
|
@ -57,8 +58,7 @@ import { ref } from 'vue';
|
||||||
hide-expand-icon
|
hide-expand-icon
|
||||||
:dense="!$q.platform.is.mobile"
|
:dense="!$q.platform.is.mobile"
|
||||||
group="expenses"
|
group="expenses"
|
||||||
@show="onClickExpenseCreate()"
|
@before-show="onClickExpenseCreate()"
|
||||||
@after-hide="refreshKey += 1"
|
|
||||||
header-class="bg-accent text-white"
|
header-class="bg-accent text-white"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
|
|
@ -77,10 +77,8 @@ import { ref } from 'vue';
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ExpenseDialogFormMobile v-if="$q.platform.is.mobile" />
|
<ExpenseDialogFormMobile v-if="$q.platform.is.mobile" />
|
||||||
<ExpenseDialogForm
|
|
||||||
v-else
|
<ExpenseDialogForm v-else :key="refreshKey" :refresh-key="refreshKey" />
|
||||||
:key="refreshKey"
|
|
||||||
/>
|
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
const is_navigator_open = ref(false);
|
const is_navigator_open = ref(false);
|
||||||
const is_showing_comment_dialog_mobile = 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_start_date = computed(() => timesheet_store.pay_period?.period_start.replaceAll('-', '/') ?? '');
|
||||||
const period_end_date = computed(() => timesheet_store.pay_period?.period_end.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">
|
<div class="column col q-px-sm q-py-xs">
|
||||||
<!-- date label and delete button -->
|
<!-- date label and delete button -->
|
||||||
<div class="col-auto row items-center q-pl-xs">
|
<div class="col-auto row items-center q-pl-xs">
|
||||||
<q-icon
|
|
||||||
name="calendar_month"
|
|
||||||
size="sm"
|
|
||||||
class="col-auto"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span
|
<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"
|
:class="approved_class"
|
||||||
>
|
>
|
||||||
{{ $d(
|
{{ $d(
|
||||||
date.extractDate(expense.date, 'YYYY-MM-DD'),
|
date.extractDate(expense.date, 'YYYY-MM-DD'),
|
||||||
{ month: 'long', day: 'numeric' }
|
{ month: 'long', day: 'numeric', year: 'numeric' }
|
||||||
) }}
|
) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
|
v-if="!expense.is_approved"
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
icon="las la-trash"
|
icon="las la-trash"
|
||||||
|
|
@ -130,7 +125,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 -->
|
<!-- avatar type icon section -->
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="getExpenseIcon(expense.type)"
|
:name="getExpenseIcon(expense.type)"
|
||||||
|
|
@ -139,26 +134,28 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- amount or mileage section -->
|
<!-- 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'">
|
<q-item-label v-if="expense.type === 'MILEAGE'">
|
||||||
{{ expense.mileage?.toFixed(1) }} km
|
{{ expense.mileage?.toFixed(1) }} km
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label v-else>
|
<q-item-label v-else>
|
||||||
$ {{ expense.amount.toFixed(2) }}
|
$ {{ expense.amount?.toFixed(2) }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<!-- attachment file -->
|
||||||
|
<div class="col-6 q-pa-xs">
|
||||||
<!-- attachment file -->
|
<q-btn
|
||||||
<div class="col-auto q-pa-xs full-width">
|
push
|
||||||
<q-btn
|
:disable="!expense.attachment_key"
|
||||||
:color="expense.is_approved ? 'white' : 'accent'"
|
:color="expense.is_approved ? 'white' : 'accent'"
|
||||||
:text-color="expense.is_approved ? 'accent' : 'white'"
|
:text-color="expense.is_approved ? 'accent' : 'white'"
|
||||||
icon="las la-paperclip"
|
icon="las la-paperclip"
|
||||||
:label="expense.attachment_name ?? `( ${$t('shared.label.empty')} )`"
|
:label="expense.attachment_name ?? $t('timesheet.expense.no_attachment')"
|
||||||
class="full-width text-lowercase q-mx-sm q-px-sm q-pb-sm inset-shadow"
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
<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 { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
|
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
||||||
|
import { isShiftOverlap } from 'src/modules/timesheets/utils/shift.util';
|
||||||
|
|
||||||
|
const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
const { locale } = useI18n();
|
||||||
|
const uiStore = useUiStore();
|
||||||
|
const shiftApi = useShiftApi();
|
||||||
|
const timesheetStore = useTimesheetStore();
|
||||||
|
|
||||||
|
const day = defineModel<TimesheetDay>({ required: true });
|
||||||
|
|
||||||
|
const { timesheetId, isTimesheetApproved = false } = defineProps<{
|
||||||
|
timesheetId: number;
|
||||||
|
isTimesheetApproved?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isDayApproved = computed(() => day.value.shifts.every(shift => shift.is_approved) && day.value.shifts.length > 0);
|
||||||
|
const isToday = computed(() => CURRENT_DATE_STRING === day.value.date);
|
||||||
|
const isWeekend = computed(() => {
|
||||||
|
const d = date.extractDate(day.value.date, 'YYYY-MM-DD');
|
||||||
|
return d.getDay() === 0 || d.getDay() === 6;
|
||||||
|
});
|
||||||
|
const isHoliday = computed(() => timesheetStore.federal_holidays.some(h => h.date === day.value.date));
|
||||||
|
const canEdit = computed(() => !isDayApproved.value && !isTimesheetApproved);
|
||||||
|
|
||||||
|
const addNewShift = () => {
|
||||||
|
if (!canEdit.value) return;
|
||||||
|
uiStore.focusNextComponent = true;
|
||||||
|
const newShift = new Shift(day.value.date);
|
||||||
|
newShift.timesheet_id = timesheetId;
|
||||||
|
day.value.shifts.push(newShift);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHolidayName = (_date: string) => {
|
||||||
|
const holiday = timesheetStore.federal_holidays.find(h => h.date === _date);
|
||||||
|
if (!holiday) return;
|
||||||
|
return locale.value === 'fr-FR' ? holiday.nameFr : holiday.nameEn;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTimeFieldBlur = () => {
|
||||||
|
const is_error = isShiftOverlap(day.value.shifts);
|
||||||
|
day.value.shifts.map(shift => shift.has_error = is_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteCurrentShift = async (shiftId: number, index: number) => {
|
||||||
|
if (shiftId <= 0)
|
||||||
|
day.value.shifts.splice(index, 1);
|
||||||
|
else
|
||||||
|
await shiftApi.deleteShiftById(shiftId);
|
||||||
|
|
||||||
|
if (day.value.shifts.length < 2) onTimeFieldBlur();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="full-width q-px-sm">
|
||||||
|
<!-- Holiday label -->
|
||||||
|
<div v-if="isHoliday" class="holiday-label">
|
||||||
|
<q-icon name="o_celebration" size="xs" color="purple-5" class="q-mr-xs" />
|
||||||
|
{{ getHolidayName(day.date) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mobile-day-card"
|
||||||
|
:class="[
|
||||||
|
isToday ? 'mobile-day-today' : '',
|
||||||
|
isWeekend ? 'mobile-day-weekend' : '',
|
||||||
|
isHoliday ? 'mobile-day-holiday' : '',
|
||||||
|
(isDayApproved || isTimesheetApproved) ? 'mobile-day-approved' : '',
|
||||||
|
]"
|
||||||
|
@dblclick="canEdit && day.shifts.length === 0 && addNewShift()"
|
||||||
|
>
|
||||||
|
<!-- Day header -->
|
||||||
|
<div
|
||||||
|
class="mobile-day-header"
|
||||||
|
:class="(isDayApproved || isTimesheetApproved) ? 'bg-accent' : (isHoliday ? 'bg-purple-5' : '')"
|
||||||
|
>
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<span class="mobile-day-date">{{ date.extractDate(day.date, 'YYYY-MM-DD').getDate() }}</span>
|
||||||
|
<div class="column q-ml-sm">
|
||||||
|
<span class="mobile-day-weekday">
|
||||||
|
{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), { weekday: 'long' }) }}
|
||||||
|
</span>
|
||||||
|
<span class="mobile-day-month">
|
||||||
|
{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), { month: 'long' }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<q-space />
|
||||||
|
<q-icon
|
||||||
|
v-if="(isDayApproved || isTimesheetApproved)"
|
||||||
|
name="o_verified"
|
||||||
|
size="sm"
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-else-if="canEdit"
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
icon="o_more_time"
|
||||||
|
:color="(isDayApproved || isTimesheetApproved) ? 'white' : 'accent'"
|
||||||
|
size="md"
|
||||||
|
@click="addNewShift"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shifts -->
|
||||||
|
<div v-if="day.shifts.length > 0" class="mobile-day-shifts">
|
||||||
|
<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"
|
||||||
|
:is-holiday="isHoliday"
|
||||||
|
@request-delete="deleteCurrentShift(shift.id, shiftIndex)"
|
||||||
|
@on-time-field-blur="onTimeFieldBlur()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty state -->
|
||||||
|
<div
|
||||||
|
v-else-if="canEdit"
|
||||||
|
class="mobile-day-empty"
|
||||||
|
>
|
||||||
|
<span class="mobile-day-empty-hint">Double-tap pour ajouter</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.holiday-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #ab47bc;
|
||||||
|
padding: 0 4px 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-day-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--q-dark);
|
||||||
|
box-shadow: 0 1px 6px rgba(0,0,0,0.08);
|
||||||
|
transition: box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-day-today {
|
||||||
|
box-shadow: 0 0 0 2.5px var(--q-accent), 0 4px 16px rgba(14, 165, 80, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-day-weekend {
|
||||||
|
border-left: 3px solid #c8ccd3;
|
||||||
|
background: #eceef2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.body--dark) .mobile-day-weekend {
|
||||||
|
border-left-color: #3a3a4a;
|
||||||
|
background: #22222e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-day-holiday {
|
||||||
|
border: 2px solid #ab47bc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-day-approved {
|
||||||
|
.mobile-day-header {
|
||||||
|
background: var(--q-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-day-header {
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: rgba(0,0,0,0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.body--dark) .mobile-day-header {
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-day-date {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--q-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-day-approved .mobile-day-date {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-day-weekday {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-day-month {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-day-approved .mobile-day-weekday,
|
||||||
|
.mobile-day-approved .mobile-day-month {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-day-shifts {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-day-empty {
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-day-empty-hint {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
color: rgba(128,128,128,0.35);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -2,34 +2,35 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
|
import TargoInput from 'src/modules/shared/components/targo-input.vue';
|
||||||
|
|
||||||
import { computed, onMounted, ref } from '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 { 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 type { Shift, ShiftOption, ShiftType } from 'src/modules/timesheets/models/shift.models';
|
||||||
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
|
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
|
||||||
|
|
||||||
// ========== state ========================================
|
// ========== state ========================================
|
||||||
|
|
||||||
const SHIFT_TYPES_WITH_PREDEFINED_TIMES: ShiftType[] = ['HOLIDAY', 'SICK', 'VACATION'];
|
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 shift = defineModel<Shift>('shift', { required: true });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
dense = false,
|
currentShifts,
|
||||||
hasShiftAfter = false,
|
hasShiftAfter = false,
|
||||||
isTimesheetApproved = false,
|
isTimesheetApproved = false,
|
||||||
errorMessage = undefined,
|
errorMessage = undefined,
|
||||||
expectedDailyHours = 8,
|
expectedDailyHours = 8,
|
||||||
currentShifts,
|
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
dense?: boolean;
|
currentShifts: Shift[];
|
||||||
hasShiftAfter?: boolean;
|
hasShiftAfter?: boolean;
|
||||||
isTimesheetApproved?: boolean;
|
isTimesheetApproved?: boolean;
|
||||||
errorMessage?: string | undefined;
|
errorMessage?: string | undefined;
|
||||||
expectedDailyHours?: number;
|
expectedDailyHours?: number;
|
||||||
currentShifts: Shift[];
|
isHoliday?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
@ -40,7 +41,7 @@
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
const shiftTypeSelected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
const shiftTypeSelected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
||||||
const selectRef = ref<QSelect | null>(null);
|
const selectRef = ref<QSelect | null>(null);
|
||||||
const isShowingCommentPopup = ref(false);
|
// const isShowingCommentPopup = ref(false);
|
||||||
const errorMessageRow = ref('');
|
const errorMessageRow = ref('');
|
||||||
const isShowingPredefinedTime = ref(shift.value.type === 'HOLIDAY');
|
const isShowingPredefinedTime = ref(shift.value.type === 'HOLIDAY');
|
||||||
const predefinedHoursString = ref('');
|
const predefinedHoursString = ref('');
|
||||||
|
|
@ -48,7 +49,9 @@
|
||||||
|
|
||||||
// ========== computed ========================================
|
// ========== 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 =========================================
|
// ========== 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) => {
|
const onShiftTypeChange = (option: ShiftOption) => {
|
||||||
shift.value.type = option.value;
|
shift.value.type = option.value;
|
||||||
|
|
||||||
|
|
@ -114,266 +111,221 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="row q-px-xs">
|
<div class="column">
|
||||||
<div class="col column">
|
<div class="column q-pa-sm">
|
||||||
<div class="col row items-center text-uppercase q-px-xs rounded-5">
|
<div class="col column">
|
||||||
<!-- comment button -->
|
<div class="row justify-center q-pb-xs q-px-sm full-width">
|
||||||
<q-btn
|
<!-- shift type -->
|
||||||
v-if="!dense"
|
<q-select
|
||||||
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
ref="selectRef"
|
||||||
:text-color="shift.comment ? ((shift.is_approved && isTimesheetApproved) ? 'white' : 'accent') : 'grey-5'"
|
v-model="shiftTypeSelected"
|
||||||
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>
|
|
||||||
|
|
||||||
<!-- shift type -->
|
|
||||||
<q-select
|
|
||||||
ref="select"
|
|
||||||
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
|
|
||||||
hide-dropdown-icon
|
|
||||||
:menu-offset="[0, 10]"
|
|
||||||
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)"
|
|
||||||
@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"
|
|
||||||
:tabindex="scope.tabindex"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
:name="scope.opt.icon"
|
|
||||||
: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'"
|
|
||||||
>
|
|
||||||
{{ $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
|
|
||||||
v-bind="scope.itemProps"
|
|
||||||
>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon :name="scope.opt.icon" />
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section class="text-left">
|
|
||||||
{{ $t(scope.label) }}
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="isShowingPredefinedTime"
|
|
||||||
class="col row items-start text-uppercase rounded-5 q-pa-xs relative-position"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="absolute-full rounded-5 q-mx-sm q-my-xs"
|
|
||||||
:class="predefinedHoursBgColor"
|
|
||||||
style="opacity: 0.3;"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<span class="col text-center text-uppercase text-h6 text-bold q-py-xs">
|
|
||||||
{{ getHoursMinutesStringFromHoursFloat(expectedDailyHours) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="col row items-start text-uppercase rounded-5 q-pa-xs"
|
|
||||||
>
|
|
||||||
<!-- punch in field -->
|
|
||||||
<div class="col q-pr-xs">
|
|
||||||
<q-input
|
|
||||||
v-model="shift.start_time"
|
|
||||||
dense
|
dense
|
||||||
:borderless="(shift.is_approved && isTimesheetApproved)"
|
borderless
|
||||||
:readonly="(shift.is_approved && isTimesheetApproved)"
|
color="accent"
|
||||||
type="time"
|
label-color="white"
|
||||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
stack-label
|
||||||
label-slot
|
label-slot
|
||||||
|
hide-dropdown-icon
|
||||||
|
:readonly="isApproved"
|
||||||
|
:options="getShiftOptions(hasPTO, currentShifts.length > 1)"
|
||||||
lazy-rules
|
lazy-rules
|
||||||
no-error-icon
|
no-error-icon
|
||||||
hide-bottom-space
|
hide-bottom-space
|
||||||
:error="shift.has_error"
|
options-selected-class="text-white text-bold bg-accent"
|
||||||
:error-message="errorMessage || errorMessageRow !== '' ? $t(errorMessage ?? errorMessageRow) : ''"
|
class="col q-px-md rounded-5 inset-shadow text-uppercase"
|
||||||
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
:class="isApproved ? 'bg-white' : ($q.dark.isActive ? 'bg-primary' : 'bg-secondary')"
|
||||||
class="rounded-5 bg-dark"
|
popup-content-class="text-uppercase text-weight-medium rounded-5 shadow-12 z-top"
|
||||||
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (!shift.is_approved && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')"
|
popup-content-style="border: 1px solid var(--q-primary)"
|
||||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
menu-anchor="bottom middle"
|
||||||
input-style="font-size: 1.2em;"
|
menu-self="top middle"
|
||||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
:menu-offset="[0, 5]"
|
||||||
@blur="onTimeFieldBlur(shift.start_time)"
|
: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-pt-sm no-wrap ellipsis"
|
||||||
|
:tabindex="scope.tabindex"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
:name="scope.opt.icon"
|
||||||
|
: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="font-size: 1.3em;"
|
||||||
|
class="col ellipsis"
|
||||||
|
:class="shift.is_approved ? 'text-accent' : ''"
|
||||||
|
>
|
||||||
|
{{ $t(scope.opt.label) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #option="scope">
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
v-bind="scope.itemProps"
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon :name="scope.opt.icon" />
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section class="text-left">
|
||||||
|
{{ $t(scope.label) }}
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- work-from-home toggle -->
|
||||||
|
<template #after>
|
||||||
|
<q-icon
|
||||||
|
v-if="shift.is_approved"
|
||||||
|
:name="shift.is_remote ? 'o_laptop' : 'o_business'"
|
||||||
|
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>
|
<template #label>
|
||||||
<span
|
<span
|
||||||
class="text-weight-bolder"
|
class="text-weight-medium text-uppercase q-px-sm no-pointer-events"
|
||||||
:class="shift.is_approved ? ' q-ml-md' : ''"
|
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-blue-grey-7'"
|
||||||
style="font-size: 0.95em;"
|
>
|
||||||
>{{ $t('shared.misc.in') }}</span>
|
{{ $t('timesheet.shift.types.label') }}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-select>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- punch out field -->
|
<div
|
||||||
<div class="col">
|
v-if="isShowingPredefinedTime"
|
||||||
<q-input
|
class="col row items-start text-uppercase rounded-5 q-pa-xs relative-position"
|
||||||
v-model="shift.end_time"
|
>
|
||||||
standout
|
<div
|
||||||
|
class="absolute-full rounded-5 q-mx-sm q-my-xs"
|
||||||
|
:class="predefinedHoursBgColor"
|
||||||
|
style="opacity: 0.3;"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<span class="col text-center text-uppercase text-h6 text-bold q-py-xs">
|
||||||
|
{{ getHoursMinutesStringFromHoursFloat(expectedDailyHours) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="col row items-start text-uppercase rounded-5 q-pt-sm"
|
||||||
|
>
|
||||||
|
<!-- punch in field -->
|
||||||
|
<div class="col">
|
||||||
|
<TargoInput
|
||||||
|
v-model="shift.start_time"
|
||||||
|
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)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- punch out field -->
|
||||||
|
<div class="col">
|
||||||
|
<TargoInput
|
||||||
|
v-model="shift.end_time"
|
||||||
|
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)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto q-pt-md">
|
||||||
|
<TargoInput
|
||||||
|
v-model="shift.comment"
|
||||||
|
no-top-padding
|
||||||
dense
|
dense
|
||||||
:borderless="(shift.is_approved && isTimesheetApproved)"
|
:readonly="isApproved"
|
||||||
:readonly="(shift.is_approved && isTimesheetApproved)"
|
:background-color="isApproved ? 'white' : undefined"
|
||||||
type="time"
|
:input-text-color="isApproved ? 'accent' : ''"
|
||||||
label-slot
|
:label="$t('timesheet.expense.employee_comment')"
|
||||||
no-error-icon
|
:append-content="isApproved ? '' : `${commentLength ?? 0}/280`"
|
||||||
hide-bottom-space
|
/>
|
||||||
: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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-auto">
|
<!-- Delete button at bottom of card -->
|
||||||
<q-btn
|
<div
|
||||||
v-if="!shift.is_approved"
|
v-if="!shift.is_approved"
|
||||||
flat
|
class="row justify-end q-px-sm q-pb-xs"
|
||||||
dense
|
>
|
||||||
color="negative"
|
<q-btn
|
||||||
icon="las la-trash"
|
flat
|
||||||
size="lg"
|
dense
|
||||||
class="full-height"
|
no-caps
|
||||||
@click="$emit('requestDelete')"
|
color="negative"
|
||||||
/>
|
icon="o_delete_outline"
|
||||||
|
label="Supprimer"
|
||||||
|
size="sm"
|
||||||
|
class="text-weight-medium"
|
||||||
|
@click="$emit('requestDelete')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-separator
|
<q-separator
|
||||||
v-if="hasShiftAfter"
|
v-if="hasShiftAfter"
|
||||||
spaced
|
spaced
|
||||||
class="q-mx-md col-12"
|
size="2px"
|
||||||
|
:color="isApproved ? 'accent2' : 'accent'"
|
||||||
|
class="q-mx-lg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -390,4 +342,13 @@
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
align-items: center;
|
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>
|
</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,105 +1,114 @@
|
||||||
<script
|
<script
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
|
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
|
||||||
|
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const timesheet_api = useTimesheetApi();
|
const timesheet_api = useTimesheetApi();
|
||||||
|
|
||||||
const show_autofill = ref(false);
|
const show_autofill = ref(false);
|
||||||
|
|
||||||
const onClickApplyPreset = async (timesheet_id: number) => {
|
const getWeekHours = (ts: typeof timesheet_store.timesheets[0]) =>
|
||||||
show_autofill.value = false;
|
ts.weekly_hours.regular + ts.weekly_hours.evening + ts.weekly_hours.emergency + ts.weekly_hours.overtime;
|
||||||
await timesheet_api.applyPreset(timesheet_id);
|
|
||||||
}
|
const onClickApplyPreset = async (timesheet_id: number) => {
|
||||||
</script>
|
show_autofill.value = false;
|
||||||
|
await timesheet_api.applyPreset(timesheet_id);
|
||||||
<template>
|
}
|
||||||
<div
|
</script>
|
||||||
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
|
||||||
class="row items-start q-px-sm q-pt-sm full-width"
|
<template>
|
||||||
>
|
<div
|
||||||
<!-- per timesheet -->
|
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
||||||
<div
|
class="row items-start q-px-sm q-pt-sm full-width"
|
||||||
v-for="timesheet, timesheet_index in timesheet_store.timesheets"
|
style="gap: 8px;"
|
||||||
:key="timesheet_index"
|
>
|
||||||
class="col column flex-center q-pa-sm"
|
<div
|
||||||
>
|
v-for="timesheet, ti in timesheet_store.timesheets"
|
||||||
<!-- container -->
|
:key="ti"
|
||||||
<div
|
class="col column"
|
||||||
class="rounded-5 relative-position q-px-sm q-pt-sm q-pb-xs full-width shadow-4"
|
>
|
||||||
style="border: 1px solid var(--q-accent);"
|
<div
|
||||||
@click="show_autofill = !show_autofill"
|
class="mobile-week-card"
|
||||||
>
|
@click="show_autofill = !show_autofill"
|
||||||
<!-- icon to show preset is available -->
|
>
|
||||||
<q-icon
|
<div class="row items-center no-wrap">
|
||||||
v-if="timesheet.days.every(day => day.shifts.length < 1)"
|
<q-icon name="o_date_range" size="xs" color="accent" class="q-mr-xs" />
|
||||||
name="schedule_send"
|
<span class="mobile-week-label">Sem. {{ ti + 1 }}</span>
|
||||||
size="sm"
|
<q-space />
|
||||||
color="accent"
|
<span class="mobile-week-hours">{{ getHoursMinutesStringFromHoursFloat(getWeekHours(timesheet)) }}</span>
|
||||||
class="absolute-top-right bg-secondary"
|
</div>
|
||||||
style="transform: translate(10px, -10px);"
|
|
||||||
/>
|
<!-- Day dots -->
|
||||||
|
<div class="row q-mt-xs" style="gap: 3px;">
|
||||||
<!-- label for week number -->
|
<div
|
||||||
<div
|
v-for="day, di in timesheet.days"
|
||||||
class="self-start text-uppercase text-weight-bolder text-overline text-accent bg-secondary absolute-top-left q-px-xs"
|
:key="di"
|
||||||
style="font-size: 1em; top: -7px; left: 10px; line-height: 1em;"
|
class="mobile-week-dot"
|
||||||
>
|
:class="day.shifts.length > 0 ? (day.shifts.every(s => s.is_approved) ? 'dot-approved' : 'dot-filled') : 'dot-empty'"
|
||||||
{{
|
>
|
||||||
getHoursMinutesStringFromHoursFloat(timesheet.weekly_hours.regular +
|
<q-icon v-if="day.shifts.every(s => s.is_approved) && day.shifts.length > 0" name="o_check" size="8px" color="white" />
|
||||||
timesheet.weekly_hours.evening +
|
</div>
|
||||||
timesheet.weekly_hours.emergency +
|
</div>
|
||||||
timesheet.weekly_hours.overtime)
|
</div>
|
||||||
}}
|
|
||||||
</div>
|
<!-- Apply preset -->
|
||||||
|
<q-slide-transition>
|
||||||
<!-- preview of current number of shifts -->
|
<div v-if="show_autofill && timesheet.days.every(d => d.shifts.length < 1)" class="q-pt-xs">
|
||||||
<div
|
<q-btn
|
||||||
class="col row flex-center"
|
flat
|
||||||
style="height: 20px;"
|
dense
|
||||||
>
|
no-caps
|
||||||
<div
|
color="accent"
|
||||||
v-for="day, day_index in timesheet.days"
|
icon="o_schedule_send"
|
||||||
:key="day_index"
|
:label="$t('timesheet.apply_preset')"
|
||||||
class="col row flex-center"
|
class="full-width text-weight-bold"
|
||||||
>
|
style="border: 1px solid currentColor; border-radius: 8px;"
|
||||||
<q-badge
|
@click="onClickApplyPreset(timesheet.timesheet_id)"
|
||||||
:color="day.shifts.length > 0 ? (day.shifts.every(shift => shift.is_approved) ? 'accent shadow-2' : 'white shadow-2') : 'blue-grey-5'"
|
/>
|
||||||
:class="day.shifts.length > 0 ? (day.shifts.every(shift => shift.is_approved) ? 'q-px-xs' : 'q-pa-sm') : ''"
|
</div>
|
||||||
:style="day.shifts.length > 0 ? '' : 'opacity: 0.5'"
|
</q-slide-transition>
|
||||||
>
|
</div>
|
||||||
<q-icon
|
</div>
|
||||||
v-if="day.shifts.every(shift => shift.is_approved) && day.shifts.length > 0"
|
</template>
|
||||||
name="check"
|
|
||||||
class="q-pa-none"
|
<style scoped>
|
||||||
/>
|
.mobile-week-card {
|
||||||
</q-badge>
|
background: rgba(14, 165, 80, 0.04);
|
||||||
</div>
|
border: 1px solid rgba(14, 165, 80, 0.2);
|
||||||
</div>
|
border-radius: 10px;
|
||||||
</div>
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
<!-- button to apply weekly schedule preset -->
|
|
||||||
<q-slide-transition>
|
.mobile-week-label {
|
||||||
<div
|
font-size: 0.7rem;
|
||||||
class="col-auto flex-center row q-pt-xs full-width"
|
font-weight: 700;
|
||||||
v-if="show_autofill"
|
text-transform: uppercase;
|
||||||
>
|
color: var(--q-accent);
|
||||||
<q-btn
|
}
|
||||||
v-if="timesheet.days.every(day => day.shifts.length < 1)"
|
|
||||||
push
|
.mobile-week-hours {
|
||||||
dense
|
font-size: 0.85rem;
|
||||||
color="accent"
|
font-weight: 800;
|
||||||
:label="$t('timesheet.apply_preset')"
|
color: var(--q-accent);
|
||||||
class="full-width"
|
}
|
||||||
@click="onClickApplyPreset(timesheet.timesheet_id)"
|
|
||||||
/>
|
.mobile-week-dot {
|
||||||
</div>
|
flex: 1;
|
||||||
</q-slide-transition>
|
height: 6px;
|
||||||
</div>
|
border-radius: 3px;
|
||||||
</div>
|
display: flex;
|
||||||
</template>
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-empty { background: rgba(0,0,0,0.08); }
|
||||||
|
.dot-filled { background: var(--q-accent); opacity: 0.5; }
|
||||||
|
.dot-approved { background: var(--q-accent); }
|
||||||
|
|
||||||
|
:global(body.body--dark) .dot-empty { background: rgba(255,255,255,0.1); }
|
||||||
|
</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>
|
|
||||||
|
|
@ -1,52 +1,75 @@
|
||||||
<script
|
<script
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { date, useQuasar } from 'quasar';
|
import { date, useQuasar } from 'quasar';
|
||||||
|
|
||||||
const q = useQuasar();
|
const q = useQuasar();
|
||||||
const { extractDate } = date;
|
const { extractDate } = date;
|
||||||
|
|
||||||
const { displayDate, dense = false, approved = false} = defineProps<{
|
const { displayDate, dense = false, approved = false } = defineProps<{
|
||||||
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 weekday_font_size = computed(() => dense ? '0.55em;' : '0.7em;');
|
const date_font_size = computed(() => dense ? '1.5em' : '2.5em');
|
||||||
const date_box_size = computed(() => dense || q.platform.is.mobile ? 'width: 40px; height: 75px;' : 'width: 75px; height: 75px;');
|
const weekday_font_size = computed(() => dense ? '0.55em;' : '0.7em;');
|
||||||
|
const date_box_size = computed(() => dense || q.platform.is.mobile ? 'width: 40px; height: 75px;' : 'width: 75px; height: 75px;');
|
||||||
const display_date = extractDate(displayDate, 'YYYY-MM-DD');
|
|
||||||
</script>
|
const display_date = extractDate(displayDate, 'YYYY-MM-DD');
|
||||||
|
</script>
|
||||||
<template>
|
|
||||||
<div
|
<template>
|
||||||
class="column flex-center rounded-10 text-center self-center bg-transparent"
|
<div
|
||||||
:style="date_box_size"
|
class="column flex-center rounded-10 text-center self-center bg-transparent relative-position"
|
||||||
>
|
:style="date_box_size"
|
||||||
<span
|
>
|
||||||
v-if="!dense"
|
<!-- Today indicator: pure CSS circle -->
|
||||||
class="col-auto text-uppercase text-weight-bold"
|
<div
|
||||||
:class="approved ? 'text-white' : ''"
|
v-if="today"
|
||||||
:style="'font-size: ' + weekday_font_size"
|
class="absolute fit q-px-sm q-py-md"
|
||||||
>
|
>
|
||||||
{{ $d(display_date, { weekday: $q.platform.is.mobile ? 'short' : 'long'}) }}
|
<div class="fit today-circle"></div>
|
||||||
</span>
|
</div>
|
||||||
<span
|
|
||||||
class="col-auto text-weight-bolder"
|
<span
|
||||||
:class="approved ? 'text-white' : ''"
|
v-if="!dense"
|
||||||
:style="'font-size: ' + date_font_size + '; line-height: 90% !important;'"
|
class="col-auto text-uppercase text-weight-bold"
|
||||||
>
|
:class="approved ? 'text-white' : ''"
|
||||||
{{ display_date.getDate() }}
|
:style="'font-size: ' + weekday_font_size"
|
||||||
</span>
|
>
|
||||||
<span
|
{{ $d(display_date, { weekday: $q.platform.is.mobile ? 'short' : 'long' }) }}
|
||||||
class="col-auto text-uppercase text-weight-bold"
|
</span>
|
||||||
:class="approved ? 'text-white' : ''"
|
|
||||||
:style="'font-size: ' + weekday_font_size"
|
<span
|
||||||
>
|
class="col-auto text-weight-bolder"
|
||||||
{{ $d(display_date, { month: $q.platform.is.mobile ? 'short' : 'long' }) }}
|
:class="today ? 'text-accent' : (approved ? 'text-white' : '')"
|
||||||
</span>
|
:style="'font-size: ' + date_font_size + '; line-height: 90% !important;'"
|
||||||
</div>
|
>
|
||||||
</template>
|
{{ display_date.getDate() }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="col-auto text-uppercase text-weight-bold"
|
||||||
|
:class="approved ? 'text-white' : ''"
|
||||||
|
:style="'font-size: ' + weekday_font_size"
|
||||||
|
>
|
||||||
|
{{ $d(display_date, { month: $q.platform.is.mobile ? 'short' : 'long' }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style
|
||||||
|
scoped
|
||||||
|
lang="css"
|
||||||
|
>
|
||||||
|
.today-circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2.5px solid var(--q-accent);
|
||||||
|
opacity: 0.5;
|
||||||
|
background: radial-gradient(circle, rgba(14, 165, 80, 0.08) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { useI18n } from 'vue-i18n';
|
import DetailsDialogShiftMenu from 'src/modules/timesheet-approval/components/details-dialog-shift-menu.vue';
|
||||||
|
import TargoInput from 'src/modules/shared/components/targo-input.vue';
|
||||||
|
|
||||||
import { computed, inject, onMounted, ref } from 'vue';
|
import { computed, inject, onMounted, ref } from 'vue';
|
||||||
import { QSelect, QInput, useQuasar, type QSelectProps, QPopupProxy } from 'quasar';
|
import { QSelect, useQuasar, colors, getCssVar } from 'quasar';
|
||||||
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';
|
||||||
import { getCurrentDailyMinutesWorked, getShiftOptions, getTimeStringFromMinutes, SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
import { getCurrentDailyMinutesWorked, getShiftOptions, getTimeStringFromMinutes, SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||||
|
|
@ -21,7 +23,6 @@
|
||||||
const shift = defineModel<Shift>('shift', { required: true });
|
const shift = defineModel<Shift>('shift', { required: true });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
errorMessage = undefined,
|
|
||||||
isTimesheetApproved = false,
|
isTimesheetApproved = false,
|
||||||
currentShifts,
|
currentShifts,
|
||||||
holiday = false,
|
holiday = false,
|
||||||
|
|
@ -30,7 +31,7 @@
|
||||||
currentShifts: Shift[];
|
currentShifts: Shift[];
|
||||||
expectedDailyHours?: number;
|
expectedDailyHours?: number;
|
||||||
isTimesheetApproved?: boolean;
|
isTimesheetApproved?: boolean;
|
||||||
errorMessage?: string | undefined;
|
errorTimesheet?: boolean | undefined;
|
||||||
holiday?: boolean | undefined;
|
holiday?: boolean | undefined;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
@ -40,7 +41,6 @@
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const q = useQuasar();
|
const q = useQuasar();
|
||||||
const { t } = useI18n();
|
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
|
@ -51,54 +51,13 @@
|
||||||
const shiftErrorMessage = ref<string | undefined>();
|
const shiftErrorMessage = ref<string | undefined>();
|
||||||
const is_showing_delete_confirm = ref(false);
|
const is_showing_delete_confirm = ref(false);
|
||||||
const isShowingPredefinedTime = ref(shift.value.type === 'HOLIDAY');
|
const isShowingPredefinedTime = ref(shift.value.type === 'HOLIDAY');
|
||||||
const popupProxyRef = ref<QPopupProxy | null>(null);
|
|
||||||
const predefinedHoursString = ref('');
|
const predefinedHoursString = ref('');
|
||||||
const predefinedHoursBgColor = ref(`bg-${shiftTypeSelected.value?.icon_color ?? ''}`);
|
const predefinedHoursBgColor = ref(`bg-${shiftTypeSelected.value?.icon_color ?? ''}`);
|
||||||
|
|
||||||
// ================== Computed ==================
|
// ================== Computed ==================
|
||||||
|
|
||||||
const hasPTO = computed(() => currentShifts.some(shift => SHIFT_TYPES_WITH_PREDEFINED_TIMES.includes(shift.type)));
|
const hasPTO = computed(() => currentShifts.some(shift => SHIFT_TYPES_WITH_PREDEFINED_TIMES.includes(shift.type)));
|
||||||
|
const isApproved = computed(() => isTimesheetApproved || shift.value.is_approved);
|
||||||
const rightClickMenuIcon = computed(() => shift.value.is_approved ? 'lock_open' : 'lock');
|
|
||||||
|
|
||||||
const rightClickMenuLabel = computed(() => shift.value.is_approved ?
|
|
||||||
t('timesheet_approvals.tooltip.unapprove') :
|
|
||||||
t('timesheet_approvals.tooltip.approve'));
|
|
||||||
|
|
||||||
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)",
|
|
||||||
}));
|
|
||||||
|
|
||||||
// ================== Methods ==================
|
// ================== Methods ==================
|
||||||
|
|
||||||
|
|
@ -136,12 +95,9 @@
|
||||||
is_showing_delete_confirm.value = state;
|
is_showing_delete_confirm.value = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onRightClickApprove = () => {
|
const onclickToogleApproval = () => {
|
||||||
if (authStore.user?.user_module_access.includes('timesheets_approval'))
|
if (authStore.user?.user_module_access.includes('timesheets_approval'))
|
||||||
shift.value.is_approved = !shift.value.is_approved;
|
shift.value.is_approved = !shift.value.is_approved;
|
||||||
|
|
||||||
if (popupProxyRef.value)
|
|
||||||
popupProxyRef.value.hide();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onShiftTypeChange = (option: ShiftOption) => {
|
const onShiftTypeChange = (option: ShiftOption) => {
|
||||||
|
|
@ -178,32 +134,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="row">
|
<div class="row full-width q-py-xs">
|
||||||
<!-- right-click to approve shift only (if in approval mode) -->
|
|
||||||
<q-popup-proxy
|
|
||||||
v-if="mode === 'approval'"
|
|
||||||
ref="popupProxyRef"
|
|
||||||
context-menu
|
|
||||||
class="rounded-5 q-px-md shadow-24 cursor-pointer"
|
|
||||||
style="border: 3px solid var(--q-primary);"
|
|
||||||
>
|
|
||||||
<q-banner
|
|
||||||
dense
|
|
||||||
class="cursor-pointer q-px-lg"
|
|
||||||
@click="onRightClickApprove"
|
|
||||||
>
|
|
||||||
<template v-slot:avatar>
|
|
||||||
<q-icon
|
|
||||||
:name="rightClickMenuIcon"
|
|
||||||
color="accent"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<span class="text-weight-bold text-accent text-uppercase">
|
|
||||||
{{ rightClickMenuLabel }}
|
|
||||||
</span>
|
|
||||||
</q-banner>
|
|
||||||
</q-popup-proxy>
|
|
||||||
|
|
||||||
<!-- delete shift confirmation dialog -->
|
<!-- delete shift confirmation dialog -->
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="is_showing_delete_confirm"
|
v-model="is_showing_delete_confirm"
|
||||||
|
|
@ -236,34 +167,53 @@
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<div
|
<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'"
|
:class="$q.platform.is.mobile ? 'col q-mb-xs q-px-xs' : 'col-4'"
|
||||||
>
|
>
|
||||||
<!-- shift type -->
|
<!-- shift type -->
|
||||||
<q-select
|
<q-select
|
||||||
ref="selectRef"
|
ref="selectRef"
|
||||||
v-model="shiftTypeSelected"
|
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"
|
@blur="onBlurShiftTypeSelect"
|
||||||
@update:model-value="onShiftTypeChange"
|
@update:model-value="onShiftTypeChange"
|
||||||
>
|
>
|
||||||
<template #selected-item="scope">
|
<template #selected-item="scope">
|
||||||
<div
|
<div
|
||||||
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
class="row items-center text-weight-bold no-wrap ellipsis"
|
||||||
:class="$q.platform.is.mobile ? 'full-height' : ''"
|
|
||||||
:tabindex="scope.tabindex"
|
:tabindex="scope.tabindex"
|
||||||
>
|
>
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="scope.opt.icon"
|
:name="scope.opt.icon"
|
||||||
:color="shift.is_approved ? 'white' : scope.opt.icon_color"
|
:color="scope.opt.icon_color"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="col-auto"
|
class="col-auto"
|
||||||
:class="shift.is_approved ? 'q-mx-xs' : 'q-mr-xs'"
|
:class="shift.is_approved ? 'q-mx-xs' : 'q-mr-xs'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
style="line-height: 1.2em;"
|
|
||||||
class="col-auto ellipsis"
|
class="col-auto ellipsis"
|
||||||
:class="!shift.is_approved ? '' : 'text-white'"
|
:class="shift.is_approved ? 'text-accent' : ''"
|
||||||
>
|
>
|
||||||
{{ $t(scope.opt.label) }}
|
{{ $t(scope.opt.label) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -311,7 +261,7 @@
|
||||||
:disable="shift.is_approved"
|
:disable="shift.is_approved"
|
||||||
dense
|
dense
|
||||||
keep-color
|
keep-color
|
||||||
size="3em"
|
size="2.75em"
|
||||||
:color="holiday ? 'purple-5' : 'accent'"
|
:color="holiday ? 'purple-5' : 'accent'"
|
||||||
icon="las la-building"
|
icon="las la-building"
|
||||||
checked-icon="las la-laptop"
|
checked-icon="las la-laptop"
|
||||||
|
|
@ -327,6 +277,15 @@
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-toggle>
|
</q-toggle>
|
||||||
</template>
|
</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>
|
</q-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -348,44 +307,41 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Else show input fields for in-out timestamps -->
|
<!-- Else show input fields for in-out timestamps -->
|
||||||
<div v-else class="col row items-start text-uppercase rounded-5 q-pa-xs">
|
<div
|
||||||
<q-input
|
v-else
|
||||||
ref="start_time"
|
class="col row items-start text-uppercase rounded-5 q-pa-xs"
|
||||||
|
>
|
||||||
|
<TargoInput
|
||||||
v-model="shift.start_time"
|
v-model="shift.start_time"
|
||||||
v-bind="timeInputProps"
|
no-top-padding
|
||||||
|
dense
|
||||||
type="time"
|
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)"
|
@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 -->
|
<!-- punch out field -->
|
||||||
<q-input
|
<TargoInput
|
||||||
ref="end_time"
|
|
||||||
v-model="shift.end_time"
|
v-model="shift.end_time"
|
||||||
v-bind="timeInputProps"
|
no-top-padding
|
||||||
|
dense
|
||||||
type="time"
|
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)"
|
@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
|
<div
|
||||||
class="row full-height"
|
v-if="mode === 'normal'"
|
||||||
:class="$q.platform.is.mobile ? 'col-12' : 'col-auto flex-center'"
|
class="row col-auto flex-center full-height"
|
||||||
>
|
>
|
||||||
<!-- comment button -->
|
<!-- comment button -->
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|
@ -393,7 +349,7 @@
|
||||||
push
|
push
|
||||||
dense
|
dense
|
||||||
:color="shift.is_approved ? 'white' : (shift.comment ? 'accent' : (holiday ? 'purple-5' : 'blue-grey-5'))"
|
:color="shift.is_approved ? 'white' : (shift.comment ? 'accent' : (holiday ? 'purple-5' : 'blue-grey-5'))"
|
||||||
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
:icon="shift.comment ? 'o_chat' : 'o_chat_bubble_outline'"
|
||||||
:text-color="shift.is_approved ? (holiday ? 'purple-5' : 'accent') : 'white'"
|
:text-color="shift.is_approved ? (holiday ? 'purple-5' : 'accent') : 'white'"
|
||||||
class="col"
|
class="col"
|
||||||
:class="$q.platform.is.mobile ? 'q-mt-xs bg-dark' : ''"
|
:class="$q.platform.is.mobile ? 'q-mt-xs bg-dark' : ''"
|
||||||
|
|
@ -417,7 +373,7 @@
|
||||||
style="border: 3px solid var(--q-accent);"
|
style="border: 3px solid var(--q-accent);"
|
||||||
>
|
>
|
||||||
<q-input
|
<q-input
|
||||||
color="white"
|
color="accent"
|
||||||
v-model="scope.value"
|
v-model="scope.value"
|
||||||
dense
|
dense
|
||||||
:readonly="shift.is_approved"
|
:readonly="shift.is_approved"
|
||||||
|
|
@ -430,7 +386,7 @@
|
||||||
@keyup.enter="scope.set"
|
@keyup.enter="scope.set"
|
||||||
>
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
<q-icon name="edit" />
|
<q-icon name="o_edit" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #counter>
|
<template #counter>
|
||||||
|
|
@ -462,7 +418,7 @@
|
||||||
dense
|
dense
|
||||||
:disable="shift.is_approved"
|
:disable="shift.is_approved"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
icon="las la-trash"
|
icon="o_delete_outline"
|
||||||
text-color="negative"
|
text-color="negative"
|
||||||
class="col"
|
class="col"
|
||||||
size="1.2em"
|
size="1.2em"
|
||||||
|
|
@ -470,6 +426,17 @@
|
||||||
@click="toggleIsShowingDeleteConfirm(true)"
|
@click="toggleIsShowingDeleteConfirm(true)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="col-auto"
|
||||||
|
>
|
||||||
|
<DetailsDialogShiftMenu
|
||||||
|
v-model="shift"
|
||||||
|
@click-toggle-approval="onclickToogleApproval"
|
||||||
|
@click-delete="toggleIsShowingDeleteConfirm(true)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -488,4 +455,9 @@ drops down, rather than the standard floating red text only -->
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
align-items: center;
|
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>
|
</style>
|
||||||
|
|
@ -1,129 +1,270 @@
|
||||||
<script
|
<script
|
||||||
setup
|
setup
|
||||||
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 { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { computed, inject, ref } from 'vue';
|
||||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { isShiftOverlap } from 'src/modules/timesheets/utils/shift.util';
|
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
import type { 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 { isShiftOverlap } from 'src/modules/timesheets/utils/shift.util';
|
||||||
|
import { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||||
// ================== State ==================
|
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
||||||
|
|
||||||
const { timesheetId, weekDayIndex, day, dense = false, approved = false, holiday = false } = defineProps<{
|
// ========== Constants ========================================
|
||||||
timesheetId: number;
|
|
||||||
weekDayIndex: number;
|
const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10);
|
||||||
day: TimesheetDay;
|
|
||||||
dense?: boolean;
|
// ========== State ========================================
|
||||||
approved?: boolean;
|
|
||||||
holiday?: boolean;
|
const day = defineModel<TimesheetDay>({ required: true });
|
||||||
}>();
|
|
||||||
|
const { timesheetId, weekDayIndex, timesheetApproved = false } = defineProps<{
|
||||||
const emit = defineEmits<{
|
timesheetId: number;
|
||||||
'deleteUnsavedShift': [void];
|
weekDayIndex: number;
|
||||||
}>();
|
dense?: boolean;
|
||||||
|
timesheetApproved?: boolean;
|
||||||
const shift_api = useShiftApi();
|
}>();
|
||||||
const timesheet_api = useTimesheetApi();
|
|
||||||
const timesheet_store = useTimesheetStore();
|
const { locale } = useI18n();
|
||||||
const preset_mouseover = ref(false);
|
const uiStore = useUiStore();
|
||||||
const shift_error_message = ref<string | undefined>();
|
const shiftApi = useShiftApi();
|
||||||
const employeeEmail = inject<string>('employeeEmail');
|
const timesheetApi = useTimesheetApi();
|
||||||
|
const timesheetStore = useTimesheetStore();
|
||||||
// ================== Methods ==================
|
const presetMouseover = ref(false);
|
||||||
|
const shiftErrorMessage = ref<string | undefined>();
|
||||||
const deleteCurrentShift = async (shift: Shift) => {
|
const employeeEmail = inject<string>('employeeEmail');
|
||||||
if (shift.id <= 0) {
|
|
||||||
shift.id = 0;
|
// ========== Computed ========================================
|
||||||
emit('deleteUnsavedShift');
|
|
||||||
} else {
|
const isDayApproved = computed(() => day.value.shifts.length > 0 && day.value.shifts.every(
|
||||||
await shift_api.deleteShiftById(shift.id, employeeEmail);
|
shift => shift.is_approved === true));
|
||||||
}
|
|
||||||
|
const isHoliday = computed(() => timesheetStore.federal_holidays.some(
|
||||||
if (day.shifts.length < 2 && shift_error_message.value !== undefined) {
|
holiday => holiday.date === day.value.date));
|
||||||
onTimeFieldBlur();
|
|
||||||
}
|
const isToday = computed(() => CURRENT_DATE_STRING === day.value.date);
|
||||||
};
|
|
||||||
|
const isEmpty = computed(() => day.value.shifts.length === 0);
|
||||||
const onTimeFieldBlur = () => {
|
|
||||||
const is_error = isShiftOverlap(day.shifts);
|
const canEdit = computed(() => !isDayApproved.value && !timesheetApproved);
|
||||||
day.shifts.map(shift => shift.has_error = is_error);
|
|
||||||
if (is_error)
|
// ================== Methods ==================
|
||||||
shift_error_message.value = 'timesheet.errors.SHIFT_OVERLAP_SHORT';
|
|
||||||
else
|
const addNewShift = () => {
|
||||||
shift_error_message.value = undefined;
|
if (!canEdit.value) return;
|
||||||
}
|
uiStore.focusNextComponent = true;
|
||||||
|
const newShift = new Shift(day.value.date);
|
||||||
const onClickApplyDailyPreset = async () => {
|
newShift.timesheet_id = timesheetId;
|
||||||
await timesheet_api.applyPreset(timesheetId, weekDayIndex, day.date, employeeEmail);
|
day.value.shifts.push(newShift);
|
||||||
}
|
};
|
||||||
</script>
|
|
||||||
|
const onDblClickCard = () => {
|
||||||
<template>
|
if (isEmpty.value && canEdit.value) addNewShift();
|
||||||
<div
|
};
|
||||||
class="column justify-center q-py-xs"
|
|
||||||
:class="approved ? '' : ''"
|
const deleteCurrentShift = async (shiftId: number, index: number) => {
|
||||||
@mouseenter="preset_mouseover = true"
|
if (shiftId <= 0)
|
||||||
@mouseleave="preset_mouseover = false"
|
day.value.shifts.splice(index, 1);
|
||||||
>
|
else
|
||||||
<!-- Button to apply preset to day -->
|
await shiftApi.deleteShiftById(shiftId, employeeEmail);
|
||||||
<transition
|
|
||||||
appear
|
if (day.value.shifts.length < 2 && shiftErrorMessage.value !== undefined) {
|
||||||
enter-active-class="animated zoomIn fast"
|
onTimeFieldBlur();
|
||||||
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"
|
const onTimeFieldBlur = () => {
|
||||||
:disable="day.shifts.length > 0"
|
const is_error = isShiftOverlap(day.value.shifts);
|
||||||
flat
|
day.value.shifts.map(shift => shift.has_error = is_error);
|
||||||
dense
|
if (is_error)
|
||||||
size="lg"
|
shiftErrorMessage.value = 'timesheet.errors.SHIFT_OVERLAP_SHORT';
|
||||||
:label="$t('timesheet.apply_preset_day')"
|
else
|
||||||
class="text-uppercase text-weight-bold text-accent q-mx-lg q-py-none rounded-5"
|
shiftErrorMessage.value = undefined;
|
||||||
style="opacity: 0.6;"
|
}
|
||||||
@click.stop="onClickApplyDailyPreset"
|
|
||||||
>
|
const onClickApplyDailyPreset = async () => {
|
||||||
<q-icon
|
await timesheetApi.applyPreset(timesheetId, weekDayIndex, day.value.date, employeeEmail);
|
||||||
name="las la-calendar-day"
|
}
|
||||||
color="accent"
|
|
||||||
size="md"
|
const getHolidayName = (date: string) => {
|
||||||
/>
|
const holiday = timesheetStore.federal_holidays.find(holiday => holiday.date === date);
|
||||||
</q-btn>
|
if (!holiday) return;
|
||||||
</transition>
|
|
||||||
|
if (locale.value === 'fr-FR')
|
||||||
<div
|
return holiday.nameFr;
|
||||||
v-for="shift, shift_index in day.shifts"
|
|
||||||
:key="shift_index"
|
else if (locale.value === 'en-CA')
|
||||||
class="col-auto"
|
return holiday.nameEn;
|
||||||
>
|
};
|
||||||
<ShiftListDayRowMobile
|
</script>
|
||||||
v-if="$q.platform.is.mobile"
|
|
||||||
v-model:shift="day.shifts[shift_index]!"
|
<template>
|
||||||
:is-timesheet-approved="approved"
|
<div
|
||||||
:error-message="shift_error_message"
|
class="row fit rounded-10 ellipsis no-wrap shift-day-row"
|
||||||
:dense="dense"
|
:data-date="day.date"
|
||||||
:current-shifts="day.shifts"
|
:class="[
|
||||||
:has-shift-after="shift_index < day.shifts.length - 1"
|
isToday ? 'shift-today-glow shadow-10' : 'shadow-4',
|
||||||
@request-delete="deleteCurrentShift(shift)"
|
isEmpty && canEdit ? 'shift-day-empty' : ''
|
||||||
@on-time-field-blur="onTimeFieldBlur()"
|
]"
|
||||||
/>
|
@dblclick="onDblClickCard"
|
||||||
|
>
|
||||||
<ShiftListDayRow
|
<!-- optional label indicating which holiday if today is a holiday -->
|
||||||
v-else
|
<span
|
||||||
v-model:shift="day.shifts[shift_index]!"
|
v-if="isHoliday"
|
||||||
:holiday="holiday"
|
class="absolute-top-left text-uppercase text-bold holiday-border text-white"
|
||||||
:current-shifts="day.shifts"
|
style="transform: translate(25px, -2px); z-index: 1;"
|
||||||
:is-timesheet-approved="approved"
|
>
|
||||||
:error-message="shift_error_message"
|
{{ getHolidayName(day.date) }}
|
||||||
@request-delete="deleteCurrentShift(shift)"
|
</span>
|
||||||
@on-time-field-blur="onTimeFieldBlur()"
|
|
||||||
/>
|
<div
|
||||||
|
class="col rounded-10"
|
||||||
</div>
|
:class="isToday ? 'bg-accent' : ''"
|
||||||
</div>
|
:style="isToday ? 'padding: 3px; ' : ''"
|
||||||
</template>
|
>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<!-- Empty state: hint to double-click or use preset -->
|
||||||
|
<div
|
||||||
|
v-if="isEmpty && canEdit"
|
||||||
|
class="col row items-center justify-center empty-day-hint"
|
||||||
|
>
|
||||||
|
<!-- Preset button on hover -->
|
||||||
|
<transition
|
||||||
|
appear
|
||||||
|
enter-active-class="animated fadeIn fast"
|
||||||
|
leave-active-class="animated fadeOut fast"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
v-if="presetMouseover && timesheetStore.has_timesheet_preset"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
no-caps
|
||||||
|
size="md"
|
||||||
|
icon="o_calendar_month"
|
||||||
|
:label="$t('timesheet.apply_preset_day')"
|
||||||
|
class="text-accent q-mx-md rounded-5"
|
||||||
|
style="opacity: 0.7;"
|
||||||
|
@click.stop="onClickApplyDailyPreset"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<!-- Hint text on hover only -->
|
||||||
|
<transition
|
||||||
|
enter-active-class="animated fadeIn fast"
|
||||||
|
leave-active-class="animated fadeOut fast"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="presetMouseover && (!timesheetStore.has_timesheet_preset)"
|
||||||
|
class="empty-day-label"
|
||||||
|
>
|
||||||
|
<q-icon name="o_more_time" size="xs" class="q-mr-xs" />
|
||||||
|
Double-cliquer pour ajouter
|
||||||
|
</span>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shift rows -->
|
||||||
|
<div
|
||||||
|
v-for="shift, shiftIndex in day.shifts"
|
||||||
|
:key="shiftIndex"
|
||||||
|
class="col-auto row"
|
||||||
|
>
|
||||||
|
<ShiftListDayRow
|
||||||
|
v-model:shift="day.shifts[shiftIndex]!"
|
||||||
|
:holiday="isHoliday"
|
||||||
|
:current-shifts="day.shifts"
|
||||||
|
:is-timesheet-approved="timesheetApproved"
|
||||||
|
:error-message="shiftErrorMessage"
|
||||||
|
@request-delete="deleteCurrentShift(shift.id, shiftIndex)"
|
||||||
|
@on-time-field-blur="onTimeFieldBlur()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right action column -->
|
||||||
|
<div class="col-auto self-stretch row items-center">
|
||||||
|
<q-icon
|
||||||
|
v-if="(isDayApproved || timesheetApproved)"
|
||||||
|
name="o_verified"
|
||||||
|
color="white"
|
||||||
|
size="lg"
|
||||||
|
class="q-px-xs"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
icon="o_more_time"
|
||||||
|
size="md"
|
||||||
|
:color="isHoliday ? 'purple-5' : 'accent'"
|
||||||
|
class="q-mx-xs add-shift-btn"
|
||||||
|
@click="addNewShift"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
anchor="center left"
|
||||||
|
self="center right"
|
||||||
|
:offset="[8, 0]"
|
||||||
|
class="text-body2"
|
||||||
|
>
|
||||||
|
{{ $t('timesheet.shift.actions.add') || 'Ajouter un quart' }}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-day-hint {
|
||||||
|
min-height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-day-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
color: rgba(255, 255, 255, 0.25);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shift-day-empty:hover .empty-day-label {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,24 @@
|
||||||
<script
|
<script
|
||||||
setup
|
setup
|
||||||
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 { ref } from 'vue';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||||
import type { QScrollArea, TouchSwipeValue } from 'quasar';
|
import type { TouchSwipeValue } from 'quasar';
|
||||||
|
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const timesheet_api = useTimesheetApi();
|
const timesheet_api = useTimesheetApi();
|
||||||
|
|
||||||
const { mode = 'normal' } = defineProps<{
|
const { mode: _mode = 'normal' } = defineProps<{
|
||||||
mode: 'normal' | 'approval';
|
mode: 'normal' | 'approval';
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const mobile_animation_direction = ref('fadeInLeft');
|
const mobile_animation_direction = ref('fadeInLeft');
|
||||||
|
|
||||||
const timesheet_page = ref<QScrollArea | null>(null);
|
|
||||||
|
|
||||||
const scroll_y = computed(() => timesheet_page.value?.getScrollPosition().top ?? 0);
|
|
||||||
|
|
||||||
const handleSwipe: TouchSwipeValue = (details) => {
|
const handleSwipe: TouchSwipeValue = (details) => {
|
||||||
mobile_animation_direction.value = details.direction === 'left' ? 'fadeInRight' : 'fadeInLeft';
|
mobile_animation_direction.value = details.direction === 'left' ? 'fadeInRight' : 'fadeInLeft';
|
||||||
if (details.distance && details.distance.x && Math.abs(details.distance.x) > 15) {
|
if (details.distance && details.distance.x && Math.abs(details.distance.x) > 15) {
|
||||||
|
|
@ -30,66 +27,41 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTodayComponentFound = (today_component: HTMLElement | undefined) => {
|
const onTodayComponentFound = (today_component: HTMLElement | undefined) => {
|
||||||
if (timesheet_page.value && today_component)
|
if (today_component) {
|
||||||
timesheet_page.value.setScrollPosition('vertical', today_component.offsetTop, 800);
|
today_component.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="column fit relative-position"
|
class="column full-width"
|
||||||
:style="$q.platform.is.mobile && $q.screen.width < $q.screen.height ? 'margin-bottom: 40px' : ''"
|
:style="$q.platform.is.mobile && $q.screen.width < $q.screen.height ? 'margin-bottom: 40px' : ''"
|
||||||
v-touch-swipe.horizontal="handleSwipe"
|
v-touch-swipe.horizontal="handleSwipe"
|
||||||
>
|
>
|
||||||
<q-scroll-area
|
<!-- Show if no timesheets found -->
|
||||||
ref="timesheet_page"
|
<div
|
||||||
:horizontal-offset="[0, 3]"
|
v-if="timesheet_store.timesheets.length < 1 && !timesheet_store.is_loading"
|
||||||
class="col absolute-full hide-scrollbar"
|
class="column flex-center q-py-xl"
|
||||||
:thumb-style="{ opacity: '0' }"
|
style="min-height: 20vh;"
|
||||||
:bar-style="{ opacity: '0' }"
|
|
||||||
>
|
>
|
||||||
<!-- Show if no timesheets found (further than one month from present) -->
|
<span class="text-uppercase text-weight-bolder text-center">{{ $t('shared.error.no_data_found') }}</span>
|
||||||
<div
|
<q-icon
|
||||||
v-if="timesheet_store.timesheets.length < 1 && !timesheet_store.is_loading"
|
name="o_event_busy"
|
||||||
class="col-auto column flex-center fit q-py-lg"
|
color="accent"
|
||||||
style="min-height: 20vh;"
|
size="8em"
|
||||||
>
|
class="absolute"
|
||||||
<span class="text-uppercase text-weight-bolder text-center">{{ $t('shared.error.no_data_found')
|
style="opacity: 0.15;"
|
||||||
}}</span>
|
/>
|
||||||
<q-icon
|
</div>
|
||||||
name="las la-calendar"
|
|
||||||
color="accent"
|
|
||||||
size="10em"
|
|
||||||
class="absolute"
|
|
||||||
style="opacity: 0.2;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Else show timesheets if found -->
|
<!-- Mobile -->
|
||||||
<ShiftList @on-current-day-component-found="onTodayComponentFound" />
|
<ShiftListMobile
|
||||||
</q-scroll-area>
|
v-else-if="$q.platform.is.mobile"
|
||||||
|
@on-current-day-component-found="onTodayComponentFound"
|
||||||
|
/>
|
||||||
|
|
||||||
<q-page-sticky
|
<!-- Desktop: no scroll area, page scrolls naturally -->
|
||||||
v-if="mode === 'normal'"
|
<ShiftList v-else />
|
||||||
position="bottom-right"
|
|
||||||
:offset="$q.screen.width > $q.screen.height ? [15, 15] : [15, 65]"
|
|
||||||
class="z-top"
|
|
||||||
>
|
|
||||||
<transition
|
|
||||||
appear
|
|
||||||
enter-active-class="animated zoomIn"
|
|
||||||
leave-active-class="animated zoomOut"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
v-if="scroll_y > 400"
|
|
||||||
fab
|
|
||||||
icon="las la-chevron-up"
|
|
||||||
color="white"
|
|
||||||
text-color="accent"
|
|
||||||
class="shadow-12"
|
|
||||||
@click="timesheet_page!.setScrollPosition('vertical', 0, 300)"
|
|
||||||
/>
|
|
||||||
</transition>
|
|
||||||
</q-page-sticky>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,98 +1,376 @@
|
||||||
<script
|
<script
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { computed, onMounted } from 'vue';
|
import { computed, onMounted } from 'vue';
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
import { date } from 'quasar';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
import { getHoursMinutesStringFromHoursFloat, getHoursMinutesBetweenTwoHHmm } from 'src/utils/date-and-time-utils';
|
||||||
const { mode = 'totals', timesheetMode = 'normal', totalHours = 0, totalExpenses = 0 } = defineProps<{
|
|
||||||
mode: 'total-hours' | 'off-hours';
|
const { mode = 'totals', timesheetMode = 'normal', totalHours = 0, totalExpenses = 0 } = defineProps<{
|
||||||
timesheetMode: 'approval' | 'normal';
|
mode: 'total-hours' | 'off-hours';
|
||||||
totalHours?: number;
|
timesheetMode: 'approval' | 'normal';
|
||||||
totalExpenses?: number;
|
weeklyHours?: number[];
|
||||||
}>();
|
totalHours?: number;
|
||||||
|
totalExpenses?: number;
|
||||||
const auth_store = useAuthStore();
|
}>();
|
||||||
const timesheetStore = useTimesheetStore();
|
|
||||||
const is_management = auth_store.user?.user_module_access.includes('timesheets_approval');
|
const auth_store = useAuthStore();
|
||||||
|
const timesheetStore = useTimesheetStore();
|
||||||
const vacationHours = computed(() => timesheetStore.paid_time_off_totals.vacation_hours);
|
const is_management = auth_store.user?.user_module_access.includes('timesheets_approval');
|
||||||
const sickHours = computed(() => timesheetStore.paid_time_off_totals.sick_hours);
|
|
||||||
const bankedHours = computed(() => timesheetStore.paid_time_off_totals.banked_hours);
|
const vacationHours = computed(() => timesheetStore.paid_time_off_totals.vacation_hours);
|
||||||
|
const sickHours = computed(() => timesheetStore.paid_time_off_totals.sick_hours);
|
||||||
onMounted(async () => {
|
const bankedHours = computed(() => timesheetStore.paid_time_off_totals.banked_hours);
|
||||||
if (timesheetMode === 'normal')
|
|
||||||
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail();
|
const _hoursPercent = computed(() => Math.min(100, (totalHours / 80) * 100));
|
||||||
})
|
|
||||||
</script>
|
// Day-by-day hours for each week
|
||||||
|
const emit = defineEmits<{ 'day-click': [dateStr: string] }>();
|
||||||
<template>
|
|
||||||
<div
|
const DAY_ABBR = ['D', 'L', 'M', 'M', 'J', 'V', 'S'];
|
||||||
class="column full-width shadow-4 rounded-5 q-pa-sm"
|
|
||||||
style="border: 1px solid var(--q-accent);"
|
const weekDayHours = computed(() => {
|
||||||
>
|
return timesheetStore.timesheets.map(ts =>
|
||||||
<div
|
ts.days.map(day => {
|
||||||
v-if="mode === 'total-hours'"
|
const hours = day.shifts.reduce((sum, shift) => {
|
||||||
class="col column full-width"
|
if (!shift.end_time || !shift.start_time) return sum;
|
||||||
>
|
const t = getHoursMinutesBetweenTwoHHmm(shift.start_time, shift.end_time);
|
||||||
<div class="col row full-width">
|
return sum + t.hours + t.minutes / 60;
|
||||||
<span class="col-auto text-uppercase text-caption text-bold text-accent">
|
}, 0);
|
||||||
{{ $t('timesheet.total_hours') }}
|
const d = date.extractDate(day.date, 'YYYY-MM-DD');
|
||||||
</span>
|
const dow = d.getDay();
|
||||||
|
const isWeekend = dow === 0 || dow === 6;
|
||||||
<span class="col text-right">{{ getHoursMinutesStringFromHoursFloat(totalHours) }}</span>
|
return {
|
||||||
</div>
|
hours, isWeekend, dateStr: day.date,
|
||||||
|
dayAbbr: DAY_ABBR[dow],
|
||||||
<div class="col row full-width">
|
dayNum: d.getDate(),
|
||||||
<span class="col-auto text-uppercase text-caption text-bold text-accent">
|
hasShifts: day.shifts.length > 0,
|
||||||
{{ $t('timesheet.total_expenses') }}
|
isApproved: day.shifts.length > 0 && day.shifts.every(s => s.is_approved),
|
||||||
</span>
|
};
|
||||||
|
})
|
||||||
<span class="col text-right">{{ totalExpenses }}$</span>
|
);
|
||||||
</div>
|
});
|
||||||
</div>
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (timesheetMode === 'normal')
|
||||||
<div
|
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail();
|
||||||
v-else
|
})
|
||||||
class="col column full-width"
|
</script>
|
||||||
>
|
|
||||||
<div class="col row full-width">
|
<template>
|
||||||
<span class="col-auto text-uppercase text-caption text-bold text-accent">
|
<div class="overview-card column full-width q-pa-md">
|
||||||
{{ $t('timesheet.vacation_available') }}
|
<!-- TOTAL HOURS MODE -->
|
||||||
</span>
|
<template v-if="mode === 'total-hours'">
|
||||||
|
<!-- Header: total + expenses inline -->
|
||||||
<div class="col row">
|
<div class="overview-header row items-center q-mb-sm">
|
||||||
<span class="col text-right">{{ getHoursMinutesStringFromHoursFloat(vacationHours) }}</span>
|
<q-icon name="o_schedule" size="sm" color="accent" class="q-mr-sm" />
|
||||||
<span class="col-auto text-right q-pl-xs">{{ ' (' + Math.floor(vacationHours / 8) }}</span>
|
<span class="text-uppercase text-weight-bold text-body2">{{ $t('timesheet.total_hours') }}</span>
|
||||||
<span class="col-auto q-pl-xs">{{ $t('shared.label.day') }}{{ (Math.floor(vacationHours / 8) !== 1 ?
|
<q-space />
|
||||||
's' : '') + ')' }}</span>
|
<span class="text-h5 text-weight-bolder text-accent">{{ getHoursMinutesStringFromHoursFloat(totalHours) }}</span>
|
||||||
</div>
|
<span v-if="totalExpenses > 0" class="text-caption q-ml-md" style="opacity:0.6">
|
||||||
</div>
|
<q-icon name="o_receipt_long" size="xs" /> {{ totalExpenses }}$
|
||||||
|
</span>
|
||||||
<div
|
</div>
|
||||||
v-if="is_management"
|
|
||||||
class="col row full-width"
|
<!-- Day bars per week -->
|
||||||
>
|
<div class="column" style="gap: 4px;">
|
||||||
<span class="col-auto text-uppercase text-caption text-bold text-accent">
|
<!-- Day abbreviation header (only on first week) -->
|
||||||
{{ $t('timesheet.sick_available') }}
|
<div class="overview-week-row" v-if="weekDayHours.length > 0">
|
||||||
</span>
|
<span class="overview-week-label"></span>
|
||||||
|
<div class="overview-day-bars">
|
||||||
<span class="col text-right">{{ getHoursMinutesStringFromHoursFloat(sickHours) }}</span>
|
<span
|
||||||
</div>
|
v-for="(day, di) in weekDayHours[0]"
|
||||||
|
:key="'h'+di"
|
||||||
<div
|
class="overview-day-header"
|
||||||
v-if="timesheetMode === 'normal'"
|
:class="day.isWeekend ? 'day-hdr-weekend' : ''"
|
||||||
class="col row full-width"
|
>{{ day.dayAbbr }}</span>
|
||||||
>
|
</div>
|
||||||
<span class="col-auto text-uppercase text-caption text-bold text-accent">
|
<span class="overview-week-hours"></span>
|
||||||
{{ $t('timesheet.banked_available') }}
|
</div>
|
||||||
</span>
|
|
||||||
|
<div
|
||||||
<span class="col text-right">{{ getHoursMinutesStringFromHoursFloat(bankedHours) }}</span>
|
v-for="(week, wi) in weekDayHours"
|
||||||
</div>
|
:key="wi"
|
||||||
</div>
|
class="overview-week-row"
|
||||||
</div>
|
>
|
||||||
</template>
|
<span class="overview-week-label">S{{ wi + 1 }}</span>
|
||||||
|
<div class="overview-day-bars">
|
||||||
|
<div
|
||||||
|
v-for="(day, di) in week"
|
||||||
|
:key="di"
|
||||||
|
class="overview-day-bar"
|
||||||
|
:class="[
|
||||||
|
day.isWeekend ? 'bar-weekend' : '',
|
||||||
|
]"
|
||||||
|
:title="day.dayNum + ' — ' + getHoursMinutesStringFromHoursFloat(day.hours)"
|
||||||
|
@click="emit('day-click', day.dateStr)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="overview-day-bar-fill"
|
||||||
|
:style="{ height: Math.min(100, (day.hours / 8) * 100) + '%' }"
|
||||||
|
:class="day.hasShifts ? (day.isApproved ? 'fill-approved' : 'fill-active') : 'fill-empty'"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="overview-week-hours">{{ getHoursMinutesStringFromHoursFloat(weeklyHours?.[wi] ?? 0) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- OFF HOURS MODE (PTO) -->
|
||||||
|
<template v-else>
|
||||||
|
<table class="pto-table">
|
||||||
|
<tr>
|
||||||
|
<td class="pto-icon"><q-icon name="o_flight_takeoff" size="xs" color="deep-orange-5" /></td>
|
||||||
|
<td class="pto-label">{{ $t('timesheet.vacation_available') }}</td>
|
||||||
|
<td class="pto-value">{{ getHoursMinutesStringFromHoursFloat(vacationHours) }}</td>
|
||||||
|
<td class="pto-extra">({{ Math.floor(vacationHours / 8) }}j)</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="is_management">
|
||||||
|
<td class="pto-icon"><q-icon name="o_medical_services" size="xs" color="light-blue-6" /></td>
|
||||||
|
<td class="pto-label">{{ $t('timesheet.sick_available') }}</td>
|
||||||
|
<td class="pto-value">{{ getHoursMinutesStringFromHoursFloat(sickHours) }}</td>
|
||||||
|
<td class="pto-extra"></td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="timesheetMode === 'normal'">
|
||||||
|
<td class="pto-icon"><q-icon name="o_account_balance_wallet" size="xs" color="amber-8" /></td>
|
||||||
|
<td class="pto-label">{{ $t('timesheet.banked_available') }}</td>
|
||||||
|
<td class="pto-value">{{ getHoursMinutesStringFromHoursFloat(bankedHours) }}</td>
|
||||||
|
<td class="pto-extra"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.overview-card {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid rgba(14, 165, 80, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
&:hover { border-color: rgba(14, 165, 80, 0.4); }
|
||||||
|
}
|
||||||
|
|
||||||
|
body.body--light .overview-card {
|
||||||
|
background: #fff;
|
||||||
|
border-color: rgba(14, 165, 80, 0.15);
|
||||||
|
box-shadow: 0 1px 8px rgba(0,0,0,0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-header {
|
||||||
|
padding-bottom: 4px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.body--light .overview-header {
|
||||||
|
border-bottom-color: rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-bar {
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.body--light .overview-bar {
|
||||||
|
background: rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--q-accent);
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-rows {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-row-sep {
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.06);
|
||||||
|
margin-top: 2px;
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.body--light .overview-row-sep {
|
||||||
|
border-top-color: rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-week-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-week-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--q-accent);
|
||||||
|
text-transform: uppercase;
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-week-hours {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
min-width: 30px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-day-bars {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
height: 20px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-day-header {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.55rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #888;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-hdr-weekend {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-day-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.04);
|
||||||
|
border-radius: 3px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-day-bar:hover {
|
||||||
|
transform: scaleY(1.15);
|
||||||
|
box-shadow: 0 0 0 1.5px var(--q-accent);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.body--dark .overview-day-bar {
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-day-bar.bar-weekend {
|
||||||
|
background: rgba(0,0,0,0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.body--dark .overview-day-bar.bar-weekend {
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-day-bar-fill {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: height 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-empty {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-active {
|
||||||
|
background: var(--q-accent);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-approved {
|
||||||
|
background: var(--q-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
color: var(--q-accent);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PTO table */
|
||||||
|
.pto-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pto-table tr {
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.body--dark) .pto-table tr {
|
||||||
|
border-bottom-color: rgba(255,255,255,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pto-table tr:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pto-icon {
|
||||||
|
width: 24px;
|
||||||
|
padding: 6px 4px 6px 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pto-label {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
color: #1e1e2a;
|
||||||
|
padding: 6px 8px 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.body--dark) .pto-label {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pto-value {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-align: right;
|
||||||
|
padding: 6px 0;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pto-extra {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #888;
|
||||||
|
padding: 6px 0 6px 4px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-value {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,285 +1,180 @@
|
||||||
<script
|
<script
|
||||||
setup
|
setup
|
||||||
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 { useQuasar } from 'quasar';
|
||||||
import { date, useQuasar } from 'quasar';
|
import { ref, computed, watch, inject, onMounted } from 'vue';
|
||||||
import { ref, computed, watch, onMounted, inject } from 'vue';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { TimesheetDayDisplay } from 'src/modules/timesheets/models/timesheet.models';
|
||||||
import { Shift } from 'src/modules/timesheets/models/shift.models';
|
|
||||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
// ========== state ========================================
|
||||||
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
const emit = defineEmits<{
|
||||||
|
'onCurrentDayComponentFound': [component: HTMLElement | undefined];
|
||||||
// ========== constants ========================================
|
}>();
|
||||||
|
|
||||||
const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10);
|
const q = useQuasar();
|
||||||
|
const timesheetApi = useTimesheetApi();
|
||||||
// ========== state ========================================
|
const timesheetStore = useTimesheetStore();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const currentDayComponent = ref<HTMLElement[] | null>(null);
|
||||||
'onCurrentDayComponentFound': [component: HTMLElement | undefined];
|
const currentDayComponentWatcher = ref(currentDayComponent);
|
||||||
}>();
|
const employeeEmail = inject<string>('employeeEmail');
|
||||||
|
|
||||||
const q = useQuasar();
|
const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10);
|
||||||
const { extractDate } = date;
|
|
||||||
const { locale } = useI18n();
|
// ========== computed ========================================
|
||||||
const uiStore = useUiStore();
|
|
||||||
const timesheetApi = useTimesheetApi();
|
const timesheetRows = computed(() => {
|
||||||
const timesheetStore = useTimesheetStore();
|
if (timesheetStore.is_loading) return [];
|
||||||
|
const rows: TimesheetDayDisplay[][] = Array.from({ length: 7 }, () => []);
|
||||||
const mobileAnimationDirection = ref('fadeInLeft');
|
timesheetStore.timesheets.flatMap(timesheet => timesheet.days.forEach((day, index) => {
|
||||||
const currentDayComponent = ref<HTMLElement[] | null>(null);
|
rows[index]!.push(new TimesheetDayDisplay(timesheet, day));
|
||||||
const currentDayComponentWatcher = ref(currentDayComponent);
|
}));
|
||||||
const employeeEmail = inject<string>('employeeEmail');
|
|
||||||
|
return rows;
|
||||||
// ========== computed ========================================
|
});
|
||||||
|
|
||||||
const animationStyle = computed(() => q.platform.is.mobile ? mobileAnimationDirection.value : 'fadeInDown');
|
const isShowingWeeklyPresets = computed(() => timesheetStore.timesheets.some(
|
||||||
|
timesheet => timesheet.days.every(day => day.shifts.length < 1)
|
||||||
// ========== methods ========================================
|
) && timesheetStore.has_timesheet_preset);
|
||||||
|
|
||||||
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
|
// Day type helpers
|
||||||
uiStore.focusNextComponent = true;
|
const isWeekend = (rowIndex: number) => rowIndex === 0 || rowIndex === 6; // Sun=0, Sat=6
|
||||||
const newShift = new Shift;
|
const _isPast = (dateStr: string) => dateStr < CURRENT_DATE_STRING;
|
||||||
newShift.date = date;
|
const isMonday = (rowIndex: number) => rowIndex === 1;
|
||||||
newShift.timesheet_id = timesheet_id;
|
|
||||||
day_shifts.push(newShift);
|
// ========== methods ========================================
|
||||||
};
|
|
||||||
|
const onClickApplyWeeklyPreset = async (timesheet_id: number) => {
|
||||||
const deleteUnsavedShift = (timesheet_index: number, day_index: number) => {
|
await timesheetApi.applyPreset(timesheet_id, undefined, undefined, employeeEmail);
|
||||||
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);
|
onMounted(async () => {
|
||||||
day.shifts = shifts_without_deleted_shift;
|
await timesheetStore.getCurrentFederalHolidays();
|
||||||
}
|
});
|
||||||
};
|
|
||||||
|
watch(currentDayComponentWatcher, () => {
|
||||||
const getDayApproval = (day: TimesheetDay) => {
|
if (currentDayComponent.value && q.platform.is.mobile) {
|
||||||
if (day.shifts.length < 1) return false;
|
emit('onCurrentDayComponentFound', currentDayComponent.value[0])
|
||||||
return day.shifts.every(shift => shift.is_approved === true);
|
}
|
||||||
};
|
});
|
||||||
|
</script>
|
||||||
const getMobileDayRef = (iso_date_string: string): string => {
|
|
||||||
return iso_date_string === CURRENT_DATE_STRING ? 'currentDayComponent' : '';
|
<template>
|
||||||
};
|
<div
|
||||||
|
:key="timesheetStore.is_loading ? 0 : 1"
|
||||||
const getHolidayName = (date: string) => {
|
class="fit column no-wrap q-pb-lg"
|
||||||
const holiday = timesheetStore.federal_holidays.find(holiday => holiday.date === date);
|
>
|
||||||
if (!holiday) return;
|
<!-- Week column headers -->
|
||||||
|
<div class="row q-px-xs q-pb-xs" style="gap: 8px;">
|
||||||
if (locale.value === 'fr-FR')
|
<div
|
||||||
return holiday.nameFr;
|
v-for="(timesheet, ti) in timesheetStore.timesheets"
|
||||||
|
:key="ti"
|
||||||
else if (locale.value === 'en-CA')
|
class="col row items-center"
|
||||||
return holiday.nameEn;
|
>
|
||||||
};
|
<span class="week-header">
|
||||||
|
<q-icon name="o_date_range" size="xs" class="q-mr-xs" />
|
||||||
const onClickApplyWeeklyPreset = async (timesheet_id: number) => {
|
{{ $t('timesheet_approvals.table.weekly_hours_' + (ti + 1)) || ('Semaine ' + (ti + 1)) }}
|
||||||
await timesheetApi.applyPreset(timesheet_id, undefined, undefined, employeeEmail);
|
</span>
|
||||||
}
|
|
||||||
|
<q-space />
|
||||||
onMounted(async () => {
|
|
||||||
await timesheetStore.getCurrentFederalHolidays();
|
<q-btn
|
||||||
});
|
v-if="isShowingWeeklyPresets && timesheet.days.every(day => day.shifts.length < 1)"
|
||||||
|
flat
|
||||||
watch(currentDayComponentWatcher, () => {
|
dense
|
||||||
if (currentDayComponent.value && q.platform.is.mobile) {
|
no-caps
|
||||||
emit('onCurrentDayComponentFound', currentDayComponent.value[0])
|
size="sm"
|
||||||
}
|
icon="o_schedule_send"
|
||||||
});
|
:label="$t('timesheet.apply_preset_week')"
|
||||||
</script>
|
color="accent"
|
||||||
|
class="text-weight-medium"
|
||||||
<template>
|
@click="onClickApplyWeeklyPreset(timesheet.timesheet_id)"
|
||||||
<div
|
/>
|
||||||
class="fit"
|
</div>
|
||||||
:class="$q.platform.is.mobile ? 'column no-wrap q-pb-lg' : 'row'"
|
</div>
|
||||||
>
|
|
||||||
<div
|
<!-- Day rows -->
|
||||||
v-for="timesheet, timesheet_index of timesheetStore.timesheets"
|
<transition-group
|
||||||
:key="timesheet.timesheet_id"
|
appear
|
||||||
class="no-wrap"
|
enter-active-class="animated fadeInDown"
|
||||||
:class="$q.platform.is.mobile ? 'col-auto column' : 'col column fit items-center'"
|
>
|
||||||
>
|
<div
|
||||||
<transition
|
v-for="row, rowIndex of timesheetRows"
|
||||||
appear
|
:key="rowIndex"
|
||||||
enter-active-class="animated fadeInDown"
|
class="col-auto"
|
||||||
leave-active-class="animated fadeOutUp"
|
:style="`animation-delay: ${rowIndex / 10}s;`"
|
||||||
>
|
>
|
||||||
<q-btn
|
<!-- Monday separator -->
|
||||||
v-if="!$q.platform.is.mobile && timesheet.days.every(day => day.shifts.length < 1) && timesheetStore.has_timesheet_preset"
|
<div v-if="isMonday(rowIndex)" class="week-separator">
|
||||||
:disable="!timesheet.days.every(day => day.shifts.length < 1)"
|
<div class="week-separator-line"></div>
|
||||||
flat
|
</div>
|
||||||
dense
|
|
||||||
:label="$t('timesheet.apply_preset_week')"
|
<div
|
||||||
class="col-auto text-uppercase text-weight-bold text-accent q-mx-lg q-py-none rounded-5"
|
class="row items-stretch"
|
||||||
@click="onClickApplyWeeklyPreset(timesheet.timesheet_id)"
|
:class="[
|
||||||
>
|
isWeekend(rowIndex) ? 'day-row-weekend' : '',
|
||||||
<q-icon
|
]"
|
||||||
name="las la-calendar-week"
|
>
|
||||||
color="accent"
|
<div
|
||||||
size="md"
|
v-for="day, dayIndex in row"
|
||||||
/>
|
:key="day.day.date"
|
||||||
</q-btn>
|
class="col row items-stretch q-pa-xs relative-position"
|
||||||
</transition>
|
>
|
||||||
|
<ShiftListDay
|
||||||
<transition-group
|
v-model="row[dayIndex]!.day"
|
||||||
appear
|
:week-day-index="rowIndex"
|
||||||
:enter-active-class="`animated ${animationStyle}`"
|
:timesheet-id="day.timesheetId"
|
||||||
>
|
:timesheet-approved="day.isTimesheetApproved"
|
||||||
<div
|
/>
|
||||||
v-for="day, day_index in timesheet.days"
|
</div>
|
||||||
:key="day.date"
|
</div>
|
||||||
:ref="getMobileDayRef(day.date)"
|
</div>
|
||||||
class="col-auto row q-pa-sm full-width relative-position"
|
</transition-group>
|
||||||
:style="`animation-delay: ${day_index / 15}s;`"
|
</div>
|
||||||
>
|
</template>
|
||||||
<!-- optional label indicating which holiday if today is a holiday -->
|
|
||||||
<span
|
<style scoped lang="scss">
|
||||||
v-if="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date)"
|
.week-header {
|
||||||
class="absolute-top-left text-uppercase text-weight-bolder text-purple-5"
|
font-size: 0.7rem;
|
||||||
style="transform: translate(25px, -7px);"
|
font-weight: 700;
|
||||||
>
|
text-transform: uppercase;
|
||||||
{{ getHolidayName(day.date) }}
|
letter-spacing: 0.04em;
|
||||||
</span>
|
color: var(--q-accent);
|
||||||
|
display: flex;
|
||||||
<!-- mobile version in portrait mode -->
|
align-items: center;
|
||||||
<div
|
padding: 4px 8px;
|
||||||
v-if="$q.platform.is.mobile && ($q.screen.width < $q.screen.height)"
|
opacity: 0.7;
|
||||||
class="col-auto full-width q-px-md q-py-sm"
|
}
|
||||||
>
|
|
||||||
<q-card
|
.week-separator {
|
||||||
class="shadow-12"
|
padding: 0 8px;
|
||||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent rounded-10' : 'bg-dark mobile-rounded-10'"
|
margin: 4px 0 2px;
|
||||||
>
|
}
|
||||||
<q-card-section
|
|
||||||
class="text-weight-bolder text-uppercase text-h6 q-py-sm text-center relative-position"
|
.week-separator-line {
|
||||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent text-white' : 'bg-primary text-white'"
|
height: 1px;
|
||||||
style="line-height: 1em;"
|
background: linear-gradient(90deg, var(--q-accent) 0%, transparent 100%);
|
||||||
>
|
opacity: 0.25;
|
||||||
<span> {{ $d(extractDate(day.date, 'YYYY-MM-DD'), {
|
}
|
||||||
weekday: 'long', day: 'numeric', month:
|
|
||||||
'long'
|
.day-row-weekend :deep(.shift-day-row) {
|
||||||
}) }}</span>
|
border-left: 3px solid #c8ccd3;
|
||||||
|
}
|
||||||
<q-icon
|
|
||||||
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
.day-row-weekend :deep(.bg-dark) {
|
||||||
name="verified"
|
background-color: #eceef2 !important;
|
||||||
size="3em"
|
}
|
||||||
color="white"
|
|
||||||
class="absolute-top-left z-top"
|
:global(body.body--dark) .day-row-weekend :deep(.shift-day-row) {
|
||||||
style="top: -0.2em; left: 0px;"
|
border-left-color: #3a3a4a;
|
||||||
/>
|
}
|
||||||
</q-card-section>
|
|
||||||
|
:global(body.body--dark) .day-row-weekend :deep(.bg-dark) {
|
||||||
<q-card-section
|
background-color: #22222e !important;
|
||||||
v-if="day.shifts.filter(shift => shift.id !== 0).length > 0"
|
}
|
||||||
class="q-pa-none transparent"
|
</style>
|
||||||
>
|
|
||||||
<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>
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,10 @@
|
||||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { RouteNames } from 'src/router/router-constants';
|
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 ====================
|
// ================= state ====================
|
||||||
|
|
||||||
|
|
@ -39,13 +42,22 @@ import { RouteNames } from 'src/router/router-constants';
|
||||||
// ================== computed ====================
|
// ================== computed ====================
|
||||||
|
|
||||||
const hasShiftErrors = computed(() => timesheetStore.all_current_shifts.filter(shift => shift.has_error === true).length > 0);
|
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 isTimesheetsApproved = computed(() => timesheetStore.timesheets.every(timesheet => timesheet.is_approved));
|
||||||
|
|
||||||
const totalHours = computed(() => timesheetStore.timesheets.reduce((sum, timesheet) =>
|
const weeklyHours = computed(() => timesheetStore.timesheets.map(timesheet =>
|
||||||
sum += timesheet.weekly_hours.regular
|
timesheet.days.reduce((daySum: number, day: TimesheetDay) => {
|
||||||
+ timesheet.weekly_hours.evening
|
return daySum + day.shifts.reduce((shiftSum: number, shift: Shift) => {
|
||||||
+ timesheet.weekly_hours.emergency
|
if (!shift.end_time || !shift.start_time || shift.type === 'SICK') return shiftSum;
|
||||||
+ timesheet.weekly_hours.overtime,
|
|
||||||
|
const time = getHoursMinutesBetweenTwoHHmm(shift.start_time, shift.end_time);
|
||||||
|
return shiftSum + time.hours + Number(time.minutes / 60);
|
||||||
|
}, 0)
|
||||||
|
}, 0)
|
||||||
|
));
|
||||||
|
|
||||||
|
const totalHours = computed(() => weeklyHours.value.reduce((sum, week) =>
|
||||||
|
sum += week,
|
||||||
0 //initial value
|
0 //initial value
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
@ -77,7 +89,7 @@ import { RouteNames } from 'src/router/router-constants';
|
||||||
timesheetStore.isShowingUnsavedWarning = false;
|
timesheetStore.isShowingUnsavedWarning = false;
|
||||||
timesheetStore.timesheets = [];
|
timesheetStore.timesheets = [];
|
||||||
timesheetStore.initial_timesheets = [];
|
timesheetStore.initial_timesheets = [];
|
||||||
await router.push({ name: timesheetStore.nextPageNameAfterUnsaveWarning ?? RouteNames.DASHBOARD});
|
await router.push({ name: timesheetStore.nextPageNameAfterUnsaveWarning ?? RouteNames.DASHBOARD });
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickSaveBeforeLeaving = async () => {
|
const onClickSaveBeforeLeaving = async () => {
|
||||||
|
|
@ -88,6 +100,14 @@ import { RouteNames } from 'src/router/router-constants';
|
||||||
await onClickLeave();
|
await onClickLeave();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scrollToDay = (dateStr: string) => {
|
||||||
|
const card = document.querySelector(`[data-date="${dateStr}"]`);
|
||||||
|
if (!card) return;
|
||||||
|
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
card.classList.add('day-highlight-flash');
|
||||||
|
setTimeout(() => card.classList.remove('day-highlight-flash'), 1500);
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (mode === 'normal')
|
if (mode === 'normal')
|
||||||
await timesheetApi.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
await timesheetApi.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
|
|
@ -96,8 +116,8 @@ import { RouteNames } from 'src/router/router-constants';
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="column items-center full-height"
|
class="column items-center"
|
||||||
:class="mode === 'normal' ? 'relative-position' : ' no-wrap'"
|
:class="mode === 'normal' ? '' : 'no-wrap'"
|
||||||
>
|
>
|
||||||
<LoadingOverlay v-model="timesheetStore.is_loading" />
|
<LoadingOverlay v-model="timesheetStore.is_loading" />
|
||||||
|
|
||||||
|
|
@ -115,27 +135,45 @@ import { RouteNames } from 'src/router/router-constants';
|
||||||
|
|
||||||
|
|
||||||
<!-- weekly overview -->
|
<!-- weekly overview -->
|
||||||
<div class="col-auto row q-px-lg full-width">
|
<div
|
||||||
<!-- supervisor weekly overview -->
|
v-if="!$q.platform.is.mobile"
|
||||||
<div
|
class="col-auto row q-px-xl q-pt-sm full-width items-start"
|
||||||
v-if="!$q.platform.is.mobile"
|
style="gap: 16px;"
|
||||||
class="col-xs-6 col-md-4 col-xl-3 q-pa-md"
|
>
|
||||||
>
|
<!-- Left: hours + expenses -->
|
||||||
|
<div class="col column" style="gap: 8px;">
|
||||||
<ShiftListWeeklyOverview
|
<ShiftListWeeklyOverview
|
||||||
mode="total-hours"
|
mode="total-hours"
|
||||||
:timesheet-mode="mode"
|
:timesheet-mode="mode"
|
||||||
|
:weekly-hours="weeklyHours"
|
||||||
:total-hours="totalHours"
|
:total-hours="totalHours"
|
||||||
:total-expenses="totalExpenses"
|
:total-expenses="totalExpenses"
|
||||||
|
@day-click="scrollToDay"
|
||||||
/>
|
/>
|
||||||
|
<!-- Expenses row under hours card — aligned right -->
|
||||||
|
<div v-if="mode === 'normal'" class="row items-center justify-end" style="gap: 10px;">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
no-caps
|
||||||
|
icon="o_receipt_long"
|
||||||
|
:label="$t('timesheet.expense.open_btn')"
|
||||||
|
color="accent"
|
||||||
|
class="text-weight-bold q-px-md"
|
||||||
|
style="border: 1px solid currentColor; border-radius: 8px;"
|
||||||
|
@click="expenseStore.open"
|
||||||
|
/>
|
||||||
|
<div class="expense-total-badge">
|
||||||
|
<q-icon name="o_payments" size="xs" class="q-mr-xs" />
|
||||||
|
{{ totalExpenses.toFixed(2) }}$
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-space v-if="!$q.platform.is.mobile" />
|
<q-space />
|
||||||
|
|
||||||
<!-- employee weekly overview -->
|
<!-- Right: PTO -->
|
||||||
<div
|
<div class="col">
|
||||||
v-if="!$q.platform.is.mobile"
|
|
||||||
class="col-xs-6 col-md-4 col-xl-3 q-pa-md"
|
|
||||||
>
|
|
||||||
<ShiftListWeeklyOverview
|
<ShiftListWeeklyOverview
|
||||||
mode="off-hours"
|
mode="off-hours"
|
||||||
:timesheet-mode="mode"
|
:timesheet-mode="mode"
|
||||||
|
|
@ -143,13 +181,13 @@ import { RouteNames } from 'src/router/router-constants';
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- top menu -->
|
<!-- toolbar: nav + save -->
|
||||||
<div
|
<div
|
||||||
v-if="mode === 'normal'"
|
v-if="mode === 'normal'"
|
||||||
class="col-auto row items-center full-width"
|
class="col-auto row items-center full-width q-px-xl q-pt-md q-pb-sm"
|
||||||
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'justify-between q-px-md' : 'q-pb-sm q-px-xl'"
|
style="gap: 8px;"
|
||||||
>
|
>
|
||||||
<!-- navigation btn -->
|
<!-- nav group -->
|
||||||
<PayPeriodNavigator
|
<PayPeriodNavigator
|
||||||
class="col-auto"
|
class="col-auto"
|
||||||
@date-selected="timesheetApi.getTimesheetsByDate"
|
@date-selected="timesheetApi.getTimesheetsByDate"
|
||||||
|
|
@ -157,46 +195,25 @@ import { RouteNames } from 'src/router/router-constants';
|
||||||
@pressed-next-button="timesheetApi.getTimesheetsByCurrentPayPeriod"
|
@pressed-next-button="timesheetApi.getTimesheetsByCurrentPayPeriod"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- mobile expenses button -->
|
<q-space />
|
||||||
<div
|
|
||||||
v-if="$q.screen.width < $q.screen.height && mode === 'normal'"
|
<!-- save only -->
|
||||||
class="col q-pl-lg"
|
<div class="row items-center" style="gap: 8px;">
|
||||||
>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
v-if="!isTimesheetsApproved"
|
||||||
rounded
|
flat
|
||||||
color="accent"
|
dense
|
||||||
icon="receipt_long"
|
no-caps
|
||||||
class="full-width"
|
:disable="timesheetStore.is_loading || hasShiftErrors || !timesheetStore.canSaveTimesheets"
|
||||||
@click="expenseStore.open"
|
icon="o_cloud_upload"
|
||||||
|
:label="$q.screen.width > 900 ? $t('shared.label.save') : ''"
|
||||||
|
:color="timesheetStore.is_loading || hasShiftErrors || !timesheetStore.canSaveTimesheets ? 'grey-5' : 'white'"
|
||||||
|
:class="timesheetStore.is_loading || hasShiftErrors || !timesheetStore.canSaveTimesheets ? 'bg-grey-4' : 'bg-accent'"
|
||||||
|
class="text-weight-bold q-px-md"
|
||||||
|
style="border-radius: 8px;"
|
||||||
|
@click="onClickSaveTimesheets"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-space v-if="$q.screen.width > $q.screen.height" />
|
|
||||||
|
|
||||||
<!-- desktop expenses button -->
|
|
||||||
<q-btn
|
|
||||||
v-if="mode === 'normal' && $q.screen.width > $q.screen.height"
|
|
||||||
push
|
|
||||||
rounded
|
|
||||||
color="accent"
|
|
||||||
icon="receipt_long"
|
|
||||||
:label="$t('timesheet.expense.open_btn')"
|
|
||||||
@click="expenseStore.open"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- desktop save timesheet changes button -->
|
|
||||||
<q-btn
|
|
||||||
v-if="!isTimesheetsApproved && $q.screen.width > $q.screen.height"
|
|
||||||
push
|
|
||||||
rounded
|
|
||||||
:disable="timesheetStore.is_loading || hasShiftErrors || !timesheetStore.canSaveTimesheets"
|
|
||||||
:color="timesheetStore.is_loading || hasShiftErrors || !timesheetStore.canSaveTimesheets ? 'grey-5' : 'accent'"
|
|
||||||
icon="upload"
|
|
||||||
:label="$t('shared.label.save')"
|
|
||||||
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'full-width' : 'q-ml-md'"
|
|
||||||
@click="onClickSaveTimesheets"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- error message widget for potential backend-provided errors -->
|
<!-- error message widget for potential backend-provided errors -->
|
||||||
|
|
@ -209,7 +226,6 @@ import { RouteNames } from 'src/router/router-constants';
|
||||||
<ShiftListScrollable
|
<ShiftListScrollable
|
||||||
v-if="mode === 'normal'"
|
v-if="mode === 'normal'"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:class="mode === 'normal' ? 'col' : 'col-auto'"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- full shift list for timesheet approval details dialog -->
|
<!-- full shift list for timesheet approval details dialog -->
|
||||||
|
|
@ -226,7 +242,7 @@ import { RouteNames } from 'src/router/router-constants';
|
||||||
style="min-height: 20vh;"
|
style="min-height: 20vh;"
|
||||||
>
|
>
|
||||||
<span class="text-uppercase text-weight-bolder text-center">{{ $t('shared.error.no_data_found')
|
<span class="text-uppercase text-weight-bolder text-center">{{ $t('shared.error.no_data_found')
|
||||||
}}</span>
|
}}</span>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="las la-calendar"
|
name="las la-calendar"
|
||||||
color="accent"
|
color="accent"
|
||||||
|
|
@ -266,4 +282,29 @@ import { RouteNames } from 'src/router/router-constants';
|
||||||
@click-save-yes="onClickSaveBeforeLeaving"
|
@click-save-yes="onClickSaveBeforeLeaving"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:global(.day-highlight-flash) {
|
||||||
|
animation: dayFlash 1.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dayFlash {
|
||||||
|
0%, 100% { box-shadow: none; }
|
||||||
|
20%, 80% { box-shadow: 0 0 0 3px var(--q-accent), 0 0 20px rgba(14, 165, 80, 0.3); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.expense-total-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #1e1e2a;
|
||||||
|
background: rgba(14, 165, 80, 0.1);
|
||||||
|
border: 1.5px solid rgba(14, 165, 80, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -23,7 +23,6 @@ export const useExpensesApi = () => {
|
||||||
const success = await expenses_store.upsertExpense(expense, employee_email);
|
const success = await expenses_store.upsertExpense(expense, employee_email);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
|
||||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
||||||
|
|
||||||
return true;
|
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 EXPENSE_TYPE: ExpenseType[] = ['PER_DIEM', 'MILEAGE', 'EXPENSES', 'ON_CALL',];
|
||||||
export const TYPES_WITH_MILEAGE_ONLY: Readonly<ExpenseType[]> = ['MILEAGE'];
|
export const TYPES_WITH_MILEAGE_ONLY: Readonly<ExpenseType[]> = ['MILEAGE'];
|
||||||
|
|
@ -9,8 +9,8 @@ export class Expense {
|
||||||
timesheet_id: number;
|
timesheet_id: number;
|
||||||
date: string; //YYYY-MM-DD
|
date: string; //YYYY-MM-DD
|
||||||
type: ExpenseType;
|
type: ExpenseType;
|
||||||
amount: number;
|
amount?: number | null;
|
||||||
mileage?: number;
|
mileage?: number | null;
|
||||||
attachment_name?: string;
|
attachment_name?: string;
|
||||||
attachment_key?: string;
|
attachment_key?: string;
|
||||||
comment: string;
|
comment: string;
|
||||||
|
|
@ -21,7 +21,7 @@ export class Expense {
|
||||||
this.id = -1;
|
this.id = -1;
|
||||||
this.timesheet_id = -1;
|
this.timesheet_id = -1;
|
||||||
this.date = date;
|
this.date = date;
|
||||||
this.type = 'EXPENSES';
|
this.type = undefined;
|
||||||
this.amount = 0;
|
this.amount = 0;
|
||||||
this.comment = '';
|
this.comment = '';
|
||||||
this.is_approved = false;
|
this.is_approved = false;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 isPresent = (val: unknown) => val !== undefined && val !== null && val !== '';
|
||||||
const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
|
const typeRequired = (val: unknown) => isPresent(val);
|
||||||
const amountRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.amount_required_for_type');
|
const amountRequired = (val: number | null | undefined) => isPresent(val) && !!val && val > 0;
|
||||||
const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.mileage_required_for_type');
|
const mileageRequired = (val: number | null | undefined) => isPresent(val) && !!val && val > 0;
|
||||||
const commentRequired = (val: unknown) => (typeof val === 'string' ? val.trim().length > 0 : false) || t('timesheet.expense.hints.comment_required');
|
const commentRequired = (val: string | null | undefined) => typeof val === 'string' ? val.trim().length > 0 : false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
typeRequired,
|
typeRequired,
|
||||||
|
|
|
||||||
|
|
@ -58,25 +58,25 @@ export const getTimeStringFromMinutes = (minutes: number): string => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SHIFT_OPTIONS: ShiftOption[] = [
|
export const SHIFT_OPTIONS: ShiftOption[] = [
|
||||||
{ label: 'timesheet.shift.types.REGULAR', value: 'REGULAR', icon: 'wb_sunny', icon_color: 'accent' },
|
{ label: 'timesheet.shift.types.REGULAR', value: 'REGULAR', icon: 'o_light_mode', icon_color: 'accent' },
|
||||||
{ label: 'timesheet.shift.types.EVENING', value: 'EVENING', icon: 'bedtime', icon_color: 'orange-5' },
|
{ label: 'timesheet.shift.types.EVENING', value: 'EVENING', icon: 'o_dark_mode', icon_color: 'orange-5' },
|
||||||
{ label: 'timesheet.shift.types.EMERGENCY', value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-5' },
|
{ label: 'timesheet.shift.types.EMERGENCY', value: 'EMERGENCY', icon: 'o_warning_amber', icon_color: 'red-5' },
|
||||||
{ label: 'timesheet.shift.types.VACATION', value: 'VACATION', icon: 'beach_access', icon_color: 'deep-orange-5' },
|
{ label: 'timesheet.shift.types.VACATION', value: 'VACATION', icon: 'o_flight_takeoff', icon_color: 'deep-orange-5' },
|
||||||
{ label: 'timesheet.shift.types.HOLIDAY', value: 'HOLIDAY', icon: 'event', icon_color: 'purple-5' },
|
{ label: 'timesheet.shift.types.HOLIDAY', value: 'HOLIDAY', icon: 'o_celebration', icon_color: 'purple-5' },
|
||||||
{ label: 'timesheet.shift.types.SICK', value: 'SICK', icon: 'medication_liquid', icon_color: 'light-blue-6' },
|
{ label: 'timesheet.shift.types.SICK', value: 'SICK', icon: 'o_medical_services', icon_color: 'light-blue-6' },
|
||||||
// { label: 'timesheet.shift.types.BANKING', value: 'BANKING', icon: 'savings', icon_color: 'pink-3' },
|
// { label: 'timesheet.shift.types.BANKING', value: 'BANKING', icon: 'o_savings', icon_color: 'pink-3' },
|
||||||
{ label: 'timesheet.shift.types.WITHDRAW_BANKED', value: 'WITHDRAW_BANKED', icon: 'attach_money', icon_color: 'yellow-4' },
|
{ label: 'timesheet.shift.types.WITHDRAW_BANKED', value: 'WITHDRAW_BANKED', icon: 'o_account_balance_wallet', icon_color: 'yellow-8' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getShiftOptions = (disablePTO: boolean, isNotUnique: boolean): ShiftOption[] => {
|
export const getShiftOptions = (disablePTO: boolean, isNotUnique: boolean): ShiftOption[] => {
|
||||||
return [
|
return [
|
||||||
{ label: 'timesheet.shift.types.REGULAR', value: 'REGULAR', icon: 'wb_sunny', icon_color: 'accent' },
|
{ label: 'timesheet.shift.types.REGULAR', value: 'REGULAR', icon: 'o_light_mode', icon_color: 'accent' },
|
||||||
{ label: 'timesheet.shift.types.EVENING', value: 'EVENING', icon: 'bedtime', icon_color: 'orange-5' },
|
{ label: 'timesheet.shift.types.EVENING', value: 'EVENING', icon: 'o_dark_mode', icon_color: 'orange-5' },
|
||||||
{ label: 'timesheet.shift.types.EMERGENCY', value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-5' },
|
{ label: 'timesheet.shift.types.EMERGENCY', value: 'EMERGENCY', icon: 'o_warning_amber', icon_color: 'red-5' },
|
||||||
{ label: 'timesheet.shift.types.VACATION', value: 'VACATION', icon: 'beach_access', icon_color: 'deep-orange-5', disable: disablePTO },
|
{ label: 'timesheet.shift.types.VACATION', value: 'VACATION', icon: 'o_flight_takeoff', icon_color: 'deep-orange-5', disable: disablePTO },
|
||||||
{ label: 'timesheet.shift.types.HOLIDAY', value: 'HOLIDAY', icon: 'event', icon_color: 'purple-5', disable: isNotUnique || disablePTO },
|
{ label: 'timesheet.shift.types.HOLIDAY', value: 'HOLIDAY', icon: 'o_celebration', icon_color: 'purple-5', disable: isNotUnique || disablePTO },
|
||||||
{ label: 'timesheet.shift.types.SICK', value: 'SICK', icon: 'medication_liquid', icon_color: 'light-blue-6', disable: disablePTO },
|
{ label: 'timesheet.shift.types.SICK', value: 'SICK', icon: 'o_medical_services', icon_color: 'light-blue-6', disable: disablePTO },
|
||||||
// { label: 'timesheet.shift.types.BANKING', value: 'BANKING', icon: 'savings', icon_color: 'pink-3' },
|
// { label: 'timesheet.shift.types.BANKING', value: 'BANKING', icon: 'o_savings', icon_color: 'pink-3' },
|
||||||
// { label: 'timesheet.shift.types.WITHDRAW_BANKED', value: 'WITHDRAW_BANKED', icon: 'attach_money', icon_color: 'yellow-4' },
|
// { label: 'timesheet.shift.types.WITHDRAW_BANKED', value: 'WITHDRAW_BANKED', icon: 'o_account_balance_wallet', icon_color: 'yellow-8' },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -13,67 +13,73 @@
|
||||||
:class="$q.platform.is.mobile ? 'column' : 'row'"
|
:class="$q.platform.is.mobile ? 'column' : 'row'"
|
||||||
>
|
>
|
||||||
<!-- left column -->
|
<!-- left column -->
|
||||||
<div class="column col flex-center q-pa-md">
|
<div class="column col items-center q-pa-md">
|
||||||
|
<div class="col q-py-md">
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- center column -->
|
|
||||||
<div class="column col-xs-12 col-md-8 col-xl-6 items-center q-pa-md">
|
|
||||||
<div class="col-auto full-width q-py-md">
|
|
||||||
<MainCarousel />
|
<MainCarousel />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="col-auto text-uppercase text-weight-bold self-start q-pt-md">{{ $t('dashboard.useful_links') }}</span>
|
|
||||||
|
|
||||||
<div class="col row full-width justify-evenly items-start q-py-md">
|
|
||||||
<div class="col-3 q-pa-sm">
|
|
||||||
<ShortcutCard
|
|
||||||
image-source="src/assets/google_thumbnail.png"
|
|
||||||
title="Google Workspace"
|
|
||||||
route="https://mail.google.com/mail/u/0/#inbox"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-3 q-pa-sm">
|
|
||||||
<ShortcutCard
|
|
||||||
image-source="src/assets/facturation_thumbnail.png"
|
|
||||||
title="Facturation"
|
|
||||||
route="https://facturation.targo.ca/facturation/accueil.php?menu=ticket_open"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-3 q-pa-sm">
|
|
||||||
<ShortcutCard
|
|
||||||
image-source="src/assets/map_targo_banner.png"
|
|
||||||
title="Map Targo"
|
|
||||||
route="https://map.targointernet.com/infrastructure/map.php"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-3 q-pa-sm">
|
|
||||||
<ShortcutCard
|
|
||||||
image-source="src/assets/info-pannes.png"
|
|
||||||
title="Info Pannes"
|
|
||||||
route="https://infopannes.solutions.hydroquebec.com/info-pannes/pannes/pannes-en-cours"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- right column -->
|
<!-- right column -->
|
||||||
<div class="column col items-center">
|
<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 q-py-sm">
|
||||||
|
<ShortcutCard
|
||||||
|
icon-image-source="o_mail"
|
||||||
|
name="Messagerie"
|
||||||
|
route="https://mail.google.com/mail/u/0/#inbox"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto full-width q-py-sm">
|
||||||
|
<ShortcutCard
|
||||||
|
icon-image-source="o_receipt_long"
|
||||||
|
name="Facturation"
|
||||||
|
route="https://facturation.targo.ca/facturation/accueil.php?menu=ticket_open"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto full-width q-py-sm">
|
||||||
|
<ShortcutCard
|
||||||
|
icon-image-source="o_location_on"
|
||||||
|
name="Map Targo"
|
||||||
|
route="https://map.targointernet.com/infrastructure/map"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto full-width q-py-sm">
|
||||||
|
<ShortcutCard
|
||||||
|
icon-image-source="o_flash_on"
|
||||||
|
name="Info Pannes"
|
||||||
|
route="https://infopannes.solutions.hydroquebec.com/info-pannes/pannes/pannes-en-cours"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto full-width q-py-sm">
|
||||||
|
<ShortcutCard
|
||||||
|
icon-image-source="o_language"
|
||||||
|
name="Intranet"
|
||||||
|
route="https://intranet.facturation.targo.ca/"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col"></div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="col-auto row full-width within-iframe"
|
class="col-auto row full-width within-iframe"
|
||||||
:class="$q.platform.is.mobile ? 'justify-center' : 'justify-end q-pl-md'"
|
:class="$q.platform.is.mobile ? 'justify-center q-pt-lg' : 'justify-end'"
|
||||||
style="height: 50vh;"
|
|
||||||
>
|
>
|
||||||
<iframe
|
<iframe
|
||||||
title="Environment Canada Weather"
|
title="Environment Canada Weather"
|
||||||
height="400px"
|
height="200px"
|
||||||
src="https://weather.gc.ca/wxlink/wxlink.html?coords=45.159%2C-73.676&lang=f"
|
src="https://weather.gc.ca/wxlink/wxlink.html?coords=45.159%2C-73.676&lang=f"
|
||||||
allowtransparency="true"
|
allowtransparency="true"
|
||||||
style="border: 0;"
|
style="border: 0;"
|
||||||
class="col-auto"
|
|
||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,26 +4,25 @@
|
||||||
>
|
>
|
||||||
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||||
|
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-page class="column bg-secondary items-center">
|
<q-page class="column bg-secondary items-center" padding>
|
||||||
<div
|
<div
|
||||||
class="col column fit"
|
class="column full-width"
|
||||||
:style="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? '' : 'width: 90vw'"
|
:style="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? '' : 'max-width: 90vw'"
|
||||||
>
|
>
|
||||||
<PageHeaderTemplate
|
<PageHeaderTemplate
|
||||||
:title="'timesheet.page_header'"
|
:title="'timesheet.page_header'"
|
||||||
:start-date="timesheet_store.pay_period?.period_start ?? ''"
|
:start-date="timesheet_store.pay_period?.period_start ?? ''"
|
||||||
:end-date="timesheet_store.pay_period?.period_end ?? ''"
|
:end-date="timesheet_store.pay_period?.period_end ?? ''"
|
||||||
class="col-auto"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TimesheetWrapper class="col" />
|
<TimesheetWrapper />
|
||||||
</div>
|
</div>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,18 @@ export const useAuthStore = defineStore('auth', () => {
|
||||||
//TODO: manage customer login process
|
//TODO: manage customer login process
|
||||||
};
|
};
|
||||||
|
|
||||||
const oidcLogin = () => {
|
const oidcLogin = async () => {
|
||||||
window.addEventListener('message', (event) => {
|
// DEV: try direct profile fetch first (works when backend has DEV_BYPASS_AUTH)
|
||||||
void handleAuthMessage(event);
|
try {
|
||||||
});
|
const result = await getProfile();
|
||||||
|
if (result.status === 200 && user.value) {
|
||||||
|
void router.push('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) { console.warn('DEV bypass login failed, falling back to OIDC:', e); }
|
||||||
|
|
||||||
|
// Normal OIDC popup flow
|
||||||
|
window.addEventListener('message', (event) => void handleAuthMessage(event));
|
||||||
|
|
||||||
const oidc_popup = window.open(`${import.meta.env.VITE_TARGO_BACKEND_URL}auth/v1/login`, 'authPopup', 'width=600,height=800');
|
const oidc_popup = window.open(`${import.meta.env.VITE_TARGO_BACKEND_URL}auth/v1/login`, 'authPopup', 'width=600,height=800');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
if (week_day_index && date)
|
if (week_day_index !== undefined && date !== undefined)
|
||||||
response = await timesheetService.applyPresetToDay(timesheet_id, week_day_index, date, employeeEmail);
|
response = await timesheetService.applyPresetToDay(timesheet_id, week_day_index, date, employeeEmail);
|
||||||
else
|
else
|
||||||
response = await timesheetService.applyPresetToWeek(timesheet_id, employeeEmail);
|
response = await timesheetService.applyPresetToWeek(timesheet_id, employeeEmail);
|
||||||
|
|
|
||||||