Merge branch 'main' of https://git.targo.ca/Targo/targo_frontend into release/nicolas/v1.2
This commit is contained in:
commit
c6187305d9
1
.env.production
Normal file
1
.env.production
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
VITE_TARGO_BACKEND_URL=PREFIX_BACKEND_URL
|
||||||
1
.gitea/workflows/README.md
Normal file
1
.gitea/workflows/README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Workflows to be compliant with CI/CD pipelines
|
||||||
150
.gitea/workflows/node-ci.yaml
Normal file
150
.gitea/workflows/node-ci.yaml
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
name: Node-CI
|
||||||
|
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
- 'docs/**'
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Gitea Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Notify Google Chat
|
||||||
|
if: ${{ failure() }} # Use always to ensure that the notification is also send on failure of former steps
|
||||||
|
uses: SimonScholz/google-chat-action@3b3519e5102dba8aa5046fd711c4b553586409bb # v1.1.0
|
||||||
|
with:
|
||||||
|
webhookUrl: '${{ secrets.GOOGLE_CHAT_WEBHOOK }}'
|
||||||
|
jobStatus: ${{ job.status }}
|
||||||
|
title: Lint failed
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Gitea Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Notify Google Chat
|
||||||
|
if: ${{ failure() }} # Use always to ensure that the notification is also send on failure of former steps
|
||||||
|
uses: SimonScholz/google-chat-action@3b3519e5102dba8aa5046fd711c4b553586409bb # v1.1.0
|
||||||
|
with:
|
||||||
|
webhookUrl: '${{ secrets.GOOGLE_CHAT_WEBHOOK }}'
|
||||||
|
jobStatus: ${{ job.status }}
|
||||||
|
title: Test failed
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Gitea Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: dist
|
||||||
|
path: dist
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: source
|
||||||
|
path: src
|
||||||
|
retention-days: 1
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: Dockerfile
|
||||||
|
path: Dockerfile
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
- name: Notify Google Chat
|
||||||
|
if: ${{ failure() }} # Use always to ensure that the notification is also send on failure of former steps
|
||||||
|
uses: SimonScholz/google-chat-action@3b3519e5102dba8aa5046fd711c4b553586409bb # v1.1.0
|
||||||
|
with:
|
||||||
|
webhookUrl: '${{ secrets.GOOGLE_CHAT_WEBHOOK }}'
|
||||||
|
jobStatus: ${{ job.status }}
|
||||||
|
title: Build failed
|
||||||
|
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: [lint, test, build]
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
#if: github.ref == 'refs/heads/main'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- name: Getting Dockerfile
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: Dockerfile
|
||||||
|
- name: Downloading dist folder
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: dist
|
||||||
|
path: dist
|
||||||
|
- name: Downloading src folder
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: source
|
||||||
|
path: source
|
||||||
|
- name: Set version number
|
||||||
|
run: echo "VERSION_NUMBER=$(date +'%y%m%d.%H%M%S')" >> $GITHUB_ENV
|
||||||
|
- name: Building docker image
|
||||||
|
run: docker build -t git.targo.ca/targo/targo-frontend-staging:2.${{ env.VERSION_NUMBER }} .
|
||||||
|
- name: Add tag Latest
|
||||||
|
run: docker tag git.targo.ca/targo/targo-frontend-staging:2.${{ env.VERSION_NUMBER }} git.targo.ca/targo/targo-frontend-staging:latest
|
||||||
|
- name: Push images to server
|
||||||
|
run: |
|
||||||
|
docker login -u ${{ secrets.CI_USER }} -p ${{ secrets.CI_PASSWORD }} git.targo.ca
|
||||||
|
docker push git.targo.ca/targo/targo-frontend-staging:2.${{ env.VERSION_NUMBER }}
|
||||||
|
docker push git.targo.ca/targo/targo-frontend-staging:latest
|
||||||
|
curl --location 'https://n8napi.targo.ca/webhook/portainer' --header 'Authorization: Basic ${{ secrets.API_SECRET}}' --form 'stack="new_targo_app_staging"'
|
||||||
27
Dockerfile
27
Dockerfile
|
|
@ -1,20 +1,15 @@
|
||||||
# Step 1 - Building the app
|
|
||||||
FROM node:22 AS build
|
|
||||||
WORKDIR /app
|
|
||||||
COPY package*.json ./
|
|
||||||
# Set environment variables
|
|
||||||
ARG BACKEND_URL
|
|
||||||
ENV VITE_TARGO_BACKEND_URL=$BACKEND_URL
|
|
||||||
# Install dependencies
|
|
||||||
RUN npm install -g @quasar/cli
|
|
||||||
COPY . .
|
|
||||||
RUN npm install
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Step 2 - Move Applicatin to Nginx
|
|
||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
COPY --from=build /app/dist/spa /usr/share/nginx/html
|
|
||||||
|
#Copy files
|
||||||
RUN mkdir /usr/share/nginx/html/src
|
RUN mkdir /usr/share/nginx/html/src
|
||||||
COPY --from=build /app/src /usr/share/nginx/html/src
|
COPY dist/spa /usr/share/nginx/html
|
||||||
|
COPY source /usr/share/nginx/html/src
|
||||||
|
|
||||||
|
#Add script to replace VITE env vars
|
||||||
|
RUN curl -o /docker-entrypoint.d/env.sh -O https://raw.githubusercontent.com/Dutchskull/Vite-Dynamic-Environment-Variables/refs/heads/main/app/env.sh
|
||||||
|
RUN dos2unix /docker-entrypoint.d/env.sh
|
||||||
|
RUN chmod +x /docker-entrypoint.d/env.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
35
env.sh
Normal file
35
env.sh
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
# ================================================================================
|
||||||
|
# File: env.sh
|
||||||
|
# Description: Replaces environment variables in asset files.
|
||||||
|
# Usage: Run this script in your terminal, ensuring APP_PREFIX and ASSET_DIRS are set.
|
||||||
|
# ================================================================================
|
||||||
|
|
||||||
|
# Set the exit flag to exit immediately if any command fails
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Check if APP_PREFIX is set
|
||||||
|
: "${APP_PREFIX:?APP_PREFIX must be set (e.g. APP_PREFIX='APP_PREFIX_')}"
|
||||||
|
|
||||||
|
# Check if ASSET_DIRS is set
|
||||||
|
: "${ASSET_DIR:?Must set ASSET_DIR to one path}"
|
||||||
|
|
||||||
|
# Check if the directory exists
|
||||||
|
if [ ! -d "$ASSET_DIR" ]; then
|
||||||
|
# If not, display a warning message and skip to the next iteration
|
||||||
|
echo "Warning: directory '$ASSET_DIR' not found, skipping."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Display the current directory being scanned
|
||||||
|
echo "Scanning directory: $ASSET_DIR"
|
||||||
|
|
||||||
|
# Iterate through each environment variable that starts with APP_PREFIX
|
||||||
|
env | grep "^${APP_PREFIX}" | while IFS='=' read -r key value; do
|
||||||
|
# Display the variable being replaced
|
||||||
|
echo " • Replacing ${key} → ${value}"
|
||||||
|
|
||||||
|
# Use find and sed to replace the variable in all files within the directory
|
||||||
|
find "$ASSET_DIR" -type f \
|
||||||
|
-exec sed -i "s|${key}|${value}|g" {} +
|
||||||
|
done
|
||||||
|
|
@ -41,7 +41,7 @@ export default defineConfig((ctx) => {
|
||||||
build: {
|
build: {
|
||||||
target: {
|
target: {
|
||||||
browser: [ 'es2022', 'firefox115', 'chrome115', 'safari14' ],
|
browser: [ 'es2022', 'firefox115', 'chrome115', 'safari14' ],
|
||||||
node: 'node20'
|
node: 'node20',
|
||||||
},
|
},
|
||||||
|
|
||||||
typescript: {
|
typescript: {
|
||||||
|
|
@ -66,7 +66,11 @@ export default defineConfig((ctx) => {
|
||||||
// polyfillModulePreload: true,
|
// polyfillModulePreload: true,
|
||||||
// distDir
|
// distDir
|
||||||
|
|
||||||
// extendViteConf (viteConf) {},
|
extendViteConf: (_config) => ({
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['tesseract.js']
|
||||||
|
}
|
||||||
|
}),
|
||||||
// viteVuePluginOptions: {},
|
// viteVuePluginOptions: {},
|
||||||
|
|
||||||
vitePlugins: [
|
vitePlugins: [
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ declare module 'vue' {
|
||||||
// for each client)
|
// for each client)
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: import.meta.env.VITE_TARGO_BACKEND_URL,
|
baseURL: import.meta.env.VITE_TARGO_BACKEND_URL,
|
||||||
withCredentials: true
|
withCredentials: true,
|
||||||
|
timeout: 5 * 60 * 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default defineBoot(({ app }) => {
|
export default defineBoot(({ app }) => {
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,11 @@ export default {
|
||||||
chat_placeholder: "Enter a Message",
|
chat_placeholder: "Enter a Message",
|
||||||
chat_thinking: "Thinking...",
|
chat_thinking: "Thinking...",
|
||||||
error: {
|
error: {
|
||||||
NO_REPLY_RECEIVED: "encountered an error while waiting for chatbot to reply",
|
NO_REPLY_RECEIVED: "FRONTEND ERROR: the backend did not include a message text in the response",
|
||||||
SEND_MESSAGE_FAILED: "unable to send message to chatbot",
|
NO_DATA_RECEIVED: "BACKEND ERROR: the backend did not receive any data from the AI agent",
|
||||||
|
NO_OUTPUT_RECEIVED: "BACKEND ERROR: the backend received data, but no output, from the AI agent",
|
||||||
|
SEND_MESSAGE_FAILED: "FRONTEND ERROR: Your message could not send or was timed out",
|
||||||
|
GENERIC_RESPONSE_ERROR: "BACKEND ERROR: An unexpected error occured while waiting for the backend to respond",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -245,6 +248,8 @@ export default {
|
||||||
day: "day",
|
day: "day",
|
||||||
empty: "empty",
|
empty: "empty",
|
||||||
name: "name",
|
name: "name",
|
||||||
|
lock: "",
|
||||||
|
unlock: "",
|
||||||
},
|
},
|
||||||
misc: {
|
misc: {
|
||||||
or: "or",
|
or: "or",
|
||||||
|
|
@ -290,6 +295,8 @@ export default {
|
||||||
apply_preset_day: "Apply schedule to day",
|
apply_preset_day: "Apply schedule to day",
|
||||||
apply_preset_week: "Apply schedule to week",
|
apply_preset_week: "Apply schedule to week",
|
||||||
save_successful: "timesheets saved",
|
save_successful: "timesheets saved",
|
||||||
|
unsaved_changes_title: "You have unsaved changes",
|
||||||
|
unsaved_changes_caption: "Save before leaving?",
|
||||||
nav_button: {
|
nav_button: {
|
||||||
calendar_date_picker: "Calendar",
|
calendar_date_picker: "Calendar",
|
||||||
current_week: "This week",
|
current_week: "This week",
|
||||||
|
|
@ -331,6 +338,8 @@ export default {
|
||||||
empty_list: 'No registered expenses',
|
empty_list: 'No registered expenses',
|
||||||
employee_comment: 'Comment',
|
employee_comment: 'Comment',
|
||||||
supervisor_comment: 'Supervisor note',
|
supervisor_comment: 'Supervisor note',
|
||||||
|
no_attachment: "no image attached",
|
||||||
|
temp_attachment_msg: "attachments are temporarily down, you can omit them from submissions and forward them to the finance department",
|
||||||
actions: {
|
actions: {
|
||||||
delete_confirm: "Delete this expense?",
|
delete_confirm: "Delete this expense?",
|
||||||
},
|
},
|
||||||
|
|
@ -347,9 +356,10 @@ export default {
|
||||||
type: "Type",
|
type: "Type",
|
||||||
types: {
|
types: {
|
||||||
PER_DIEM: "Per Diem",
|
PER_DIEM: "Per Diem",
|
||||||
EXPENSES: "expense",
|
EXPENSES: "reimbursement",
|
||||||
MILEAGE: "mileage",
|
MILEAGE: "mileage",
|
||||||
ON_CALL: "on-call allowance",
|
ON_CALL: "on-call allowance",
|
||||||
|
COMMISSION: "Commission",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,11 @@ export default {
|
||||||
chat_placeholder: "Entrez un Message",
|
chat_placeholder: "Entrez un Message",
|
||||||
chat_thinking: "Réflexion en cours...",
|
chat_thinking: "Réflexion en cours...",
|
||||||
error: {
|
error: {
|
||||||
NO_REPLY_RECEIVED: "Une erreur est survenu lors de la réception de la réponse du chatbot",
|
NO_REPLY_RECEIVED: "ERREUR FRONTEND: Le frontend n'a pas recu de message de l'IA dans la réponse du backend",
|
||||||
SEND_MESSAGE_FAILED: "Une erreur est survenu lors de l'envoi de votre message",
|
NO_DATA_RECEIVED: "ERREUR BACKEND: L'agent IA n'a pas inclu de data dans sa réponse au backend",
|
||||||
|
NO_OUTPUT_RECEIVED: "ERREUR BACKEND: L'agent IA a inclu du data, mais pas de output, dans sa réponse au backend",
|
||||||
|
SEND_MESSAGE_FAILED: "ERREUR FRONTEND: Une erreur est survenue lors de l'envoi de votre message, ou la connexion a été perdu",
|
||||||
|
GENERIC_RESPONSE_ERROR: "ERREUR BACKEND: Une erreur est survenu lors de la réception de la réponse du AI",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -245,6 +248,8 @@ export default {
|
||||||
day: "jour",
|
day: "jour",
|
||||||
empty: "vide",
|
empty: "vide",
|
||||||
name: "nom",
|
name: "nom",
|
||||||
|
lock: "verrouiller",
|
||||||
|
unlock: "déverrouiller",
|
||||||
},
|
},
|
||||||
misc: {
|
misc: {
|
||||||
or: "ou",
|
or: "ou",
|
||||||
|
|
@ -290,6 +295,8 @@ export default {
|
||||||
apply_preset_day: "Appliquer horaire pour la journée",
|
apply_preset_day: "Appliquer horaire pour la journée",
|
||||||
apply_preset_week: "Appliquer horaire pour la semaine",
|
apply_preset_week: "Appliquer horaire pour la semaine",
|
||||||
save_successful: "feuilles de temps enregistrées",
|
save_successful: "feuilles de temps enregistrées",
|
||||||
|
unsaved_changes_title: "Vous avez des changements non-enregistrés",
|
||||||
|
unsaved_changes_caption: "Sauvegardez avant de quitter?",
|
||||||
nav_button: {
|
nav_button: {
|
||||||
calendar_date_picker: "Calendrier",
|
calendar_date_picker: "Calendrier",
|
||||||
current_week: "Semaine actuelle",
|
current_week: "Semaine actuelle",
|
||||||
|
|
@ -331,6 +338,8 @@ export default {
|
||||||
empty_list: 'Aucun dépense enregistrée',
|
empty_list: 'Aucun dépense enregistrée',
|
||||||
employee_comment: 'Commentaire',
|
employee_comment: 'Commentaire',
|
||||||
supervisor_comment: 'Note du Superviseur',
|
supervisor_comment: 'Note du Superviseur',
|
||||||
|
no_attachment: "aucune pièce jointe",
|
||||||
|
temp_attachment_msg: "Les pièces jointes sont désactivés temporairement, vous pouvez laisser le champ vide et acheminez vos recus au département de la comptabilité",
|
||||||
actions: {
|
actions: {
|
||||||
delete_confirm: "Supprimer cette dépense?",
|
delete_confirm: "Supprimer cette dépense?",
|
||||||
},
|
},
|
||||||
|
|
@ -347,9 +356,10 @@ export default {
|
||||||
type: "Type",
|
type: "Type",
|
||||||
types: {
|
types: {
|
||||||
PER_DIEM: "Per diem",
|
PER_DIEM: "Per diem",
|
||||||
EXPENSES: "dépense",
|
EXPENSES: "remboursement",
|
||||||
MILEAGE: "kilométrage",
|
MILEAGE: "kilométrage",
|
||||||
ON_CALL: "Prime de garde",
|
ON_CALL: "Prime de garde",
|
||||||
|
COMMISSION: "Commission",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
setup
|
setup
|
||||||
>
|
>
|
||||||
|
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();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -31,6 +33,26 @@
|
||||||
/>
|
/>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-toolbar-title>
|
</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">
|
||||||
|
{{ authStore.user?.first_name.charAt(0) }}{{ authStore.user?.last_name.charAt(0) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!$q.platform.is.mobile" class="row items-end">
|
||||||
|
<div class="text-uppercase text-h4 text-weight-medium text-accent q-px-xs">
|
||||||
|
{{ authStore.user?.first_name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-uppercase text-h6 text-weight-light q-pr-md">
|
||||||
|
{{ authStore.user?.last_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</q-toolbar>
|
</q-toolbar>
|
||||||
</q-header>
|
</q-header>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import { RouteNames } from 'src/router/router-constants';
|
import { RouteNames } from 'src/router/router-constants';
|
||||||
import { ModuleNames, type UserModuleAccess } from 'src/modules/shared/models/user.models';
|
import { ModuleNames, type UserModuleAccess } from 'src/modules/shared/models/user.models';
|
||||||
|
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: "home", route: RouteNames.DASHBOARD, required_module: ModuleNames.DASHBOARD },
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
|
|
||||||
const q = useQuasar();
|
const q = useQuasar();
|
||||||
const auth_store = useAuthStore();
|
const auth_store = useAuthStore();
|
||||||
|
const authApi = useAuthApi();
|
||||||
const ui_store = useUiStore();
|
const ui_store = useUiStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const is_mini = ref(true);
|
const is_mini = ref(true);
|
||||||
|
|
@ -33,12 +35,8 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = async () => {
|
||||||
auth_store.logout();
|
await authApi.logout();
|
||||||
|
|
||||||
router.push({ name: 'login' }).catch(err => {
|
|
||||||
console.error('could not log you out: ', err);
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,15 @@
|
||||||
// const is_remembered = ref<boolean>(false);
|
// const is_remembered = ref<boolean>(false);
|
||||||
const is_employee_email = computed(() => email.value.includes('@targ'));
|
const is_employee_email = computed(() => email.value.includes('@targ'));
|
||||||
const is_game_time = computed(() => email.value.includes('allumette'));
|
const is_game_time = computed(() => email.value.includes('allumette'));
|
||||||
|
|
||||||
|
const onSubmitConnectionRequest = () => {
|
||||||
|
console.log('submit requested');
|
||||||
|
if (is_employee_email.value) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClickEmployeeConnect = () => {
|
||||||
|
auth_api.oidcLogin();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -27,28 +36,31 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="q-pt-sm q-px-xl q-pb-lg ">
|
<div class="q-px-xl q-pb-lg ">
|
||||||
<q-card-section class="text-center text-uppercase">
|
<div class="text-h6 text-weight-bold text-center text-uppercase q-py-sm">
|
||||||
<div class="text-h6 text-weight-bold">
|
|
||||||
{{ $t('login.page_header') }}
|
{{ $t('login.page_header') }}
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-form @submit="auth_api.login">
|
<q-form
|
||||||
|
@submit="onSubmitConnectionRequest"
|
||||||
|
autocomplete="on"
|
||||||
|
>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="email"
|
v-model="email"
|
||||||
dense
|
dense
|
||||||
outlined
|
outlined
|
||||||
|
label-slot
|
||||||
|
name="email"
|
||||||
color="accent"
|
color="accent"
|
||||||
label-color="accent"
|
label-color="accent"
|
||||||
class="rounded-5 inset-shadow bg-white"
|
class="rounded-5 inset-shadow bg-white"
|
||||||
label-slot
|
|
||||||
input-class="text-h6 text-primary"
|
input-class="text-h6 text-primary"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-weight-bolder text-uppercase text-overline"> {{ $t('login.email') }} </span>
|
<span class="text-weight-bolder text-uppercase text-overline"> {{ $t('login.email') }} </span>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<!-- Stay-logged-in section, removed temporarly until customer module is up -->
|
<!-- Stay-logged-in section, removed temporarly until customer module is up -->
|
||||||
<!-- <q-card-section
|
<!-- <q-card-section
|
||||||
horizontal
|
horizontal
|
||||||
|
|
@ -86,7 +98,6 @@
|
||||||
<!-- <q-card-section class="text-center q-pa-none q-mt-none">
|
<!-- <q-card-section class="text-center q-pa-none q-mt-none">
|
||||||
<RouterLink disabled class="text-primary" to="/signup">{{ $t('loginPage.signUp') }}</RouterLink>
|
<RouterLink disabled class="text-primary" to="/signup">{{ $t('loginPage.signUp') }}</RouterLink>
|
||||||
</q-card-section> -->
|
</q-card-section> -->
|
||||||
</q-form>
|
|
||||||
|
|
||||||
<q-card-section class="row q-pt-sm">
|
<q-card-section class="row q-pt-sm">
|
||||||
<q-separator
|
<q-separator
|
||||||
|
|
@ -128,16 +139,18 @@
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
push
|
||||||
rounded
|
rounded
|
||||||
|
type="submit"
|
||||||
color="accent"
|
color="accent"
|
||||||
icon="img:src/assets/logo-targo-simple.svg"
|
icon="img:src/assets/logo-targo-simple.svg"
|
||||||
:label="$t('login.button.employee')"
|
:label="$t('login.button.employee')"
|
||||||
class="full-width row"
|
class="full-width row"
|
||||||
@click="auth_api.oidcLogin"
|
@click="onClickEmployeeConnect"
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</q-slide-transition>
|
</q-slide-transition>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
</q-form>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
<div v-if="is_game_time">
|
<div v-if="is_game_time">
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,10 @@ export const useAuthApi = () => {
|
||||||
authStore.oidcLogin();
|
authStore.oidcLogin();
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = async () => {
|
||||||
authStore.logout();
|
authStore.user = undefined;
|
||||||
|
await authStore.logout();
|
||||||
|
window.location.href = "https://auth.targo.ca/application/o/montargo/end-session/";
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ export const AuthService = {
|
||||||
//TODO: OIDC customer sign-in, eventually
|
//TODO: OIDC customer sign-in, eventually
|
||||||
},
|
},
|
||||||
|
|
||||||
logout: () => {
|
logout: async () => {
|
||||||
// TODO: logout logic
|
// TODO: logout logic
|
||||||
api.post('/auth/logout')
|
await api.post('/auth/logout');
|
||||||
},
|
},
|
||||||
|
|
||||||
refreshToken: () => {
|
refreshToken: () => {
|
||||||
|
|
|
||||||
|
|
@ -2,49 +2,33 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
|
import MarkdownIt from 'markdown-it';
|
||||||
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
// import MarkdownIt from 'markdown-it'
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
import type { Message } from 'src/modules/chatbot/models/dialogue-message.model';
|
import type { Message } from 'src/modules/chatbot/models/dialogue-message.model';
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
// ========== state ========================================
|
||||||
const auth_store = useAuthStore();
|
|
||||||
|
|
||||||
const { message } = defineProps<{
|
const { message } = defineProps<{
|
||||||
message: Message;
|
message: Message;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const message_text = computed(() => message.text.includes('chatbot.') ? t(message.text) : message.text)
|
const { t } = useI18n();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const md = new MarkdownIt({
|
||||||
|
linkify: true,
|
||||||
|
typographer: true,
|
||||||
|
})
|
||||||
|
|
||||||
const is_error = computed(() => message.text.includes('NO_REPLY_RECEIVED') || message.text.includes('SEND_MESSAGE_FAILED'));
|
// ========== computed ========================================
|
||||||
|
|
||||||
// Initialize Markdown parser
|
const message_text = computed(() => message.text.includes('chatbot.') ?
|
||||||
// const md = new MarkdownIt({
|
t(message.text) :
|
||||||
// // breaks: false, // Support line breaks
|
message.text.replaceAll("\\n", "\n")
|
||||||
// linkify: true, // Make URLs clickable
|
);
|
||||||
// html: false // Prevent raw HTML injection
|
const is_error = computed(() => message.text.includes('chatbot.error.'));
|
||||||
// })
|
|
||||||
|
|
||||||
// // Removes all the h1,h2,h3 to make the Md more user friendly
|
|
||||||
// const cleanMarkdown = (markdown: string): string => {
|
|
||||||
// if (!markdown) return ''
|
|
||||||
// return markdown
|
|
||||||
// .replace(/\r\n/g, '\n') // normalize Windows line endings
|
|
||||||
// .replace(/([.!?])(\s+)(?=[A-Z])/g, '$1\n') // insert line break after sentence-ending punctuation
|
|
||||||
// .replace(/\n{3,}/g, '\n\n') // squash triple+ line breaks
|
|
||||||
// .replace(/^#{1,3}\s(.*)$/gm, '#### $1') // downgrade headings
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Compute parsed content
|
|
||||||
// const parsedText = computed((): string => {
|
|
||||||
// const cleaned = cleanMarkdown(message.text || '')
|
|
||||||
// if (cleaned.includes('chatbot.')) {
|
|
||||||
// const translated_message = t(message.text);
|
|
||||||
// return md.render(translated_message);
|
|
||||||
// }
|
|
||||||
// return md.render(cleaned);
|
|
||||||
// })
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -67,11 +51,12 @@
|
||||||
|
|
||||||
<q-chat-message
|
<q-chat-message
|
||||||
v-else
|
v-else
|
||||||
|
text-html
|
||||||
:sent="message.sent"
|
:sent="message.sent"
|
||||||
:name="message.sent ? auth_store.user?.first_name : 'TargoBot'"
|
:name="message.sent ? authStore.user?.first_name : 'TargoBot'"
|
||||||
:bg-color="message.sent ? 'accent' : 'info'"
|
:bg-color="message.sent ? 'accent' : 'info'"
|
||||||
:text-color="message.sent ? 'white' : ''"
|
:text-color="message.sent ? 'white' : ''"
|
||||||
:text="[message_text]"
|
:text="[md.renderInline(message_text)]"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,26 +10,42 @@
|
||||||
import DetailsDialogChartExpenses from 'src/modules/timesheet-approval/components/details-dialog-chart-expenses.vue';
|
import DetailsDialogChartExpenses from 'src/modules/timesheet-approval/components/details-dialog-chart-expenses.vue';
|
||||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||||
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
|
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
|
||||||
|
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
||||||
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
|
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
|
||||||
|
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
import { Expense } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
import { date } from 'quasar';
|
||||||
|
|
||||||
|
// ========== state ========================================
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheetStore = useTimesheetStore();
|
||||||
|
const expenseStore = useExpensesStore();
|
||||||
const timesheetApprovalApi = useTimesheetApprovalApi();
|
const timesheetApprovalApi = useTimesheetApprovalApi();
|
||||||
const is_dialog_open = ref(false);
|
const shiftApi = useShiftApi();
|
||||||
|
const isDialogOpen = ref(false);
|
||||||
|
|
||||||
|
// ========== computed ========================================
|
||||||
|
|
||||||
|
const isApproved = computed(() => timesheetStore.timesheets.every(timesheet => timesheet.is_approved));
|
||||||
|
|
||||||
const isApproved = computed(() => timesheet_store.timesheets.every(timesheet => timesheet.is_approved));
|
|
||||||
const approveButtonLabel = computed(() => isApproved.value ?
|
const approveButtonLabel = computed(() => isApproved.value ?
|
||||||
t('timesheet_approvals.table.verified') :
|
t('shared.label.unlock') :
|
||||||
t('timesheet_approvals.table.unverified')
|
t('shared.label.lock')
|
||||||
);
|
);
|
||||||
const approveButtonIcon = computed(() => isApproved.value ? 'lock' : 'lock_open');
|
|
||||||
const hasExpenses = computed(() => timesheet_store.timesheets.some(timesheet =>
|
const approveButtonIcon = computed(() => isApproved.value ? 'las la-lock' : 'las la-unlock');
|
||||||
|
|
||||||
|
const hasExpenses = computed(() => timesheetStore.timesheets.some(timesheet =>
|
||||||
Object.values(timesheet.weekly_expenses).some(hours => hours > 0))
|
Object.values(timesheet.weekly_expenses).some(hours => hours > 0))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ========== methods ========================================
|
||||||
|
|
||||||
const onClickApproveAll = async () => {
|
const onClickApproveAll = async () => {
|
||||||
const employeeEmail = timesheet_store.current_pay_period_overview?.email;
|
const employeeEmail = timesheetStore.current_pay_period_overview?.email;
|
||||||
const isApproved = timesheet_store.timesheets.every(timesheet => timesheet.is_approved);
|
const isApproved = timesheetStore.timesheets.every(timesheet => timesheet.is_approved);
|
||||||
|
|
||||||
if (employeeEmail !== undefined && isApproved !== undefined) {
|
if (employeeEmail !== undefined && isApproved !== undefined) {
|
||||||
await timesheetApprovalApi.toggleTimesheetsApprovalByEmployeeEmail(
|
await timesheetApprovalApi.toggleTimesheetsApprovalByEmployeeEmail(
|
||||||
|
|
@ -38,19 +54,32 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onClickSaveTimesheets = async () => {
|
||||||
|
await shiftApi.saveShiftChanges(timesheetStore.current_pay_period_overview?.email);
|
||||||
|
expenseStore.is_showing_create_form = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClickExpenseCreate = () => {
|
||||||
|
expenseStore.mode = 'create';
|
||||||
|
if (timesheetStore.pay_period)
|
||||||
|
expenseStore.current_expense = new Expense(timesheetStore.pay_period.period_start);
|
||||||
|
else
|
||||||
|
expenseStore.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="timesheet_store.is_details_dialog_open"
|
v-model="timesheetStore.is_details_dialog_open"
|
||||||
full-width
|
full-width
|
||||||
full-height
|
full-height
|
||||||
transition-show="jump-down"
|
transition-show="jump-down"
|
||||||
transition-hide="jump-down"
|
transition-hide="jump-down"
|
||||||
backdrop-filter="blur(6px)"
|
backdrop-filter="blur(6px)"
|
||||||
@show="is_dialog_open = true"
|
@show="isDialogOpen = true"
|
||||||
@hide="is_dialog_open = false"
|
@hide="isDialogOpen = false"
|
||||||
@before-hide="timesheet_store.getTimesheetOverviews"
|
@before-hide="timesheetStore.getTimesheetOverviews"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="column bg-secondary hide-scrollbar shadow-12 rounded-15 q-pb-sm no-wrap"
|
class="column bg-secondary hide-scrollbar shadow-12 rounded-15 q-pb-sm no-wrap"
|
||||||
|
|
@ -58,13 +87,14 @@
|
||||||
>
|
>
|
||||||
<!-- employee name -->
|
<!-- employee name -->
|
||||||
<div class="col-auto row flex-center q-px-none q-py-sm sticky-top bg-secondary full-width shadow-4">
|
<div class="col-auto row flex-center q-px-none q-py-sm sticky-top bg-secondary full-width shadow-4">
|
||||||
<span class="col text-h4 text-weight-bolder text-uppercase q-px-lg">
|
<span class="col-auto text-h4 text-weight-bolder text-uppercase q-px-lg">
|
||||||
{{ timesheet_store.selected_employee_name }}
|
{{ timesheetStore.selected_employee_name }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="col-auto q-px-lg">
|
<div class="col-auto q-px-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
:push="isApproved"
|
||||||
|
:outline="!isApproved"
|
||||||
dense
|
dense
|
||||||
size="lg"
|
size="lg"
|
||||||
color="accent"
|
color="accent"
|
||||||
|
|
@ -84,11 +114,28 @@
|
||||||
</transition>
|
</transition>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<q-space />
|
||||||
|
|
||||||
|
<div class="col-auto q-px-md">
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="lg"
|
||||||
|
:disable="timesheetStore.is_loading || !timesheetStore.canSaveTimesheets"
|
||||||
|
:color="timesheetStore.is_loading || !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>
|
</div>
|
||||||
|
|
||||||
<!-- employee pay period details using chart -->
|
<!-- employee pay period details using chart -->
|
||||||
<div
|
<div
|
||||||
v-if="is_dialog_open"
|
v-if="isDialogOpen"
|
||||||
|
:key="hasExpenses ? 0 : 1"
|
||||||
class="col-auto q-px-md no-wrap"
|
class="col-auto q-px-md no-wrap"
|
||||||
:class="$q.platform.is.mobile ? 'column' : 'row'"
|
:class="$q.platform.is.mobile ? 'column' : 'row'"
|
||||||
>
|
>
|
||||||
|
|
@ -100,15 +147,54 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto">
|
<div class="col-auto column">
|
||||||
|
<q-separator
|
||||||
|
spaced
|
||||||
|
size="4px"
|
||||||
|
class="q-mx-md"
|
||||||
|
/>
|
||||||
|
|
||||||
<ExpenseDialogList mode="approval" />
|
<ExpenseDialogList mode="approval" />
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
v-if="!isApproved"
|
||||||
|
v-model="expenseStore.is_showing_create_form"
|
||||||
|
hide-expand-icon
|
||||||
|
:dense="!$q.platform.is.mobile"
|
||||||
|
group="expenses"
|
||||||
|
@show="onClickExpenseCreate()"
|
||||||
|
header-class="bg-accent text-white q-mx-md rounded-5"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="row items-center">
|
||||||
|
<q-icon
|
||||||
|
name="add_circle_outline"
|
||||||
|
size="md"
|
||||||
|
class="col-auto"
|
||||||
|
:class="expenseStore.is_showing_create_form ? 'invisible' : ''"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="col-auto text-uppercase text-weight-bold text-h6 q-ml-xs q-mr-sm">
|
||||||
|
{{ $t('timesheet.expense.add_expense') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ExpenseDialogForm :email="timesheetStore.current_pay_period_overview?.email"/>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-separator
|
||||||
|
spaced
|
||||||
|
size="4px"
|
||||||
|
class="q-mx-md"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- list of shifts -->
|
<!-- list of shifts -->
|
||||||
<div class="col-auto">
|
<div class="col-auto q-mt-md">
|
||||||
<TimesheetWrapper
|
<TimesheetWrapper
|
||||||
mode="approval"
|
mode="approval"
|
||||||
:employee-email="timesheet_store.current_pay_period_overview?.email"
|
:employee-email="timesheetStore.current_pay_period_overview?.email"
|
||||||
class="col-auto"
|
class="col-auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
import { type OverviewColumns, pay_period_overview_columns, PayPeriodOverviewFilters, type TimesheetApprovalOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
|
import { type OverviewColumns, pay_period_overview_columns, PayPeriodOverviewFilters, type TimesheetApprovalOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
|
||||||
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
|
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
|
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||||
|
|
||||||
// ========== constants ========================================
|
// ========== constants ========================================
|
||||||
|
|
||||||
|
|
@ -46,11 +47,12 @@
|
||||||
|
|
||||||
const q = useQuasar();
|
const q = useQuasar();
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
const auth_store = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheetApi = useTimesheetApi();
|
||||||
const timesheet_approval_api = useTimesheetApprovalApi();
|
const timesheetStore = useTimesheetStore();
|
||||||
|
const timesheetApprovalApi = useTimesheetApprovalApi();
|
||||||
|
|
||||||
const overview_filters = ref<PayPeriodOverviewFilters>({
|
const overviewFilters = ref<PayPeriodOverviewFilters>({
|
||||||
is_showing_inactive: false,
|
is_showing_inactive: false,
|
||||||
is_showing_team_only: true,
|
is_showing_team_only: true,
|
||||||
supervisors: [],
|
supervisors: [],
|
||||||
|
|
@ -63,21 +65,21 @@
|
||||||
uiStore.user_preferences.is_timesheet_approval_grid
|
uiStore.user_preferences.is_timesheet_approval_grid
|
||||||
);
|
);
|
||||||
|
|
||||||
const overview_rows = computed(() =>
|
const overviewRows = computed(() =>
|
||||||
timesheet_store.pay_period_overviews.filter(overview => overview)
|
timesheetStore.pay_period_overviews.filter(overview => overview)
|
||||||
);
|
);
|
||||||
|
|
||||||
// ========== methods ========================================
|
// ========== methods ========================================
|
||||||
|
|
||||||
const onClickedDetails = async (row: TimesheetApprovalOverview) => {
|
const onClickedDetails = async (row: TimesheetApprovalOverview) => {
|
||||||
timesheet_store.current_pay_period_overview = row;
|
timesheetStore.current_pay_period_overview = row;
|
||||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(row.email);
|
await timesheetApi.getTimesheetsByCurrentPayPeriod(row.email);
|
||||||
|
|
||||||
timesheet_store.is_details_dialog_open = true;
|
timesheetStore.is_details_dialog_open = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClickApproveAll = async (email: string, is_approved: boolean) => {
|
const onClickApproveAll = async (email: string, is_approved: boolean) => {
|
||||||
await timesheet_approval_api.toggleTimesheetsApprovalByEmployeeEmail(email, is_approved);
|
await timesheetApprovalApi.toggleTimesheetsApprovalByEmployeeEmail(email, is_approved);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterEmployeeRows = (rows: readonly TimesheetApprovalOverview[], terms: PayPeriodOverviewFilters): TimesheetApprovalOverview[] => {
|
const filterEmployeeRows = (rows: readonly TimesheetApprovalOverview[], terms: PayPeriodOverviewFilters): TimesheetApprovalOverview[] => {
|
||||||
|
|
@ -88,7 +90,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (terms.is_showing_team_only) {
|
if (terms.is_showing_team_only) {
|
||||||
result = result.filter(row => row.supervisor !== null && row.supervisor.email === (auth_store.user ? auth_store.user.email : ''));
|
result = result.filter(row => row.supervisor !== null && row.supervisor.email === (authStore.user ? authStore.user.email : ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (terms.name_search_string.length > 0) {
|
if (terms.name_search_string.length > 0) {
|
||||||
|
|
@ -114,20 +116,20 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="full-width">
|
<div class="full-width">
|
||||||
<LoadingOverlay v-model="timesheet_store.is_loading" />
|
<LoadingOverlay v-model="timesheetStore.is_loading" />
|
||||||
<q-table
|
<q-table
|
||||||
dense
|
dense
|
||||||
row-key="email"
|
row-key="email"
|
||||||
color="accent"
|
color="accent"
|
||||||
separator="none"
|
separator="none"
|
||||||
hide-pagination
|
hide-pagination
|
||||||
:rows="overview_rows"
|
:rows="overviewRows"
|
||||||
:columns="pay_period_overview_columns"
|
:columns="pay_period_overview_columns"
|
||||||
:table-colspan="pay_period_overview_columns.length"
|
:table-colspan="pay_period_overview_columns.length"
|
||||||
:visible-columns="VISIBLE_COLUMNS"
|
:visible-columns="VISIBLE_COLUMNS"
|
||||||
:grid="isGridMode"
|
:grid="isGridMode"
|
||||||
:pagination="{ sortBy: 'is_active' }"
|
:pagination="{ sortBy: 'is_active' }"
|
||||||
:filter="overview_filters"
|
:filter="overviewFilters"
|
||||||
:filter-method="filterEmployeeRows"
|
:filter-method="filterEmployeeRows"
|
||||||
:rows-per-page-options="[0]"
|
:rows-per-page-options="[0]"
|
||||||
class="bg-transparent"
|
class="bg-transparent"
|
||||||
|
|
@ -138,20 +140,20 @@
|
||||||
:no-results-label="$t('shared.error.no_search_results')"
|
:no-results-label="$t('shared.error.no_search_results')"
|
||||||
:loading-label="$t('shared.label.loading')"
|
:loading-label="$t('shared.label.loading')"
|
||||||
table-header-style="min-width: 80xp; max-width: 80px;"
|
table-header-style="min-width: 80xp; max-width: 80px;"
|
||||||
:style="overview_rows.length > 0 ? `max-height: ${maxHeight}px;` : ''"
|
:style="overviewRows.length > 0 ? `max-height: ${maxHeight}px;` : ''"
|
||||||
:table-style="{ tableLayout: 'fixed' }"
|
:table-style="{ tableLayout: 'fixed' }"
|
||||||
@row-click="(_evt, row: TimesheetApprovalOverview) => onClickedDetails(row)"
|
@row-click="(_evt, row: TimesheetApprovalOverview) => onClickedDetails(row)"
|
||||||
>
|
>
|
||||||
<template #top>
|
<template #top>
|
||||||
<OverviewListTopMobile
|
<OverviewListTopMobile
|
||||||
v-if="$q.platform.is.mobile"
|
v-if="$q.platform.is.mobile"
|
||||||
v-model:filters="overview_filters"
|
v-model:filters="overviewFilters"
|
||||||
v-model:visible-columns="VISIBLE_COLUMNS"
|
v-model:visible-columns="VISIBLE_COLUMNS"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<OverviewListTop
|
<OverviewListTop
|
||||||
v-else
|
v-else
|
||||||
v-model:filters="overview_filters"
|
v-model:filters="overviewFilters"
|
||||||
v-model:visible-columns="VISIBLE_COLUMNS"
|
v-model:visible-columns="VISIBLE_COLUMNS"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -192,7 +194,7 @@
|
||||||
mode="out-in"
|
mode="out-in"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:key="props.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
|
:key="props.rowIndex + (timesheetStore.pay_period?.pay_period_no ?? 0)"
|
||||||
class="rounded-5"
|
class="rounded-5"
|
||||||
:style="`animation-delay: ${props.rowIndex / 15}s; opacity: ${props.row.is_active ? '1' : '0.5'};`"
|
:style="`animation-delay: ${props.rowIndex / 15}s; opacity: ${props.row.is_active ? '1' : '0.5'};`"
|
||||||
>
|
>
|
||||||
|
|
@ -265,7 +267,7 @@
|
||||||
<template #item="props: { row: TimesheetApprovalOverview, rowIndex: number }">
|
<template #item="props: { row: TimesheetApprovalOverview, rowIndex: number }">
|
||||||
<OverviewListItem
|
<OverviewListItem
|
||||||
v-model="props.row.is_approved"
|
v-model="props.row.is_approved"
|
||||||
:key="props.row.email + timesheet_store.pay_period?.pay_period_no"
|
:key="props.row.email + timesheetStore.pay_period?.pay_period_no"
|
||||||
:row="props.row"
|
:row="props.row"
|
||||||
@click-details="onClickedDetails"
|
@click-details="onClickedDetails"
|
||||||
@click-approval-all="is_approved => onClickApproveAll(props.row.email, is_approved)"
|
@click-approval-all="is_approved => onClickApproveAll(props.row.email, is_approved)"
|
||||||
|
|
@ -275,7 +277,7 @@
|
||||||
<!-- Template for custome failed-to-load state -->
|
<!-- Template for custome failed-to-load state -->
|
||||||
<template #no-data="{ message, filter }">
|
<template #no-data="{ message, filter }">
|
||||||
<div
|
<div
|
||||||
v-if="!timesheet_store.is_loading"
|
v-if="!timesheetStore.is_loading"
|
||||||
class="full-width column items-center text-accent"
|
class="full-width column items-center text-accent"
|
||||||
>
|
>
|
||||||
<q-icon
|
<q-icon
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
|
||||||
|
const expenseStore = useExpensesStore();
|
||||||
|
const isMaximized = ref(false);
|
||||||
|
const imageDimension = ref('400px');
|
||||||
|
|
||||||
|
const onClickMaximizeButton = () => {
|
||||||
|
isMaximized.value = !isMaximized.value;
|
||||||
|
imageDimension.value = isMaximized.value ? '100%' : '400px';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-dialog
|
||||||
|
v-model="expenseStore.isShowingAttachmentDialog"
|
||||||
|
backdrop-filter="blur(4px)"
|
||||||
|
:full-height="isMaximized"
|
||||||
|
:full-width="isMaximized"
|
||||||
|
>
|
||||||
|
<q-card class="q-pa-md flex-center relative-position">
|
||||||
|
<q-img
|
||||||
|
spinner-color="accent"
|
||||||
|
fit="contain"
|
||||||
|
:height="imageDimension"
|
||||||
|
:width="imageDimension"
|
||||||
|
:src="expenseStore.attachmentURL"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
size="lg"
|
||||||
|
color="accent"
|
||||||
|
:icon="isMaximized ? 'zoom_in_map' : 'zoom_out_map'"
|
||||||
|
class="absolute-bottom-right q-ma-sm rounded-5"
|
||||||
|
style="opacity: 0.5;"
|
||||||
|
@click="onClickMaximizeButton"
|
||||||
|
/>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
@ -20,6 +20,10 @@
|
||||||
const expense = defineModel<Expense>({ default: new Expense(new Date().toISOString().slice(0, 10)) })
|
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<{
|
||||||
|
email?: string | undefined;
|
||||||
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const ui_store = useUiStore();
|
const ui_store = useUiStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
@ -60,7 +64,16 @@
|
||||||
|
|
||||||
const requestExpenseCreationOrUpdate = async () => {
|
const requestExpenseCreationOrUpdate = async () => {
|
||||||
if (file.value)
|
if (file.value)
|
||||||
await expenses_api.upsertExpense(expenses_store.current_expense, file.value, employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL');
|
await expenses_api.upsertExpense(
|
||||||
|
expenses_store.current_expense,
|
||||||
|
email ?? employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL',
|
||||||
|
file.value
|
||||||
|
);
|
||||||
|
else
|
||||||
|
await expenses_api.upsertExpense(
|
||||||
|
expenses_store.current_expense,
|
||||||
|
email ?? employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL'
|
||||||
|
);
|
||||||
|
|
||||||
expenses_store.is_showing_create_form = true;
|
expenses_store.is_showing_create_form = true;
|
||||||
expenses_store.mode = 'create';
|
expenses_store.mode = 'create';
|
||||||
|
|
@ -258,6 +271,8 @@
|
||||||
dense
|
dense
|
||||||
stack-label
|
stack-label
|
||||||
label-slot
|
label-slot
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<q-icon
|
<q-icon
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="column items-center q-pa-none">
|
<div class="column items-center q-pa-none">
|
||||||
|
<!-- title bar with close button -->
|
||||||
<div class="col row full-width bg-primary">
|
<div class="col row full-width bg-primary">
|
||||||
<q-item-label class="col text-h6 text-weight-bolder text-uppercase text-white q-py-sm q-px-md">
|
<q-item-label class="col text-h6 text-weight-bolder text-uppercase text-white q-py-sm q-px-md">
|
||||||
{{ $t('timesheet.expense.title') }}
|
{{ $t('timesheet.expense.title') }}
|
||||||
|
|
@ -41,6 +42,19 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- REMOVE: ONCE ATTACHMENTS ARE FULLY IMPLEMENTED -->
|
||||||
|
<div class="col-auto row bg-warning flex-center q-px-md q-py-xs full-width">
|
||||||
|
<q-icon
|
||||||
|
name="las la-exclamation-triangle"
|
||||||
|
size="md"
|
||||||
|
class="q-px-md"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="text-bold text-center">
|
||||||
|
{{ $t('timesheet.expense.temp_attachment_msg') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col row flex-center full-width q-pt-sm q-px-md">
|
<div class="col row flex-center full-width q-pt-sm q-px-md">
|
||||||
<div class="col-auto row items-center q-px-md">
|
<div class="col-auto row items-center q-px-md">
|
||||||
<span
|
<span
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
>
|
>
|
||||||
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { date, Notify } from 'quasar';
|
import { date, Notify } from 'quasar';
|
||||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
|
|
@ -28,6 +28,12 @@
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const is_showing_update_form = ref(false);
|
const is_showing_update_form = ref(false);
|
||||||
|
|
||||||
|
// ========== computed ===================================
|
||||||
|
|
||||||
|
const attachmentButtonColor = computed(() => expense.value.attachment_name ?
|
||||||
|
(expense.value.is_approved ? 'white' : 'accent') :
|
||||||
|
'grey-5');
|
||||||
|
|
||||||
// ===================== methods =========================
|
// ===================== methods =========================
|
||||||
|
|
||||||
const requestExpenseDeletion = async () => {
|
const requestExpenseDeletion = async () => {
|
||||||
|
|
@ -46,7 +52,11 @@
|
||||||
expenses_store.current_expense = unwrapAndClone(expense.value);
|
expenses_store.current_expense = unwrapAndClone(expense.value);
|
||||||
expenses_store.current_expense.is_approved = !expenses_store.current_expense.is_approved;
|
expenses_store.current_expense.is_approved = !expenses_store.current_expense.is_approved;
|
||||||
|
|
||||||
const success = await expenses_store.upsertExpense(expenses_store.current_expense, timesheet_store.current_pay_period_overview?.email);
|
const success = await expenses_store.upsertExpense(
|
||||||
|
expenses_store.current_expense,
|
||||||
|
timesheet_store.current_pay_period_overview?.email
|
||||||
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
expense.value.is_approved = !expense.value.is_approved;
|
expense.value.is_approved = !expense.value.is_approved;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -57,6 +67,12 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onClickAttachment = async () => {
|
||||||
|
expenses_store.isShowingAttachmentDialog = true;
|
||||||
|
await expenses_store.getAttachmentURL(expense.value.attachment_key);
|
||||||
|
console.log('image url: ', expenses_store.attachmentURL);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -75,7 +91,6 @@
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="getExpenseIcon(expense.type)"
|
:name="getExpenseIcon(expense.type)"
|
||||||
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'white' : 'blue-grey-8')"
|
|
||||||
size="lg"
|
size="lg"
|
||||||
class="q-pr-md"
|
class="q-pr-md"
|
||||||
/>
|
/>
|
||||||
|
|
@ -117,14 +132,25 @@
|
||||||
<div class="col row items-center justify-start">
|
<div class="col row items-center justify-start">
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
push
|
||||||
:color="expense.is_approved ? 'white' : 'accent'"
|
:disable="expense.attachment_name === undefined"
|
||||||
|
:color="attachmentButtonColor"
|
||||||
:text-color="expense.is_approved ? 'accent' : 'white'"
|
:text-color="expense.is_approved ? 'accent' : 'white'"
|
||||||
class="col-auto q-px-sm q-mr-sm"
|
class="col-auto q-px-sm q-mr-sm"
|
||||||
icon="attach_file"
|
icon="attach_file"
|
||||||
|
@click.stop="onClickAttachment"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-item-label class="col">
|
<q-item-label class="col">
|
||||||
attachment_name.jpg
|
<span v-if="expense.attachment_name">
|
||||||
|
{{ expense.attachment_name }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-italic text-blue-grey-5 text-uppercase"
|
||||||
|
>
|
||||||
|
{{ $t('timesheet.expense.no_attachment') }}
|
||||||
|
</span>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
||||||
import ExpenseDialogHeader from 'src/modules/timesheets/components/expense-dialog-header.vue';
|
import ExpenseDialogHeader from 'src/modules/timesheets/components/expense-dialog-header.vue';
|
||||||
import ExpenseDialogFormMobile from 'src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue';
|
import ExpenseDialogFormMobile from 'src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue';
|
||||||
|
import ExpenseDialogAttachmentViewer from 'src/modules/timesheets/components/expense-dialog-attachment-viewer.vue';
|
||||||
|
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
|
@ -32,6 +33,8 @@
|
||||||
transition-show="jump-down"
|
transition-show="jump-down"
|
||||||
transition-hide="jump-down"
|
transition-hide="jump-down"
|
||||||
>
|
>
|
||||||
|
<ExpenseDialogAttachmentViewer class="z-top" />
|
||||||
|
|
||||||
<q-card
|
<q-card
|
||||||
class="q-pa-none rounded-10 shadow-24 bg-secondary"
|
class="q-pa-none rounded-10 shadow-24 bg-secondary"
|
||||||
style=" min-width: 70vw;"
|
style=" min-width: 70vw;"
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,13 @@
|
||||||
if (file.value)
|
if (file.value)
|
||||||
await expenses_api.upsertExpense(
|
await expenses_api.upsertExpense(
|
||||||
expenses_store.current_expense,
|
expenses_store.current_expense,
|
||||||
file.value, employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL'
|
employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL',
|
||||||
|
file.value
|
||||||
|
);
|
||||||
|
else
|
||||||
|
await expenses_api.upsertExpense(
|
||||||
|
expenses_store.current_expense,
|
||||||
|
employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL'
|
||||||
);
|
);
|
||||||
|
|
||||||
emit('onUpdateClicked');
|
emit('onUpdateClicked');
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,6 @@
|
||||||
<!-- avatar type icon section -->
|
<!-- avatar type icon section -->
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="getExpenseIcon(expense.type)"
|
:name="getExpenseIcon(expense.type)"
|
||||||
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'white' : 'primary')"
|
|
||||||
size="lg"
|
size="lg"
|
||||||
class="col-auto q-pr-sm"
|
class="col-auto q-pr-sm"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -5,25 +5,31 @@
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { QSelect, QInput } from 'quasar';
|
import { QSelect, QInput } from 'quasar';
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import { SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
import { getCurrentDailyMinutesWorked, getTimeStringFromMinutes, SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||||
import type { Shift } 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';
|
||||||
|
|
||||||
const ui_store = useUiStore();
|
// ========== state ========================================
|
||||||
|
|
||||||
|
const SHIFT_TYPES_WITH_PREDEFINED_TIMES: ShiftType[] = ['HOLIDAY', 'SICK', 'VACATION'];
|
||||||
const COMMENT_LENGTH_MAX = 280;
|
const COMMENT_LENGTH_MAX = 280;
|
||||||
|
|
||||||
const shift = defineModel<Shift>('shift', { required: true });
|
const shift = defineModel<Shift>('shift', { required: true });
|
||||||
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
|
||||||
const select_ref = ref<QSelect | null>(null);
|
|
||||||
const is_showing_comment_popup = ref(false);
|
|
||||||
const comment_length = computed(() => shift.value.comment?.length ?? 0);
|
|
||||||
const error_message = ref('');
|
|
||||||
|
|
||||||
const { errorMessage = undefined, dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
const {
|
||||||
|
dense = false,
|
||||||
|
hasShiftAfter = false,
|
||||||
|
isTimesheetApproved = false,
|
||||||
|
errorMessage = undefined,
|
||||||
|
expectedDailyHours = 8,
|
||||||
|
currentShifts,
|
||||||
|
} = defineProps<{
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
hasShiftAfter?: boolean;
|
hasShiftAfter?: boolean;
|
||||||
isTimesheetApproved?: boolean;
|
isTimesheetApproved?: boolean;
|
||||||
errorMessage?: string | undefined;
|
errorMessage?: string | undefined;
|
||||||
|
expectedDailyHours?: number;
|
||||||
|
currentShifts: Shift[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
@ -31,8 +37,23 @@
|
||||||
'onTimeFieldBlur': [void];
|
'onTimeFieldBlur': [void];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const ui_store = useUiStore();
|
||||||
|
const shiftTypeSelected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
||||||
|
const select_ref = ref<QSelect | null>(null);
|
||||||
|
const is_showing_comment_popup = ref(false);
|
||||||
|
const error_message = ref('');
|
||||||
|
const isShowingPredefinedTime = ref(shift.value.type === 'HOLIDAY');
|
||||||
|
const predefinedHoursString = ref('');
|
||||||
|
const predefinedHoursBgColor = ref(`bg-${shiftTypeSelected.value?.icon_color ?? ''}`);
|
||||||
|
|
||||||
|
// ========== computed ========================================
|
||||||
|
|
||||||
|
const comment_length = computed(() => shift.value.comment?.length ?? 0);
|
||||||
|
|
||||||
|
// ========== methods =========================================
|
||||||
|
|
||||||
const onBlurShiftTypeSelect = () => {
|
const onBlurShiftTypeSelect = () => {
|
||||||
if (shift_type_selected.value === undefined) {
|
if (shiftTypeSelected.value === undefined) {
|
||||||
shift.value.type = 'REGULAR';
|
shift.value.type = 'REGULAR';
|
||||||
shift.value.id = 0;
|
shift.value.id = 0;
|
||||||
emit('requestDelete');
|
emit('requestDelete');
|
||||||
|
|
@ -56,11 +77,35 @@
|
||||||
return 'negative';
|
return 'negative';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onShiftTypeChange = (option: ShiftOption) => {
|
||||||
|
shift.value.type = option.value;
|
||||||
|
|
||||||
|
if (SHIFT_TYPES_WITH_PREDEFINED_TIMES.includes(option.value)) {
|
||||||
|
predefinedHoursBgColor.value = `bg-${option.icon_color}`;
|
||||||
|
shift.value.start_time = '00:00';
|
||||||
|
|
||||||
|
if (option.value === 'SICK' || option.value === 'VACATION') {
|
||||||
|
const workedMinutes = getCurrentDailyMinutesWorked(currentShifts);
|
||||||
|
console.log('worked minutes: ', workedMinutes);
|
||||||
|
const expectedWorkedMinutes = expectedDailyHours * 60;
|
||||||
|
const leftOverMinutes = expectedWorkedMinutes - workedMinutes;
|
||||||
|
|
||||||
|
shift.value.end_time = getTimeStringFromMinutes(leftOverMinutes);
|
||||||
|
isShowingPredefinedTime.value = false;
|
||||||
|
} else {
|
||||||
|
isShowingPredefinedTime.value = true;
|
||||||
|
predefinedHoursString.value = getHoursMinutesStringFromHoursFloat(expectedDailyHours);
|
||||||
|
shift.value.end_time = getTimeStringFromMinutes(expectedDailyHours * 60);
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
isShowingPredefinedTime.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (ui_store.focus_next_component) {
|
if (ui_store.focus_next_component) {
|
||||||
select_ref.value?.focus();
|
select_ref.value?.focus();
|
||||||
select_ref.value?.showPopup();
|
select_ref.value?.showPopup();
|
||||||
shift_type_selected.value = undefined;
|
shiftTypeSelected.value = undefined;
|
||||||
ui_store.focus_next_component = false;
|
ui_store.focus_next_component = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,7 +169,7 @@
|
||||||
<!-- shift type -->
|
<!-- shift type -->
|
||||||
<q-select
|
<q-select
|
||||||
ref="select"
|
ref="select"
|
||||||
v-model="shift_type_selected"
|
v-model="shiftTypeSelected"
|
||||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||||
dense
|
dense
|
||||||
:borderless="(shift.is_approved && isTimesheetApproved)"
|
:borderless="(shift.is_approved && isTimesheetApproved)"
|
||||||
|
|
@ -141,7 +186,7 @@
|
||||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||||
popup-content-style="border: 2px solid var(--q-accent)"
|
popup-content-style="border: 2px solid var(--q-accent)"
|
||||||
@blur="onBlurShiftTypeSelect"
|
@blur="onBlurShiftTypeSelect"
|
||||||
@update:model-value="option => shift.type = option.value"
|
@update:model-value="onShiftTypeChange"
|
||||||
>
|
>
|
||||||
<template #selected-item="scope">
|
<template #selected-item="scope">
|
||||||
<div
|
<div
|
||||||
|
|
@ -227,7 +272,25 @@
|
||||||
</q-select>
|
</q-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col row items-start text-uppercase rounded-5 q-pa-xs">
|
<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 -->
|
<!-- punch in field -->
|
||||||
<div class="col q-pr-xs">
|
<div class="col q-pr-xs">
|
||||||
<q-input
|
<q-input
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,29 @@
|
||||||
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, QInput, useQuasar, type QSelectProps, QPopupProxy } from 'quasar';
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import { SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
|
||||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
|
||||||
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 type { Shift, ShiftOption, ShiftType } from 'src/modules/timesheets/models/shift.models';
|
||||||
|
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
|
||||||
|
|
||||||
// ================== State ==================
|
// ========== Constants ========================================
|
||||||
|
|
||||||
|
const SHIFT_TYPES_WITH_PREDEFINED_TIMES: ShiftType[] = ['HOLIDAY', 'SICK', 'VACATION'];
|
||||||
|
const COMMENT_LENGTH_MAX = 280;
|
||||||
|
|
||||||
|
// ========== State ========================================
|
||||||
|
|
||||||
const shift = defineModel<Shift>('shift', { required: true });
|
const shift = defineModel<Shift>('shift', { required: true });
|
||||||
|
|
||||||
const { errorMessage = undefined, isTimesheetApproved = false, holiday = false } = defineProps<{
|
const {
|
||||||
|
errorMessage = undefined,
|
||||||
|
isTimesheetApproved = false,
|
||||||
|
currentShifts,
|
||||||
|
holiday = false,
|
||||||
|
expectedDailyHours = 8,
|
||||||
|
} = defineProps<{
|
||||||
|
currentShifts: Shift[];
|
||||||
|
expectedDailyHours?: number;
|
||||||
isTimesheetApproved?: boolean;
|
isTimesheetApproved?: boolean;
|
||||||
errorMessage?: string | undefined;
|
errorMessage?: string | undefined;
|
||||||
holiday?: boolean | undefined;
|
holiday?: boolean | undefined;
|
||||||
|
|
@ -25,9 +39,6 @@
|
||||||
'onTimeFieldBlur': [void];
|
'onTimeFieldBlur': [void];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
||||||
const COMMENT_LENGTH_MAX = 280;
|
|
||||||
|
|
||||||
const q = useQuasar();
|
const q = useQuasar();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const ui_store = useUiStore();
|
const ui_store = useUiStore();
|
||||||
|
|
@ -39,10 +50,15 @@
|
||||||
const selectRef = ref<QSelect | null>(null);
|
const selectRef = ref<QSelect | null>(null);
|
||||||
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 popupProxyRef = ref<QPopupProxy | null>(null);
|
const popupProxyRef = ref<QPopupProxy | null>(null);
|
||||||
|
const predefinedHoursString = ref('');
|
||||||
|
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 rightClickMenuIcon = computed(() => shift.value.is_approved ? 'lock_open' : 'lock');
|
const rightClickMenuIcon = computed(() => shift.value.is_approved ? 'lock_open' : 'lock');
|
||||||
|
|
||||||
const rightClickMenuLabel = computed(() => shift.value.is_approved ?
|
const rightClickMenuLabel = computed(() => shift.value.is_approved ?
|
||||||
|
|
@ -77,7 +93,7 @@
|
||||||
menuOffset: [0, 10],
|
menuOffset: [0, 10],
|
||||||
menuAnchor: "bottom middle",
|
menuAnchor: "bottom middle",
|
||||||
menuSelf: "top middle",
|
menuSelf: "top middle",
|
||||||
options: SHIFT_OPTIONS,
|
options: getShiftOptions(hasPTO.value, currentShifts.length > 1),
|
||||||
class: `col rounded-5 q-mx-xs bg-dark ${!shift.value.is_approved && !isTimesheetApproved ? '' : 'inset-shadow'}`,
|
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",
|
popupContentClass: "text-uppercase text-weight-bold text-center rounded-5",
|
||||||
style: shift.value.is_approved ? (holiday ? 'background-color: #7b1fa2 !important' : 'background-color: #0a7d32 !important;') : '',
|
style: shift.value.is_approved ? (holiday ? 'background-color: #7b1fa2 !important' : 'background-color: #0a7d32 !important;') : '',
|
||||||
|
|
@ -128,6 +144,30 @@
|
||||||
popupProxyRef.value.hide();
|
popupProxyRef.value.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onShiftTypeChange = (option: ShiftOption) => {
|
||||||
|
shift.value.type = option.value;
|
||||||
|
|
||||||
|
if (SHIFT_TYPES_WITH_PREDEFINED_TIMES.includes(option.value)) {
|
||||||
|
predefinedHoursBgColor.value = `bg-${option.icon_color}`;
|
||||||
|
shift.value.start_time = '00:00';
|
||||||
|
|
||||||
|
if (option.value === 'SICK' || option.value === 'VACATION') {
|
||||||
|
const workedMinutes = getCurrentDailyMinutesWorked(currentShifts);
|
||||||
|
console.log('worked minutes: ', workedMinutes);
|
||||||
|
const expectedWorkedMinutes = expectedDailyHours * 60;
|
||||||
|
const leftOverMinutes = expectedWorkedMinutes - workedMinutes;
|
||||||
|
|
||||||
|
shift.value.end_time = getTimeStringFromMinutes(leftOverMinutes);
|
||||||
|
} else {
|
||||||
|
isShowingPredefinedTime.value = true;
|
||||||
|
predefinedHoursString.value = getHoursMinutesStringFromHoursFloat(expectedDailyHours);
|
||||||
|
shift.value.start_time = '00:00';
|
||||||
|
shift.value.end_time = `${expectedDailyHours}:00`;
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
isShowingPredefinedTime.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (ui_store.focus_next_component) {
|
if (ui_store.focus_next_component) {
|
||||||
selectRef.value?.focus();
|
selectRef.value?.focus();
|
||||||
|
|
@ -171,7 +211,7 @@
|
||||||
backdrop-filter="blur(6px)"
|
backdrop-filter="blur(6px)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="column flex-center q-pa-md bg-secondary shadow-24 rounded-10"
|
class="column flex-center q-pa-md bg-dark shadow-24 rounded-10"
|
||||||
style="border: 2px solid var(--q-accent);"
|
style="border: 2px solid var(--q-accent);"
|
||||||
>
|
>
|
||||||
<span class="col-auto text-h6 text-bold text-uppercase">{{
|
<span class="col-auto text-h6 text-bold text-uppercase">{{
|
||||||
|
|
@ -206,7 +246,7 @@
|
||||||
v-model="shiftTypeSelected"
|
v-model="shiftTypeSelected"
|
||||||
v-bind="shiftTypeSelectProps"
|
v-bind="shiftTypeSelectProps"
|
||||||
@blur="onBlurShiftTypeSelect"
|
@blur="onBlurShiftTypeSelect"
|
||||||
@update:model-value="option => shift.type = option.value"
|
@update:model-value="onShiftTypeChange"
|
||||||
>
|
>
|
||||||
<template #selected-item="scope">
|
<template #selected-item="scope">
|
||||||
<div
|
<div
|
||||||
|
|
@ -291,8 +331,25 @@
|
||||||
</q-select>
|
</q-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col row items-start text-uppercase rounded-5 q-pa-xs">
|
<div class="col row">
|
||||||
<!-- punch in field -->
|
<!-- If shift type has predefined timestamps -->
|
||||||
|
<div
|
||||||
|
v-if="isShowingPredefinedTime"
|
||||||
|
class="col row q-pa-xs relative-position flex-center"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Else show input fields for in-out timestamps -->
|
||||||
|
<div v-else class="col row items-start text-uppercase rounded-5 q-pa-xs">
|
||||||
<q-input
|
<q-input
|
||||||
ref="start_time"
|
ref="start_time"
|
||||||
v-model="shift.start_time"
|
v-model="shift.start_time"
|
||||||
|
|
@ -325,6 +382,7 @@
|
||||||
>{{ $t('shared.misc.out') }}</span>
|
>{{ $t('shared.misc.out') }}</span>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="row full-height"
|
class="row full-height"
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@
|
||||||
:is-timesheet-approved="approved"
|
:is-timesheet-approved="approved"
|
||||||
:error-message="shift_error_message"
|
:error-message="shift_error_message"
|
||||||
:dense="dense"
|
:dense="dense"
|
||||||
|
:current-shifts="day.shifts"
|
||||||
:has-shift-after="shift_index < day.shifts.length - 1"
|
:has-shift-after="shift_index < day.shifts.length - 1"
|
||||||
@request-delete="deleteCurrentShift(shift)"
|
@request-delete="deleteCurrentShift(shift)"
|
||||||
@on-time-field-blur="onTimeFieldBlur()"
|
@on-time-field-blur="onTimeFieldBlur()"
|
||||||
|
|
@ -116,6 +117,7 @@
|
||||||
v-else
|
v-else
|
||||||
v-model:shift="day.shifts[shift_index]!"
|
v-model:shift="day.shifts[shift_index]!"
|
||||||
:holiday="holiday"
|
:holiday="holiday"
|
||||||
|
:current-shifts="day.shifts"
|
||||||
:is-timesheet-approved="approved"
|
:is-timesheet-approved="approved"
|
||||||
:error-message="shift_error_message"
|
:error-message="shift_error_message"
|
||||||
@request-delete="deleteCurrentShift(shift)"
|
@request-delete="deleteCurrentShift(shift)"
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,30 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
|
import { computed, onMounted } from 'vue';
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
import { useAuthStore } from 'src/stores/auth-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 { mode = 'totals', timesheetMode = 'normal', totalHours = 0, vacationHours = 0, sickHours = 0, bankedHours = 0, totalExpenses = 0 } = defineProps<{
|
const { mode = 'totals', timesheetMode = 'normal', totalHours = 0, totalExpenses = 0 } = defineProps<{
|
||||||
mode: 'total-hours' | 'off-hours';
|
mode: 'total-hours' | 'off-hours';
|
||||||
timesheetMode: 'approval' | 'normal';
|
timesheetMode: 'approval' | 'normal';
|
||||||
totalHours?: number;
|
totalHours?: number;
|
||||||
vacationHours?: number;
|
|
||||||
sickHours?: number;
|
|
||||||
bankedHours?: number;
|
|
||||||
totalExpenses?: number;
|
totalExpenses?: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const auth_store = useAuthStore();
|
const auth_store = useAuthStore();
|
||||||
|
const timesheetStore = useTimesheetStore();
|
||||||
const is_management = auth_store.user?.user_module_access.includes('timesheets_approval');
|
const is_management = auth_store.user?.user_module_access.includes('timesheets_approval');
|
||||||
|
|
||||||
|
const vacationHours = computed(() => timesheetStore.paid_time_off_totals.vacation_hours);
|
||||||
|
const sickHours = computed(() => timesheetStore.paid_time_off_totals.sick_hours);
|
||||||
|
const bankedHours = computed(() => timesheetStore.paid_time_off_totals.banked_hours);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (timesheetMode === 'normal')
|
||||||
|
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail();
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,17 @@
|
||||||
import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
|
import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
|
||||||
import ShiftListWeeklyOverview from 'src/modules/timesheets/components/shift-list-weekly-overview.vue';
|
import ShiftListWeeklyOverview from 'src/modules/timesheets/components/shift-list-weekly-overview.vue';
|
||||||
import ShiftListWeeklyOverviewMobile from 'src/modules/timesheets/components/mobile/shift-list-weekly-overview-mobile.vue';
|
import ShiftListWeeklyOverviewMobile from 'src/modules/timesheets/components/mobile/shift-list-weekly-overview-mobile.vue';
|
||||||
|
import UnsavedChangesDialog from 'src/modules/timesheets/components/unsaved-changes-dialog.vue';
|
||||||
|
|
||||||
import { date, Notify } from 'quasar';
|
import { date, Notify } from 'quasar';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import { computed, onMounted, provide } from 'vue';
|
import { computed, onMounted, provide } from 'vue';
|
||||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||||
import { 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';
|
||||||
|
|
||||||
// ================= state ====================
|
// ================= state ====================
|
||||||
|
|
||||||
|
|
@ -27,18 +30,25 @@
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const expenses_store = useExpensesStore();
|
const router = useRouter();
|
||||||
const timesheet_store = useTimesheetStore();
|
const expenseStore = useExpensesStore();
|
||||||
const timesheet_api = useTimesheetApi();
|
const timesheetStore = useTimesheetStore();
|
||||||
const shift_api = useShiftApi();
|
const timesheetApi = useTimesheetApi();
|
||||||
|
const shiftApi = useShiftApi();
|
||||||
|
|
||||||
// ================== computed ====================
|
// ================== computed ====================
|
||||||
|
|
||||||
const has_shift_errors = computed(() => timesheet_store.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 timesheetStore.canSaveTimesheets = computed(() => {
|
||||||
|
// /* eslint-disable-next-line */
|
||||||
|
// const currentShifts = timesheetStore.timesheets.flatMap(timesheet => timesheet.days.flatMap(day => day.shifts.map(shift => { const { has_error, ...shft } = shift; return shft; })));
|
||||||
|
// const initialShifts = timesheetStore.initial_timesheets.flatMap(timesheet => timesheet.days.flatMap(day => day.shifts));
|
||||||
|
|
||||||
const is_timesheets_approved = computed(() => timesheet_store.timesheets.every(timesheet => timesheet.is_approved))
|
// return JSON.stringify(currentShifts) !== JSON.stringify(initialShifts);
|
||||||
|
// });
|
||||||
|
|
||||||
const total_hours = computed(() => timesheet_store.timesheets.reduce((sum, timesheet) =>
|
const totalHours = computed(() => timesheetStore.timesheets.reduce((sum, timesheet) =>
|
||||||
sum += timesheet.weekly_hours.regular
|
sum += timesheet.weekly_hours.regular
|
||||||
+ timesheet.weekly_hours.evening
|
+ timesheet.weekly_hours.evening
|
||||||
+ timesheet.weekly_hours.emergency
|
+ timesheet.weekly_hours.emergency
|
||||||
|
|
@ -46,7 +56,7 @@
|
||||||
0) //initial value
|
0) //initial value
|
||||||
);
|
);
|
||||||
|
|
||||||
const total_expenses = computed(() => timesheet_store.timesheets.reduce((sum, timesheet) =>
|
const totalExpenses = computed(() => timesheetStore.timesheets.reduce((sum, timesheet) =>
|
||||||
sum + timesheet.weekly_expenses.expenses
|
sum + timesheet.weekly_expenses.expenses
|
||||||
+ timesheet.weekly_expenses.on_call
|
+ timesheet.weekly_expenses.on_call
|
||||||
+ timesheet.weekly_expenses.per_diem,
|
+ timesheet.weekly_expenses.per_diem,
|
||||||
|
|
@ -60,34 +70,56 @@
|
||||||
|
|
||||||
const onClickSaveTimesheets = async () => {
|
const onClickSaveTimesheets = async () => {
|
||||||
if (mode === 'normal') {
|
if (mode === 'normal') {
|
||||||
await shift_api.saveShiftChanges();
|
await shiftApi.saveShiftChanges();
|
||||||
Notify.create({
|
Notify.create({
|
||||||
message: t('timesheet.save_successful'),
|
message: t('timesheet.save_successful'),
|
||||||
color: 'accent',
|
color: 'accent',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await shift_api.saveShiftChanges(timesheet_store.current_pay_period_overview?.email);
|
await shiftApi.saveShiftChanges(timesheetStore.current_pay_period_overview?.email);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onClickLeave = async () => {
|
||||||
|
timesheetStore.isShowingUnsavedWarning = false;
|
||||||
|
timesheetStore.timesheets = [];
|
||||||
|
timesheetStore.initial_timesheets = [];
|
||||||
|
await router.push({ name: timesheetStore.nextPageNameAfterUnsaveWarning ?? RouteNames.DASHBOARD});
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClickSaveBeforeLeaving = async () => {
|
||||||
|
if (mode === 'approval') return;
|
||||||
|
|
||||||
|
timesheetStore.isShowingUnsavedWarning = false;
|
||||||
|
await onClickSaveTimesheets();
|
||||||
|
await onClickLeave();
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (mode === 'normal')
|
if (mode === 'normal')
|
||||||
await timesheet_api.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
await timesheetApi.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="column items-center full-height" :class="mode === 'normal' ? 'relative-position' : ' no-wrap'">
|
<div
|
||||||
<LoadingOverlay v-model="timesheet_store.is_loading" />
|
class="column items-center full-height"
|
||||||
|
:class="mode === 'normal' ? 'relative-position' : ' no-wrap'"
|
||||||
|
>
|
||||||
|
<LoadingOverlay v-model="timesheetStore.is_loading" />
|
||||||
|
|
||||||
<!-- label for approval mode to delimit that this is the timesheet -->
|
<!-- label for approval mode to delimit that this is the timesheet -->
|
||||||
<span
|
<div
|
||||||
v-if="mode === 'approval'"
|
v-if="mode === 'approval'"
|
||||||
class="col-auto text-uppercase text-bold text-h5"
|
class="col-auto row full-width q-px-xl"
|
||||||
>
|
>
|
||||||
|
<span class="col-auto text-uppercase text-bold text-h5">
|
||||||
{{ $t('timesheet.page_header') }}
|
{{ $t('timesheet.page_header') }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<q-space />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- weekly overview -->
|
<!-- weekly overview -->
|
||||||
<div class="col-auto row q-px-lg full-width">
|
<div class="col-auto row q-px-lg full-width">
|
||||||
|
|
@ -99,8 +131,8 @@
|
||||||
<ShiftListWeeklyOverview
|
<ShiftListWeeklyOverview
|
||||||
mode="total-hours"
|
mode="total-hours"
|
||||||
:timesheet-mode="mode"
|
:timesheet-mode="mode"
|
||||||
:total-hours="total_hours"
|
:total-hours="totalHours"
|
||||||
:total-expenses="total_expenses"
|
:total-expenses="totalExpenses"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -127,9 +159,9 @@
|
||||||
<!-- navigation btn -->
|
<!-- navigation btn -->
|
||||||
<PayPeriodNavigator
|
<PayPeriodNavigator
|
||||||
class="col-auto"
|
class="col-auto"
|
||||||
@date-selected="timesheet_api.getTimesheetsByDate"
|
@date-selected="timesheetApi.getTimesheetsByDate"
|
||||||
@pressed-previous-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
|
@pressed-previous-button="timesheetApi.getTimesheetsByCurrentPayPeriod"
|
||||||
@pressed-next-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
|
@pressed-next-button="timesheetApi.getTimesheetsByCurrentPayPeriod"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- mobile expenses button -->
|
<!-- mobile expenses button -->
|
||||||
|
|
@ -143,7 +175,7 @@
|
||||||
color="accent"
|
color="accent"
|
||||||
icon="receipt_long"
|
icon="receipt_long"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
@click="expenses_store.open"
|
@click="expenseStore.open"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -157,16 +189,16 @@
|
||||||
color="accent"
|
color="accent"
|
||||||
icon="receipt_long"
|
icon="receipt_long"
|
||||||
:label="$t('timesheet.expense.open_btn')"
|
:label="$t('timesheet.expense.open_btn')"
|
||||||
@click="expenses_store.open"
|
@click="expenseStore.open"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- desktop save timesheet changes button -->
|
<!-- desktop save timesheet changes button -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="!is_timesheets_approved && $q.screen.width > $q.screen.height"
|
v-if="!isTimesheetsApproved && $q.screen.width > $q.screen.height"
|
||||||
push
|
push
|
||||||
rounded
|
rounded
|
||||||
:disable="timesheet_store.is_loading || has_shift_errors"
|
:disable="timesheetStore.is_loading || hasShiftErrors || !timesheetStore.canSaveTimesheets"
|
||||||
:color="timesheet_store.is_loading || has_shift_errors ? 'grey-5' : 'accent'"
|
:color="timesheetStore.is_loading || hasShiftErrors || !timesheetStore.canSaveTimesheets ? 'grey-5' : 'accent'"
|
||||||
icon="upload"
|
icon="upload"
|
||||||
:label="$t('shared.label.save')"
|
:label="$t('shared.label.save')"
|
||||||
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'full-width' : 'q-ml-md'"
|
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'full-width' : 'q-ml-md'"
|
||||||
|
|
@ -194,9 +226,9 @@
|
||||||
:class="$q.platform.is.mobile ? 'fit no-wrap' : 'full-width'"
|
:class="$q.platform.is.mobile ? 'fit no-wrap' : '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' : ''"
|
||||||
>
|
>
|
||||||
<!-- Show if no timesheets found (further than one month from present) -->
|
<!-- If no timesheets found -->
|
||||||
<div
|
<div
|
||||||
v-if="timesheet_store.timesheets.length < 1 && !timesheet_store.is_loading"
|
v-if="timesheetStore.timesheets.length < 1 && !timesheetStore.is_loading"
|
||||||
class="col-auto column flex-center fit q-py-lg"
|
class="col-auto column flex-center fit q-py-lg"
|
||||||
style="min-height: 20vh;"
|
style="min-height: 20vh;"
|
||||||
>
|
>
|
||||||
|
|
@ -211,7 +243,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Else show timesheets if found -->
|
<!-- Else show timesheets -->
|
||||||
<ShiftList
|
<ShiftList
|
||||||
v-else
|
v-else
|
||||||
class="col-auto"
|
class="col-auto"
|
||||||
|
|
@ -221,7 +253,7 @@
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
||||||
square
|
square
|
||||||
:disable="timesheet_store.is_loading"
|
:disable="timesheetStore.is_loading || hasShiftErrors || !timesheetStore.canSaveTimesheets"
|
||||||
size="lg"
|
size="lg"
|
||||||
color="accent"
|
color="accent"
|
||||||
icon="upload"
|
icon="upload"
|
||||||
|
|
@ -232,8 +264,13 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExpenseDialog
|
<ExpenseDialog
|
||||||
:is-approved="is_timesheets_approved"
|
:is-approved="isTimesheetsApproved"
|
||||||
class="z-top"
|
class="z-top"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UnsavedChangesDialog
|
||||||
|
@click-save-no="onClickLeave"
|
||||||
|
@click-save-yes="onClickSaveBeforeLeaving"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
74
src/modules/timesheets/components/unsaved-changes-dialog.vue
Normal file
74
src/modules/timesheets/components/unsaved-changes-dialog.vue
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
||||||
|
// ========== state ========================================
|
||||||
|
const emit = defineEmits<{
|
||||||
|
clickSaveNo: [];
|
||||||
|
clickSaveYes: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const timesheetStore = useTimesheetStore();
|
||||||
|
|
||||||
|
// ========== methods ========================================
|
||||||
|
|
||||||
|
const onClickSaveOptionButton = (option: 'cancel' | 'no' | 'yes') => {
|
||||||
|
switch(option) {
|
||||||
|
case 'cancel':
|
||||||
|
timesheetStore.isShowingUnsavedWarning = false;
|
||||||
|
break;
|
||||||
|
case 'no':
|
||||||
|
emit('clickSaveNo');
|
||||||
|
break;
|
||||||
|
case 'yes':
|
||||||
|
emit('clickSaveYes');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-dialog
|
||||||
|
v-model="timesheetStore.isShowingUnsavedWarning"
|
||||||
|
backdrop-filter="blur(4px)"
|
||||||
|
>
|
||||||
|
<q-card
|
||||||
|
class="bg-dark shadow-12 flex-center"
|
||||||
|
style="border: 2px solid var(--q-accent);"
|
||||||
|
>
|
||||||
|
<div class="column flex-center q-py-sm q-px-md text-center">
|
||||||
|
<span class="col-auto text-bold text-uppercase" style="font-size: 1.2em;">
|
||||||
|
{{ $t('timesheet.unsaved_changes_title') }}
|
||||||
|
</span>
|
||||||
|
<span class="">{{ $t('timesheet.unsaved_changes_caption') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto row full-width">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
:label="$t('shared.label.cancel')"
|
||||||
|
class="col"
|
||||||
|
@click="onClickSaveOptionButton('cancel')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="negative"
|
||||||
|
:label="$t('shared.misc.no')"
|
||||||
|
class="col"
|
||||||
|
@click="onClickSaveOptionButton('no')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="accent"
|
||||||
|
:label="$t('shared.misc.yes')"
|
||||||
|
class="col"
|
||||||
|
@click="onClickSaveOptionButton('yes')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
@ -8,10 +8,18 @@ export const useExpensesApi = () => {
|
||||||
const expenses_store = useExpensesStore();
|
const expenses_store = useExpensesStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
||||||
const upsertExpense = async (expense: Expense, file: File, employee_email: string): Promise<string> => {
|
const upsertExpense = async (expense: Expense, employee_email: string, file?: File): Promise<string> => {
|
||||||
const presignedURL = expenses_store.uploadAttachment(file);
|
if (file) {
|
||||||
if (!presignedURL) return 'PRESIGN_FAILED';
|
const attachmentKey = await expenses_store.uploadAttachment(file);
|
||||||
|
|
||||||
|
if (!attachmentKey)
|
||||||
|
console.error('failed to upload attachment');
|
||||||
|
else {
|
||||||
|
expense.attachment_key = attachmentKey;
|
||||||
|
expense.attachment_name = file.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('employee email provided for expense: ', employee_email)
|
||||||
const success = await expenses_store.upsertExpense(expense, employee_email);
|
const success = await expenses_store.upsertExpense(expense, employee_email);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,35 @@ import { useShiftStore } from "src/stores/shift-store";
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
|
|
||||||
export const useShiftApi = () => {
|
export const useShiftApi = () => {
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheetStore = useTimesheetStore();
|
||||||
const shift_store = useShiftStore();
|
const shift_store = useShiftStore();
|
||||||
|
|
||||||
const deleteShiftById = async (shift_id: number, employee_email?: string) => {
|
const deleteShiftById = async (shift_id: number, employee_email?: string) => {
|
||||||
timesheet_store.is_loading = true;
|
timesheetStore.is_loading = true;
|
||||||
const success = await shift_store.deleteShiftById(shift_id, employee_email);
|
const success = await shift_store.deleteShiftById(shift_id, employee_email);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
timesheetStore.timesheets = [];
|
||||||
|
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
||||||
|
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail(employee_email);
|
||||||
}
|
}
|
||||||
|
|
||||||
timesheet_store.is_loading = false;
|
timesheetStore.is_loading = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveShiftChanges = async (employee_email?: string) => {
|
const saveShiftChanges = async (employee_email?: string) => {
|
||||||
timesheet_store.is_loading = true;
|
timesheetStore.is_loading = true;
|
||||||
|
|
||||||
const update_success = await shift_store.updateShifts(employee_email);
|
const update_success = await shift_store.updateShifts(employee_email);
|
||||||
const create_success = await shift_store.createNewShifts(employee_email);
|
const create_success = await shift_store.createNewShifts(employee_email);
|
||||||
|
|
||||||
if (create_success || update_success){
|
if (create_success || update_success){
|
||||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
timesheetStore.timesheets = [];
|
||||||
|
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
||||||
|
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail(employee_email);
|
||||||
}
|
}
|
||||||
|
|
||||||
timesheet_store.is_loading = false;
|
timesheetStore.is_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,71 @@
|
||||||
|
import { useShiftStore } from "src/stores/shift-store";
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
|
|
||||||
|
|
||||||
export const useTimesheetApi = () => {
|
export const useTimesheetApi = () => {
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheetStore = useTimesheetStore();
|
||||||
|
const shiftStore = useShiftStore();
|
||||||
|
|
||||||
const getTimesheetsByDate = async (date_string: string, employee_email?: string) => {
|
const getTimesheetsByDate = async (date_string: string, employeeEmail?: string) => {
|
||||||
timesheet_store.timesheets = [];
|
timesheetStore.timesheets = [];
|
||||||
timesheet_store.is_loading = true;
|
timesheetStore.is_loading = true;
|
||||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
|
const success = await timesheetStore.getPayPeriodByDateOrYearAndNumber(date_string);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(employeeEmail);
|
||||||
timesheet_store.is_loading = false;
|
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail(employeeEmail);
|
||||||
|
timesheetStore.is_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
timesheet_store.is_loading = false;
|
timesheetStore.is_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTimesheetsByCurrentPayPeriod = async (employee_email?: string) => {
|
const getTimesheetsByCurrentPayPeriod = async (employeeEmail?: string) => {
|
||||||
if (timesheet_store.pay_period === undefined) return false;
|
if (timesheetStore.pay_period === undefined) return false;
|
||||||
|
|
||||||
timesheet_store.is_loading = true;
|
timesheetStore.is_loading = true;
|
||||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber();
|
const success = await timesheetStore.getPayPeriodByDateOrYearAndNumber();
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(employeeEmail);
|
||||||
timesheet_store.is_loading = false;
|
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail(employeeEmail);
|
||||||
|
timesheetStore.is_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
timesheet_store.is_loading = false;
|
timesheetStore.is_loading = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyPreset = async (timesheet_id: number, week_day_index?: number, date?: string, employeeEmail?: string) => {
|
const applyPreset = async (timesheet_id: number, week_day_index?: number, date?: string, employeeEmail?: string) => {
|
||||||
if (timesheet_store.timesheets.map(timesheet => timesheet.timesheet_id).includes(timesheet_id)) {
|
timesheetStore.is_loading = true;
|
||||||
timesheet_store.is_loading = true;
|
|
||||||
try {
|
|
||||||
let response;
|
|
||||||
|
|
||||||
if (week_day_index && date)
|
const success = await timesheetStore.applyPreset(timesheet_id, week_day_index, date, employeeEmail);
|
||||||
response = await timesheetService.applyPresetToDay(timesheet_id, week_day_index, date, employeeEmail);
|
|
||||||
else
|
|
||||||
response = await timesheetService.applyPresetToWeek(timesheet_id, employeeEmail);
|
|
||||||
|
|
||||||
if (response.success)
|
if (!success) {
|
||||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employeeEmail);
|
timesheetStore.is_loading = false;
|
||||||
} catch (error) {
|
return;
|
||||||
console.error('Error applying weekly timesheet: ', error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
timesheet_store.is_loading = false;
|
const timesheets = JSON.stringify(timesheetStore.timesheets);
|
||||||
|
const initialTimesheets = JSON.stringify(timesheetStore.initial_timesheets);
|
||||||
|
|
||||||
|
if (timesheets !== initialTimesheets) {
|
||||||
|
await shiftStore.updateShifts(employeeEmail);
|
||||||
|
await shiftStore.createNewShifts(employeeEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(employeeEmail);
|
||||||
|
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail(employeeEmail);
|
||||||
|
|
||||||
|
timesheetStore.is_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTimesheetsBySwiping = async (direction: number) => {
|
const getTimesheetsBySwiping = async (direction: number) => {
|
||||||
timesheet_store.is_loading = true;
|
timesheetStore.is_loading = true;
|
||||||
|
|
||||||
timesheet_store.getNextOrPreviousPayPeriod(direction);
|
timesheetStore.getNextOrPreviousPayPeriod(direction);
|
||||||
await timesheet_store.getPayPeriodByDateOrYearAndNumber();
|
await timesheetStore.getPayPeriodByDateOrYearAndNumber();
|
||||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail();
|
await timesheetStore.getTimesheetsByOptionalEmployeeEmail();
|
||||||
|
|
||||||
timesheet_store.is_loading = false;
|
timesheetStore.is_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -33,3 +33,8 @@ export interface ExpenseOption {
|
||||||
value: ExpenseType;
|
value: ExpenseType;
|
||||||
icon: string;
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AttachmentPresignedURLResponse {
|
||||||
|
url: string;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { QSelectOption } from "quasar";
|
||||||
|
|
||||||
export const SHIFT_TYPES: ShiftType[] = [
|
export const SHIFT_TYPES: ShiftType[] = [
|
||||||
'REGULAR',
|
'REGULAR',
|
||||||
'EVENING',
|
'EVENING',
|
||||||
|
|
@ -39,9 +41,10 @@ export class Shift {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShiftOption {
|
export interface ShiftOption extends QSelectOption {
|
||||||
label: string;
|
label: string;
|
||||||
value: ShiftType;
|
value: ShiftType;
|
||||||
icon: string;
|
icon: string;
|
||||||
icon_color: string;
|
icon_color: string;
|
||||||
|
disable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
export interface TimesheetResponse {
|
export interface TimesheetResponse {
|
||||||
has_preset_schedule: boolean;
|
has_preset_schedule: boolean;
|
||||||
employee_fullname: string;
|
employee_fullname: string;
|
||||||
|
daily_expected_hours: number;
|
||||||
timesheets: Timesheet[];
|
timesheets: Timesheet[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { api } from "src/boot/axios";
|
import { api } from "src/boot/axios";
|
||||||
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
|
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
|
||||||
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
import type { AttachmentPresignedURLResponse, Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
|
|
||||||
export const ExpenseService = {
|
export const ExpenseService = {
|
||||||
createExpense: async (expense: Expense): Promise<{ success: boolean, data: Expense, error?: unknown }> => {
|
createExpense: async (expense: Expense, email?: string): Promise<{ success: boolean, data: Expense, error?: unknown }> => {
|
||||||
const response = await api.post('expense/create', expense);
|
const response = await api.post(`expense/create${email ? '?employee_email=' + email : ''}`, expense);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -18,14 +18,19 @@ export const ExpenseService = {
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getPresignedUploadURL: async (file: File, checksum_crc32: string): Promise<BackendResponse<string>> => {
|
getPresignedUploadURL: async (file: File, checksum_crc32: string): Promise<BackendResponse<AttachmentPresignedURLResponse>> => {
|
||||||
const [file_name, file_type] = file.name.split('.');
|
const [file_name, file_type] = file.name.split('.');
|
||||||
const response = await api.post(`attachments/s3/upload?file-name=${file_name}&file-type=${file_type}&checksumCRC32=${checksum_crc32}`);
|
const response = await api.post(`attachments/s3/upload?file-name=${file_name}&file-type=${file_type}&checksumCRC32=${checksum_crc32}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadAttachmentWithPresignedUrl: async (file: File, url: string) => {
|
uploadAttachmentWithPresignedUrl: async (file: File, url: string): Promise<number> => {
|
||||||
const response = await api.put(url, file, { headers: { 'Content-Type': file.type, }, withCredentials: false });
|
const response = await api.put(url, file, { headers: { 'Content-Type': `image/${file.type}`, }, withCredentials: false });
|
||||||
console.log('response to upload: ', response);
|
return response.status;
|
||||||
|
},
|
||||||
|
|
||||||
|
getPresignedDownloadURL: async (key: string): Promise<BackendResponse<string>> => {
|
||||||
|
const response = await api.get<BackendResponse<string>>(`attachments/s3/download?attachmentKey=${key}`);
|
||||||
|
return response.data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -4,6 +4,7 @@ import type { TimesheetResponse } from "src/modules/timesheets/models/timesheet.
|
||||||
import type { TimesheetApprovalOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
import type { TimesheetApprovalOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||||
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
|
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
|
||||||
import type { FederalHoliday } from "src/modules/timesheets/models/federal-holidays.models";
|
import type { FederalHoliday } from "src/modules/timesheets/models/federal-holidays.models";
|
||||||
|
import type { PaidTimeOff } from "src/modules/employee-list/models/employee-profile.models";
|
||||||
|
|
||||||
export const timesheetService = {
|
export const timesheetService = {
|
||||||
getAllFederalHolidays: async (year?: number): Promise<FederalHoliday[]> => {
|
getAllFederalHolidays: async (year?: number): Promise<FederalHoliday[]> => {
|
||||||
|
|
@ -36,6 +37,16 @@ export const timesheetService = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getPaidTimeOffTotalsWithOptionalEmployeeEmail: async (email?: string): Promise<BackendResponse<PaidTimeOff>> => {
|
||||||
|
if (email) {
|
||||||
|
const response = await api.get<BackendResponse<PaidTimeOff>>(`paid-time-off/totals?email=${email}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.get<BackendResponse<PaidTimeOff>>('paid-time-off/totals');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
applyPresetToWeek: async (timesheet_id: number, employeeEmail?: string): Promise<BackendResponse<boolean>> => {
|
applyPresetToWeek: async (timesheet_id: number, employeeEmail?: string): Promise<BackendResponse<boolean>> => {
|
||||||
if (employeeEmail) {
|
if (employeeEmail) {
|
||||||
const response = await api.post<BackendResponse<boolean>>(`schedule-presets/apply-preset/${employeeEmail}`, { timesheet_id });
|
const response = await api.post<BackendResponse<boolean>>(`schedule-presets/apply-preset/${employeeEmail}`, { timesheet_id });
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { date } from "quasar";
|
import { date } from "quasar";
|
||||||
import type { SchedulePresetShift } from "src/modules/employee-list/models/schedule-presets.models";
|
import type { SchedulePresetShift } from "src/modules/employee-list/models/schedule-presets.models";
|
||||||
import type { Shift, ShiftOption } from "src/modules/timesheets/models/shift.models";
|
import type { Shift, ShiftOption, ShiftType } from "src/modules/timesheets/models/shift.models";
|
||||||
|
|
||||||
export const isShiftOverlap = (shifts: Shift[] | SchedulePresetShift[]): boolean => {
|
export const isShiftOverlap = (shifts: Shift[] | SchedulePresetShift[]): boolean => {
|
||||||
if (shifts.length < 2) return false;
|
if (shifts.length < 2) return false;
|
||||||
|
|
@ -26,6 +26,37 @@ export const isShiftOverlap = (shifts: Shift[] | SchedulePresetShift[]): boolean
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCurrentDailyMinutesWorked = (shifts: Shift[]): number => {
|
||||||
|
const shiftTypesToIgnore: ShiftType[] = ['HOLIDAY', 'SICK', 'VACATION'];
|
||||||
|
let minutes = 0;
|
||||||
|
|
||||||
|
shifts.forEach(shift => {
|
||||||
|
if (shiftTypesToIgnore.includes(shift.type)) return;
|
||||||
|
|
||||||
|
const startTime = new Date(`1970-01-01T${shift.start_time}:00`);
|
||||||
|
const endTime = new Date(`1970-01-01T${shift.end_time}:00`);
|
||||||
|
|
||||||
|
const diff = date.getDateDiff(endTime, startTime, 'minutes');
|
||||||
|
minutes += diff;
|
||||||
|
});
|
||||||
|
|
||||||
|
return minutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTimeStringFromMinutes = (minutes: number): string => {
|
||||||
|
const h = Math.floor(minutes / 60);
|
||||||
|
const m = minutes % 60;
|
||||||
|
|
||||||
|
if (h < 10 ) {
|
||||||
|
if (m < 10)
|
||||||
|
return `0${h}:0${m}`;
|
||||||
|
|
||||||
|
return `0${h}:${m}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${h}:${m}`;
|
||||||
|
}
|
||||||
|
|
||||||
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: 'wb_sunny', icon_color: 'accent' },
|
||||||
{ label: 'timesheet.shift.types.EVENING', value: 'EVENING', icon: 'bedtime', icon_color: 'orange-5' },
|
{ label: 'timesheet.shift.types.EVENING', value: 'EVENING', icon: 'bedtime', icon_color: 'orange-5' },
|
||||||
|
|
@ -36,3 +67,16 @@ export const SHIFT_OPTIONS: ShiftOption[] = [
|
||||||
// { label: 'timesheet.shift.types.BANKING', value: 'BANKING', icon: 'savings', icon_color: 'pink-3' },
|
// { label: 'timesheet.shift.types.BANKING', value: 'BANKING', icon: '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: 'attach_money', icon_color: 'yellow-4' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const getShiftOptions = (disablePTO: boolean, isNotUnique: boolean): ShiftOption[] => {
|
||||||
|
return [
|
||||||
|
{ label: 'timesheet.shift.types.REGULAR', value: 'REGULAR', icon: 'wb_sunny', icon_color: 'accent' },
|
||||||
|
{ label: 'timesheet.shift.types.EVENING', value: 'EVENING', icon: 'bedtime', icon_color: 'orange-5' },
|
||||||
|
{ label: 'timesheet.shift.types.EMERGENCY', value: 'EMERGENCY', icon: 'ring_volume', 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.HOLIDAY', value: 'HOLIDAY', icon: 'event', 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.BANKING', value: 'BANKING', icon: 'savings', icon_color: 'pink-3' },
|
||||||
|
// { label: 'timesheet.shift.types.WITHDRAW_BANKED', value: 'WITHDRAW_BANKED', icon: 'attach_money', icon_color: 'yellow-4' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import { useAuthStore } from 'src/stores/auth-store';
|
||||||
import { RouteNames } from 'src/router/router-constants';
|
import { RouteNames } from 'src/router/router-constants';
|
||||||
import { useChatbotStore } from 'src/stores/chatbot-store';
|
import { useChatbotStore } from 'src/stores/chatbot-store';
|
||||||
import type { UserModuleAccess } from 'src/modules/shared/models/user.models';
|
import type { UserModuleAccess } from 'src/modules/shared/models/user.models';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If not building with SSR mode, you can
|
* If not building with SSR mode, you can
|
||||||
|
|
@ -30,19 +31,29 @@ export default defineRouter(function (/* { store, ssrContext } */) {
|
||||||
history: createHistory(process.env.VUE_ROUTER_BASE),
|
history: createHistory(process.env.VUE_ROUTER_BASE),
|
||||||
});
|
});
|
||||||
|
|
||||||
Router.beforeEach(async (destination_page) => {
|
Router.beforeEach(async (to, from) => {
|
||||||
const auth_store = useAuthStore();
|
const auth_store = useAuthStore();
|
||||||
const result = await auth_store.getProfile() ?? { status: 400, message: 'unknown error occured' };
|
const result = await auth_store.getProfile() ?? { status: 400, message: 'unknown error occured' };
|
||||||
|
|
||||||
if (destination_page.meta.requires_auth && !auth_store.user || (result.status >= 400 && destination_page.name !== RouteNames.LOGIN)) {
|
if (to.meta.requires_auth && !auth_store.user || (result.status >= 400 && to.name !== RouteNames.LOGIN)) {
|
||||||
console.error('no user account found');
|
console.error('no user account found');
|
||||||
return { name: 'login' };
|
return { name: 'login' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (destination_page.meta.required_module && auth_store.user) {
|
if (to.meta.required_module && auth_store.user) {
|
||||||
if (!auth_store.user.user_module_access.includes(destination_page.meta.required_module as UserModuleAccess))
|
if (!auth_store.user.user_module_access.includes(to.meta.required_module as UserModuleAccess))
|
||||||
return {name: 'error'};
|
return {name: 'error'};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (from.name === RouteNames.TIMESHEET) {
|
||||||
|
const timesheetStore = useTimesheetStore();
|
||||||
|
|
||||||
|
if(timesheetStore.canSaveTimesheets) {
|
||||||
|
timesheetStore.nextPageNameAfterUnsaveWarning = to.name;
|
||||||
|
timesheetStore.isShowingUnsavedWarning = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
Router.afterEach( (destination_page) => {
|
Router.afterEach( (destination_page) => {
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,8 @@ export const useAuthStore = defineStore('auth', () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = async () => {
|
||||||
user.value = undefined;
|
await AuthService.logout();
|
||||||
AuthService.logout();
|
|
||||||
const logout_popup = window.open('https://auth.targo.ca/application/o/montargo/end-session/', 'logoutPopup', 'width=200,height=200');
|
|
||||||
|
|
||||||
setInterval(() => logout_popup?.close(), 2000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAuthMessage = async (event: MessageEvent) => {
|
const handleAuthMessage = async (event: MessageEvent) => {
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,18 @@ export const useChatbotStore = defineStore("chatbot", () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const chatbot_response = await chatbotService.sendChatMessage(user_message, current_page_context.value);
|
const chatbot_response = await chatbotService.sendChatMessage(user_message, current_page_context.value);
|
||||||
if (chatbot_response) {
|
|
||||||
|
if (chatbot_response.text) {
|
||||||
last_chatbot_message.text = chatbot_response.text;
|
last_chatbot_message.text = chatbot_response.text;
|
||||||
last_chatbot_message.isThinking = false;
|
last_chatbot_message.isThinking = false;
|
||||||
} else {
|
} else {
|
||||||
last_chatbot_message.text = 'chatbot.error.NO_REPLY_RECEIVED';
|
last_chatbot_message.text = 'chatbot.error.NO_REPLY_RECEIVED';
|
||||||
last_chatbot_message.isThinking = false;
|
last_chatbot_message.isThinking = false;
|
||||||
}
|
}
|
||||||
}
|
} catch (error) {
|
||||||
catch (error) {
|
|
||||||
last_chatbot_message.text = 'chatbot.error.SEND_MESSAGE_FAILED';
|
last_chatbot_message.text = 'chatbot.error.SEND_MESSAGE_FAILED';
|
||||||
last_chatbot_message.isThinking = false;
|
last_chatbot_message.isThinking = false;
|
||||||
|
|
||||||
console.error('error sending message: ', error);
|
console.error('error sending message: ', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,11 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
const is_open = ref(false);
|
const is_open = ref(false);
|
||||||
const is_loading = ref(false);
|
const is_loading = ref(false);
|
||||||
const is_showing_create_form = ref(false);
|
const is_showing_create_form = ref(false);
|
||||||
|
const attachmentURL = ref<string>('');
|
||||||
const mode = ref<'create' | 'update' | 'delete'>('create');
|
const mode = ref<'create' | 'update' | 'delete'>('create');
|
||||||
const current_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
|
const current_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
|
||||||
const initial_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
|
const initial_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
|
||||||
|
const isShowingAttachmentDialog = ref(false);
|
||||||
const is_save_disabled = computed(() => JSON.stringify(current_expense.value) === JSON.stringify(initial_expense.value))
|
const is_save_disabled = computed(() => JSON.stringify(current_expense.value) === JSON.stringify(initial_expense.value))
|
||||||
|
|
||||||
const open = (): void => {
|
const open = (): void => {
|
||||||
|
|
@ -34,7 +36,7 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
const upsertExpense = async (expense: Expense, email?: string): Promise<boolean> => {
|
const upsertExpense = async (expense: Expense, email?: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
if (expense.id < 0) {
|
if (expense.id < 0) {
|
||||||
const data = await ExpenseService.createExpense(expense);
|
const data = await ExpenseService.createExpense(expense, email);
|
||||||
return data.success;
|
return data.success;
|
||||||
}
|
}
|
||||||
const data = await ExpenseService.updateExpense(expense, email);
|
const data = await ExpenseService.updateExpense(expense, email);
|
||||||
|
|
@ -51,22 +53,48 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
return data.success;
|
return data.success;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadAttachment = async (file: File) => {
|
/**
|
||||||
|
* Attemps to upload the provided image file to the S3 storage bucket for attachments.
|
||||||
|
*
|
||||||
|
* @param file image file to be uploaded to the S3 storage
|
||||||
|
* @returns Key `string` associated with the uploaded image file if successful,
|
||||||
|
* `undefined` if it fails.
|
||||||
|
*/
|
||||||
|
const uploadAttachment = async (file: File): Promise<string | undefined> => {
|
||||||
try {
|
try {
|
||||||
const checksum = await computeCRC32Base64(file);
|
const checksum = await computeCRC32Base64(file);
|
||||||
const presignedUrlResponse = await ExpenseService.getPresignedUploadURL(file, checksum);
|
const presignedUrlResponse = await ExpenseService.getPresignedUploadURL(file, checksum);
|
||||||
|
|
||||||
if (presignedUrlResponse.success && presignedUrlResponse.data) {
|
if (!presignedUrlResponse.success || !presignedUrlResponse.data) {
|
||||||
const { url, key } = JSON.parse(presignedUrlResponse.data);
|
console.error('failed to get presigned URL from server');
|
||||||
console.log('key: ', key);
|
return;
|
||||||
|
|
||||||
await ExpenseService.uploadAttachmentWithPresignedUrl(file, url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { url, key } = presignedUrlResponse.data;
|
||||||
|
|
||||||
|
const responseStatus = await ExpenseService.uploadAttachmentWithPresignedUrl(file, url);
|
||||||
|
|
||||||
|
if (responseStatus >= 400) {
|
||||||
|
console.error('an error occured during upload: error ', responseStatus);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAttachmentURL = async (key?: string) => {
|
||||||
|
if (!key)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const presignedAttachmentURL = await ExpenseService.getPresignedDownloadURL(key);
|
||||||
|
|
||||||
|
if (presignedAttachmentURL.success && presignedAttachmentURL.data)
|
||||||
|
attachmentURL.value = presignedAttachmentURL.data;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
is_open,
|
is_open,
|
||||||
is_loading,
|
is_loading,
|
||||||
|
|
@ -74,11 +102,14 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
mode,
|
mode,
|
||||||
current_expense,
|
current_expense,
|
||||||
initial_expense,
|
initial_expense,
|
||||||
|
isShowingAttachmentDialog,
|
||||||
is_save_disabled,
|
is_save_disabled,
|
||||||
|
attachmentURL,
|
||||||
open,
|
open,
|
||||||
upsertExpense,
|
upsertExpense,
|
||||||
deleteExpenseById,
|
deleteExpenseById,
|
||||||
close,
|
close,
|
||||||
uploadAttachment,
|
uploadAttachment,
|
||||||
|
getAttachmentURL,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -8,10 +8,12 @@ import { timesheetService } from 'src/modules/timesheets/services/timesheet-serv
|
||||||
import type { PayPeriodOverviewResponse, TimesheetApprovalOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
import type { PayPeriodOverviewResponse, TimesheetApprovalOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||||
import type { PayPeriod } from 'src/modules/shared/models/pay-period.models';
|
import type { PayPeriod } from 'src/modules/shared/models/pay-period.models';
|
||||||
import type { Timesheet } from 'src/modules/timesheets/models/timesheet.models';
|
import type { Timesheet } from 'src/modules/timesheets/models/timesheet.models';
|
||||||
|
import type { PaidTimeOff } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
import type { PayPeriodEvent } from 'src/modules/timesheet-approval/models/pay-period-event.models';
|
import type { PayPeriodEvent } from 'src/modules/timesheet-approval/models/pay-period-event.models';
|
||||||
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
||||||
import { type FederalHoliday, TARGO_HOLIDAY_NAMES_FR } from 'src/modules/timesheets/models/federal-holidays.models';
|
import { type FederalHoliday, TARGO_HOLIDAY_NAMES_FR } from 'src/modules/timesheets/models/federal-holidays.models';
|
||||||
|
import type { RouteNames } from 'src/router/router-constants';
|
||||||
|
import type { RouteRecordNameGeneric } from 'vue-router';
|
||||||
|
|
||||||
export const useTimesheetStore = defineStore('timesheet', () => {
|
export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
@ -20,6 +22,16 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
const timesheets = ref<Timesheet[]>([]);
|
const timesheets = ref<Timesheet[]>([]);
|
||||||
const all_current_shifts = computed(() => timesheets.value.flatMap(week => week.days.flatMap(day => day.shifts)) ?? []);
|
const all_current_shifts = computed(() => timesheets.value.flatMap(week => week.days.flatMap(day => day.shifts)) ?? []);
|
||||||
const initial_timesheets = ref<Timesheet[]>([]);
|
const initial_timesheets = ref<Timesheet[]>([]);
|
||||||
|
const canSaveTimesheets = computed(() => {
|
||||||
|
/* eslint-disable-next-line */
|
||||||
|
const currentShifts = timesheets.value.flatMap(timesheet => timesheet.days.flatMap(day => day.shifts.map(shift => { const { has_error, ...shft } = shift; return shft; })));
|
||||||
|
const initialShifts = initial_timesheets.value.flatMap(timesheet => timesheet.days.flatMap(day => day.shifts));
|
||||||
|
|
||||||
|
return JSON.stringify(currentShifts) !== JSON.stringify(initialShifts);
|
||||||
|
});
|
||||||
|
const paid_time_off_totals = ref<PaidTimeOff>({ sick_hours: 0, vacation_hours: 0, banked_hours: 0 });
|
||||||
|
const isShowingUnsavedWarning = ref(false);
|
||||||
|
const nextPageNameAfterUnsaveWarning = ref<RouteNames | RouteRecordNameGeneric>();
|
||||||
|
|
||||||
const pay_period_overviews = ref<TimesheetApprovalOverview[]>([]);
|
const pay_period_overviews = ref<TimesheetApprovalOverview[]>([]);
|
||||||
const pay_period_infos = ref<PayPeriodOverviewResponse>();
|
const pay_period_infos = ref<PayPeriodOverviewResponse>();
|
||||||
|
|
@ -119,7 +131,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
has_timesheet_preset.value = response.data.has_preset_schedule;
|
has_timesheet_preset.value = response.data.has_preset_schedule;
|
||||||
selected_employee_name.value = response.data.employee_fullname;
|
selected_employee_name.value = response.data.employee_fullname;
|
||||||
timesheets.value = response.data.timesheets;
|
timesheets.value = response.data.timesheets;
|
||||||
initial_timesheets.value = unwrapAndClone(timesheets.value);
|
initial_timesheets.value = unwrapAndClone(response.data.timesheets);
|
||||||
} else {
|
} else {
|
||||||
selected_employee_name.value = '';
|
selected_employee_name.value = '';
|
||||||
timesheets.value = [];
|
timesheets.value = [];
|
||||||
|
|
@ -135,6 +147,18 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPaidTimeOffTotalsWithOptionalEmployeeEmail = async (email?: string) => {
|
||||||
|
try {
|
||||||
|
const response = await timesheetService.getPaidTimeOffTotalsWithOptionalEmployeeEmail(email);
|
||||||
|
|
||||||
|
if (response.success && response.data)
|
||||||
|
paid_time_off_totals.value = response.data;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error getting paid time off totals: ', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const toggleTimesheetsApprovalByEmployeeEmail = async (email: string, approval_status: boolean): Promise<boolean> => {
|
const toggleTimesheetsApprovalByEmployeeEmail = async (email: string, approval_status: boolean): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const timesheet_ids = timesheets.value.map(timesheet => timesheet.timesheet_id);
|
const timesheet_ids = timesheets.value.map(timesheet => timesheet.timesheet_id);
|
||||||
|
|
@ -148,6 +172,24 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyPreset = async (timesheet_id: number, week_day_index?: number, date?: string, employeeEmail?: string): Promise<boolean> => {
|
||||||
|
if (timesheets.value.map(timesheet => timesheet.timesheet_id).includes(timesheet_id)) {
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if (week_day_index && date)
|
||||||
|
response = await timesheetService.applyPresetToDay(timesheet_id, week_day_index, date, employeeEmail);
|
||||||
|
else
|
||||||
|
response = await timesheetService.applyPresetToWeek(timesheet_id, employeeEmail);
|
||||||
|
|
||||||
|
return response.success ?? false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error applying weekly timesheet: ', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const getPayPeriodReport = async (report_filters: TimesheetApprovalCSVReportFilters) => {
|
const getPayPeriodReport = async (report_filters: TimesheetApprovalCSVReportFilters) => {
|
||||||
try {
|
try {
|
||||||
if (!pay_period.value) return false;
|
if (!pay_period.value) return false;
|
||||||
|
|
@ -223,21 +265,27 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
current_pay_period_overview,
|
current_pay_period_overview,
|
||||||
pay_period_infos,
|
pay_period_infos,
|
||||||
selected_employee_name,
|
selected_employee_name,
|
||||||
|
canSaveTimesheets,
|
||||||
has_timesheet_preset,
|
has_timesheet_preset,
|
||||||
timesheets,
|
timesheets,
|
||||||
all_current_shifts,
|
all_current_shifts,
|
||||||
initial_timesheets,
|
initial_timesheets,
|
||||||
federal_holidays,
|
federal_holidays,
|
||||||
|
paid_time_off_totals,
|
||||||
|
isShowingUnsavedWarning,
|
||||||
|
nextPageNameAfterUnsaveWarning,
|
||||||
getCurrentFederalHolidays,
|
getCurrentFederalHolidays,
|
||||||
getNextOrPreviousPayPeriod,
|
getNextOrPreviousPayPeriod,
|
||||||
getPayPeriodByDateOrYearAndNumber,
|
getPayPeriodByDateOrYearAndNumber,
|
||||||
getTimesheetOverviews,
|
getTimesheetOverviews,
|
||||||
getTimesheetsByOptionalEmployeeEmail,
|
getTimesheetsByOptionalEmployeeEmail,
|
||||||
toggleTimesheetsApprovalByEmployeeEmail,
|
toggleTimesheetsApprovalByEmployeeEmail,
|
||||||
|
applyPreset,
|
||||||
getPayPeriodReport,
|
getPayPeriodReport,
|
||||||
openReportDialog,
|
openReportDialog,
|
||||||
closeReportDialog,
|
closeReportDialog,
|
||||||
subscribeToPayPeriodObservable,
|
subscribeToPayPeriodObservable,
|
||||||
unsubscribeToPayPeriodObservable,
|
unsubscribeToPayPeriodObservable,
|
||||||
|
getPaidTimeOffTotalsWithOptionalEmployeeEmail,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -20,14 +20,27 @@ export const getMinutes = (hours: number) => {
|
||||||
return minutes > 1 ? minutes.toString() : '0';
|
return minutes > 1 ? minutes.toString() : '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getHoursMinutesStringFromHoursFloat = (hours: number): string => {
|
export const getHoursMinutesStringFromHoursFloat = (hours: number, minutes?: number): string => {
|
||||||
let flat_hours = Math.floor(hours);
|
let flatHours = Math.floor(hours);
|
||||||
let minutes = Math.round((hours - flat_hours) * 60);
|
let flatMinutes = minutes ?? Math.round((hours - flatHours) * 60);
|
||||||
|
|
||||||
if (minutes === 60) {
|
if (flatMinutes === 60) {
|
||||||
flat_hours += 1;
|
flatHours += 1;
|
||||||
minutes = 0;
|
flatMinutes = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${flat_hours}h${minutes > 1 ? ' ' + minutes : ''}`
|
return `${flatHours}h${flatMinutes > 1 ? ' ' + flatMinutes : ''}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHoursMinutesBetweenTwoHHmm = (startTime: string, endTime: string): {
|
||||||
|
hours: number,
|
||||||
|
minutes: number,
|
||||||
|
} => {
|
||||||
|
const [startHours, startMinutes] = startTime.split(':');
|
||||||
|
const [endHours, endMinutes] = endTime.split(':');
|
||||||
|
|
||||||
|
return {
|
||||||
|
hours: Number(endHours) - Number(startHours),
|
||||||
|
minutes: Number(endMinutes) - Number(startMinutes),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user