build(scaffolding): set up all folders and files, most of them empty, built some logic, clarified file names.

This commit is contained in:
Nicolas Drolet 2025-07-25 16:58:07 -04:00
parent a8e6b8750d
commit a63ae452a8
96 changed files with 11984 additions and 0 deletions

62
.gitignore vendored Normal file
View File

@ -0,0 +1,62 @@
# Node
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Cordova related directories and files
/src-cordova/node_modules
/src-cordova/platforms
/src-cordova/plugins
/src-cordova/www
# Capacitor related directories and files
/src-capacitor/www
/src-capacitor/node_modules
# Logs
logs/
*.log
# Quasar
.quasar/
dist/
public/statics/
/quasar.config.*.temporary.compiled*
# Vite / Quasar Build
dist/
coverage/
.cache/
*.local
# Environment Files
.env*
!.env.example
# VSCode
.vscode/
# OS
.DS_Store
Thumbs.db
# Testing
cypress/videos/
cypress/screenshots/
coverage/
# Optional IDE/project files
.idea/
*.iml
*.suo
*.ntvs*
*.njsproj
*.sln
# Prettier / Lint
.prettiercache
# Husky
.husky/_/

5
.npmrc Normal file
View File

@ -0,0 +1,5 @@
# pnpm-related options
shamefully-hoist=true
strict-peer-dependencies=false
# to get the latest compatible packages when creating the project https://github.com/pnpm/pnpm/issues/6463
resolution-mode=highest

115
README.md
View File

@ -0,0 +1,115 @@
# 🌐 Targo 2.0 Frontend
> A modern, scalable frontend for managing employees, timesheets, and time-off requests in a rural ISP environment — built with [Vue 3](https://vuejs.org/) and [Quasar Framework](https://quasar.dev/).
---
## 🚀 Tech Stack
| Layer | Technology |
| ------------ | ------------------------------------- |
| UI Framework | [Quasar (Vue 3)](https://quasar.dev/) |
| Router | Vue Router 4 |
| State | Pinia |
| HTTP | Axios |
| Auth | OAuth2/OIDC (popup-based) via backend |
| i18n | Vue I18n |
| Build Tools | Vite (default) |
| Testing | (Planned) Vitest, Cypress |
---
## 📁 Project Structure
```bash
src/
├── boot/ # Initialization scripts (e.g. axios, auth)
├── modules/ # Feature modules
│ ├── dashboard/ # Role-based dashboards (admin, technician, etc.)
│ ├── auth/ # Login popup, logout flow
│ ├── users/ # User management
│ ├── shared/ # Common components, services, etc.
│ ├── time-sheet/ # Create, validate, export timesheets and expenses
│ ├── router/ # Vue Router config
│ ├── validations/ # ??????
│ ├── i18n/ # Internationalization references
│ ├── pages/ # Route-level pages (fallbacks, misc)
│ ├── stores/ # Pinia stores
│ ├── layouts/ # App shell (toolbar, drawer)
│ ├── css/ # Stylesheets
│ ├── assets/ # Images, styles, icons
└── App.vue # Root component
```
---
## ⚙️ Setup Instructions
### 1. Install Dependencies
```bash
npm install
# or
pnpm install
```
### 2. Start the Dev Server
```bash
quasar dev
```
### 3. Build for Production
```bash
quasar build
```
---
## 🥪 Testing (Planned)
```bash
# Unit tests (Vitest)
npm run test:unit
# E2E tests (Cypress)
npm run test:e2e
```
---
## 📦 Recommended Quasar Extensions
```bash
quasar ext add @quasar/pwa # PWA mode
quasar ext add @quasar/testing # Testing utils
```
---
## 🧹 Linting & Formatting
```bash
npm run lint
npm run format
```
Pre-commit hooks are managed with:
* [husky](https://github.com/typicode/husky)
* [lint-staged](https://github.com/okonet/lint-staged)
---
## 🧠 Notes
* All routes are prefixed for internal navigation.
* Common services and components live in `modules/shared/`.
* Avoid placing OIDC or sensitive logic in the frontend — everything auth-related is delegated to the backend.
---
## 📄 License
This project is proprietary and internal to **\[Targo Communication]**.

83
eslint.config.js Normal file
View File

@ -0,0 +1,83 @@
import js from '@eslint/js'
import globals from 'globals'
import pluginVue from 'eslint-plugin-vue'
import pluginQuasar from '@quasar/app-vite/eslint'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
export default defineConfigWithVueTs(
{
/**
* Ignore the following files.
* Please note that pluginQuasar.configs.recommended() already ignores
* the "node_modules" folder for you (and all other Quasar project
* relevant folders and files).
*
* ESLint requires "ignores" key to be the only one in this object
*/
// ignores: []
},
pluginQuasar.configs.recommended(),
js.configs.recommended,
/**
* https://eslint.vuejs.org
*
* pluginVue.configs.base
* -> Settings and rules to enable correct ESLint parsing.
* pluginVue.configs[ 'flat/essential']
* -> base, plus rules to prevent errors or unintended behavior.
* pluginVue.configs["flat/strongly-recommended"]
* -> Above, plus rules to considerably improve code readability and/or dev experience.
* pluginVue.configs["flat/recommended"]
* -> Above, plus rules to enforce subjective community defaults to ensure consistency.
*/
pluginVue.configs[ 'flat/essential' ],
{
files: ['**/*.ts', '**/*.vue'],
rules: {
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports' }
],
}
},
// https://github.com/vuejs/eslint-config-typescript
vueTsConfigs.recommendedTypeChecked,
{
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.browser,
...globals.node, // SSR, Electron, config files
process: 'readonly', // process.env.*
ga: 'readonly', // Google Analytics
cordova: 'readonly',
Capacitor: 'readonly',
chrome: 'readonly', // BEX related
browser: 'readonly' // BEX related
}
},
// add your custom rules here
rules: {
'prefer-promise-reject-errors': 'off',
// allow debugger during development only
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
},
{
files: [ 'src-pwa/custom-service-worker.ts' ],
languageOptions: {
globals: {
...globals.serviceworker
}
}
}
)

21
index.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title><%= productName %></title>
<meta charset="utf-8">
<meta name="description" content="<%= productDescription %>">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png">
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
<link rel="icon" type="image/ico" href="favicon.ico">
</head>
<body>
<!-- quasar:entry-point -->
</body>
</html>

10586
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "targo_frontend",
"version": "0.0.1",
"description": "A Quasar PWA Project for managing employee logic",
"productName": "App Targo",
"author": "Nicolas Drolet <nicolasd@targointernet.com>",
"type": "module",
"private": true,
"scripts": {
"lint": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\"",
"test": "echo \"No test specified\" && exit 0",
"dev": "quasar dev",
"build": "quasar build",
"postinstall": "quasar prepare"
},
"dependencies": {
"@quasar/extras": "^1.17.0",
"axios": "^1.11.0",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.4.1",
"quasar": "^2.18.2",
"vue": "^3.5.18",
"vue-i18n": "^11.1.11",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@vue/test-utils": "^2.4.6",
"cypress": "^14.5.2",
"@eslint/js": "^9.14.0",
"eslint": "^9.31.0",
"eslint-plugin-vue": "^10.3.0",
"globals": "^15.12.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.2",
"vitest": "^3.2.4",
"vue-tsc": "^2.0.29",
"@vue/eslint-config-typescript": "^14.4.0",
"vite-plugin-checker": "^0.9.0",
"@types/node": "^20.5.9",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@quasar/app-vite": "^2.1.0",
"autoprefixer": "^10.4.2",
"typescript": "~5.5.3"
},
"engines": {
"node": "^28 || ^26 || ^24 || ^22 || ^20 || ^18",
"npm": ">= 6.13.4",
"yarn": ">= 1.21.1"
}
}

29
postcss.config.js Normal file
View File

@ -0,0 +1,29 @@
// https://github.com/michael-ciniawsky/postcss-load-config
import autoprefixer from 'autoprefixer'
// import rtlcss from 'postcss-rtlcss'
export default {
plugins: [
// https://github.com/postcss/autoprefixer
autoprefixer({
overrideBrowserslist: [
'last 4 Chrome versions',
'last 4 Firefox versions',
'last 4 Edge versions',
'last 4 Safari versions',
'last 4 Android versions',
'last 4 ChromeAndroid versions',
'last 4 FirefoxAndroid versions',
'last 4 iOS versions'
]
}),
// https://github.com/elchininet/postcss-rtlcss
// If you want to support RTL css, then
// 1. yarn/pnpm/bun/npm install postcss-rtlcss
// 2. optionally set quasar.config.js > framework > lang to an RTL language
// 3. uncomment the following line (and its import statement above):
// rtlcss()
]
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

235
quasar.config.ts Normal file
View File

@ -0,0 +1,235 @@
// Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file
import { defineConfig } from '#q-app/wrappers';
import { fileURLToPath } from 'node:url';
export default defineConfig((ctx) => {
return {
// https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
// preFetch: true,
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: [
'i18n',
'axios'
],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css
css: [
'app.scss'
],
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
// 'ionicons-v4',
// 'mdi-v7',
// 'fontawesome-v6',
// 'eva-icons',
// 'themify',
// 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
'roboto-font', // optional, you are not bound to it
'material-icons', // optional, you are not bound to it
],
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build
build: {
target: {
browser: [ 'es2022', 'firefox115', 'chrome115', 'safari14' ],
node: 'node20'
},
typescript: {
strict: true,
vueShim: true
// extendTsConfig (tsConfig) {}
},
vueRouterMode: 'hash', // available values: 'hash', 'history'
// vueRouterBase,
// vueDevtools,
// vueOptionsAPI: false,
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
// publicPath: '/',
// analyze: true,
// env: {},
// rawDefine: {}
// ignorePublicFolder: true,
// minify: false,
// polyfillModulePreload: true,
// distDir
// extendViteConf (viteConf) {},
// viteVuePluginOptions: {},
vitePlugins: [
['@intlify/unplugin-vue-i18n/vite', {
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
// compositionOnly: false,
// if you want to use named tokens in your Vue I18n messages, such as 'Hello {name}',
// you need to set `runtimeOnly: false`
// runtimeOnly: false,
ssr: ctx.modeName === 'ssr',
// you need to set i18n resource including paths !
include: [ fileURLToPath(new URL('./src/i18n', import.meta.url)) ]
}],
['vite-plugin-checker', {
vueTsc: true,
eslint: {
lintCommand: 'eslint -c ./eslint.config.js "./src*/**/*.{ts,js,mjs,cjs,vue}"',
useFlatConfig: true
}
}, { server: false }]
]
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver
devServer: {
// https: true,
open: true // opens browser window automatically
},
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework
framework: {
config: {},
// iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact
// (like functional components as one of the examples),
// you can manually specify Quasar components/directives to be available everywhere:
//
// components: [],
// directives: [],
// Quasar plugins
plugins: []
},
// animations: 'all', // --- includes all animations
// https://v2.quasar.dev/options/animations
animations: [],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#sourcefiles
// sourceFiles: {
// rootComponent: 'src/App.vue',
// router: 'src/router/index',
// store: 'src/store/index',
// pwaRegisterServiceWorker: 'src-pwa/register-service-worker',
// pwaServiceWorker: 'src-pwa/custom-service-worker',
// pwaManifestFile: 'src-pwa/manifest.json',
// electronMain: 'src-electron/electron-main',
// electronPreload: 'src-electron/electron-preload'
// bexManifestFile: 'src-bex/manifest.json
// },
// https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
ssr: {
prodPort: 3000, // The default port that the production server should use
// (gets superseded if process.env.PORT is specified at runtime)
middlewares: [
'render' // keep this as last one
],
// extendPackageJson (json) {},
// extendSSRWebserverConf (esbuildConf) {},
// manualStoreSerialization: true,
// manualStoreSsrContextInjection: true,
// manualStoreHydration: true,
// manualPostHydrationTrigger: true,
pwa: false
// pwaOfflineHtmlFilename: 'offline.html', // do NOT use index.html as name!
// pwaExtendGenerateSWOptions (cfg) {},
// pwaExtendInjectManifestOptions (cfg) {}
},
// https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
pwa: {
workboxMode: 'GenerateSW' // 'GenerateSW' or 'InjectManifest'
// swFilename: 'sw.js',
// manifestFilename: 'manifest.json',
// extendManifestJson (json) {},
// useCredentialsForManifestTag: true,
// injectPwaMetaTags: false,
// extendPWACustomSWConf (esbuildConf) {},
// extendGenerateSWOptions (cfg) {},
// extendInjectManifestOptions (cfg) {}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-cordova-apps/configuring-cordova
cordova: {
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
electron: {
// extendElectronMainConf (esbuildConf) {},
// extendElectronPreloadConf (esbuildConf) {},
// extendPackageJson (json) {},
// Electron preload scripts (if any) from /src-electron, WITHOUT file extension
preloadScripts: [ 'electron-preload' ],
// specify the debugging port to use for the Electron app when running in development mode
inspectPort: 5858,
bundler: 'packager', // 'packager' or 'builder'
packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',
// Windows only
// win32metadata: { ... }
},
builder: {
// https://www.electron.build/configuration/configuration
appId: 'quasar-project'
}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
bex: {
// extendBexScriptsConf (esbuildConf) {},
// extendBexManifestJson (json) {},
/**
* The list of extra scripts (js/ts) not in your bex manifest that you want to
* compile and use in your browser extension. Maybe dynamic use them?
*
* Each entry in the list should be a relative filename to /src-bex/
*
* @example [ 'my-script.ts', 'sub-folder/my-other-script.js' ]
*/
extraScripts: []
}
}
});

7
src/App.vue Normal file
View File

@ -0,0 +1,7 @@
<script setup lang="ts">
//
</script>
<template>
<router-view />
</template>

31
src/boot/axios.ts Normal file
View File

@ -0,0 +1,31 @@
import { defineBoot } from '#q-app/wrappers';
import axios, { type AxiosInstance } from 'axios';
declare module 'vue' {
interface ComponentCustomProperties {
$axios: AxiosInstance;
$api: AxiosInstance;
}
}
// Be careful when using SSR for cross-request state pollution
// due to creating a Singleton instance here;
// If any client changes this (global) instance, it might be a
// good idea to move this instance creation inside of the
// "export default () => {}" function below (which runs individually
// for each client)
const api = axios.create({ baseURL: 'https://api.example.com' });
export default defineBoot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api
app.config.globalProperties.$axios = axios;
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
// so you won't necessarily have to import axios in each vue file
app.config.globalProperties.$api = api;
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
// so you can easily perform requests against your app's API
});
export { api };

33
src/boot/i18n.ts Normal file
View File

@ -0,0 +1,33 @@
import { defineBoot } from '#q-app/wrappers';
import { createI18n } from 'vue-i18n';
import messages from 'src/modules/i18n';
export type MessageLanguages = keyof typeof messages;
// Type-define 'en-US' as the master schema for the resource
export type MessageSchema = typeof messages['en-CA'];
// See https://vue-i18n.intlify.dev/guide/advanced/typescript.html#global-resource-schema-type-definition
/* eslint-disable @typescript-eslint/no-empty-object-type */
declare module 'vue-i18n' {
// define the locale messages schema
export interface DefineLocaleMessage extends MessageSchema {}
// define the datetime format schema
export interface DefineDateTimeFormat {}
// define the number format schema
export interface DefineNumberFormat {}
}
/* eslint-enable @typescript-eslint/no-empty-object-type */
export default defineBoot(({ app }) => {
const i18n = createI18n<{ message: MessageSchema }, MessageLanguages>({
locale: 'en-CA',
legacy: false,
messages,
});
// Set i18n instance on app
app.use(i18n);
});

0
src/boot/oidc-client.ts Normal file
View File

7
src/env.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: string;
VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined;
VUE_ROUTER_BASE: string | undefined;
}
}

View File

@ -0,0 +1,17 @@
import { useAuthStore } from "src/modules/stores/auth.store";
export const useAuthAccess = () => {
const authStore = useAuthStore();
const isLoggedIn = async () => {
return authStore.hasAuthToken;
};
const isAuthorizedUser = async (email: string) => {
return authStore.isAuthorizedUser(email);
};
const forgotPassword = async (email: string) => {
return authStore.forgotPassword(email);
};
}

View File

@ -0,0 +1,25 @@
import { useAuthStore } from "src/modules/stores/auth.store";
export const useAuthSession = () => {
const authStore = useAuthStore();
const login = async () => {
return authStore.login();
};
const oidcLogin = async () => {
return authStore.oidcLogin();
};
const logout = async () => {
return authStore.logout();
};
const setUser = (user: Record<string, any>) => {
return authStore.setUser( user );
};
const setAuthToken = (token: string) => {
return authStore.setAuthToken( token );
};
}

View File

View File

View File

View File

1
src/modules/css/app.scss Normal file
View File

@ -0,0 +1 @@
// app global css in SCSS form

View File

@ -0,0 +1,25 @@
// Quasar SCSS (& Sass) Variables
// --------------------------------------------------
// To customize the look and feel of this app, you can override
// the Sass/SCSS variables found in Quasar's source Sass/SCSS files.
// Check documentation for full list of Quasar variables
// Your own variables (that are declared here) and Quasar's own
// ones will be available out of the box in your .vue/.scss/.sass files
// It's highly recommended to change the default colors
// to match your app's branding.
// Tip: Use the "Theme Builder" on Quasar's documentation website.
$primary : #1976D2;
$secondary : #26A69A;
$accent : #9C27B0;
$dark : #1D1D1D;
$dark-page : #121212;
$positive : #21BA45;
$negative : #C10015;
$info : #31CCEC;
$warning : #F2C037;

View File

View File

View File

View File

View File

View File

@ -0,0 +1,7 @@
// This is just an example,
// so you can safely delete all default props below
export default {
failed: 'Action failed',
success: 'Action was successful'
};

View File

@ -0,0 +1,5 @@
import enCA from './en-ca';
export default {
'en-ca': enCA
};

View File

@ -0,0 +1,37 @@
<template>
<q-layout view="lHh Lpr lFf" class="wrapper">
<NavBar />
<q-page-container :class="pageClass">
<router-view class="q-pa-sm" />
</q-page-container>
<FooterBar />
</q-layout>
</template>
<script lang="ts" setup>
import { RouterView } from 'vue-router';
import { useQuasar } from 'quasar';
import NavBar from 'src/modules/shared/components/navs/navBars/NavBar.vue';
import FooterBar from 'src/modules/shared/components/navs/footerBars/FooterBar.vue';
import { computed } from 'vue';
const $q = useQuasar();
const pageClass = computed(() => {
return !$q.screen.xs ? ' container' : 'full-width container';
});
</script>
<style lang="scss">
.wrapper {
display: flex;
flex-direction: column;
background-color: $grey-3;
}
.container {
flex-grow: 1;
width: 90%;
margin: 0 auto;
text-align: center;
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
<div>
<div style="font-size: 30vh">
404
</div>
<div class="text-h2" style="opacity:.4">
Oops. Nothing here...
</div>
<q-btn
class="q-mt-xl"
color="white"
text-color="blue"
unelevated
to="/"
label="Go Home"
no-caps
/>
</div>
</div>
</template>
<script setup lang="ts">
//
</script>

View File

View File

View File

@ -0,0 +1,35 @@
import { defineRouter } from '#q-app/wrappers';
import {
createMemoryHistory,
createRouter,
createWebHashHistory,
createWebHistory,
} from 'vue-router';
import routes from './routes';
/*
* If not building with SSR mode, you can
* directly export the Router instantiation;
*
* The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Router instance.
*/
export default defineRouter(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
// Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
history: createHistory(process.env.VUE_ROUTER_BASE),
});
return Router;
});

View File

@ -0,0 +1,18 @@
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [{ path: '', component: () => import('pages/IndexPage.vue') }],
},
// Always leave this as last one,
// but you can also remove it
{
path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue'),
},
];
export default routes;

View File

@ -0,0 +1,110 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useAuthStore } from 'src/modules/stores/auth.store';
// import { getInitials } from 'src/helpers/object';
const route = useRoute();
const router = useRouter();
const authStore = useAuthStore();
const userConnected = authStore.user;
const userRole: string = userConnected.role;
const leftDrawerOpen = ref(false);
function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value;
}
const goToUsers = () => {
router.replace('/users');
};
const goToShiftsValidations = () => {
router.replace('/time_sheet_validations');
};
const goToHome = () => {
router.replace('/');
};
// const goToHelp = () => {
// router.replace('/help');
// };
</script>
<template>
<q-footer bordered class="bg-white">
<q-tabs
no-caps
active-color="primary"
indicator-color="transparent"
class="text-grey-8"
>
<q-tab name="home" icon="home" @click="goToHome" />
<!-- <q-tab name="help" icon="help" @click="goToHelp" /> -->
<q-tab name="menu" icon="menu" @click="toggleLeftDrawer" />
<q-drawer v-model="leftDrawerOpen" side="right">
<q-scroll-area
style="
height: calc(100% - 150px);
margin-top: 150px;
border-right: 1px solid #ddd;
"
>
<q-list padding>
<q-item
clickable
v-ripple
:active="route.path === '/users'"
active-class="bg-primary text-white"
@click="goToUsers"
v-if="userRole === 'admin'"
>
<q-item-section avatar>
<q-icon name="list" />
</q-item-section>
<q-item-section> {{ $t('navBar.navItem_1') }} </q-item-section>
</q-item>
<q-item
clickable
v-ripple
:active="route.path === '/time_sheet_validations'"
active-class="bg-primary text-white"
@click="goToShiftsValidations"
v-if="userRole === 'admin'"
>
<q-item-section avatar>
<q-icon name="supervisor_account" />
</q-item-section>
<q-item-section>{{ $t('navBar.navItem_2') }} </q-item-section>
</q-item>
</q-list>
</q-scroll-area>
<q-img
class="absolute-top"
src="https://cdn.quasar.dev/img/material.png"
style="height: 150px"
>
<div class="absolute-bottom bg-transparent">
<!-- <q-avatar color="primary" size="68px" class="q-mb-sm">
{{
getInitials(
`${userConnected.first_name} ${userConnected.last_name}`,
)
}}</q-avatar -->
>
<div class="text-weight-bold">
{{ userConnected.firstName }} {{ userConnected.lastName }}
</div>
<div>{{ userConnected.email }}</div>
</div>
</q-img>
</q-drawer>
</q-tabs>
</q-footer>
</template>

View File

@ -0,0 +1,24 @@
<template>
<div class="gt-sm footer">
<div class="container">© {{ $t('footerLayout.title') }}</div>
</div>
</template>
<style scoped lang="scss">
.footer {
width: 100%;
height: 8em;
flex-shrink: 0;
margin: 3em auto 0;
justify-content: center;
}
.container {
padding: 1em;
width: 90%;
height: 100%;
border-top: 2px solid $primary;
display: flex;
align-items: center;
}
</style>

View File

@ -0,0 +1,10 @@
<script setup lang="ts">
import FooterBarMobile from './footer-bar-mobile.vue';
import FooterBarWeb from './footer-bar-web.vue';
</script>
<template>
<FooterBarWeb class="gt-sm" />
<FooterBarMobile class="lt-md" />
</template>

View File

@ -0,0 +1,47 @@
<script lang="ts" setup>
import { useRoute } from 'vue-router';
import { computed } from 'vue';
import { useAuthStore } from 'src/modules/stores/auth.store';
// import dialogs from 'src/components/dialogs';
const authStore = useAuthStore();
const user = authStore.user;
// const { NotificationsDialog, AccountDialog } = dialogs;
const route = useRoute();
const backRoutes = [
'newUser',
'userById',
'timeSheet',
'timeSheetValidationsId',
];
const isBackRoute = computed(
() => backRoutes.indexOf(route.name as string) !== -1,
);
</script>
<template>
<q-toolbar v-if="!isBackRoute">
<q-toolbar-title>
<!-- {{ $t('navBar.mobileIndexTitle') }} {{ user.first_name }} -->
</q-toolbar-title>
<NotificationsDialog />
<AccountDialog />
</q-toolbar>
<q-toolbar v-else>
<q-toolbar-title>
<q-btn
icon="chevron_left"
flat
round
dense
color="white"
@click="$router.go(-1)"
/>
</q-toolbar-title>
<div class="text-h6 text-white">
{{ $t(`pagesTitles.${route.meta.title}`) }}
</div>
<q-space />
</q-toolbar>
</template>

View File

@ -0,0 +1,136 @@
<template>
<q-toolbar>
<q-toolbar-title>
<RouterLink class="q-mr-sm navbar-brand" to="/">
<q-img
src="/public/icons/logo-targo-white.svg"
alt="logo"
style="width: 50px"
to="/"
></q-img
></RouterLink>
</q-toolbar-title>
<div>
<q-btn
class="q-mr-xs"
to="/users"
flat
color="white"
:label="$t('navBar.navItem_1')"
no-caps
v-if="userRole === 'admin'"
/>
<q-btn
class="q-mr-xs"
to="/time_sheet_validations"
flat
color="white"
:label="$t('navBar.navItem_2')"
no-caps
v-if="userRole === 'admin'"
/>
<LangSwitch class="q-mr-xs text-white" />
<NotificationsDialog />
<q-btn round color="white">
<q-avatar color="white" text-color="primary">{{
// getInitials(`${userConnected.first_name} ${userConnected.last_name}`)
}}</q-avatar>
<q-menu fit transition-show="flip-right" transition-hide="flip-left">
<q-list>
<q-item>
<div class="text-subtitle1 q-mb-xs text-no-wrap text-center">
<!-- {{ userConnected.first_name }} {{ userConnected.last_name }} -->
</div>
</q-item>
<q-separator />
<q-item v-ripple clickable @click="goToProfile">
<q-item-section avatar
><q-icon name="mdi-account" color="primary" size="2em"
/></q-item-section>
<q-item-section>{{ $t('navBar.menuItem_1') }}</q-item-section>
</q-item>
<!-- <q-item
v-if="userType === 'employee'"
v-ripple
clickable
@click="goToTimeSheet"
>
<q-item-section avatar>
<q-icon name="work_history" color="primary" size="2em" />
</q-item-section>
<q-item-section>{{ $t('navBar.menuItem_4') }}</q-item-section>
</q-item>
<q-item
v-if="userType === 'employee'"
v-ripple
clickable
@click="goToCalender"
>
<q-item-section avatar>
<q-icon name="calendar_month" color="primary" size="2em" />
</q-item-section>
<q-item-section>{{ $t('navBar.menuItem_5') }}</q-item-section>
</q-item> -->
<q-item v-ripple clickable @click="goToHelp">
<q-item-section avatar>
<q-icon name="help" color="primary" size="2em" />
</q-item-section>
<q-item-section>{{ $t('navBar.menuItem_2') }}</q-item-section>
</q-item>
<q-separator />
<q-item v-ripple clickable @click="handleLogout">
<q-item-section avatar>
<q-icon name="logout" color="primary" size="2em" />
</q-item-section>
<q-item-section>{{ $t('navBar.menuItem_3') }}</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</div>
</q-toolbar>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
// import { getInitials } from 'src/helpers/object';
import { useAuthStore } from 'src/modules/stores/auth.store';
import LangSwitch from 'src/components/LangSwitch.vue';
// import dialogs from 'src/components/dialogs';
// import authenticationApi from 'src/composables/useAuthentication';
// const { logout } = authenticationApi.useAuthUser();
// const { NotificationsDialog } = dialogs;
const authStore = useAuthStore();
const router = useRouter();
const $q = useQuasar();
const userConnected = authStore.user;
const userRole = userConnected.role;
// const userType = userConnected.type;
const goToProfile = () => {
router.replace('/profile');
};
const goToHelp = () => {
router.replace('/help');
};
const goToCalender = () => {
const pdfUrl = '/calendrier_annuel.pdf';
window.open(pdfUrl, '_blank');
};
const goToTimeSheet = () => {
router.replace('/time_sheet');
};
const handleLogout = async () => {
// const response = await logout();
// const { type, message } = response;
// $q.notify({ type, message });
};
</script>

View File

@ -0,0 +1,12 @@
<script lang="ts" setup>
import NavBarMobile from './nav-bar-mobile.vue';
import NavBarWeb from './nav-bar-web.vue';
</script>
<template>
<q-header elevated>
<NavBarMobile class="lt-md" />
<NavBarWeb class="gt-sm" />
</q-header>
</template>

View File

@ -0,0 +1,6 @@
export interface User {
firstName: string;
lastName: string;
email: string;
role: string;
}

View File

@ -0,0 +1,29 @@
export const deepEqual = (o1: any, o2: any) => {
if (o1 === o2) {
return true;
}
if (
o1 == null ||
o2 == null ||
typeof o1 !== 'object' ||
typeof o2 !== 'object'
) {
return false;
}
const keys1 = Object.keys(o1);
const keys2 = Object.keys(o2);
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
if (!deepEqual(o1[key], o2[key])) {
return false;
}
}
return true;
};

View File

@ -0,0 +1,25 @@
import { defineStore } from 'pinia';
export const useAlertStore = defineStore('alert', {
state: () => ({
message: '',
type: 'info' as 'success' | 'error' | 'info' | 'warning',
visible: false,
}),
actions: {
showAlert(msg: string, type: 'success' | 'error' | 'info' | 'warning' = 'info') {
this.message = msg;
this.type = type;
this.visible = true;
// Auto-hide after 3 seconds (optional)
setTimeout(() => this.hideAlert(), 3000);
},
hideAlert() {
this.visible = false;
this.message = '';
},
},
});

View File

@ -0,0 +1,60 @@
import { defineStore } from "pinia";
import router from "src/modules/router";
import { api } from "src/boot/axios";
import { User } from "../shared/components/models/models-user";
interface AuthState {
token: string | null;
user: User;
loading: boolean;
error: string | null;
}
export const useAuthStore = defineStore('auth', {
state: (): AuthState => ({
token: null,
user: {
firstName: 'Guest',
lastName: 'Guest',
email: 'guest@guest.com',
role: 'guest'
},
loading: false,
error: null,
}),
getters: {
hasAuthToken: (state) => !!state.token,
},
actions: {
async login() {
return "standard login";
},
async oidcLogin() {
return "openIDConnect login";
},
async logout() {
return "logout";
},
setAuthToken(token: string) {
return "setting auth token";
},
setUser(user: Record<string, any>) {
return "setting user info";
},
isAuthorizedUser(email: string) {
return "checking user authorization";
},
async forgotPassword(email: string) {
return "resetting password";
}
},
});

View File

@ -0,0 +1,32 @@
import { defineStore } from '#q-app/wrappers'
import { createPinia } from 'pinia'
/*
* When adding new properties to stores, you should also
* extend the `PiniaCustomProperties` interface.
* @see https://pinia.vuejs.org/core-concepts/plugins.html#typing-new-store-properties
*/
declare module 'pinia' {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface PiniaCustomProperties {
// add your custom properties here, if any
}
}
/*
* If not building with SSR mode, you can
* directly export the Store instantiation;
*
* The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Store instance.
*/
export default defineStore((/* { ssrContext } */) => {
const pinia = createPinia()
// You can add Pinia plugins here
// pinia.use(SomePiniaPlugin)
return pinia
})

10
src/modules/stores/store-flag.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import 'quasar/dist/types/feature-flag';
declare module 'quasar/dist/types/feature-flag' {
interface QuasarFeatureFlags {
store: true;
}
}

View File

View File

View File

View File

View File

View File

9
src/quasar.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
/* eslint-disable */
// Forces TS to apply `@quasar/app-vite` augmentations of `quasar` package
// Removing this would break `quasar/wrappers` imports as those typings are declared
// into `@quasar/app-vite`
// As a side effect, since `@quasar/app-vite` reference `quasar` to augment it,
// this declaration also apply `quasar` own
// augmentations (eg. adds `$q` into Vue component context)
/// <reference types="@quasar/app-vite" />

10
src/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/* eslint-disable */
/// <reference types="vite/client" />
// Mocks all files ending in `.vue` showing them as plain Vue instances
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

3
tsconfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "./.quasar/tsconfig.json"
}