Merge pull request 'Merge to master' (#21) from test into master
Reviewed-on: verdnatura/salix-front#21
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"plugins": ["@babel/plugin-syntax-dynamic-import"],
|
||||||
|
"env": {
|
||||||
|
"test": {
|
||||||
|
"plugins": ["dynamic-import-node"],
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
"modules": "commonjs",
|
||||||
|
"targets": {
|
||||||
|
"node": "current"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
> 1%
|
|
||||||
last 2 versions
|
|
||||||
not dead
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
|
@ -0,0 +1,8 @@
|
||||||
|
/dist
|
||||||
|
/src-bex/www
|
||||||
|
/src-capacitor
|
||||||
|
/src-cordova
|
||||||
|
/.quasar
|
||||||
|
/node_modules
|
||||||
|
.eslintrc.js
|
||||||
|
babel.config.js
|
103
.eslintrc.js
|
@ -1,31 +1,78 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
|
||||||
env: {
|
// This option interrupts the configuration hierarchy at this file
|
||||||
node: true,
|
// Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
|
||||||
},
|
root: true,
|
||||||
extends: [
|
|
||||||
"plugin:vue/vue3-essential",
|
parserOptions: {
|
||||||
"eslint:recommended",
|
parser: '@babel/eslint-parser',
|
||||||
"@vue/typescript/recommended",
|
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
|
||||||
"@vue/prettier",
|
sourceType: 'module', // Allows for the use of imports
|
||||||
"@vue/prettier/@typescript-eslint",
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
|
||||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
"**/__tests__/*.{j,t}s?(x)",
|
|
||||||
"**/tests/unit/**/*.spec.{j,t}s?(x)",
|
|
||||||
],
|
|
||||||
env: {
|
|
||||||
jest: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Rules order is important, please avoid shuffling them
|
||||||
|
extends: [
|
||||||
|
// Base ESLint recommended rules
|
||||||
|
'eslint:recommended',
|
||||||
|
|
||||||
|
// Uncomment any of the lines below to choose desired strictness,
|
||||||
|
// but leave only one uncommented!
|
||||||
|
// See https://eslint.vuejs.org/rules/#available-rules
|
||||||
|
//'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
|
||||||
|
'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
|
||||||
|
// 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
|
||||||
|
|
||||||
|
// https://github.com/prettier/eslint-config-prettier#installation
|
||||||
|
// usage with Prettier, provided by 'eslint-config-prettier'.
|
||||||
|
'prettier',
|
||||||
|
],
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files
|
||||||
|
// required to lint *.vue files
|
||||||
|
'vue',
|
||||||
|
|
||||||
|
// https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674
|
||||||
|
// Prettier has not been included as plugin to avoid performance impact
|
||||||
|
// add it as an extension for your IDE
|
||||||
|
],
|
||||||
|
|
||||||
|
globals: {
|
||||||
|
ga: 'readonly', // Google Analytics
|
||||||
|
cordova: 'readonly',
|
||||||
|
__statics: 'readonly',
|
||||||
|
__QUASAR_SSR__: 'readonly',
|
||||||
|
__QUASAR_SSR_SERVER__: 'readonly',
|
||||||
|
__QUASAR_SSR_CLIENT__: 'readonly',
|
||||||
|
__QUASAR_SSR_PWA__: 'readonly',
|
||||||
|
process: 'readonly',
|
||||||
|
Capacitor: 'readonly',
|
||||||
|
chrome: 'readonly',
|
||||||
|
defineProps: 'readonly', // Vue SFC setup compiler macro
|
||||||
|
defineEmits: 'readonly', // Vue SFC setup compiler macro
|
||||||
|
defineExpose: 'readonly', // Vue SFC setup compiler macro
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
},
|
||||||
|
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['**/*.spec.{js,ts}'],
|
||||||
|
extends: [
|
||||||
|
// Add Cypress-specific lint rules, globals and Cypress plugin
|
||||||
|
// See https://github.com/cypress-io/eslint-plugin-cypress#rules
|
||||||
|
'plugin:cypress/recommended',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,26 +1,33 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.thumbs.db
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
# Quasar core related directories
|
||||||
|
.quasar
|
||||||
/dist
|
/dist
|
||||||
|
|
||||||
/tests/e2e/videos/
|
# Cordova related directories and files
|
||||||
/tests/e2e/screenshots/
|
/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
|
||||||
|
|
||||||
# local env files
|
# BEX related directories and files
|
||||||
.env.local
|
/src-bex/www
|
||||||
.env.*.local
|
/src-bex/js/core
|
||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
// to edit target browsers: use "browserslist" field in package.json
|
||||||
|
require('autoprefixer'),
|
||||||
|
],
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
singleQuote: true,
|
||||||
|
printWidth: 120,
|
||||||
|
tabWidth: 4,
|
||||||
|
semi: true,
|
||||||
|
endOfLine: 'auto',
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"johnsoncodehk.volar",
|
||||||
|
"wayou.vscode-todo-highlight"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
"octref.vetur",
|
||||||
|
"hookyqr.beautify",
|
||||||
|
"dbaeumer.jshint",
|
||||||
|
"ms-vscode.vscode-typescript-tslint-plugin"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"editor.bracketPairColorization.enabled": true,
|
||||||
|
"editor.guides.bracketPairs": true,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "johnsoncodehk.volar",
|
||||||
|
"editor.codeActionsOnSave": ["source.fixAll.eslint"],
|
||||||
|
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
|
||||||
|
"json.schemas": [
|
||||||
|
{
|
||||||
|
"fileMatch": ["cypress.json"],
|
||||||
|
"url": "https://on.cypress.io/cypress.schema.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||||
|
},
|
||||||
|
"[vue]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
FROM node:stretch-slim
|
||||||
|
RUN npm install -g @quasar/cli
|
||||||
|
WORKDIR /app
|
||||||
|
COPY dist/spa ./
|
||||||
|
CMD ["quasar", "serve", "./", "--history", "--hostname", "0.0.0.0"]
|
|
@ -0,0 +1,100 @@
|
||||||
|
#!/usr/bin/env groovy
|
||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
options {
|
||||||
|
disableConcurrentBuilds()
|
||||||
|
}
|
||||||
|
environment {
|
||||||
|
PROJECT_NAME = 'lilium'
|
||||||
|
STACK_NAME = "${env.PROJECT_NAME}-${env.BRANCH_NAME}"
|
||||||
|
}
|
||||||
|
stages {
|
||||||
|
stage('Checkout') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
switch (env.BRANCH_NAME) {
|
||||||
|
case 'master':
|
||||||
|
env.NODE_ENV = 'production'
|
||||||
|
env.FRONT_REPLICAS = 2
|
||||||
|
break
|
||||||
|
case 'test':
|
||||||
|
env.NODE_ENV = 'test'
|
||||||
|
env.FRONT_REPLICAS = 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setEnv()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Install') {
|
||||||
|
environment {
|
||||||
|
NODE_ENV = ""
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
nodejs('node-v14') {
|
||||||
|
sh 'npm install -g @quasar/cli'
|
||||||
|
sh 'npm install --no-audit --prefer-offline'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Test') {
|
||||||
|
when { not { anyOf {
|
||||||
|
branch 'test'
|
||||||
|
branch 'master'
|
||||||
|
}}}
|
||||||
|
environment {
|
||||||
|
NODE_ENV = ""
|
||||||
|
}
|
||||||
|
parallel {
|
||||||
|
stage('Frontend') {
|
||||||
|
steps {
|
||||||
|
nodejs('node-v14') {
|
||||||
|
sh 'npm run test:unit:ci'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Build') {
|
||||||
|
when { anyOf {
|
||||||
|
branch 'test'
|
||||||
|
branch 'master'
|
||||||
|
}}
|
||||||
|
environment {
|
||||||
|
CREDENTIALS = credentials('docker-registry')
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
nodejs('node-v14') {
|
||||||
|
sh 'quasar build'
|
||||||
|
}
|
||||||
|
dockerBuild()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Deploy') {
|
||||||
|
when { anyOf {
|
||||||
|
branch 'test'
|
||||||
|
branch 'master'
|
||||||
|
}}
|
||||||
|
environment {
|
||||||
|
DOCKER_HOST = "${env.SWARM_HOST}"
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sh "docker stack deploy --with-registry-auth --compose-file docker-compose.yml ${env.STACK_NAME}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
script {
|
||||||
|
if (!['master', 'test'].contains(env.BRANCH_NAME)) {
|
||||||
|
try {
|
||||||
|
junit 'junitresults.xml'
|
||||||
|
junit 'junit.xml'
|
||||||
|
} catch (e) {
|
||||||
|
echo e.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
57
README.md
|
@ -1,34 +1,43 @@
|
||||||
# salix-front
|
# Salix (salix-front)
|
||||||
|
|
||||||
## Project setup
|
Salix front-end
|
||||||
```
|
|
||||||
|
## Install the dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn
|
||||||
|
# or
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compiles and hot-reloads for development
|
### Start the app in development mode (hot-code reloading, error reporting, etc.)
|
||||||
```
|
|
||||||
npm run serve
|
```bash
|
||||||
|
quasar dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compiles and minifies for production
|
### Lint the files
|
||||||
```
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run your unit tests
|
```bash
|
||||||
```
|
yarn lint
|
||||||
npm run test:unit
|
# or
|
||||||
```
|
|
||||||
|
|
||||||
### Run your end-to-end tests
|
|
||||||
```
|
|
||||||
npm run test:e2e
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lints and fixes files
|
|
||||||
```
|
|
||||||
npm run lint
|
npm run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
### Customize configuration
|
### Format the files
|
||||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
|
||||||
|
```bash
|
||||||
|
yarn format
|
||||||
|
# or
|
||||||
|
npm run format
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build the app for production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
quasar build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize the configuration
|
||||||
|
|
||||||
|
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js).
|
||||||
|
|
|
@ -1,3 +1,18 @@
|
||||||
|
/* eslint-env node */
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
let extend = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The .babelrc file has been created to assist Jest for transpiling.
|
||||||
|
* You should keep your application's babel rules in this file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (fs.existsSync('./.babelrc')) {
|
||||||
|
extend = './.babelrc';
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
presets: ["@vue/cli-plugin-babel/preset"],
|
presets: ['@quasar/babel-preset-app'],
|
||||||
|
extends: extend,
|
||||||
};
|
};
|
||||||
|
|
14
cypress.json
|
@ -1,3 +1,15 @@
|
||||||
{
|
{
|
||||||
"pluginsFile": "tests/e2e/plugins/index.js"
|
"baseUrl": "http://localhost:8080/",
|
||||||
|
"fixturesFolder": "tests/cypress/fixtures",
|
||||||
|
"integrationFolder": "tests/cypress/integration",
|
||||||
|
"pluginsFile": "tests/cypress/plugins/index.js",
|
||||||
|
"screenshotsFolder": "tests/cypress/screenshots",
|
||||||
|
"supportFile": "tests/cypress/support/index.js",
|
||||||
|
"videosFolder": "tests/cypress/videos",
|
||||||
|
"video": true,
|
||||||
|
"component": {
|
||||||
|
"componentFolder": "src",
|
||||||
|
"testFiles": "**/*.spec.js",
|
||||||
|
"supportFile": "tests/cypress/support/unit.js"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
version: '3.7'
|
||||||
|
services:
|
||||||
|
main:
|
||||||
|
image: registry.verdnatura.es/salix-frontend:${BRANCH_NAME:?}
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./Dockerfile
|
||||||
|
ports:
|
||||||
|
- 4000
|
||||||
|
deploy:
|
||||||
|
replicas: ${FRONT_REPLICAS:?}
|
||||||
|
placement:
|
||||||
|
constraints:
|
||||||
|
- node.role == worker
|
|
@ -1,6 +1,57 @@
|
||||||
|
const esModules = ['quasar', 'quasar/lang', 'lodash-es'].join('|');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
preset: "@vue/cli-plugin-unit-jest/presets/typescript-and-babel",
|
globals: {
|
||||||
transform: {
|
__DEV__: true,
|
||||||
"^.+\\.vue$": "vue-jest",
|
// TODO: Remove if resolved natively
|
||||||
},
|
// See https://github.com/vuejs/vue-jest/issues/175
|
||||||
|
'vue-jest': {
|
||||||
|
pug: { doctype: 'html' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Jest assumes we are testing in node environment, specify jsdom environment instead
|
||||||
|
testEnvironment: 'jsdom',
|
||||||
|
// noStackTrace: true,
|
||||||
|
// bail: true,
|
||||||
|
// cache: false,
|
||||||
|
// verbose: true,
|
||||||
|
// watch: true,
|
||||||
|
reporters: ['default', 'jest-junit'],
|
||||||
|
collectCoverage: false,
|
||||||
|
coverageDirectory: '<rootDir>/tests/jest/coverage',
|
||||||
|
collectCoverageFrom: ['<rootDir>/src/**/*.vue', '<rootDir>/src/**/*.js', '<rootDir>/src/**/*.jsx'],
|
||||||
|
// Needed in JS codebases too because of feature flags
|
||||||
|
coveragePathIgnorePatterns: ['/node_modules/', '.d.ts$'],
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
// branches: 50,
|
||||||
|
// functions: 50,
|
||||||
|
// lines: 50,
|
||||||
|
// statements: 50
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testMatch: ['<rootDir>/src/**/__tests__/*.(spec|test).+(ts|js)?(x)'],
|
||||||
|
moduleFileExtensions: ['vue', 'js', 'jsx', 'json'],
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^quasar$': 'quasar/dist/quasar.esm.prod.js',
|
||||||
|
'^~/(.*)$': '<rootDir>/$1',
|
||||||
|
'^src/(.*)$': '<rootDir>/src/$1',
|
||||||
|
'^app/(.*)$': '<rootDir>/$1',
|
||||||
|
'^components/(.*)$': '<rootDir>/src/components/$1',
|
||||||
|
'^layouts/(.*)$': '<rootDir>/src/layouts/$1',
|
||||||
|
'^pages/(.*)$': '<rootDir>/src/pages/$1',
|
||||||
|
'^assets/(.*)$': '<rootDir>/src/assets/$1',
|
||||||
|
'^boot/(.*)$': '<rootDir>/src/boot/$1',
|
||||||
|
'.*css$': '@quasar/quasar-app-extension-testing-unit-jest/stub.css',
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
'.*\\.vue$': 'vue-jest',
|
||||||
|
'.*\\.js$': 'babel-jest',
|
||||||
|
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
|
||||||
|
// use these if NPM is being flaky, care as hosting could interfere with these
|
||||||
|
// '.*\\.vue$': '@quasar/quasar-app-extension-testing-unit-jest/node_modules/vue-jest',
|
||||||
|
// '.*\\.js$': '@quasar/quasar-app-extension-testing-unit-jest/node_modules/babel-jest'
|
||||||
|
},
|
||||||
|
transformIgnorePatterns: [`node_modules/(?!(${esModules}))`],
|
||||||
|
snapshotSerializers: ['jest-serializer-vue'],
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"src/*": ["src/*"],
|
||||||
|
"app/*": ["*"],
|
||||||
|
"components/*": ["src/components/*"],
|
||||||
|
"layouts/*": ["src/layouts/*"],
|
||||||
|
"pages/*": ["src/pages/*"],
|
||||||
|
"assets/*": ["src/assets/*"],
|
||||||
|
"boot/*": ["src/boot/*"],
|
||||||
|
"stores/*": ["src/stores/*"],
|
||||||
|
"vue$": ["node_modules/vue/dist/vue.runtime.esm-bundler.js"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["dist", ".quasar", "node_modules"],
|
||||||
|
"vueCompilerOptions": {
|
||||||
|
"experimentalDisableTemplateSupport": true
|
||||||
|
}
|
||||||
|
}
|
103
package.json
|
@ -1,47 +1,60 @@
|
||||||
{
|
{
|
||||||
"name": "salix-front",
|
"name": "salix-front",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"description": "Salix front-end",
|
||||||
"scripts": {
|
"productName": "Salix",
|
||||||
"serve": "vue-cli-service serve",
|
"author": "Verdnatura",
|
||||||
"build": "vue-cli-service build",
|
"private": true,
|
||||||
"test:unit": "vue-cli-service test:unit",
|
"scripts": {
|
||||||
"test:e2e": "vue-cli-service test:e2e",
|
"lint": "eslint --ext .js,.vue ./",
|
||||||
"lint": "vue-cli-service lint"
|
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
|
||||||
},
|
"test": "echo \"See package.json => scripts for available tests.\" && exit 0",
|
||||||
"dependencies": {
|
"test:unit": "jest --reporters=default --watchAll",
|
||||||
"@quasar/extras": "^1.0.0",
|
"test:unit:ci": "jest --ci --reporters=default --reporters=jest-junit --maxWorkers=2",
|
||||||
"core-js": "^3.6.5",
|
"test:unit:coverage": "jest --coverage",
|
||||||
"quasar": "^2.0.0",
|
"serve:test:coverage": "quasar serve test/jest/coverage/lcov-report/ --port 8788",
|
||||||
"vue": "^3.0.0",
|
"concurrently:dev:jest": "concurrently \"quasar dev\" \"jest --watch\"",
|
||||||
"vue-class-component": "^8.0.0-0",
|
"test:e2e": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress open\"",
|
||||||
"vue-router": "^4.0.0-0",
|
"test:e2e:ci": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress run\""
|
||||||
"vuex": "^4.0.0-0"
|
},
|
||||||
},
|
"dependencies": {
|
||||||
"devDependencies": {
|
"@quasar/extras": "^1.14.0",
|
||||||
"@types/jest": "^24.0.19",
|
"axios": "^0.21.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
"core-js": "^3.6.5",
|
||||||
"@typescript-eslint/parser": "^4.18.0",
|
"quasar": "^2.7.3",
|
||||||
"@vue/cli-plugin-babel": "~4.5.15",
|
"vue": "^3.0.0",
|
||||||
"@vue/cli-plugin-e2e-cypress": "~4.5.15",
|
"vue-i18n": "^9.0.0",
|
||||||
"@vue/cli-plugin-eslint": "~4.5.15",
|
"vue-router": "^4.0.0"
|
||||||
"@vue/cli-plugin-router": "~4.5.15",
|
},
|
||||||
"@vue/cli-plugin-typescript": "~4.5.15",
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-unit-jest": "~4.5.15",
|
"@babel/eslint-parser": "^7.13.14",
|
||||||
"@vue/cli-plugin-vuex": "~4.5.15",
|
"@intlify/vue-i18n-loader": "^4.1.0",
|
||||||
"@vue/cli-service": "~4.5.15",
|
"@quasar/app-webpack": "^3.5.3",
|
||||||
"@vue/compiler-sfc": "^3.0.0",
|
"@quasar/quasar-app-extension-testing-e2e-cypress": "^4.1.2",
|
||||||
"@vue/eslint-config-prettier": "^6.0.0",
|
"@quasar/quasar-app-extension-testing-unit-jest": "^3.0.0-alpha.10",
|
||||||
"@vue/eslint-config-typescript": "^7.0.0",
|
"eslint": "^8.10.0",
|
||||||
"@vue/test-utils": "^2.0.0-0",
|
"eslint-config-prettier": "^8.1.0",
|
||||||
"eslint": "^6.7.2",
|
"eslint-plugin-cypress": "^2.11.3",
|
||||||
"eslint-plugin-prettier": "^3.3.1",
|
"eslint-plugin-jest": "^25.2.2",
|
||||||
"eslint-plugin-vue": "^7.0.0",
|
"eslint-plugin-vue": "^8.5.0",
|
||||||
"prettier": "^2.2.1",
|
"eslint-webpack-plugin": "^3.1.1",
|
||||||
"sass": "1.32.12",
|
"jest-junit": "^13.0.0",
|
||||||
"sass-loader": "^10.1.0",
|
"prettier": "^2.5.1"
|
||||||
"typescript": "~4.1.5",
|
},
|
||||||
"vue-cli-plugin-quasar": "~4.0.4",
|
"browserslist": [
|
||||||
"vue-jest": "^5.0.0-0"
|
"last 10 Chrome versions",
|
||||||
}
|
"last 10 Firefox versions",
|
||||||
|
"last 4 Edge versions",
|
||||||
|
"last 7 Safari versions",
|
||||||
|
"last 8 Android versions",
|
||||||
|
"last 8 ChromeAndroid versions",
|
||||||
|
"last 8 FirefoxAndroid versions",
|
||||||
|
"last 10 iOS versions",
|
||||||
|
"last 5 Opera versions"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.22.1",
|
||||||
|
"npm": ">= 6.13.4",
|
||||||
|
"yarn": ">= 1.21.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 316 B |
After Width: | Height: | Size: 735 B |
After Width: | Height: | Size: 3.8 KiB |
|
@ -1,17 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -0,0 +1,237 @@
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file runs in a Node context (it's NOT transpiled by Babel), so use only
|
||||||
|
* the ES6 features that are supported by your Node version. https://node.green/
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Configuration for your app
|
||||||
|
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js
|
||||||
|
|
||||||
|
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||||
|
|
||||||
|
const { configure } = require('quasar/wrappers');
|
||||||
|
|
||||||
|
module.exports = configure(function (ctx) {
|
||||||
|
return {
|
||||||
|
// https://v2.quasar.dev/quasar-cli-webpack/supporting-ts
|
||||||
|
supportTS: false,
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-webpack/prefetch-feature
|
||||||
|
// preFetch: true,
|
||||||
|
|
||||||
|
// app boot file (/src/boot)
|
||||||
|
// --> boot files are part of "main.js"
|
||||||
|
// https://v2.quasar.dev/quasar-cli-webpack/boot-files
|
||||||
|
boot: ['i18n', 'axios'],
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
|
||||||
|
css: ['app.scss'],
|
||||||
|
|
||||||
|
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||||
|
extras: [
|
||||||
|
// 'ionicons-v4',
|
||||||
|
// 'mdi-v5',
|
||||||
|
// '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-webpack/quasar-config-js#Property%3A-build
|
||||||
|
build: {
|
||||||
|
vueRouterMode: 'hash', // available values: 'hash', 'history'
|
||||||
|
|
||||||
|
// transpile: false,
|
||||||
|
// publicPath: '/',
|
||||||
|
|
||||||
|
// Add dependencies for transpiling with Babel (Array of string/regex)
|
||||||
|
// (from node_modules, which are by default not transpiled).
|
||||||
|
// Applies only if "transpile" is set to true.
|
||||||
|
// transpileDependencies: [],
|
||||||
|
|
||||||
|
// rtl: true, // https://quasar.dev/options/rtl-support
|
||||||
|
// preloadChunks: true,
|
||||||
|
// showProgress: false,
|
||||||
|
// gzip: true,
|
||||||
|
// analyze: true,
|
||||||
|
|
||||||
|
// Options below are automatically set depending on the env, set them if you want to override
|
||||||
|
// extractCSS: false,
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-webpack/handling-webpack
|
||||||
|
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
|
||||||
|
|
||||||
|
chainWebpack(chain) {
|
||||||
|
chain.module
|
||||||
|
.rule("i18n")
|
||||||
|
.resourceQuery(/blockType=i18n/)
|
||||||
|
.type('javascript/auto')
|
||||||
|
.use("i18n")
|
||||||
|
.loader("@intlify/vue-i18n-loader")
|
||||||
|
.end();
|
||||||
|
|
||||||
|
chain.plugin('eslint-webpack-plugin')
|
||||||
|
.use(ESLintPlugin, [{ extensions: ['js', 'vue'] }]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-devServer
|
||||||
|
devServer: {
|
||||||
|
server: {
|
||||||
|
type: 'http',
|
||||||
|
},
|
||||||
|
port: 8080,
|
||||||
|
open: true, // opens browser window automatically
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3000',
|
||||||
|
logLevel: 'debug',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-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: ['Notify', 'Dialog'],
|
||||||
|
},
|
||||||
|
|
||||||
|
// animations: 'all', // --- includes all animations
|
||||||
|
// https://quasar.dev/options/animations
|
||||||
|
animations: [],
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-webpack/developing-ssr/configuring-ssr
|
||||||
|
ssr: {
|
||||||
|
pwa: false,
|
||||||
|
|
||||||
|
// manualStoreHydration: true,
|
||||||
|
// manualPostHydrationTrigger: true,
|
||||||
|
|
||||||
|
prodPort: 3000, // The default port that the production server should use
|
||||||
|
// (gets superseded if process.env.PORT is specified at runtime)
|
||||||
|
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 30,
|
||||||
|
// Tell browser when a file from the server should expire from cache (in ms)
|
||||||
|
|
||||||
|
chainWebpackWebserver(chain) {
|
||||||
|
chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [{ extensions: ['js'] }]);
|
||||||
|
},
|
||||||
|
|
||||||
|
middlewares: [
|
||||||
|
ctx.prod ? 'compression' : '',
|
||||||
|
'render', // keep this as last one
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-webpack/developing-pwa/configuring-pwa
|
||||||
|
pwa: {
|
||||||
|
workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
|
||||||
|
workboxOptions: {}, // only for GenerateSW
|
||||||
|
|
||||||
|
// for the custom service worker ONLY (/src-pwa/custom-service-worker.[js|ts])
|
||||||
|
// if using workbox in InjectManifest mode
|
||||||
|
|
||||||
|
chainWebpackCustomSW(chain) {
|
||||||
|
chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [{ extensions: ['js'] }]);
|
||||||
|
},
|
||||||
|
|
||||||
|
manifest: {
|
||||||
|
name: `Salix`,
|
||||||
|
short_name: `Salix`,
|
||||||
|
description: `Salix front-end`,
|
||||||
|
display: 'standalone',
|
||||||
|
orientation: 'portrait',
|
||||||
|
background_color: '#ffffff',
|
||||||
|
theme_color: '#027be3',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: 'icons/icon-128x128.png',
|
||||||
|
sizes: '128x128',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'icons/icon-192x192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'icons/icon-256x256.png',
|
||||||
|
sizes: '256x256',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'icons/icon-384x384.png',
|
||||||
|
sizes: '384x384',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'icons/icon-512x512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/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-webpack/developing-capacitor-apps/configuring-capacitor
|
||||||
|
capacitor: {
|
||||||
|
hideSplashscreen: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-electron-apps/configuring-electron
|
||||||
|
electron: {
|
||||||
|
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: 'salix-front',
|
||||||
|
},
|
||||||
|
|
||||||
|
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
|
||||||
|
|
||||||
|
chainWebpackMain(chain) {
|
||||||
|
chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [{ extensions: ['js'] }]);
|
||||||
|
},
|
||||||
|
|
||||||
|
chainWebpackPreload(chain) {
|
||||||
|
chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [{ extensions: ['js'] }]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"@quasar/testing-unit-jest": {
|
||||||
|
"babel": "babelrc",
|
||||||
|
"options": ["scripts"]
|
||||||
|
},
|
||||||
|
"@quasar/testing-e2e-cypress": {
|
||||||
|
"options": ["scripts"]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"unit-jest": {
|
||||||
|
"runnerCommand": "jest --ci"
|
||||||
|
},
|
||||||
|
"e2e-cypress": {
|
||||||
|
"runnerCommand": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress run\""
|
||||||
|
},
|
||||||
|
"unit-cypress": {
|
||||||
|
"runnerCommand": "cypress run-ct"
|
||||||
|
}
|
||||||
|
}
|
186
src/App.vue
|
@ -1,114 +1,78 @@
|
||||||
|
<script setup>
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useSession } from 'src/composables/useSession';
|
||||||
|
|
||||||
|
const quasar = useQuasar();
|
||||||
|
const router = useRouter();
|
||||||
|
const session = useSession();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { isLoggedIn } = session;
|
||||||
|
|
||||||
|
quasar.iconMapFn = (iconName) => {
|
||||||
|
if (iconName.startsWith('vn:')) {
|
||||||
|
const name = iconName.substring(3);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cls: `icon-${name}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function responseError(error) {
|
||||||
|
let message = error.message;
|
||||||
|
let logOut = false;
|
||||||
|
|
||||||
|
switch (error.response?.status) {
|
||||||
|
case 401:
|
||||||
|
message = 'login.loginError';
|
||||||
|
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
message = 'errors.statusUnauthorized';
|
||||||
|
logOut = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 403:
|
||||||
|
message = 'errors.statusUnauthorized';
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
message = 'errors.statusInternalServerError';
|
||||||
|
break;
|
||||||
|
case 502:
|
||||||
|
message = 'errors.statusBadGateway';
|
||||||
|
break;
|
||||||
|
case 504:
|
||||||
|
message = 'errors.statusGatewayTimeout';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let translatedMessage = t(message);
|
||||||
|
if (!translatedMessage) translatedMessage = message;
|
||||||
|
|
||||||
|
quasar.notify({
|
||||||
|
message: translatedMessage,
|
||||||
|
type: 'negative',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (logOut) {
|
||||||
|
session.destroy();
|
||||||
|
router.push({ path: '/login' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.interceptors.response.use((response) => response, responseError);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-layout view="lHh Lpr lFf">
|
<router-view />
|
||||||
<q-header elevated class="glossy">
|
|
||||||
<q-toolbar>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
round
|
|
||||||
@click="leftDrawerOpen = !leftDrawerOpen"
|
|
||||||
aria-label="Menu"
|
|
||||||
icon="menu"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-toolbar-title> Quasar App </q-toolbar-title>
|
|
||||||
|
|
||||||
<div>Quasar v{{ $q.version }}</div>
|
|
||||||
</q-toolbar>
|
|
||||||
</q-header>
|
|
||||||
|
|
||||||
<q-drawer v-model="leftDrawerOpen" show-if-above bordered class="bg-grey-2">
|
|
||||||
<q-list>
|
|
||||||
<q-item-label header>Essential Links</q-item-label>
|
|
||||||
<q-item clickable tag="a" target="_blank" href="https://quasar.dev">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="school" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Docs</q-item-label>
|
|
||||||
<q-item-label caption>quasar.dev</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item
|
|
||||||
clickable
|
|
||||||
tag="a"
|
|
||||||
target="_blank"
|
|
||||||
href="https://github.com/quasarframework/"
|
|
||||||
>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="code" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Github</q-item-label>
|
|
||||||
<q-item-label caption>github.com/quasarframework</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item
|
|
||||||
clickable
|
|
||||||
tag="a"
|
|
||||||
target="_blank"
|
|
||||||
href="https://chat.quasar.dev"
|
|
||||||
>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="chat" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Discord Chat Channel</q-item-label>
|
|
||||||
<q-item-label caption>chat.quasar.dev</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item
|
|
||||||
clickable
|
|
||||||
tag="a"
|
|
||||||
target="_blank"
|
|
||||||
href="https://forum.quasar.dev"
|
|
||||||
>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="forum" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Forum</q-item-label>
|
|
||||||
<q-item-label caption>forum.quasar.dev</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item
|
|
||||||
clickable
|
|
||||||
tag="a"
|
|
||||||
target="_blank"
|
|
||||||
href="https://twitter.com/quasarframework"
|
|
||||||
>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="rss_feed" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Twitter</q-item-label>
|
|
||||||
<q-item-label caption>@quasarframework</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-drawer>
|
|
||||||
|
|
||||||
<q-page-container>
|
|
||||||
<HelloWorld />
|
|
||||||
</q-page-container>
|
|
||||||
</q-layout>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<style lang="scss">
|
||||||
import { ref } from "vue";
|
.body--light {
|
||||||
import HelloWorld from "./components/HelloWorld.vue";
|
background-color: #eee;
|
||||||
|
}
|
||||||
export default {
|
</style>
|
||||||
name: "LayoutDefault",
|
|
||||||
|
|
||||||
components: {
|
|
||||||
HelloWorld,
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
return {
|
|
||||||
leftDrawerOpen: ref(false),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
|
||||||
|
import { createWrapper } from 'app/tests/jest/jestHelpers';
|
||||||
|
import App from '../App.vue';
|
||||||
|
import { useSession } from 'src/composables/useSession';
|
||||||
|
|
||||||
|
const mockPush = jest.fn();
|
||||||
|
const mockLoggedIn = jest.fn();
|
||||||
|
const mockDestroy = jest.fn();
|
||||||
|
const session = useSession();
|
||||||
|
|
||||||
|
jest.mock('vue-router', () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
push: mockPush,
|
||||||
|
currentRoute: { value: 'myCurrentRoute' }
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('src/composables/useSession', () => ({
|
||||||
|
useSession: () => ({
|
||||||
|
isLoggedIn: mockLoggedIn,
|
||||||
|
destroy: mockDestroy
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('vue-i18n', () => ({
|
||||||
|
createI18n: () => { },
|
||||||
|
useI18n: () => ({
|
||||||
|
t: () => { }
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
let vm;
|
||||||
|
beforeAll(() => {
|
||||||
|
const options = {
|
||||||
|
global: {
|
||||||
|
stubs: ['router-view']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
vm = createWrapper(App, options).vm;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a login error message', async () => {
|
||||||
|
jest.spyOn(vm.quasar, 'notify');
|
||||||
|
|
||||||
|
session.isLoggedIn.mockReturnValue(false);
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
response: {
|
||||||
|
status: 401
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await vm.responseError(response);
|
||||||
|
|
||||||
|
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
|
||||||
|
{
|
||||||
|
type: 'negative',
|
||||||
|
message: 'login.loginError'
|
||||||
|
}
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an unauthorized error message', async () => {
|
||||||
|
jest.spyOn(vm.quasar, 'notify');
|
||||||
|
|
||||||
|
session.isLoggedIn.mockReturnValue(true);
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
response: {
|
||||||
|
status: 401
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await vm.responseError(response);
|
||||||
|
|
||||||
|
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
|
||||||
|
{
|
||||||
|
type: 'negative',
|
||||||
|
message: 'errors.statusUnauthorized'
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(session.destroy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
Before Width: | Height: | Size: 6.7 KiB |
|
@ -1,15 +1,70 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 356 360">
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<path
|
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
d="M43.4 303.4c0 3.8-2.3 6.3-7.1 6.3h-15v-22h14.4c4.3 0 6.2 2.2 6.2 5.2 0 2.6-1.5 4.4-3.4 5 2.8.4 4.9 2.5 4.9 5.5zm-8-13H24.1v6.9H35c2.1 0 4-1.3 4-3.8 0-2.2-1.3-3.1-3.7-3.1zm5.1 12.6c0-2.3-1.8-3.7-4-3.7H24.2v7.7h11.7c3.4 0 4.6-1.8 4.6-4zm36.3 4v2.7H56v-22h20.6v2.7H58.9v6.8h14.6v2.3H58.9v7.5h17.9zm23-5.8v8.5H97v-8.5l-11-13.4h3.4l8.9 11 8.8-11h3.4l-10.8 13.4zm19.1-1.8V298c0-7.9 5.2-10.7 12.7-10.7 7.5 0 13 2.8 13 10.7v1.4c0 7.9-5.5 10.8-13 10.8s-12.7-3-12.7-10.8zm22.7 0V298c0-5.7-3.9-8-10-8-6 0-9.8 2.3-9.8 8v1.4c0 5.8 3.8 8.1 9.8 8.1 6 0 10-2.3 10-8.1zm37.2-11.6v21.9h-2.9l-15.8-17.9v17.9h-2.8v-22h3l15.6 18v-18h2.9zm37.9 10.2v1.3c0 7.8-5.2 10.4-12.4 10.4H193v-22h11.2c7.2 0 12.4 2.8 12.4 10.3zm-3 0c0-5.3-3.3-7.6-9.4-7.6h-8.4V307h8.4c6 0 9.5-2 9.5-7.7V298zm50.8-7.6h-9.7v19.3h-3v-19.3h-9.7v-2.6h22.4v2.6zm34.4-2.6v21.9h-3v-10.1h-16.8v10h-2.8v-21.8h2.8v9.2H296v-9.2h2.9zm34.9 19.2v2.7h-20.7v-22h20.6v2.7H316v6.8h14.5v2.3H316v7.5h17.8zM24 340.2v7.3h13.9v2.4h-14v9.6H21v-22h20v2.7H24zm41.5 11.4h-9.8v7.9H53v-22h13.3c5.1 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6H66c3.1 0 5.3-1.5 5.3-4.7 0-3.3-2.2-4.1-5.3-4.1H55.7v8.8zm47.9 6.2H89l-2 4.3h-3.2l10.7-22.2H98l10.7 22.2h-3.2l-2-4.3zm-1-2.3l-6.3-13-6 13h12.2zm46.3-15.3v21.9H146v-17.2L135.7 358h-2.1l-10.2-15.6v17h-2.8v-21.8h3l11 16.9 11.3-17h3zm35 19.3v2.6h-20.7v-22h20.6v2.7H166v6.8h14.5v2.3H166v7.6h17.8zm47-19.3l-8.3 22h-3l-7.1-18.6-7 18.6h-3l-8.2-22h3.3L204 356l6.8-18.5h3.4L221 356l6.6-18.5h3.3zm10 11.6v-1.4c0-7.8 5.2-10.7 12.7-10.7 7.6 0 13 2.9 13 10.7v1.4c0 7.9-5.4 10.8-13 10.8-7.5 0-12.7-3-12.7-10.8zm22.8 0v-1.4c0-5.7-4-8-10-8s-9.9 2.3-9.9 8v1.4c0 5.8 3.8 8.2 9.8 8.2 6.1 0 10-2.4 10-8.2zm28.3 2.4h-9.8v7.9h-2.8v-22h13.2c5.2 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6h10.2c3 0 5.2-1.5 5.2-4.7 0-3.3-2.1-4.1-5.2-4.1h-10.2v8.8zm40.3-1.5l-6.8 5.6v6.4h-2.9v-22h2.9v12.3l15.2-12.2h3.7l-9.9 8.1 10.3 13.8h-3.6l-8.9-12z" />
|
<svg version="1.1"
|
||||||
<path fill="#050A14"
|
id="Capa_1" inkscape:version="0.91 r13725" sodipodi:docname="logo.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:ns="&ns_sfw;" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
d="M188.4 71.7a10.4 10.4 0 01-20.8 0 10.4 10.4 0 1120.8 0zM224.2 45c-2.2-3.9-5-7.5-8.2-10.7l-12 7c-3.7-3.2-8-5.7-12.6-7.3a49.4 49.4 0 00-9.7 13.9 59 59 0 0140.1 14l7.6-4.4a57 57 0 00-5.2-12.5zM178 125.1c4.5 0 9-.6 13.4-1.7v-14a40 40 0 0012.5-7.2 47.7 47.7 0 00-7.1-15.3 59 59 0 01-32.2 27.7v8.7c4.4 1.2 8.9 1.8 13.4 1.8zM131.8 45c-2.3 4-4 8.1-5.2 12.5l12 7a40 40 0 000 14.4c5.7 1.5 11.3 2 16.9 1.5a59 59 0 01-8-41.7l-7.5-4.3c-3.2 3.2-6 6.7-8.2 10.6z" />
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 400 168.6"
|
||||||
<path fill="#00B4FF"
|
style="enable-background:new 0 0 400 168.6;" xml:space="preserve">
|
||||||
d="M224.2 98.4c2.3-3.9 4-8 5.2-12.4l-12-7a40 40 0 000-14.5c-5.7-1.5-11.3-2-16.9-1.5a59 59 0 018 41.7l7.5 4.4c3.2-3.2 6-6.8 8.2-10.7zm-92.4 0c2.2 4 5 7.5 8.2 10.7l12-7a40 40 0 0012.6 7.3c4-4.1 7.3-8.8 9.7-13.8a59 59 0 01-40-14l-7.7 4.4c1.2 4.3 3 8.5 5.2 12.4zm46.2-80c-4.5 0-9 .5-13.4 1.7V34a40 40 0 00-12.5 7.2c1.5 5.7 4 10.8 7.1 15.4a59 59 0 0132.2-27.7V20a53.3 53.3 0 00-13.4-1.8z" />
|
<style type="text/css">
|
||||||
<path fill="#00B4FF"
|
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#3D3D3F;}
|
||||||
d="M178 9.2a62.6 62.6 0 11-.1 125.2A62.6 62.6 0 01178 9.2m0-9.2a71.7 71.7 0 100 143.5A71.7 71.7 0 00178 0z" />
|
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#8EBB27;}
|
||||||
<path fill="#050A14"
|
.st2{fill:#8EBB27;}
|
||||||
d="M96.6 212v4.3c-9.2-.8-15.4-5.8-15.4-17.8V180h4.6v18.4c0 8.6 4 12.6 10.8 13.5zm16-31.9v18.4c0 8.9-4.3 12.8-10.9 13.5v4.4c9.2-.7 15.5-5.6 15.5-18v-18.3h-4.7zM62.2 199v-2.2c0-12.7-8.8-17.4-21-17.4-12.1 0-20.7 4.7-20.7 17.4v2.2c0 12.8 8.6 17.6 20.7 17.6 1.5 0 3-.1 4.4-.3l11.8 6.2 2-3.3-8.2-4-6.4-3.1a32 32 0 01-3.6.2c-9.8 0-16-3.9-16-13.3v-2.2c0-9.3 6.2-13.1 16-13.1 9.9 0 16.3 3.8 16.3 13.1v2.2c0 5.3-2.1 8.7-5.6 10.8l4.8 2.4c3.4-2.8 5.5-7 5.5-13.2zM168 215.6h5.1L156 179.7h-4.8l17 36zM143 205l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.8-3.7H143zm133.7 10.7h5.2l-17.3-35.9h-4.8l17 36zm-25-10.7l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.7-3.7h-14.8zm73.8-2.5c6-1.2 9-5.4 9-11.4 0-8-4.5-10.9-12.9-10.9h-21.4v35.5h4.6v-31.3h16.5c5 0 8.5 1.4 8.5 6.7 0 5.2-3.5 7.7-8.5 7.7h-11.4v4.1h10.7l9.3 12.8h5.5l-9.9-13.2zm-117.4 9.9c-9.7 0-14.7-2.5-18.6-6.3l-2.2 3.8c5.1 5 11 6.7 21 6.7 1.6 0 3.1-.1 4.6-.3l-1.9-4h-3zm18.4-7c0-6.4-4.7-8.6-13.8-9.4l-10.1-1c-6.7-.7-9.3-2.2-9.3-5.6 0-2.5 1.4-4 4.6-5l-1.8-3.8c-4.7 1.4-7.5 4.2-7.5 8.9 0 5.2 3.4 8.7 13 9.6l11.3 1.2c6.4.6 8.9 2 8.9 5.4 0 2.7-2.1 4.7-6 5.8l1.8 3.9c5.3-1.6 8.9-4.7 8.9-10zm-20.3-21.9c7.9 0 13.3 1.8 18.1 5.7l1.8-3.9a30 30 0 00-19.6-5.9c-2 0-4 .1-5.7.3l1.9 4 3.5-.2z" />
|
.st3{fill:#F19300;}
|
||||||
<path fill="#00B4FF"
|
</style>
|
||||||
d="M.5 251.9c29.6-.5 59.2-.8 88.8-1l88.7-.3 88.7.3 44.4.4 44.4.6-44.4.6-44.4.4-88.7.3-88.7-.3a7981 7981 0 01-88.8-1z" />
|
<sodipodi:namedview bordercolor="#666666" borderopacity="1" fit-margin-bottom="0" fit-margin-left="0" fit-margin-right="0" fit-margin-top="0" gridtolerance="10" guidetolerance="10" id="namedview41" inkscape:current-layer="Capa_1" inkscape:cx="200" inkscape:cy="84.28212" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="1016" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="1920" inkscape:window-y="27" inkscape:zoom="3.09" objecttolerance="10" pagecolor="#ffffff" showgrid="false">
|
||||||
<path fill="none" d="M-565.2 324H-252v15.8h-313.2z" />
|
</sodipodi:namedview>
|
||||||
</svg>
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M106.1,40L92.3,0h10.9l5.6,20.6l0.5,1.7c0.7,2.5,1.2,4.5,1.6,6.2c0.2-0.8,0.4-1.8,0.7-2.9
|
||||||
|
c0.3-1.1,0.7-2.6,1.2-4.3L118.7,0h10.8l-13.9,40H106.1z"/>
|
||||||
|
<path class="st1" d="M386.1,40h-9.8c0-0.5,0.1-1,0.1-1.5l0.2-1.6c-1.7,1.4-3.5,2.4-5.2,3c-1.7,0.6-3.5,1-5.3,1
|
||||||
|
c-2.8,0-4.9-0.8-6.1-2.3c-1.2-1.6-1.5-3.7-0.7-6.3c0.7-2.4,1.9-4.4,3.6-6c1.7-1.5,4-2.6,6.8-3.2c1.5-0.3,3.5-0.7,5.8-1.1
|
||||||
|
c3.5-0.5,5.4-1.3,5.7-2.4l0.2-0.7c0.2-0.9,0.1-1.5-0.4-2c-0.5-0.4-1.4-0.7-2.7-0.7c-1.4,0-2.6,0.3-3.5,0.8c-1,0.6-1.7,1.4-2.2,2.5
|
||||||
|
h-8.9c1.4-3.3,3.5-5.8,6.2-7.5c2.7-1.6,6.2-2.4,10.5-2.4c2.6,0,4.7,0.3,6.4,1c1.6,0.6,2.8,1.6,3.4,2.9c0.4,0.9,0.6,2,0.6,3.3
|
||||||
|
c-0.1,1.3-0.5,3.3-1.3,6.2l-3.1,11.2c-0.4,1.3-0.5,2.4-0.5,3.2c0,0.8,0.2,1.3,0.7,1.5L386.1,40z M379.4,26.1
|
||||||
|
c-0.9,0.5-2.3,0.9-4.3,1.3c-1,0.2-1.7,0.3-2.2,0.5c-1.3,0.3-2.2,0.7-2.8,1.2c-0.6,0.5-1.1,1.2-1.3,2c-0.3,1.1-0.2,1.9,0.3,2.5
|
||||||
|
c0.5,0.6,1.2,1,2.3,1c1.7,0,3.1-0.5,4.4-1.4c1.3-1,2.2-2.2,2.6-3.7L379.4,26.1z"/>
|
||||||
|
<path class="st1" d="M337.3,40l8.3-29.5h9.3l-1.4,5.2c1.6-2,3.3-3.5,5.1-4.4c1.8-0.9,3.9-1.4,6.3-1.5l-2.7,9.6
|
||||||
|
c-0.4-0.1-0.8-0.1-1.2-0.1c-0.4,0-0.8,0-1.1,0c-1.5,0-2.8,0.2-3.9,0.7c-1.1,0.4-2.1,1.1-2.9,2.1c-0.5,0.6-1,1.5-1.5,2.6
|
||||||
|
c-0.5,1.1-1.1,3-1.8,5.6l-2.8,9.9H337.3z"/>
|
||||||
|
<path class="st1" d="M340.8,10.5L332.5,40h-9.5l1.1-4.1c-1.6,1.6-3.3,2.9-4.9,3.6c-1.7,0.8-3.5,1.2-5.4,1.2
|
||||||
|
c-3.3,0-5.5-0.8-6.7-2.5c-1.2-1.7-1.3-4.2-0.4-7.4l5.7-20.3h9.7L317.6,27c-0.7,2.4-0.8,4.1-0.5,5c0.4,0.9,1.3,1.4,2.8,1.4
|
||||||
|
c1.7,0,3.1-0.6,4.1-1.7c1.1-1.1,2-2.9,2.7-5.5l4.4-15.8H340.8z"/>
|
||||||
|
<path class="st1" d="M290.1,16.3l1.6-5.8h4l2.3-8.3h9.7l-2.3,8.3h5l-1.6,5.8h-5l-3.6,12.8c-0.5,2-0.7,3.3-0.3,3.9
|
||||||
|
c0.3,0.6,1.2,1,2.6,1l0.7,0l0.5,0l-1.7,6.2c-1.1,0.2-2.1,0.3-3.1,0.5c-1,0.1-2,0.2-2.9,0.2c-3.4,0-5.4-0.8-6.2-2.5
|
||||||
|
c-0.8-1.6-0.4-5.1,1.1-10.5l3.2-11.4H290.1z"/>
|
||||||
|
<path class="st1" d="M283.5,40h-9.8c0-0.5,0.1-1,0.1-1.5L274,37c-1.7,1.4-3.5,2.4-5.2,3c-1.7,0.6-3.5,1-5.3,1
|
||||||
|
c-2.8,0-4.9-0.8-6.1-2.3c-1.2-1.6-1.5-3.7-0.7-6.3c0.7-2.4,1.9-4.4,3.6-6c1.7-1.5,4-2.6,6.8-3.2c1.5-0.3,3.5-0.7,5.8-1.1
|
||||||
|
c3.5-0.5,5.4-1.3,5.7-2.4l0.2-0.7c0.2-0.9,0.1-1.5-0.4-2c-0.5-0.4-1.4-0.7-2.7-0.7c-1.4,0-2.6,0.3-3.5,0.8c-1,0.6-1.7,1.4-2.2,2.5
|
||||||
|
H261c1.4-3.3,3.5-5.8,6.2-7.5c2.7-1.6,6.2-2.4,10.5-2.4c2.6,0,4.7,0.3,6.4,1c1.6,0.6,2.8,1.6,3.4,2.9c0.4,0.9,0.6,2,0.6,3.3
|
||||||
|
c-0.1,1.3-0.5,3.3-1.3,6.2l-3.1,11.2c-0.4,1.3-0.5,2.4-0.5,3.2c0,0.8,0.2,1.3,0.7,1.5L283.5,40z M276.7,26.1
|
||||||
|
c-0.9,0.5-2.3,0.9-4.3,1.3c-1,0.2-1.7,0.3-2.2,0.5c-1.3,0.3-2.2,0.7-2.8,1.2c-0.6,0.5-1.1,1.2-1.3,2c-0.3,1.1-0.2,1.9,0.3,2.5
|
||||||
|
c0.5,0.6,1.2,1,2.3,1c1.7,0,3.1-0.5,4.4-1.4c1.3-1,2.2-2.2,2.6-3.7L276.7,26.1z"/>
|
||||||
|
<path class="st0" d="M219.6,0l-11.2,40h-9.7l1.1-3.9c-1.5,1.6-3.1,2.8-4.8,3.6c-1.6,0.8-3.4,1.2-5.3,1.2c-3.7,0-6.3-1.4-7.8-4.3
|
||||||
|
c-1.5-2.9-1.6-6.6-0.3-11.2c1.3-4.7,3.5-8.4,6.7-11.4c3.1-2.9,6.5-4.4,10.1-4.4c1.9,0,3.6,0.4,4.8,1.2c1.3,0.8,2.2,1.9,2.8,3.5
|
||||||
|
L210,0H219.6z M189.8,24.9c-0.7,2.6-0.8,4.7-0.2,6.1c0.6,1.4,1.8,2.1,3.7,2.1c1.8,0,3.4-0.7,4.8-2.1c1.3-1.4,2.4-3.4,3.1-6.1
|
||||||
|
c0.7-2.5,0.7-4.4,0.1-5.8c-0.6-1.4-1.8-2-3.7-2c-1.7,0-3.3,0.7-4.7,2.1C191.5,20.6,190.4,22.5,189.8,24.9z"/>
|
||||||
|
<path class="st0" d="M153.6,40l8.3-29.5h9.3l-1.4,5.2c1.6-2,3.3-3.5,5.1-4.4c1.8-0.9,7.9-1.4,10.3-1.5l-2.7,9.6
|
||||||
|
c-0.4-0.1-0.8-0.1-1.2-0.1c-0.4,0-0.8,0-1.1,0c-1.5,0-6.8,0.2-7.9,0.7c-1.1,0.4-2.1,1.1-2.9,2.1c-0.5,0.6-1,1.5-1.5,2.6
|
||||||
|
c-0.5,1.1-1.1,3-1.8,5.6l-2.8,9.9H153.6z"/>
|
||||||
|
<path class="st0" d="M143.5,30.7h9.3c-1.8,3.2-4.2,5.7-7.2,7.5c-3,1.8-6.4,2.7-10.2,2.7c-4.6,0-7.8-1.4-9.7-4.2
|
||||||
|
c-1.9-2.8-2.2-6.6-0.8-11.4c1.4-4.9,3.8-8.8,7.3-11.6c3.5-2.9,7.5-4.3,12-4.3c4.7,0,8,1.5,9.8,4.3c1.9,2.9,2.1,6.9,0.7,12
|
||||||
|
l-0.3,1.1l-0.2,0.6h-20c-0.6,2.1-0.6,3.7,0,4.8c0.6,1.1,1.8,1.6,3.5,1.6c1.3,0,2.4-0.3,3.4-0.8C142.1,32.6,142.9,31.8,143.5,30.7z
|
||||||
|
M135.4,22.1l11,0c0.5-1.9,0.4-3.4-0.3-4.4c-0.7-1.1-1.8-1.6-3.5-1.6c-1.6,0-3,0.5-4.3,1.6C137.1,18.6,136.1,20.1,135.4,22.1z"/>
|
||||||
|
<path class="st2" d="M241.2,40.4l-8.4-24.6l-8.5,24.6h-9.6l12.6-40h10.8L244,21l0.5,1.7c0.7,2.5,1.2,4.5,1.6,6.2l0.7-2.9
|
||||||
|
c0.3-1.1,0.7-2.6,1.2-4.3l5.9-21.2h10.8l-13.9,40H241.2z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st3" d="M106.1,54.4h4.8l48.9,113.9h-5.9L137,129H79.9l-16.8,39.3H57L106.1,54.4z M135.3,124.2l-26.8-62.7l-26.9,62.7
|
||||||
|
H135.3z"/>
|
||||||
|
<path class="st3" d="M178.1,168.3V54.4h5.6v108.7h69.8v5.1H178.1z"/>
|
||||||
|
<path class="st3" d="M271.1,168.3V54.4h5.6v113.9H271.1z"/>
|
||||||
|
<path class="st3" d="M300.2,54.4l42,53.6l42-53.6h6.4l-45.4,57.7l44.1,56.1H383l-40.7-52l-40.7,52h-6.7l44.1-56.1l-45.4-57.7
|
||||||
|
H300.2z"/>
|
||||||
|
<g>
|
||||||
|
<path class="st3" d="M5.8,168.3L5.3,163l0.2,2.7L5.3,163c0.4,0,10.4-1.1,18.9-11.8c10.5-13.1,14.1-35.2,10.5-63.9
|
||||||
|
C31,57.7,35.4,34.8,47.6,19.1C60.3,3,76.6,0.9,77.3,0.8l0.6,5.3c-0.1,0-11.9,1.6-22.4,12.1c-14,14-19.3,37.7-15.5,68.4
|
||||||
|
c3.8,30.7-0.1,53.6-11.8,68.1C18.3,167.1,6.3,168.2,5.8,168.3z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 6.2 KiB |
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="40mm" height="40mm" viewBox="0 0 40 40" version="1.1" id="svg823" inkscape:version="0.92.4 (5da689c313, 2019-01-14)" sodipodi:docname="logo.svg">
|
||||||
|
<defs id="defs817"/>
|
||||||
|
<sodipodi:namedview id="base" pagecolor="#2f2f2f" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:zoom="5.635625" inkscape:cx="70.551181" inkscape:cy="75.590551" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="false" inkscape:window-width="1920" inkscape:window-height="1043" inkscape:window-x="1920" inkscape:window-y="0" inkscape:window-maximized="1"/>
|
||||||
|
<metadata id="metadata820">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||||
|
<dc:title/>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(0,-257)">
|
||||||
|
<path style="fill:#88bd32;stroke-width:0.83333331" inkscape:connector-curvature="0" id="path4" d="m 27.583333,261.08333 c 0.25,-0.0833 0.5,-0.16666 0.75,-0.16666 L 39,259.5 l -0.166667,6.08333 c -0.166666,5 -3.083333,9.5 -6.666666,10.5 -0.25,0.0833 -0.5,0.16667 -0.75,0.16667 L 20.75,277.66667 l 0.166667,-6.08334 c 0.166666,-5.08333 3.083333,-9.5 6.666666,-10.5 z" class="st0"/>
|
||||||
|
<path style="fill:#88bd32;stroke-width:0.83333331" inkscape:connector-curvature="0" id="path6" d="m 5.9166667,281.91667 c 0.1666667,-0.0833 0.4166667,-0.0833 0.5833334,-0.0833 L 14.25,280.75 14.16667,285.08333 C 14.08334,288.75 11.91667,292 9.3333364,292.75 c -0.25,0.0833 -0.4166667,0.0833 -0.5833334,0.0833 L 1.0000001,293.91667 1.0833334,289.5 c 0.1666667,-3.58333 2.25,-6.91667 4.8333333,-7.58333 z" class="st0"/>
|
||||||
|
<g id="g10" style="fill:#f7931e;fill-opacity:1;stroke:none;stroke-opacity:1" transform="matrix(0.83333333,0,0,0.83333333,8.0000002e-8,257)">
|
||||||
|
<path style="fill:#f7931e;fill-opacity:1;stroke:none;stroke-opacity:1" inkscape:connector-curvature="0" id="path8" d="m 12,48 c -0.4,0 -0.7,-0.3 -0.7,-0.6 0,-0.4 0.2,-0.7 0.6,-0.8 0,0 3,-0.3 5.5,-3.4 3,-3.8 4.1,-10.1 3,-18.4 C 19.3,16.3 20.6,9.7 24.1,5.3 27.8,0.6 32.5,0 32.7,0 c 0.4,0 0.7,0.2 0.8,0.6 0,0.4 -0.2,0.7 -0.6,0.8 0,0 -4.3,0.6 -7.6,4.7 -3.3,4.2 -4.4,10.4 -3.4,18.5 1.1,8.8 0,15.4 -3.4,19.5 -2.8,3.5 -6.2,3.9 -6.5,3.9 0.1,0 0.1,0 0,0 z" class="st1"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useSession } from 'src/composables/useSession';
|
||||||
|
|
||||||
|
const { getToken } = useSession();
|
||||||
|
|
||||||
|
axios.defaults.baseURL = '/api/';
|
||||||
|
|
||||||
|
axios.interceptors.request.use(
|
||||||
|
function (context) {
|
||||||
|
const token = getToken();
|
||||||
|
|
||||||
|
if (token.length && context.headers) {
|
||||||
|
context.headers.Authorization = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { boot } from 'quasar/wrappers';
|
||||||
|
import { createI18n } from 'vue-i18n';
|
||||||
|
import messages from 'src/i18n';
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
locale: 'en',
|
||||||
|
messages,
|
||||||
|
legacy: false
|
||||||
|
});
|
||||||
|
|
||||||
|
export default boot(({ app }) => {
|
||||||
|
// Set i18n instance on app
|
||||||
|
app.use(i18n);
|
||||||
|
});
|
||||||
|
|
||||||
|
export { i18n };
|
|
@ -0,0 +1,55 @@
|
||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useNavigation } from 'src/composables/useNavigation';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
navigation.fetchFavorites();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-menu
|
||||||
|
anchor="bottom left"
|
||||||
|
class="row q-pa-md q-col-gutter-lg"
|
||||||
|
max-width="350px"
|
||||||
|
max-height="400px"
|
||||||
|
v-if="navigation.favorites.value.length"
|
||||||
|
>
|
||||||
|
<div v-for="module of navigation.favorites.value" :key="module.title" class="row no-wrap q-pa-xs flex-item">
|
||||||
|
<q-btn
|
||||||
|
align="evenly"
|
||||||
|
padding="16px"
|
||||||
|
flat
|
||||||
|
stack
|
||||||
|
size="lg"
|
||||||
|
:icon="module.icon"
|
||||||
|
color="orange-6"
|
||||||
|
class="col-4 button"
|
||||||
|
:to="{ name: module.stateName }"
|
||||||
|
>
|
||||||
|
<div class="text-center text-orange-6 button-text">
|
||||||
|
{{ t(`${module.name}.pageTitles.${module.title}`) }}
|
||||||
|
</div>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.flex-item {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
line-height: normal;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.button-text {
|
||||||
|
font-size: 10px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,17 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-page class="flex flex-center">
|
|
||||||
<img
|
|
||||||
alt="Quasar logo"
|
|
||||||
src="../assets/logo.svg"
|
|
||||||
style="width: 200px; height: 200px"
|
|
||||||
/>
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style></style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: "HelloWorld",
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -0,0 +1,175 @@
|
||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRole } from 'src/composables/useRole';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { useNavigation } from 'src/composables/useNavigation';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { hasAny } = useRole();
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
const quasar = useQuasar();
|
||||||
|
|
||||||
|
async function onToggleFavoriteModule(moduleName, event) {
|
||||||
|
await navigation.toggleFavorite(moduleName, event);
|
||||||
|
|
||||||
|
quasar.notify({
|
||||||
|
message: t('globals.dataSaved'),
|
||||||
|
type: 'positive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
:default-opened="true"
|
||||||
|
:label="t('globals.favoriteModules')"
|
||||||
|
v-if="navigation.favorites.value.length"
|
||||||
|
>
|
||||||
|
<q-list padding>
|
||||||
|
<template v-for="module in navigation.favorites.value" :key="module.title">
|
||||||
|
<div class="module" v-if="!module.children">
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
active-class="text-orange"
|
||||||
|
:key="module.title"
|
||||||
|
:to="{ name: module.stateName }"
|
||||||
|
v-if="!module.roles || !module.roles.length || hasAny(module.roles)"
|
||||||
|
>
|
||||||
|
<q-item-section avatar :if="module.icon">
|
||||||
|
<q-icon :name="module.icon" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>{{ t(`${module.name}.pageTitles.${module.title}`) }}</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<div @click="onToggleFavoriteModule(module.name, $event)" class="row items-center">
|
||||||
|
<q-icon name="vn:pin_off"></q-icon>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="module.children">
|
||||||
|
<q-expansion-item
|
||||||
|
class="module"
|
||||||
|
active-class="text-orange"
|
||||||
|
:label="t(`${module.name}.pageTitles.${module.title}`)"
|
||||||
|
v-if="!module.roles || !module.roles.length || hasAny(module.roles)"
|
||||||
|
:to="{ name: module.stateName }"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon :name="module.icon"></q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>{{ t(`${module.name}.pageTitles.${module.title}`) }}</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<div @click="onToggleFavoriteModule(module.name, $event)" class="row items-center">
|
||||||
|
<q-icon name="vn:pin_off"></q-icon>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
</template>
|
||||||
|
<template v-for="section in module.children" :key="section.title">
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
active-class="text-orange"
|
||||||
|
:to="{ name: section.stateName }"
|
||||||
|
v-if="!section.roles || !section.roles.length || hasAny(section.roles)"
|
||||||
|
>
|
||||||
|
<q-item-section avatar :if="section.icon">
|
||||||
|
<q-icon :name="section.icon" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>{{ t(`${module.name}.pageTitles.${section.title}`) }}</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</q-list>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-expansion-item :label="t('moduleIndex.allModules')">
|
||||||
|
<q-list padding>
|
||||||
|
<template v-for="module in navigation.modules.value" :key="module.title">
|
||||||
|
<div class="module" v-if="!module.children">
|
||||||
|
<q-item
|
||||||
|
class="module"
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
active-class="text-orange"
|
||||||
|
:key="module.title"
|
||||||
|
:to="{ name: module.stateName }"
|
||||||
|
v-if="!module.roles || !module.roles.length || hasAny(module.roles)"
|
||||||
|
>
|
||||||
|
<q-item-section avatar :if="module.icon">
|
||||||
|
<q-icon :name="module.icon" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>{{ t(`${module.name}.pageTitles.${module.title}`) }}</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<div
|
||||||
|
@click="onToggleFavoriteModule(module.name, $event)"
|
||||||
|
class="row items-center"
|
||||||
|
v-if="module.name != 'dashboard'"
|
||||||
|
>
|
||||||
|
<q-icon name="vn:pin"></q-icon>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="module.children">
|
||||||
|
<q-expansion-item
|
||||||
|
class="module"
|
||||||
|
active-class="text-orange"
|
||||||
|
:label="t(`${module.name}.pageTitles.${module.title}`)"
|
||||||
|
v-if="!module.roles || !module.roles.length || hasAny(module.roles)"
|
||||||
|
:to="{ name: module.stateName }"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon :name="module.icon"></q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>{{ t(`${module.name}.pageTitles.${module.title}`) }}</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<div
|
||||||
|
@click="onToggleFavoriteModule(module.name, $event)"
|
||||||
|
class="row items-center"
|
||||||
|
v-if="module.name != 'dashboard'"
|
||||||
|
>
|
||||||
|
<q-icon name="vn:pin"></q-icon>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
</template>
|
||||||
|
<template v-for="section in module.children" :key="section.title">
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
active-class="text-orange"
|
||||||
|
:to="{ name: section.stateName }"
|
||||||
|
v-if="!section.roles || !section.roles.length || hasAny(section.roles)"
|
||||||
|
>
|
||||||
|
<q-item-section avatar :if="section.icon">
|
||||||
|
<q-icon :name="section.icon" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>{{ t(`${module.name}.pageTitles.${section.title}`) }}</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</q-list>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.module .icon-pin,
|
||||||
|
.module .icon-pin_off {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.module:hover .icon-pin,
|
||||||
|
.module:hover .icon-pin_off {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,91 @@
|
||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useState } from 'src/composables/useState';
|
||||||
|
import { useSession } from 'src/composables/useSession';
|
||||||
|
import UserPanel from 'src/components/UserPanel.vue';
|
||||||
|
import FavoriteModules from './FavoriteModules.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const session = useSession();
|
||||||
|
const state = useState();
|
||||||
|
const user = state.getUser();
|
||||||
|
const token = session.getToken();
|
||||||
|
|
||||||
|
function onToggleDrawer() {
|
||||||
|
state.drawer.value = !state.drawer.value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-header class="bg-dark" color="white" elevated bordered>
|
||||||
|
<q-toolbar class="q-py-sm q-px-md">
|
||||||
|
<q-btn flat @click="onToggleDrawer()" round dense icon="menu">
|
||||||
|
<q-tooltip bottom anchor="bottom right">
|
||||||
|
{{ t('globals.collapseMenu') }}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<router-link to="/">
|
||||||
|
<q-btn flat round class="q-ml-xs" v-if="$q.screen.gt.xs">
|
||||||
|
<q-avatar square size="md">
|
||||||
|
<q-img src="~/assets/logo_icon.svg" alt="Logo" />
|
||||||
|
</q-avatar>
|
||||||
|
<q-tooltip bottom>
|
||||||
|
{{ t('globals.backToDashboard') }}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</router-link>
|
||||||
|
<q-toolbar-title shrink class="text-weight-bold">Salix</q-toolbar-title>
|
||||||
|
<q-space></q-space>
|
||||||
|
<div class="q-pl-sm q-gutter-sm row items-center no-wrap">
|
||||||
|
<!-- <q-btn v-if="$q.screen.gt.xs" dense flat size="md" icon="add">
|
||||||
|
<q-menu>
|
||||||
|
<q-list style="min-width: 150px">
|
||||||
|
<q-item :to="{ path: '/customer/create' }" clickable>
|
||||||
|
<q-item-section>New customer</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable>
|
||||||
|
<q-item-section>New ticket</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn> -->
|
||||||
|
<!-- <q-btn v-if="$q.screen.gt.xs" dense flat round size="md" icon="notifications">
|
||||||
|
<q-badge color="red" text-color="white" floating> 2 </q-badge>
|
||||||
|
<q-tooltip bottom>
|
||||||
|
{{ t('globals.notifications') }}
|
||||||
|
</q-tooltip>
|
||||||
|
<q-menu class="q-pa-md" style="min-width: 250px">
|
||||||
|
<strong>Notifications</strong>
|
||||||
|
<q-separator />
|
||||||
|
<div style="text-align: center; font-size: 2em">
|
||||||
|
<q-spinner-puff color="orange" />
|
||||||
|
</div>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn> -->
|
||||||
|
<q-btn dense flat no-wrap id="favoriteModules">
|
||||||
|
<q-avatar size="lg">
|
||||||
|
<q-icon name="apps" size="s" />
|
||||||
|
</q-avatar>
|
||||||
|
<q-tooltip bottom>
|
||||||
|
{{ t('globals.favoriteModules') }}
|
||||||
|
</q-tooltip>
|
||||||
|
<FavoriteModules />
|
||||||
|
</q-btn>
|
||||||
|
<q-btn dense flat no-wrap id="user">
|
||||||
|
<q-avatar size="lg">
|
||||||
|
<q-img
|
||||||
|
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
|
||||||
|
spinner-color="white"
|
||||||
|
>
|
||||||
|
</q-img>
|
||||||
|
</q-avatar>
|
||||||
|
<q-icon name="arrow_drop_down" size="s" />
|
||||||
|
<q-tooltip bottom>
|
||||||
|
{{ t('globals.userPanel') }}
|
||||||
|
</q-tooltip>
|
||||||
|
<UserPanel />
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-toolbar>
|
||||||
|
</q-header>
|
||||||
|
</template>
|
|
@ -0,0 +1,150 @@
|
||||||
|
<script setup>
|
||||||
|
import axios from 'axios';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const $props = defineProps({
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
autoLoad: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
sortBy: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
rowsPerPage: {
|
||||||
|
type: Number,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
offset: {
|
||||||
|
type: Number,
|
||||||
|
default: 500,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(['onNavigate']);
|
||||||
|
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const hasMoreData = ref(false);
|
||||||
|
const pagination = ref({
|
||||||
|
sortBy: $props.sortBy,
|
||||||
|
rowsPerPage: $props.rowsPerPage,
|
||||||
|
page: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = ref([]);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if ($props.autoLoad) fetch();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetch() {
|
||||||
|
const { page, rowsPerPage, sortBy, descending } = pagination.value;
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
limit: rowsPerPage,
|
||||||
|
skip: rowsPerPage * (page - 1),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sortBy) filter.order = sortBy;
|
||||||
|
|
||||||
|
const { data } = await axios.get($props.url, {
|
||||||
|
params: { filter },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
isLoading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMoreData.value = data.length === rowsPerPage;
|
||||||
|
|
||||||
|
for (const row of data) rows.value.push(row);
|
||||||
|
|
||||||
|
pagination.value.rowsNumber = rows.value.length;
|
||||||
|
pagination.value.page = page;
|
||||||
|
pagination.value.rowsPerPage = rowsPerPage;
|
||||||
|
pagination.value.sortBy = sortBy;
|
||||||
|
pagination.value.descending = descending;
|
||||||
|
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onLoad(...params) {
|
||||||
|
const done = params[1];
|
||||||
|
if (rows.value.length === 0) return done(false);
|
||||||
|
|
||||||
|
pagination.value.page = pagination.value.page + 1;
|
||||||
|
|
||||||
|
await fetch();
|
||||||
|
|
||||||
|
const endOfPages = !hasMoreData.value;
|
||||||
|
done(endOfPages);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-infinite-scroll @load="onLoad" :offset="offset" class="column items-center">
|
||||||
|
<div class="card-list q-gutter-y-md">
|
||||||
|
<q-card class="card" v-for="row of rows" :key="row.id">
|
||||||
|
<q-item v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
|
||||||
|
<q-item-section class="q-pa-md" @click="$emit('onNavigate', row.id)">
|
||||||
|
<slot name="header" :row="row">
|
||||||
|
<div class="text-h6">{{ row.name }}</div>
|
||||||
|
<q-item-label caption>#{{ row.id }}</q-item-label>
|
||||||
|
</slot>
|
||||||
|
<slot name="labels" :row="row"></slot>
|
||||||
|
</q-item-section>
|
||||||
|
<q-separator vertical />
|
||||||
|
<q-card-actions vertical class="justify-between">
|
||||||
|
<slot name="actions" :row="row">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
color="orange"
|
||||||
|
icon="arrow_circle_right"
|
||||||
|
@click="$emit('onNavigate', row.id)"
|
||||||
|
>
|
||||||
|
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn flat round color="grey-7" icon="preview">
|
||||||
|
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</slot>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-item>
|
||||||
|
</q-card>
|
||||||
|
<div v-if="!rows.length && !isLoading" class="info-row q-pa-md text-center">
|
||||||
|
<h5>
|
||||||
|
{{ t('components.smartCard.noData') }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div v-if="isLoading" class="info-row q-pa-md text-center">
|
||||||
|
<q-spinner color="orange" size="md" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-infinite-scroll>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card-list {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 60em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,121 @@
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, computed } from 'vue';
|
||||||
|
import { Dark } from 'quasar';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { useState } from 'src/composables/useState';
|
||||||
|
import { useSession } from 'src/composables/useSession';
|
||||||
|
|
||||||
|
const state = useState();
|
||||||
|
const session = useSession();
|
||||||
|
const router = useRouter();
|
||||||
|
const { t, locale } = useI18n();
|
||||||
|
|
||||||
|
const darkMode = computed({
|
||||||
|
get() {
|
||||||
|
return Dark.isActive;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
Dark.set(value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = state.getUser();
|
||||||
|
const token = session.getToken();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
updatePreferences();
|
||||||
|
});
|
||||||
|
|
||||||
|
function updatePreferences() {
|
||||||
|
if (user.value.darkMode) {
|
||||||
|
darkMode.value = user.value.darkMode;
|
||||||
|
}
|
||||||
|
if (user.value.lang) {
|
||||||
|
locale.value = user.value.lang;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDarkMode(value) {
|
||||||
|
const query = `/UserConfigs/${user.value.id}`;
|
||||||
|
await axios.patch(query, {
|
||||||
|
darkMode: value,
|
||||||
|
});
|
||||||
|
user.value.darkMode = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveLanguage(value) {
|
||||||
|
const query = `/Accounts/${user.value.id}`;
|
||||||
|
await axios.patch(query, {
|
||||||
|
lang: value,
|
||||||
|
});
|
||||||
|
user.value.lang = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
session.destroy();
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-menu anchor="bottom left">
|
||||||
|
<div class="row no-wrap q-pa-md">
|
||||||
|
<div class="column panel">
|
||||||
|
<div class="text-h6 q-mb-md">{{ t('components.userPanel.settings') }}</div>
|
||||||
|
<q-toggle
|
||||||
|
v-model="locale"
|
||||||
|
@update:model-value="saveLanguage"
|
||||||
|
:label="t(`globals.lang['${locale}']`)"
|
||||||
|
icon="public"
|
||||||
|
color="orange"
|
||||||
|
false-value="es"
|
||||||
|
true-value="en"
|
||||||
|
/>
|
||||||
|
<q-toggle
|
||||||
|
v-model="darkMode"
|
||||||
|
@update:model-value="saveDarkMode"
|
||||||
|
:label="t(`globals.theme`)"
|
||||||
|
checked-icon="dark_mode"
|
||||||
|
color="orange"
|
||||||
|
unchecked-icon="light_mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-separator vertical inset class="q-mx-lg" />
|
||||||
|
|
||||||
|
<div class="column items-center panel">
|
||||||
|
<q-avatar size="80px">
|
||||||
|
<q-img
|
||||||
|
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
|
||||||
|
spinner-color="white"
|
||||||
|
/>
|
||||||
|
</q-avatar>
|
||||||
|
|
||||||
|
<div class="text-subtitle1 q-mt-md">
|
||||||
|
<strong>{{ user.nickname }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="text-subtitle3 text-grey-7 q-mb-xs">@{{ user.name }}</div>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
id="logout"
|
||||||
|
color="orange"
|
||||||
|
flat
|
||||||
|
:label="t('globals.logOut')"
|
||||||
|
size="sm"
|
||||||
|
icon="logout"
|
||||||
|
@click="logout()"
|
||||||
|
v-close-popup
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.panel {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
|
||||||
|
import { createWrapper } from 'app/tests/jest/jestHelpers';
|
||||||
|
import Leftmenu from '../LeftMenu.vue';
|
||||||
|
|
||||||
|
const mockPush = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('vue-router', () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
push: mockPush,
|
||||||
|
currentRoute: { value: 'myCurrentRoute' }
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('src/router/routes', () => ([
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Main',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/dashboard',
|
||||||
|
name: 'Dashboard',
|
||||||
|
meta: { title: 'dashboard', icon: 'dashboard' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/customer',
|
||||||
|
name: 'Customer',
|
||||||
|
meta: {
|
||||||
|
title: 'customers',
|
||||||
|
icon: 'vn:client'
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'CustomerMain',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'list',
|
||||||
|
name: 'CustomerList',
|
||||||
|
meta: {
|
||||||
|
title: 'list',
|
||||||
|
icon: 'view_list',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
name: 'CustomerCreate',
|
||||||
|
meta: {
|
||||||
|
title: 'createCustomer',
|
||||||
|
icon: 'vn:addperson',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
|
||||||
|
describe('Leftmenu', () => {
|
||||||
|
let vm;
|
||||||
|
beforeAll(() => {
|
||||||
|
vm = createWrapper(Leftmenu).vm;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the proper formated object without the children property', async () => {
|
||||||
|
const expectedMenuItem = {
|
||||||
|
stateName: 'Dashboard',
|
||||||
|
name: 'dashboard',
|
||||||
|
roles: [],
|
||||||
|
icon: 'dashboard',
|
||||||
|
title: 'dashboard'
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstMenuItem = vm.navigation.modules.value[0];
|
||||||
|
expect(firstMenuItem.children).toBeUndefined();
|
||||||
|
expect(firstMenuItem).toEqual(expect.objectContaining(expectedMenuItem));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a proper formated object with two child items', async () => {
|
||||||
|
const expectedMenuItem = [{
|
||||||
|
name: 'CustomerList',
|
||||||
|
title: 'list',
|
||||||
|
icon: 'view_list',
|
||||||
|
stateName: 'CustomerList'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CustomerCreate',
|
||||||
|
title: 'createCustomer',
|
||||||
|
icon: 'vn:addperson',
|
||||||
|
stateName: 'CustomerCreate'
|
||||||
|
}];
|
||||||
|
|
||||||
|
const secondMenuItem = vm.navigation.modules.value[1];
|
||||||
|
expect(secondMenuItem.children).toEqual(expect.arrayContaining(expectedMenuItem));
|
||||||
|
expect(secondMenuItem.children.length).toEqual(2)
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,151 @@
|
||||||
|
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
|
||||||
|
import { createWrapper, axios } from 'app/tests/jest/jestHelpers';
|
||||||
|
import SmartCard from '../SmartCard.vue';
|
||||||
|
|
||||||
|
const mockPush = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('vue-router', () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
push: mockPush,
|
||||||
|
currentRoute: { value: 'myCurrentRoute' }
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('SmartCard', () => {
|
||||||
|
const expectedUrl = '/api/customers';
|
||||||
|
let vm;
|
||||||
|
beforeAll(() => {
|
||||||
|
const options = {
|
||||||
|
attrs: {
|
||||||
|
url: expectedUrl,
|
||||||
|
sortBy: 'id DESC',
|
||||||
|
rowsPerPage: 3
|
||||||
|
}
|
||||||
|
};
|
||||||
|
vm = createWrapper(SmartCard, options).vm;
|
||||||
|
|
||||||
|
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||||
|
data: [
|
||||||
|
{ id: 1, name: 'Tony Stark' },
|
||||||
|
{ id: 2, name: 'Jessica Jones' },
|
||||||
|
{ id: 3, name: 'Bruce Wayne' },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vm.rows = [];
|
||||||
|
vm.pagination.page = 1;
|
||||||
|
vm.hasMoreData = true;
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fetch()', () => {
|
||||||
|
it('should call to the fetch() method and set the data on the rows property', async () => {
|
||||||
|
const expectedOptions = {
|
||||||
|
params: {
|
||||||
|
filter: {
|
||||||
|
order: 'id DESC',
|
||||||
|
limit: 3,
|
||||||
|
skip: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await vm.fetch();
|
||||||
|
|
||||||
|
expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||||
|
expect(vm.rows.length).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call to the fetch() method and then call it again to paginate', async () => {
|
||||||
|
const expectedOptions = {
|
||||||
|
params: {
|
||||||
|
filter: {
|
||||||
|
order: 'id DESC',
|
||||||
|
limit: 3,
|
||||||
|
skip: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await vm.fetch();
|
||||||
|
|
||||||
|
expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||||
|
expect(vm.rows.length).toEqual(3);
|
||||||
|
|
||||||
|
const expectedOptionsPaginated = {
|
||||||
|
params: {
|
||||||
|
filter: {
|
||||||
|
order: 'id DESC',
|
||||||
|
limit: 3,
|
||||||
|
skip: 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.pagination.page = 2;
|
||||||
|
|
||||||
|
await vm.fetch();
|
||||||
|
|
||||||
|
expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptionsPaginated);
|
||||||
|
expect(vm.rows.length).toEqual(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onLoad()', () => {
|
||||||
|
it('should call to the done() callback and not increment the pagination', async () => {
|
||||||
|
const index = 1;
|
||||||
|
const done = jest.fn();
|
||||||
|
|
||||||
|
await vm.onLoad(index, done);
|
||||||
|
|
||||||
|
expect(vm.pagination.page).toEqual(1);
|
||||||
|
expect(done).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should increment the pagination and then call to the done() callback', async () => {
|
||||||
|
vm.rows = [
|
||||||
|
{ id: 1, name: 'Tony Stark' },
|
||||||
|
{ id: 2, name: 'Jessica Jones' },
|
||||||
|
{ id: 3, name: 'Bruce Wayne' },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(vm.pagination.page).toEqual(1);
|
||||||
|
|
||||||
|
const index = 1;
|
||||||
|
const done = jest.fn();
|
||||||
|
|
||||||
|
await vm.onLoad(index, done);
|
||||||
|
|
||||||
|
expect(vm.pagination.page).toEqual(2);
|
||||||
|
expect(done).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call to the done() callback with true as argument to finish pagination', async () => {
|
||||||
|
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||||
|
data: [
|
||||||
|
{ id: 1, name: 'Tony Stark' },
|
||||||
|
{ id: 2, name: 'Jessica Jones' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
vm.rows = [
|
||||||
|
{ id: 1, name: 'Tony Stark' },
|
||||||
|
{ id: 2, name: 'Jessica Jones' },
|
||||||
|
{ id: 3, name: 'Bruce Wayne' },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(vm.pagination.page).toEqual(1);
|
||||||
|
|
||||||
|
const index = 1;
|
||||||
|
const done = jest.fn();
|
||||||
|
|
||||||
|
vm.hasMoreData = false;
|
||||||
|
|
||||||
|
await vm.onLoad(index, done);
|
||||||
|
|
||||||
|
expect(vm.pagination.page).toEqual(2);
|
||||||
|
expect(done).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { describe, expect, it } from '@jest/globals';
|
||||||
|
import { useNavigation } from '../useNavigation';
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
describe('useNavigation', () => {
|
||||||
|
it('should return the routes for all modules', async () => {
|
||||||
|
expect(navigation.modules.value.length).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a proper formated object without the children property', async () => {
|
||||||
|
const expectedMenuItem = {
|
||||||
|
stateName: 'Dashboard',
|
||||||
|
name: 'dashboard',
|
||||||
|
roles: [],
|
||||||
|
icon: 'dashboard',
|
||||||
|
title: 'dashboard'
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstMenuItem = navigation.modules.value[0]
|
||||||
|
expect(firstMenuItem.children).toBeUndefined();
|
||||||
|
expect(firstMenuItem).toEqual(expect.objectContaining(expectedMenuItem));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a proper formated object with two child items', async () => {
|
||||||
|
const expectedMenuItem = [{
|
||||||
|
name: 'CustomerList',
|
||||||
|
title: 'list',
|
||||||
|
icon: 'view_list',
|
||||||
|
stateName: 'CustomerList'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CustomerCreate',
|
||||||
|
title: 'createCustomer',
|
||||||
|
icon: 'vn:addperson',
|
||||||
|
stateName: 'CustomerCreate'
|
||||||
|
}];
|
||||||
|
|
||||||
|
const secondMenuItem = navigation.modules.value[1]
|
||||||
|
expect(secondMenuItem.children).toEqual(expect.arrayContaining(expectedMenuItem));
|
||||||
|
expect(secondMenuItem.children.length).toEqual(2)
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { describe, expect, it, jest } from '@jest/globals';
|
||||||
|
import { axios, flushPromises } from 'app/tests/jest/jestHelpers';
|
||||||
|
import { useRole } from '../useRole';
|
||||||
|
const role = useRole();
|
||||||
|
|
||||||
|
describe('useRole', () => {
|
||||||
|
|
||||||
|
describe('fetch', () => {
|
||||||
|
it('should call setUser and setRoles of the state with the expected data', async () => {
|
||||||
|
const rolesData = [
|
||||||
|
{
|
||||||
|
role: {
|
||||||
|
name: 'salesPerson'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: {
|
||||||
|
name: 'admin'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const fetchedUser = {
|
||||||
|
id: 999,
|
||||||
|
name: `T'Challa`,
|
||||||
|
nickname: 'Black Panther',
|
||||||
|
lang: 'en',
|
||||||
|
userConfig: {
|
||||||
|
darkMode: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const expectedUser = {
|
||||||
|
id: 999,
|
||||||
|
name: `T'Challa`,
|
||||||
|
nickname: 'Black Panther',
|
||||||
|
lang: 'en',
|
||||||
|
darkMode: false,
|
||||||
|
}
|
||||||
|
const expectedRoles = ['salesPerson', 'admin']
|
||||||
|
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||||
|
data: { roles: rolesData, user: fetchedUser }
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(role.state, 'setUser');
|
||||||
|
jest.spyOn(role.state, 'setRoles');
|
||||||
|
|
||||||
|
role.fetch();
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(role.state.setUser).toHaveBeenCalledWith(expectedUser);
|
||||||
|
expect(role.state.setRoles).toHaveBeenCalledWith(expectedRoles);
|
||||||
|
|
||||||
|
role.state.setRoles([])
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('hasAny', () => {
|
||||||
|
it('should return true if a role matched', async () => {
|
||||||
|
role.state.setRoles(['admin'])
|
||||||
|
const hasRole = role.hasAny(['admin']);
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(hasRole).toBe(true);
|
||||||
|
|
||||||
|
role.state.setRoles([])
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if no roles matched', async () => {
|
||||||
|
const hasRole = role.hasAny(['admin']);
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(hasRole).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,133 @@
|
||||||
|
import { describe, expect, it, jest } from '@jest/globals';
|
||||||
|
import { useSession } from '../useSession';
|
||||||
|
import { useState } from '../useState';
|
||||||
|
import { axios } from 'app/tests/jest/jestHelpers';
|
||||||
|
|
||||||
|
const session = useSession();
|
||||||
|
const state = useState();
|
||||||
|
|
||||||
|
describe('session', () => {
|
||||||
|
describe('getToken / setToken', () => {
|
||||||
|
it('should return an empty string if no token is found in local or session storage', async () => {
|
||||||
|
const expectedToken = ''
|
||||||
|
|
||||||
|
const token = session.getToken();
|
||||||
|
|
||||||
|
expect(token).toEqual(expectedToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the token stored in local or session storage', async () => {
|
||||||
|
const expectedToken = 'myToken'
|
||||||
|
const data = {
|
||||||
|
token: expectedToken,
|
||||||
|
keepLogin: false
|
||||||
|
}
|
||||||
|
session.setToken(data);
|
||||||
|
|
||||||
|
const token = session.getToken();
|
||||||
|
|
||||||
|
expect(token).toEqual(expectedToken);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('destroy', () => {
|
||||||
|
it('should remove the token from the local storage and set a blank user', async () => {
|
||||||
|
const previousUser = {
|
||||||
|
id: 999,
|
||||||
|
name: `T'Challa`,
|
||||||
|
nickname: 'Black Panther',
|
||||||
|
lang: 'en',
|
||||||
|
darkMode: false,
|
||||||
|
}
|
||||||
|
const expectedUser = {
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
nickname: '',
|
||||||
|
lang: '',
|
||||||
|
darkMode: null,
|
||||||
|
}
|
||||||
|
let user = state.getUser();
|
||||||
|
|
||||||
|
localStorage.setItem('token', 'tokenToBeGone');
|
||||||
|
state.setUser(previousUser)
|
||||||
|
|
||||||
|
expect(localStorage.getItem('token')).toEqual('tokenToBeGone');
|
||||||
|
expect(user.value).toEqual(previousUser);
|
||||||
|
|
||||||
|
|
||||||
|
session.destroy();
|
||||||
|
|
||||||
|
user = state.getUser();
|
||||||
|
expect(localStorage.getItem('token')).toBeNull();
|
||||||
|
expect(user.value).toEqual(expectedUser);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('login', () => {
|
||||||
|
const expectedUser = {
|
||||||
|
id: 999,
|
||||||
|
name: `T'Challa`,
|
||||||
|
nickname: 'Black Panther',
|
||||||
|
lang: 'en',
|
||||||
|
userConfig: {
|
||||||
|
darkMode: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rolesData = [
|
||||||
|
{
|
||||||
|
role: {
|
||||||
|
name: 'salesPerson'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: {
|
||||||
|
name: 'admin'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should fetch the user roles and then set token in the sessionStorage', async () => {
|
||||||
|
const expectedRoles = ['salesPerson', 'admin']
|
||||||
|
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||||
|
data: { roles: rolesData, user: expectedUser }
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedToken = 'mySessionToken'
|
||||||
|
const keepLogin = false
|
||||||
|
|
||||||
|
await session.login(expectedToken, keepLogin);
|
||||||
|
|
||||||
|
const roles = state.getRoles();
|
||||||
|
const localToken = localStorage.getItem('token');
|
||||||
|
const sessionToken = sessionStorage.getItem('token');
|
||||||
|
|
||||||
|
expect(roles.value).toEqual(expectedRoles);
|
||||||
|
expect(localToken).toBeNull();
|
||||||
|
expect(sessionToken).toEqual(expectedToken);
|
||||||
|
|
||||||
|
session.destroy() // this clears token and user for any other test
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch the user roles and then set token in the localStorage', async () => {
|
||||||
|
const expectedRoles = ['salesPerson', 'admin']
|
||||||
|
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||||
|
data: { roles: rolesData, user: expectedUser }
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedToken = 'myLocalToken'
|
||||||
|
const keepLogin = true
|
||||||
|
|
||||||
|
await session.login(expectedToken, keepLogin);
|
||||||
|
|
||||||
|
const roles = state.getRoles();
|
||||||
|
const localToken = localStorage.getItem('token');
|
||||||
|
const sessionToken = sessionStorage.getItem('token');
|
||||||
|
|
||||||
|
expect(roles.value).toEqual(expectedRoles);
|
||||||
|
expect(localToken).toEqual(expectedToken);
|
||||||
|
expect(sessionToken).toBeNull();
|
||||||
|
|
||||||
|
session.destroy() // this clears token and user for any other test
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,92 @@
|
||||||
|
import routes from 'src/router/routes';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const favorites = ref([]);
|
||||||
|
const modules = ref([]);
|
||||||
|
|
||||||
|
const mainRoute = routes.find((route) => route.path === '/');
|
||||||
|
const moduleRoutes = (mainRoute && mainRoute.children) || [];
|
||||||
|
|
||||||
|
for (const route of moduleRoutes) {
|
||||||
|
const module = {
|
||||||
|
stateName: route.name,
|
||||||
|
name: route.name.toLowerCase(),
|
||||||
|
roles: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (route.meta) {
|
||||||
|
Object.assign(module, route.meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.children && route.children.length) {
|
||||||
|
const [moduleMain] = route.children;
|
||||||
|
const routes = moduleMain.children;
|
||||||
|
|
||||||
|
module.children = routes.map((route) => {
|
||||||
|
const submodule = {
|
||||||
|
stateName: route.name,
|
||||||
|
name: route.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(submodule, route.meta);
|
||||||
|
|
||||||
|
return submodule;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
modules.value.push(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNavigation() {
|
||||||
|
const salixModules = {
|
||||||
|
customer: 'Clients',
|
||||||
|
claim: 'Claims',
|
||||||
|
entry: 'Entries',
|
||||||
|
invoiceIn: 'Invoices In',
|
||||||
|
invoiceOut: 'Invoices Out',
|
||||||
|
item: 'Items',
|
||||||
|
monitor: 'Monitors',
|
||||||
|
order: 'Orders',
|
||||||
|
route: 'Routes',
|
||||||
|
supplier: 'Suppliers',
|
||||||
|
ticket: 'Tickets',
|
||||||
|
travel: 'Travels',
|
||||||
|
user: 'Users',
|
||||||
|
worker: 'Workers',
|
||||||
|
zone: 'Zones',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchFavorites() {
|
||||||
|
const response = await axios.get('StarredModules/getStarredModules');
|
||||||
|
|
||||||
|
const filteredModules = modules.value.filter((module) => {
|
||||||
|
return response.data.find((element) => element.moduleFk == salixModules[module.name]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (favorites.value = filteredModules);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleFavorite(moduleName, event) {
|
||||||
|
if (event.defaultPrevented) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const params = { moduleName: salixModules[moduleName] };
|
||||||
|
const query = 'StarredModules/toggleStarredModule';
|
||||||
|
await axios.post(query, params);
|
||||||
|
|
||||||
|
updateFavorites(moduleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFavorites(name) {
|
||||||
|
if (!favorites.value.find((module) => module.name == name)) {
|
||||||
|
const newStarreModule = modules.value.find((module) => module.name == name);
|
||||||
|
favorites.value.push(newStarreModule);
|
||||||
|
} else {
|
||||||
|
const moduleToRemove = favorites.value.find((module) => module.name == name);
|
||||||
|
favorites.value.splice(favorites.value.indexOf(moduleToRemove), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { modules, favorites, toggleFavorite, fetchFavorites, updateFavorites };
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { useState } from './useState';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export function useRole() {
|
||||||
|
const state = useState();
|
||||||
|
|
||||||
|
async function fetch() {
|
||||||
|
const { data } = await axios.get('Accounts/acl');
|
||||||
|
const roles = data.roles.map(userRoles => userRoles.role.name);
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
id: data.user.id,
|
||||||
|
name: data.user.name,
|
||||||
|
nickname: data.user.nickname,
|
||||||
|
lang: data.user.lang || 'es',
|
||||||
|
darkMode: data.user.userConfig.darkMode,
|
||||||
|
}
|
||||||
|
state.setUser(userData);
|
||||||
|
state.setRoles(roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAny(roles) {
|
||||||
|
const roleStore = state.getRoles();
|
||||||
|
|
||||||
|
for (const role of roles) {
|
||||||
|
if (roleStore.value.indexOf(role) !== -1) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetch,
|
||||||
|
hasAny,
|
||||||
|
state
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { useState } from './useState';
|
||||||
|
import { useRole } from './useRole';
|
||||||
|
|
||||||
|
export function useSession() {
|
||||||
|
function getToken() {
|
||||||
|
const localToken = localStorage.getItem('token');
|
||||||
|
const sessionToken = sessionStorage.getItem('token');
|
||||||
|
|
||||||
|
return localToken || sessionToken || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setToken(data) {
|
||||||
|
if (data.keepLogin) {
|
||||||
|
localStorage.setItem('token', data.token);
|
||||||
|
} else {
|
||||||
|
sessionStorage.setItem('token', data.token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroy() {
|
||||||
|
if (localStorage.getItem('token'))
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
|
||||||
|
if (sessionStorage.getItem('token'))
|
||||||
|
sessionStorage.removeItem('token');
|
||||||
|
|
||||||
|
const { setUser } = useState();
|
||||||
|
|
||||||
|
setUser({
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
nickname: '',
|
||||||
|
lang: '',
|
||||||
|
darkMode: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(token, keepLogin) {
|
||||||
|
const { fetch } = useRole();
|
||||||
|
|
||||||
|
setToken({ token, keepLogin });
|
||||||
|
|
||||||
|
await fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLoggedIn() {
|
||||||
|
const localToken = localStorage.getItem('token');
|
||||||
|
const sessionToken = sessionStorage.getItem('token');
|
||||||
|
|
||||||
|
return !!(localToken || sessionToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getToken,
|
||||||
|
setToken,
|
||||||
|
destroy,
|
||||||
|
login,
|
||||||
|
isLoggedIn,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
const user = ref({
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
nickname: '',
|
||||||
|
lang: '',
|
||||||
|
darkMode: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const roles = ref([]);
|
||||||
|
|
||||||
|
const drawer = ref(true);
|
||||||
|
|
||||||
|
export function useState() {
|
||||||
|
function getUser() {
|
||||||
|
return computed(() => {
|
||||||
|
return {
|
||||||
|
id: user.value.id,
|
||||||
|
name: user.value.name,
|
||||||
|
nickname: user.value.nickname,
|
||||||
|
lang: user.value.lang,
|
||||||
|
darkMode: user.value.darkMode,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUser(data) {
|
||||||
|
user.value = {
|
||||||
|
id: data.id,
|
||||||
|
name: data.name,
|
||||||
|
nickname: data.nickname,
|
||||||
|
lang: data.lang,
|
||||||
|
darkMode: data.darkMode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoles() {
|
||||||
|
return computed(() => {
|
||||||
|
return roles.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRoles(data) {
|
||||||
|
roles.value = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getUser,
|
||||||
|
setUser,
|
||||||
|
getRoles,
|
||||||
|
setRoles,
|
||||||
|
drawer
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
// app global css in SCSS form
|
||||||
|
@import './icons.scss';
|
After Width: | Height: | Size: 159 KiB |
|
@ -0,0 +1,399 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'icomoon';
|
||||||
|
src: url('fonts/icomoon.eot?g6kvgn');
|
||||||
|
src: url('fonts/icomoon.eot?g6kvgn#iefix') format('embedded-opentype'),
|
||||||
|
url('fonts/icomoon.ttf?g6kvgn') format('truetype'),
|
||||||
|
url('fonts/icomoon.woff?g6kvgn') format('woff'),
|
||||||
|
url('fonts/icomoon.svg?g6kvgn#icomoon') format('svg');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
[class^="icon-"], [class*=" icon-"] {
|
||||||
|
/* use !important to prevent issues with browser extensions that change fonts */
|
||||||
|
font-family: 'icomoon' !important;
|
||||||
|
speak: never;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
text-transform: none;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
/* Better Font Rendering =========== */
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-pin:before {
|
||||||
|
content: "\e950";
|
||||||
|
}
|
||||||
|
.icon-pin_off:before {
|
||||||
|
content: "\e95b";
|
||||||
|
}
|
||||||
|
.icon-frozen:before {
|
||||||
|
content: "\e900";
|
||||||
|
}
|
||||||
|
.icon-Person:before {
|
||||||
|
content: "\e901";
|
||||||
|
}
|
||||||
|
.icon-handmadeArtificial:before {
|
||||||
|
content: "\e902";
|
||||||
|
}
|
||||||
|
.icon-fruit:before {
|
||||||
|
content: "\e903";
|
||||||
|
}
|
||||||
|
.icon-funeral:before {
|
||||||
|
content: "\e904";
|
||||||
|
}
|
||||||
|
.icon-noPayMethod:before {
|
||||||
|
content: "\e905";
|
||||||
|
}
|
||||||
|
.icon-preserved:before {
|
||||||
|
content: "\e906";
|
||||||
|
}
|
||||||
|
.icon-greenery:before {
|
||||||
|
content: "\e907";
|
||||||
|
}
|
||||||
|
.icon-planta:before {
|
||||||
|
content: "\e908";
|
||||||
|
}
|
||||||
|
.icon-handmade:before {
|
||||||
|
content: "\e909";
|
||||||
|
}
|
||||||
|
.icon-accessory:before {
|
||||||
|
content: "\e90a";
|
||||||
|
}
|
||||||
|
.icon-artificial:before {
|
||||||
|
content: "\e90b";
|
||||||
|
}
|
||||||
|
.icon-flower:before {
|
||||||
|
content: "\e90c";
|
||||||
|
}
|
||||||
|
.icon-fixedPrice:before {
|
||||||
|
content: "\e90d";
|
||||||
|
}
|
||||||
|
.icon-addperson:before {
|
||||||
|
content: "\e90e";
|
||||||
|
}
|
||||||
|
.icon-supplierfalse:before {
|
||||||
|
content: "\e90f";
|
||||||
|
}
|
||||||
|
.icon-invoice-out:before {
|
||||||
|
content: "\e910";
|
||||||
|
}
|
||||||
|
.icon-invoice-in:before {
|
||||||
|
content: "\e911";
|
||||||
|
}
|
||||||
|
.icon-invoice-in-create:before {
|
||||||
|
content: "\e912";
|
||||||
|
}
|
||||||
|
.icon-basketadd:before {
|
||||||
|
content: "\e913";
|
||||||
|
}
|
||||||
|
.icon-basket:before {
|
||||||
|
content: "\e914";
|
||||||
|
}
|
||||||
|
.icon-uniE915:before {
|
||||||
|
content: "\e915";
|
||||||
|
}
|
||||||
|
.icon-uniE916:before {
|
||||||
|
content: "\e916";
|
||||||
|
}
|
||||||
|
.icon-uniE917:before {
|
||||||
|
content: "\e917";
|
||||||
|
}
|
||||||
|
.icon-uniE918:before {
|
||||||
|
content: "\e918";
|
||||||
|
}
|
||||||
|
.icon-uniE919:before {
|
||||||
|
content: "\e919";
|
||||||
|
}
|
||||||
|
.icon-uniE91A:before {
|
||||||
|
content: "\e91a";
|
||||||
|
}
|
||||||
|
.icon-isTooLittle:before {
|
||||||
|
content: "\e91b";
|
||||||
|
}
|
||||||
|
.icon-deliveryprices:before {
|
||||||
|
content: "\e91c";
|
||||||
|
}
|
||||||
|
.icon-onlinepayment:before {
|
||||||
|
content: "\e91d";
|
||||||
|
}
|
||||||
|
.icon-risk:before {
|
||||||
|
content: "\e91e";
|
||||||
|
}
|
||||||
|
.icon-noweb:before {
|
||||||
|
content: "\e91f";
|
||||||
|
}
|
||||||
|
.icon-no036:before {
|
||||||
|
content: "\e920";
|
||||||
|
}
|
||||||
|
.icon-disabled:before {
|
||||||
|
content: "\e921";
|
||||||
|
}
|
||||||
|
.icon-treatments:before {
|
||||||
|
content: "\e922";
|
||||||
|
}
|
||||||
|
.icon-invoice:before {
|
||||||
|
content: "\e923";
|
||||||
|
}
|
||||||
|
.icon-photo:before {
|
||||||
|
content: "\e924";
|
||||||
|
}
|
||||||
|
.icon-supplier:before {
|
||||||
|
content: "\e925";
|
||||||
|
}
|
||||||
|
.icon-languaje:before {
|
||||||
|
content: "\e926";
|
||||||
|
}
|
||||||
|
.icon-credit:before {
|
||||||
|
content: "\e927";
|
||||||
|
}
|
||||||
|
.icon-client:before {
|
||||||
|
content: "\e928";
|
||||||
|
}
|
||||||
|
.icon-shipment-01:before {
|
||||||
|
content: "\e929";
|
||||||
|
}
|
||||||
|
.icon-account:before {
|
||||||
|
content: "\e92a";
|
||||||
|
}
|
||||||
|
.icon-inventory:before {
|
||||||
|
content: "\e92b";
|
||||||
|
}
|
||||||
|
.icon-unavailable:before {
|
||||||
|
content: "\e92c";
|
||||||
|
}
|
||||||
|
.icon-wiki:before {
|
||||||
|
content: "\e92d";
|
||||||
|
}
|
||||||
|
.icon-attach:before {
|
||||||
|
content: "\e92e";
|
||||||
|
}
|
||||||
|
.icon-exit:before {
|
||||||
|
content: "\e92f";
|
||||||
|
}
|
||||||
|
.icon-anonymous:before {
|
||||||
|
content: "\e930";
|
||||||
|
}
|
||||||
|
.icon-net:before {
|
||||||
|
content: "\e931";
|
||||||
|
}
|
||||||
|
.icon-buyrequest:before {
|
||||||
|
content: "\e932";
|
||||||
|
}
|
||||||
|
.icon-thermometer:before {
|
||||||
|
content: "\e933";
|
||||||
|
}
|
||||||
|
.icon-entry:before {
|
||||||
|
content: "\e934";
|
||||||
|
}
|
||||||
|
.icon-deletedTicket:before {
|
||||||
|
content: "\e935";
|
||||||
|
}
|
||||||
|
.icon-logout:before {
|
||||||
|
content: "\e936";
|
||||||
|
}
|
||||||
|
.icon-catalog:before {
|
||||||
|
content: "\e937";
|
||||||
|
}
|
||||||
|
.icon-agency:before {
|
||||||
|
content: "\e938";
|
||||||
|
}
|
||||||
|
.icon-delivery:before {
|
||||||
|
content: "\e939";
|
||||||
|
}
|
||||||
|
.icon-wand:before {
|
||||||
|
content: "\e93a";
|
||||||
|
}
|
||||||
|
.icon-buscaman:before {
|
||||||
|
content: "\e93b";
|
||||||
|
}
|
||||||
|
.icon-pbx:before {
|
||||||
|
content: "\e93c";
|
||||||
|
}
|
||||||
|
.icon-calendar:before {
|
||||||
|
content: "\e93d";
|
||||||
|
}
|
||||||
|
.icon-splitline:before {
|
||||||
|
content: "\e93e";
|
||||||
|
}
|
||||||
|
.icon-consignatarios:before {
|
||||||
|
content: "\e93f";
|
||||||
|
}
|
||||||
|
.icon-tax:before {
|
||||||
|
content: "\e940";
|
||||||
|
}
|
||||||
|
.icon-notes:before {
|
||||||
|
content: "\e941";
|
||||||
|
}
|
||||||
|
.icon-lines:before {
|
||||||
|
content: "\e942";
|
||||||
|
}
|
||||||
|
.icon-zone:before {
|
||||||
|
content: "\e943";
|
||||||
|
}
|
||||||
|
.icon-greuge:before {
|
||||||
|
content: "\e944";
|
||||||
|
}
|
||||||
|
.icon-ticketAdd:before {
|
||||||
|
content: "\e945";
|
||||||
|
}
|
||||||
|
.icon-components:before {
|
||||||
|
content: "\e946";
|
||||||
|
}
|
||||||
|
.icon-pets:before {
|
||||||
|
content: "\e947";
|
||||||
|
}
|
||||||
|
.icon-linesprepaired:before {
|
||||||
|
content: "\e948";
|
||||||
|
}
|
||||||
|
.icon-control:before {
|
||||||
|
content: "\e949";
|
||||||
|
}
|
||||||
|
.icon-revision:before {
|
||||||
|
content: "\e94a";
|
||||||
|
}
|
||||||
|
.icon-deaulter:before {
|
||||||
|
content: "\e94b";
|
||||||
|
}
|
||||||
|
.icon-services:before {
|
||||||
|
content: "\e94c";
|
||||||
|
}
|
||||||
|
.icon-albaran:before {
|
||||||
|
content: "\e94d";
|
||||||
|
}
|
||||||
|
.icon-solunion:before {
|
||||||
|
content: "\e94e";
|
||||||
|
}
|
||||||
|
.icon-stowaway:before {
|
||||||
|
content: "\e94f";
|
||||||
|
}
|
||||||
|
.icon-apps:before {
|
||||||
|
content: "\e951";
|
||||||
|
}
|
||||||
|
.icon-info:before {
|
||||||
|
content: "\e952";
|
||||||
|
}
|
||||||
|
.icon-columndelete:before {
|
||||||
|
content: "\e953";
|
||||||
|
}
|
||||||
|
.icon-columnadd:before {
|
||||||
|
content: "\e954";
|
||||||
|
}
|
||||||
|
.icon-deleteline:before {
|
||||||
|
content: "\e955";
|
||||||
|
}
|
||||||
|
.icon-item:before {
|
||||||
|
content: "\e956";
|
||||||
|
}
|
||||||
|
.icon-worker:before {
|
||||||
|
content: "\e957";
|
||||||
|
}
|
||||||
|
.icon-headercol:before {
|
||||||
|
content: "\e958";
|
||||||
|
}
|
||||||
|
.icon-reserva:before {
|
||||||
|
content: "\e959";
|
||||||
|
}
|
||||||
|
.icon-100:before {
|
||||||
|
content: "\e95a";
|
||||||
|
}
|
||||||
|
.icon-sign:before {
|
||||||
|
content: "\e95d";
|
||||||
|
}
|
||||||
|
.icon-polizon:before {
|
||||||
|
content: "\e95e";
|
||||||
|
}
|
||||||
|
.icon-solclaim:before {
|
||||||
|
content: "\e95f";
|
||||||
|
}
|
||||||
|
.icon-actions:before {
|
||||||
|
content: "\e960";
|
||||||
|
}
|
||||||
|
.icon-details:before {
|
||||||
|
content: "\e961";
|
||||||
|
}
|
||||||
|
.icon-traceability:before {
|
||||||
|
content: "\e962";
|
||||||
|
}
|
||||||
|
.icon-claims:before {
|
||||||
|
content: "\e963";
|
||||||
|
}
|
||||||
|
.icon-regentry:before {
|
||||||
|
content: "\e964";
|
||||||
|
}
|
||||||
|
.icon-transaction:before {
|
||||||
|
content: "\e966";
|
||||||
|
}
|
||||||
|
.icon-History:before {
|
||||||
|
content: "\e968";
|
||||||
|
}
|
||||||
|
.icon-mana:before {
|
||||||
|
content: "\e96a";
|
||||||
|
}
|
||||||
|
.icon-ticket:before {
|
||||||
|
content: "\e96b";
|
||||||
|
}
|
||||||
|
.icon-niche:before {
|
||||||
|
content: "\e96c";
|
||||||
|
}
|
||||||
|
.icon-tags:before {
|
||||||
|
content: "\e96d";
|
||||||
|
}
|
||||||
|
.icon-volume:before {
|
||||||
|
content: "\e96e";
|
||||||
|
}
|
||||||
|
.icon-bin:before {
|
||||||
|
content: "\e96f";
|
||||||
|
}
|
||||||
|
.icon-splur:before {
|
||||||
|
content: "\e970";
|
||||||
|
}
|
||||||
|
.icon-barcode:before {
|
||||||
|
content: "\e971";
|
||||||
|
}
|
||||||
|
.icon-botanical:before {
|
||||||
|
content: "\e972";
|
||||||
|
}
|
||||||
|
.icon-clone:before {
|
||||||
|
content: "\e973";
|
||||||
|
}
|
||||||
|
.icon-sms:before {
|
||||||
|
content: "\e975";
|
||||||
|
}
|
||||||
|
.icon-eye:before {
|
||||||
|
content: "\e976";
|
||||||
|
}
|
||||||
|
.icon-doc:before {
|
||||||
|
content: "\e977";
|
||||||
|
}
|
||||||
|
.icon-package:before {
|
||||||
|
content: "\e978";
|
||||||
|
}
|
||||||
|
.icon-settings:before {
|
||||||
|
content: "\e979";
|
||||||
|
}
|
||||||
|
.icon-bucket:before {
|
||||||
|
content: "\e97a";
|
||||||
|
}
|
||||||
|
.icon-mandatory:before {
|
||||||
|
content: "\e97b";
|
||||||
|
}
|
||||||
|
.icon-recovery:before {
|
||||||
|
content: "\e97c";
|
||||||
|
}
|
||||||
|
.icon-payment:before {
|
||||||
|
content: "\e97e";
|
||||||
|
}
|
||||||
|
.icon-grid:before {
|
||||||
|
content: "\e980";
|
||||||
|
}
|
||||||
|
.icon-web:before {
|
||||||
|
content: "\e982";
|
||||||
|
}
|
||||||
|
.icon-dfiscales:before {
|
||||||
|
content: "\e984";
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
$positive: #21ba45;
|
||||||
|
$negative: #c10015;
|
||||||
|
$info: #31ccec;
|
||||||
|
$warning: #f2c037;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import toLowerCase from './toLowerCase';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
toLowerCase,
|
||||||
|
};
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function toLowerCase(value) {
|
||||||
|
return value.toLowerCase();
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
export default {
|
||||||
|
globals: {
|
||||||
|
lang: {
|
||||||
|
es: 'Spanish',
|
||||||
|
en: 'English',
|
||||||
|
},
|
||||||
|
collapseMenu: 'Collapse left menu',
|
||||||
|
backToDashboard: 'Return to dashboard',
|
||||||
|
notifications: 'Notifications',
|
||||||
|
userPanel: 'User panel',
|
||||||
|
favoriteModules: 'Favorite modules',
|
||||||
|
theme: 'Theme',
|
||||||
|
logOut: 'Log out',
|
||||||
|
dataSaved: 'Data saved',
|
||||||
|
},
|
||||||
|
moduleIndex: {
|
||||||
|
allModules: 'All modules'
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
statusUnauthorized: 'Access denied',
|
||||||
|
statusInternalServerError: 'An internal server error has ocurred',
|
||||||
|
statusBadGateway: 'It seems that the server has fall down',
|
||||||
|
statusGatewayTimeout: 'Could not contact the server',
|
||||||
|
},
|
||||||
|
login: {
|
||||||
|
title: 'Login',
|
||||||
|
username: 'Username',
|
||||||
|
password: 'Password',
|
||||||
|
submit: 'Log in',
|
||||||
|
keepLogin: 'Keep me logged in',
|
||||||
|
loginSuccess: 'You have successfully logged in',
|
||||||
|
loginError: 'Invalid username or password'
|
||||||
|
},
|
||||||
|
dashboard: {
|
||||||
|
pageTitles: {
|
||||||
|
dashboard: 'Dashboard',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
customer: {
|
||||||
|
pageTitles: {
|
||||||
|
customers: 'Customers',
|
||||||
|
list: 'List',
|
||||||
|
createCustomer: 'Create customer',
|
||||||
|
basicData: 'Basic Data'
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
phone: 'Phone',
|
||||||
|
email: 'Email',
|
||||||
|
customerOrders: 'Display customer orders',
|
||||||
|
moreOptions: 'More options'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ticket: {
|
||||||
|
pageTitles: {
|
||||||
|
tickets: 'Tickets',
|
||||||
|
list: 'List',
|
||||||
|
createTicket: 'Create ticket',
|
||||||
|
basicData: 'Basic Data'
|
||||||
|
},
|
||||||
|
boxing: {
|
||||||
|
expedition: 'Expedition',
|
||||||
|
item: 'Item',
|
||||||
|
created: 'Created',
|
||||||
|
worker: 'Worker',
|
||||||
|
selectTime: 'Select time:',
|
||||||
|
selectVideo: 'Select video:',
|
||||||
|
notFound: 'No videos available'
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
topbar: {},
|
||||||
|
userPanel: {
|
||||||
|
settings: 'Settings',
|
||||||
|
logOut: 'Log Out',
|
||||||
|
},
|
||||||
|
smartCard: {
|
||||||
|
noData: 'No data to display',
|
||||||
|
openCard: 'View card',
|
||||||
|
openSummary: 'Open summary'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,82 @@
|
||||||
|
export default {
|
||||||
|
globals: {
|
||||||
|
lang: {
|
||||||
|
es: 'Español',
|
||||||
|
en: 'Inglés',
|
||||||
|
},
|
||||||
|
collapseMenu: 'Contraer menú lateral',
|
||||||
|
backToDashboard: 'Volver al tablón',
|
||||||
|
notifications: 'Notificaciones',
|
||||||
|
userPanel: 'Panel de usuario',
|
||||||
|
favoriteModules: 'Módulos favoritos',
|
||||||
|
theme: 'Tema',
|
||||||
|
logOut: 'Cerrar sesión',
|
||||||
|
dataSaved: 'Datos guardados'
|
||||||
|
},
|
||||||
|
moduleIndex: {
|
||||||
|
allModules: 'Todos los módulos'
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
statusUnauthorized: 'Acceso denegado',
|
||||||
|
statusInternalServerError: 'Ha ocurrido un error interno del servidor',
|
||||||
|
statusBadGateway: 'Parece ser que el servidor ha caído',
|
||||||
|
statusGatewayTimeout: 'No se ha podido contactar con el servidor',
|
||||||
|
},
|
||||||
|
login: {
|
||||||
|
title: 'Inicio de sesión',
|
||||||
|
username: 'Nombre de usuario',
|
||||||
|
password: 'Contraseña',
|
||||||
|
submit: 'Iniciar sesión',
|
||||||
|
keepLogin: 'Mantener sesión iniciada',
|
||||||
|
loginSuccess: 'Inicio de sesión correcto',
|
||||||
|
loginError: 'Nombre de usuario o contraseña incorrectos',
|
||||||
|
},
|
||||||
|
dashboard: {
|
||||||
|
pageTitles: {
|
||||||
|
dashboard: 'Tablón',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
customer: {
|
||||||
|
pageTitles: {
|
||||||
|
customers: 'Clientes',
|
||||||
|
list: 'Listado',
|
||||||
|
createCustomer: 'Crear cliente',
|
||||||
|
basicData: 'Datos básicos'
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
phone: 'Teléfono',
|
||||||
|
email: 'Email',
|
||||||
|
customerOrders: 'Mostrar órdenes del cliente',
|
||||||
|
moreOptions: 'Más opciones'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ticket: {
|
||||||
|
pageTitles: {
|
||||||
|
tickets: 'Tickets',
|
||||||
|
list: 'Listado',
|
||||||
|
createTicket: 'Crear ticket',
|
||||||
|
basicData: 'Datos básicos'
|
||||||
|
},
|
||||||
|
boxing: {
|
||||||
|
expedition: 'Expedición',
|
||||||
|
item: 'Artículo',
|
||||||
|
created: 'Creado',
|
||||||
|
worker: 'Trabajador',
|
||||||
|
selectTime: 'Seleccionar hora:',
|
||||||
|
selectVideo: 'Seleccionar vídeo:',
|
||||||
|
notFound: 'No hay vídeos disponibles'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
topbar: {},
|
||||||
|
userPanel: {
|
||||||
|
settings: 'Configuración',
|
||||||
|
logOut: 'Cerrar sesión',
|
||||||
|
},
|
||||||
|
smartCard: {
|
||||||
|
noData: 'Sin datos que mostrar',
|
||||||
|
openCard: 'Ver ficha',
|
||||||
|
openSummary: 'Abrir detalles'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import en from './en';
|
||||||
|
import es from './es';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
en: en,
|
||||||
|
es: es,
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
<!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>
|
||||||
|
<!-- DO NOT touch the following DIV -->
|
||||||
|
<div id="q-app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<script setup>
|
||||||
|
import Navbar from 'src/components/Navbar.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-layout view="hHh lpR fFf">
|
||||||
|
<Navbar />
|
||||||
|
<router-view></router-view>
|
||||||
|
</q-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
12
src/main.ts
|
@ -1,12 +0,0 @@
|
||||||
import { createApp } from "vue";
|
|
||||||
import App from "./App.vue";
|
|
||||||
import router from "./router";
|
|
||||||
import store from "./store";
|
|
||||||
import { Quasar } from "quasar";
|
|
||||||
import quasarUserOptions from "./quasar-user-options";
|
|
||||||
|
|
||||||
createApp(App)
|
|
||||||
.use(Quasar, quasarUserOptions)
|
|
||||||
.use(store)
|
|
||||||
.use(router)
|
|
||||||
.mount("#app");
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<q-page class="q-pa-md">
|
||||||
|
<q-card class="q-pa-md">Basic Data</q-card>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
|
@ -0,0 +1,138 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, onMounted } from 'vue';
|
||||||
|
import { useState } from 'src/composables/useState';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const state = useState();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetch();
|
||||||
|
});
|
||||||
|
|
||||||
|
const entityId = computed(function () {
|
||||||
|
return router.currentRoute.value.params.id;
|
||||||
|
});
|
||||||
|
const customer = ref({});
|
||||||
|
|
||||||
|
async function fetch() {
|
||||||
|
const { data } = await axios.get(`Clients/${entityId.value}`);
|
||||||
|
|
||||||
|
if (data) customer.value = data;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||||
|
<q-scroll-area class="fit text-grey-8 descriptor">
|
||||||
|
<div class="header bg-orange q-pa-sm">
|
||||||
|
<router-link :to="{ path: '/customer/list' }">
|
||||||
|
<q-btn round flat dense size="md" icon="view_list" color="white">
|
||||||
|
<q-tooltip>Customer list</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</router-link>
|
||||||
|
<router-link :to="{ path: '/customer/list' }">
|
||||||
|
<q-btn round flat dense size="md" icon="launch" color="white">
|
||||||
|
<q-tooltip>Customer preview</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<q-btn round flat dense size="md" icon="more_vert" color="white">
|
||||||
|
<q-tooltip>More options</q-tooltip>
|
||||||
|
<q-menu>
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable v-ripple>Option 1</q-item>
|
||||||
|
<q-item clickable v-ripple>Option 2</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>{{ customer.name }}</h5>
|
||||||
|
|
||||||
|
<q-list>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label caption>Customer ID</q-item-label>
|
||||||
|
<q-item-label>#{{ customer.id }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label caption>Email</q-item-label>
|
||||||
|
<q-item-label>{{ customer.email }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn size="md" icon="vn:ticket" color="orange">
|
||||||
|
<q-tooltip>Ticket list</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-btn size="md" icon="vn:invoice-out" color="orange">
|
||||||
|
<q-tooltip>Invoice Out list</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-btn size="md" icon="vn:basketadd" color="orange">
|
||||||
|
<q-tooltip>Order list</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-btn size="md" icon="face" color="orange">
|
||||||
|
<q-tooltip>View user</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-btn size="md" icon="expand_more" color="orange">
|
||||||
|
<q-tooltip>More options</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-list>
|
||||||
|
<q-item :to="{ name: 'CustomerBasicData' }" clickable v-ripple active-class="text-orange">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="person" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>Basic data</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-ripple>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="notes" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>Notes</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-expansion-item icon="more" label="More options" expand-icon-toggle expand-separator>
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable v-ripple>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="person" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>Option</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-expansion-item>
|
||||||
|
</q-list>
|
||||||
|
</q-scroll-area>
|
||||||
|
</q-drawer>
|
||||||
|
<q-page-container>
|
||||||
|
<router-view></router-view>
|
||||||
|
</q-page-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.descriptor {
|
||||||
|
h5 {
|
||||||
|
margin: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-card__actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script setup>
|
||||||
|
import { reactive, watch } from 'vue'
|
||||||
|
|
||||||
|
const customer = reactive({
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => customer.name, () => {
|
||||||
|
console.log('customer.name changed');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-page class="q-pa-md">
|
||||||
|
<q-card class="q-pa-md">
|
||||||
|
<q-form @submit="onSubmit" @reset="onReset" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="customer.name"
|
||||||
|
label="Your name *"
|
||||||
|
hint="Name and surname"
|
||||||
|
lazy-rules
|
||||||
|
:rules="[val => val && val.length > 0 || 'Please type something']"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
type="number"
|
||||||
|
v-model="age"
|
||||||
|
label="Your age *"
|
||||||
|
lazy-rules
|
||||||
|
:rules="[
|
||||||
|
val => val !== null && val !== '' || 'Please type your age',
|
||||||
|
val => val > 0 && val < 100 || 'Please type a real age'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<q-btn label="Submit" type="submit" color="primary" />
|
||||||
|
<q-btn label="Reset" type="reset" color="primary" flat class="q-ml-sm" />
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 60em;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,66 @@
|
||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import SmartCard from 'src/components/SmartCard.vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
function navigate(id) {
|
||||||
|
router.push({ path: `/customer/${id}` });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-page class="q-pa-md">
|
||||||
|
<smart-card url="/Clients" sort-by="id DESC" @on-navigate="navigate" auto-load>
|
||||||
|
<template #labels="{ row }">
|
||||||
|
<q-list>
|
||||||
|
<q-item class="q-pa-none">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label caption>{{ t('customer.list.email') }}</q-item-label>
|
||||||
|
<q-item-label>{{ row.email }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item class="q-pa-none">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label caption>{{ t('customer.list.phone') }}</q-item-label>
|
||||||
|
<q-item-label>{{ row.phone }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<q-btn color="grey-7" round flat icon="more_vert">
|
||||||
|
<q-tooltip>{{ t('customer.list.moreOptions') }}</q-tooltip>
|
||||||
|
<q-menu cover auto-close>
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="add" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>Add a note</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="history" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>Display customer history</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
|
||||||
|
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn flat round color="grey-7" icon="preview">
|
||||||
|
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn flat round color="grey-7" icon="vn:ticket">
|
||||||
|
<q-tooltip>{{ t('customer.list.customerOrders') }}</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
</smart-card>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useState } from 'src/composables/useState';
|
||||||
|
//import LeftMenu from 'src/components/LeftMenu.vue';
|
||||||
|
|
||||||
|
const state = useState();
|
||||||
|
const miniState = ref(true);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-drawer
|
||||||
|
v-model="state.drawer.value"
|
||||||
|
show-if-above
|
||||||
|
:mini="miniState"
|
||||||
|
@mouseover="miniState = false"
|
||||||
|
@mouseout="miniState = true"
|
||||||
|
mini-to-overlay
|
||||||
|
:width="256"
|
||||||
|
:breakpoint="500"
|
||||||
|
>
|
||||||
|
<q-scroll-area class="fit text-grey-8">
|
||||||
|
<LeftMenu />
|
||||||
|
</q-scroll-area>
|
||||||
|
</q-drawer>
|
||||||
|
<q-page-container>
|
||||||
|
<router-view></router-view>
|
||||||
|
</q-page-container>
|
||||||
|
</template>
|
|
@ -0,0 +1,90 @@
|
||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useState } from 'src/composables/useState';
|
||||||
|
import LeftMenu from 'src/components/LeftMenu.vue';
|
||||||
|
import { useNavigation } from 'src/composables/useNavigation';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const state = useState();
|
||||||
|
const miniState = ref(true);
|
||||||
|
const modules = useNavigation();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-drawer
|
||||||
|
v-model="state.drawer.value"
|
||||||
|
show-if-above
|
||||||
|
:mini="miniState"
|
||||||
|
@mouseover="miniState = false"
|
||||||
|
@mouseout="miniState = true"
|
||||||
|
mini-to-overlay
|
||||||
|
:width="256"
|
||||||
|
:breakpoint="500"
|
||||||
|
>
|
||||||
|
<q-scroll-area class="fit text-grey-8">
|
||||||
|
<LeftMenu />
|
||||||
|
</q-scroll-area>
|
||||||
|
</q-drawer>
|
||||||
|
<q-page-container>
|
||||||
|
<q-page class="q-pa-md">
|
||||||
|
<!-- <q-banner v-if="$q.screen.gt.xs" inline-actions rounded class="bg-orange text-white q-mb-lg">
|
||||||
|
Employee notification message
|
||||||
|
<template #action>
|
||||||
|
<q-btn flat label="Dismiss" />
|
||||||
|
</template>
|
||||||
|
</q-banner> -->
|
||||||
|
|
||||||
|
<div class="row items-start wrap q-col-gutter-md q-mb-lg">
|
||||||
|
<div class="col-12 col-md" v-if="modules.favorites.value.length">
|
||||||
|
<div class="text-h6 text-grey-8 q-mb-sm">{{ t('globals.favoriteModules') }}</div>
|
||||||
|
<q-card class="row flex-container">
|
||||||
|
<div
|
||||||
|
v-for="module of modules.favorites.value"
|
||||||
|
:key="module.title"
|
||||||
|
class="row no-wrap q-pa-xs flex-item"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
align="evenly"
|
||||||
|
padding="16px"
|
||||||
|
flat
|
||||||
|
stack
|
||||||
|
size="lg"
|
||||||
|
:icon="module.icon"
|
||||||
|
color="orange-6"
|
||||||
|
class="col-4 button"
|
||||||
|
:to="{ name: module.stateName }"
|
||||||
|
>
|
||||||
|
<div class="text-center text-orange-6 button-text">
|
||||||
|
{{ t(`${module.name}.pageTitles.${module.title}`) }}
|
||||||
|
</div>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</q-page-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.flex-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.flex-item {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
line-height: normal;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.button-text {
|
||||||
|
font-size: 10px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,92 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { useSession } from 'src/composables/useSession';
|
||||||
|
|
||||||
|
const quasar = useQuasar();
|
||||||
|
const session = useSession();
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const username = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const keepLogin = ref(true);
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
const { data } = await axios.post('Accounts/login', {
|
||||||
|
user: username.value,
|
||||||
|
password: password.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
await session.login(data.token, keepLogin.value);
|
||||||
|
|
||||||
|
quasar.notify({
|
||||||
|
message: t('login.loginSuccess'),
|
||||||
|
type: 'positive',
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentRoute = router.currentRoute.value;
|
||||||
|
if (currentRoute.query && currentRoute.query.redirect) {
|
||||||
|
router.push(currentRoute.query.redirect);
|
||||||
|
} else {
|
||||||
|
router.push({ name: 'Dashboard' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-layout>
|
||||||
|
<q-page-container>
|
||||||
|
<q-page>
|
||||||
|
<div id="login">
|
||||||
|
<q-card class="login q-pa-xl">
|
||||||
|
<q-img src="~/assets/logo.svg" alt="Logo" fit="contain" :ratio="16 / 9" class="q-mb-md" />
|
||||||
|
<q-form @submit="onSubmit" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
color="orange"
|
||||||
|
v-model="username"
|
||||||
|
:label="t('login.username')"
|
||||||
|
lazy-rules
|
||||||
|
:rules="[(val) => (val && val.length > 0) || 'Please type something']"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
color="orange"
|
||||||
|
type="password"
|
||||||
|
v-model="password"
|
||||||
|
:label="t('login.password')"
|
||||||
|
lazy-rules
|
||||||
|
:rules="[(val) => (val && val.length > 0) || 'Please type something']"
|
||||||
|
/>
|
||||||
|
<q-toggle v-model="keepLogin" :label="t('login.keepLogin')" color="orange" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<q-btn :label="t('login.submit')" type="submit" color="orange" />
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</q-page-container>
|
||||||
|
</q-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#login {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
|
||||||
|
import { createWrapper, axios } from 'app/tests/jest/jestHelpers';
|
||||||
|
import Login from '../Login.vue';
|
||||||
|
|
||||||
|
const mockPush = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('vue-router', () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
push: mockPush,
|
||||||
|
currentRoute: { value: 'myCurrentRoute' }
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Login', () => {
|
||||||
|
let vm;
|
||||||
|
beforeAll(() => {
|
||||||
|
vm = createWrapper(Login).vm;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully set the token into session', async () => {
|
||||||
|
const expectedUser = {
|
||||||
|
id: 999,
|
||||||
|
name: `T'Challa`,
|
||||||
|
nickname: 'Black Panther',
|
||||||
|
lang: 'en',
|
||||||
|
userConfig: {
|
||||||
|
darkMode: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jest.spyOn(axios, 'post').mockResolvedValue({ data: { token: 'token' } });
|
||||||
|
jest.spyOn(axios, 'get').mockResolvedValue({ data: { roles: [], user: expectedUser } });
|
||||||
|
jest.spyOn(vm.quasar, 'notify');
|
||||||
|
|
||||||
|
expect(vm.session.getToken()).toEqual('');
|
||||||
|
|
||||||
|
await vm.onSubmit();
|
||||||
|
|
||||||
|
expect(vm.session.getToken()).toEqual('token');
|
||||||
|
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
|
||||||
|
{ 'type': 'positive' }
|
||||||
|
));
|
||||||
|
vm.session.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set the token into session if any error occurred', async () => {
|
||||||
|
jest.spyOn(axios, 'post').mockReturnValue({ data: null });
|
||||||
|
jest.spyOn(vm.quasar, 'notify');
|
||||||
|
|
||||||
|
await vm.onSubmit();
|
||||||
|
|
||||||
|
expect(vm.quasar.notify).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,20 @@
|
||||||
|
<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: 0.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>
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<q-card>Basic Data</q-card>
|
||||||
|
</template>
|
|
@ -0,0 +1,159 @@
|
||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { computed, ref, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { date, useQuasar } from 'quasar';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const quasar = useQuasar();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetch();
|
||||||
|
});
|
||||||
|
|
||||||
|
const entityId = computed(function () {
|
||||||
|
return router.currentRoute.value.params.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
const expeditions = ref({});
|
||||||
|
const lastExpedition = ref();
|
||||||
|
const slide = ref(null);
|
||||||
|
const videoList = ref([]);
|
||||||
|
const time = ref({
|
||||||
|
min: 0,
|
||||||
|
max: 24,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetch() {
|
||||||
|
const filter = {
|
||||||
|
where: {
|
||||||
|
ticketFk: entityId.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { data } = await axios.get(`/Expeditions/filter`, {
|
||||||
|
params: { filter },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data) expeditions.value = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getVideoList(expeditionId, timed) {
|
||||||
|
lastExpedition.value = expeditionId;
|
||||||
|
const params = {
|
||||||
|
id: expeditionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (timed) {
|
||||||
|
Object.assign(params, { from: timed.min, to: timed.max });
|
||||||
|
}
|
||||||
|
const { data } = await axios.get(`/Boxings/getVideoList`, { params: params });
|
||||||
|
|
||||||
|
const list = [];
|
||||||
|
for (const video of data) {
|
||||||
|
const videName = video.split('.')[0].split('T')[1].replaceAll('-', ':');
|
||||||
|
list.push({
|
||||||
|
label: videName,
|
||||||
|
value: video,
|
||||||
|
url: `api/Boxings/getVideo?id=${expeditionId}&filename=${video}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
videoList.value = list.reverse();
|
||||||
|
if (list[0]) {
|
||||||
|
slide.value = list[0].value;
|
||||||
|
time.value = {
|
||||||
|
min: parseInt(list[0].label.split(':')[0]),
|
||||||
|
max: parseInt(list[list.length - 1].label.split(':')[0]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.length) {
|
||||||
|
return quasar.notify({
|
||||||
|
message: t('ticket.boxing.notFound'),
|
||||||
|
type: 'negative',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-layout view="hhh lpr ffr" class="fit">
|
||||||
|
<q-drawer show-if-above side="right" bordered>
|
||||||
|
<q-scroll-area class="fit">
|
||||||
|
<q-list bordered separator style="max-width: 318px">
|
||||||
|
<q-item v-if="lastExpedition && videoList.length">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label class="text-h6">
|
||||||
|
{{ t('ticket.boxing.selectTime') }} ({{ time.min }}-{{ time.max }})
|
||||||
|
</q-item-label>
|
||||||
|
<q-range
|
||||||
|
v-model="time"
|
||||||
|
@change="getVideoList(lastExpedition, time)"
|
||||||
|
:min="0"
|
||||||
|
:max="24"
|
||||||
|
:step="1"
|
||||||
|
:left-label-value="time.min + ':00'"
|
||||||
|
:right-label-value="time.max + ':00'"
|
||||||
|
label
|
||||||
|
markers
|
||||||
|
snap
|
||||||
|
color="orange"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item v-if="lastExpedition && videoList.length">
|
||||||
|
<q-item-section>
|
||||||
|
<q-select
|
||||||
|
color="orange"
|
||||||
|
v-model="slide"
|
||||||
|
:options="videoList"
|
||||||
|
:label="t('ticket.boxing.selectVideo')"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="schedule" />
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
v-for="expedition in expeditions"
|
||||||
|
:key="expedition.id"
|
||||||
|
@click="getVideoList(expedition.id)"
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label class="text-h6">#{{ expedition.id }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label caption>{{ t('ticket.boxing.created') }}</q-item-label>
|
||||||
|
<q-item-label>
|
||||||
|
{{ date.formatDate(expedition.created, 'YYYY-MM-DD HH:mm:ss') }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label caption>{{ t('ticket.boxing.item') }}</q-item-label>
|
||||||
|
<q-item-label>{{ expedition.packagingItemFk }}</q-item-label>
|
||||||
|
<q-item-label caption>{{ t('ticket.boxing.worker') }}</q-item-label>
|
||||||
|
<q-item-label>{{ expedition.userName }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-scroll-area>
|
||||||
|
</q-drawer>
|
||||||
|
|
||||||
|
<q-page-container>
|
||||||
|
<q-page>
|
||||||
|
<q-card>
|
||||||
|
<q-carousel animated v-model="slide" height="max-content">
|
||||||
|
<q-carousel-slide v-for="video in videoList" :key="video.value" :name="video.value">
|
||||||
|
<q-video :src="video.url" :ratio="16 / 9" />
|
||||||
|
</q-carousel-slide>
|
||||||
|
</q-carousel>
|
||||||
|
</q-card>
|
||||||
|
</q-page>
|
||||||
|
</q-page-container>
|
||||||
|
</q-layout>
|
||||||
|
</template>
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script setup>
|
||||||
|
//import { computed } from 'vue';
|
||||||
|
//import { useState } from 'src/composables/useState';
|
||||||
|
//import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
//const state = useState();
|
||||||
|
//const router = useRouter();
|
||||||
|
/*const entityId = computed(function () {
|
||||||
|
return router.currentRoute.value.params.id;
|
||||||
|
});*/
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<!--<q-drawer v-model="state.drawer.value" show-if-above :width="200" :breakpoint="500">
|
||||||
|
<q-scroll-area class="fit text-grey-8">
|
||||||
|
<router-link :to="{ path: '/customer/list' }">
|
||||||
|
<q-icon name="arrow_back" size="md" color="primary" />
|
||||||
|
</router-link>
|
||||||
|
<div>Customer ID: {{ entityId }}</div>
|
||||||
|
</q-scroll-area>
|
||||||
|
</q-drawer>-->
|
||||||
|
<q-page-container>
|
||||||
|
<router-view></router-view>
|
||||||
|
</q-page-container>
|
||||||
|
</template>
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
|
||||||
|
import { createWrapper, axios } from 'app/tests/jest/jestHelpers';
|
||||||
|
import TicketBoxing from '../TicketBoxing.vue';
|
||||||
|
|
||||||
|
const mockPush = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('vue-router', () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
push: mockPush,
|
||||||
|
currentRoute: {
|
||||||
|
value: {
|
||||||
|
params: {
|
||||||
|
id: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('TicketBoxing', () => {
|
||||||
|
let vm;
|
||||||
|
beforeAll(() => {
|
||||||
|
vm = createWrapper(TicketBoxing).vm;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getVideoList()', () => {
|
||||||
|
it('should when response videoList use to list', async () => {
|
||||||
|
const expeditionId = 1;
|
||||||
|
const timed = {
|
||||||
|
min: 1,
|
||||||
|
max: 2
|
||||||
|
}
|
||||||
|
const videoList = [
|
||||||
|
"2022-01-01T01-01-00.mp4",
|
||||||
|
"2022-02-02T02-02-00.mp4",
|
||||||
|
"2022-03-03T03-03-00.mp4",
|
||||||
|
]
|
||||||
|
|
||||||
|
jest.spyOn(axios, 'get').mockResolvedValue({ data: videoList });
|
||||||
|
jest.spyOn(vm.quasar, 'notify');
|
||||||
|
|
||||||
|
await vm.getVideoList(expeditionId, timed);
|
||||||
|
|
||||||
|
expect(vm.videoList.length).toEqual(videoList.length);
|
||||||
|
expect(vm.slide).toEqual(videoList.reverse()[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should if not have video show notify', async () => {
|
||||||
|
const expeditionId = 1;
|
||||||
|
const timed = {
|
||||||
|
min: 1,
|
||||||
|
max: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
jest.spyOn(axios, 'get').mockResolvedValue({ data: [] });
|
||||||
|
jest.spyOn(vm.quasar, 'notify')
|
||||||
|
|
||||||
|
await vm.getVideoList(expeditionId, timed);
|
||||||
|
|
||||||
|
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
|
||||||
|
{ 'type': 'negative' }
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,110 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const customers = [
|
||||||
|
{
|
||||||
|
id: 1101,
|
||||||
|
name: 'Bruce Wayne',
|
||||||
|
username: 'batman',
|
||||||
|
email: 'batman@gotham',
|
||||||
|
phone: '555-555-5555',
|
||||||
|
expanded: ref(false),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1102,
|
||||||
|
name: 'James Gordon',
|
||||||
|
username: 'jamesgordon',
|
||||||
|
email: 'jamesgordon@gotham',
|
||||||
|
phone: '555-555-1111',
|
||||||
|
expanded: ref(false),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function navigate(id) {
|
||||||
|
router.push({ path: `/customer/${id}` });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-page class="q-pa-md">
|
||||||
|
<div class="column items-center q-gutter-y-md">
|
||||||
|
<q-card v-for="customer in customers" :key="customer.id" class="card">
|
||||||
|
<!-- v-ripple :to="{ path: '/dashboard' }" -->
|
||||||
|
<q-item v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
|
||||||
|
<q-item-section class="q-pa-md">
|
||||||
|
<div class="text-h6">{{ customer.name }}</div>
|
||||||
|
<q-item-label caption>@{{ customer.username }}</q-item-label>
|
||||||
|
<div class="q-mt-md">
|
||||||
|
<q-list>
|
||||||
|
<q-item class="q-pa-none">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label caption>Email</q-item-label>
|
||||||
|
<q-item-label>{{ customer.email }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item class="q-pa-none">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label caption>Phone</q-item-label>
|
||||||
|
<q-item-label>{{ customer.phone }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
<q-btn color="grey-7" round flat icon="more_vert">
|
||||||
|
<q-menu cover auto-close>
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable>
|
||||||
|
<q-item-section>Action 1</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable>
|
||||||
|
<q-item-section>Action 2</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
<q-separator vertical />
|
||||||
|
<q-card-actions vertical class="justify-between">
|
||||||
|
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(customer.id)" />
|
||||||
|
<q-btn flat round color="accent" icon="preview" />
|
||||||
|
<q-btn flat round color="accent" icon="vn:ticket" />
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn
|
||||||
|
color="grey"
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:icon="customer.expanded.value ? 'keyboard_arrow_up' : 'keyboard_arrow_down'"
|
||||||
|
@click="customer.expanded.value = !customer.expanded.value"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-item>
|
||||||
|
<q-slide-transition>
|
||||||
|
<div v-show="customer.expanded.value">
|
||||||
|
<q-separator />
|
||||||
|
<q-card-section class="text-subitle2">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Address</q-item-label>
|
||||||
|
<q-item-label caption>Avenue 11</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-card-section>
|
||||||
|
</div>
|
||||||
|
</q-slide-transition>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 60em;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useState } from 'src/composables/useState';
|
||||||
|
//import LeftMenu from 'src/components/LeftMenu.vue';
|
||||||
|
|
||||||
|
const state = useState();
|
||||||
|
const miniState = ref(true);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-drawer
|
||||||
|
v-model="state.drawer.value"
|
||||||
|
show-if-above
|
||||||
|
:mini="miniState"
|
||||||
|
@mouseover="miniState = false"
|
||||||
|
@mouseout="miniState = true"
|
||||||
|
mini-to-overlay
|
||||||
|
:width="256"
|
||||||
|
:breakpoint="500"
|
||||||
|
>
|
||||||
|
<!--<q-scroll-area class="fit text-grey-8">
|
||||||
|
<LeftMenu />
|
||||||
|
</q-scroll-area>-->
|
||||||
|
</q-drawer>
|
||||||
|
<q-page-container>
|
||||||
|
<router-view></router-view>
|
||||||
|
</q-page-container>
|
||||||
|
</template>
|
|
@ -1,11 +0,0 @@
|
||||||
import "./styles/quasar.scss";
|
|
||||||
import lang from "quasar/lang/es.js";
|
|
||||||
import "@quasar/extras/roboto-font/roboto-font.css";
|
|
||||||
import "@quasar/extras/material-icons/material-icons.css";
|
|
||||||
|
|
||||||
// To be used on app.use(Quasar, { ... })
|
|
||||||
export default {
|
|
||||||
config: {},
|
|
||||||
plugins: {},
|
|
||||||
lang: lang,
|
|
||||||
};
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { route } from 'quasar/wrappers';
|
||||||
|
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router';
|
||||||
|
// import { Notify } from 'quasar';
|
||||||
|
import routes from './routes';
|
||||||
|
import { i18n } from 'src/boot/i18n';
|
||||||
|
import { useState } from 'src/composables/useState';
|
||||||
|
import { useSession } from 'src/composables/useSession';
|
||||||
|
import { useRole } from 'src/composables/useRole';
|
||||||
|
|
||||||
|
const state = useState();
|
||||||
|
const session = useSession();
|
||||||
|
const role = useRole();
|
||||||
|
const { t } = i18n.global;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 route(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.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE),
|
||||||
|
});
|
||||||
|
|
||||||
|
Router.beforeEach(async (to, from, next) => {
|
||||||
|
const { isLoggedIn } = session;
|
||||||
|
|
||||||
|
if (!isLoggedIn() && to.name !== 'Login') {
|
||||||
|
return next({ name: 'Login', query: { redirect: to.fullPath } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
// try {
|
||||||
|
const stateRoles = state.getRoles().value;
|
||||||
|
if (stateRoles.length === 0) {
|
||||||
|
await role.fetch();
|
||||||
|
}
|
||||||
|
// } catch (error) {
|
||||||
|
// Notify.create({
|
||||||
|
// message: t('errors.statusUnauthorized'),
|
||||||
|
// type: 'negative',
|
||||||
|
// });
|
||||||
|
|
||||||
|
// session.destroy();
|
||||||
|
// return next({ path: '/login' });
|
||||||
|
// }
|
||||||
|
|
||||||
|
const matches = to.matched;
|
||||||
|
const hasRequiredRoles = matches.every(route => {
|
||||||
|
const meta = route.meta;
|
||||||
|
if (meta && meta.roles)
|
||||||
|
return role.hasAny(meta.roles)
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasRequiredRoles) {
|
||||||
|
return next({ path: '/' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
Router.afterEach((to) => {
|
||||||
|
let title = t(`login.title`);
|
||||||
|
|
||||||
|
const matches = to.matched;
|
||||||
|
let moduleName;
|
||||||
|
if (matches && matches.length > 1) {
|
||||||
|
const module = matches[1];
|
||||||
|
const moduleTitle = module.meta && module.meta.title;
|
||||||
|
moduleName = module.name.toLowerCase();
|
||||||
|
if (moduleTitle) {
|
||||||
|
title = t(`${moduleName}.pageTitles.${moduleTitle}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const childPage = to.meta;
|
||||||
|
const childPageTitle = childPage && childPage.title;
|
||||||
|
if (childPageTitle && matches.length > 2) {
|
||||||
|
if (title != '') title += ': ';
|
||||||
|
|
||||||
|
const pageTitle = t(`${moduleName}.pageTitles.${childPageTitle}`);
|
||||||
|
const idParam = to.params && to.params.id;
|
||||||
|
const idPageTitle = `${idParam} - ${pageTitle}`;
|
||||||
|
const builtTitle = idParam ? idPageTitle : pageTitle;
|
||||||
|
|
||||||
|
title += builtTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.title = title;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Router;
|
||||||
|
});
|
|
@ -1,26 +0,0 @@
|
||||||
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
|
|
||||||
import Home from "../views/Home.vue";
|
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
name: "Home",
|
|
||||||
component: Home,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/about",
|
|
||||||
name: "About",
|
|
||||||
// route level code-splitting
|
|
||||||
// this generates a separate chunk (about.[hash].js) for this route
|
|
||||||
// which is lazy-loaded when the route is visited.
|
|
||||||
component: () =>
|
|
||||||
import(/* webpackChunkName: "about" */ "../views/About.vue"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const router = createRouter({
|
|
||||||
history: createWebHistory(process.env.BASE_URL),
|
|
||||||
routes,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { RouterView } from 'vue-router';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
path: '/customer',
|
||||||
|
name: 'Customer',
|
||||||
|
meta: {
|
||||||
|
roles: ['developer'],
|
||||||
|
title: 'customers',
|
||||||
|
icon: 'vn:client'
|
||||||
|
},
|
||||||
|
component: RouterView,
|
||||||
|
redirect: { name: 'CustomerMain' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'CustomerMain',
|
||||||
|
component: () => import('src/pages/Customer/CustomerMain.vue'),
|
||||||
|
redirect: { name: 'CustomerList' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'list',
|
||||||
|
name: 'CustomerList',
|
||||||
|
meta: {
|
||||||
|
title: 'list',
|
||||||
|
icon: 'view_list',
|
||||||
|
},
|
||||||
|
component: () => import('src/pages/Customer/CustomerList.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
name: 'CustomerCreate',
|
||||||
|
meta: {
|
||||||
|
title: 'createCustomer',
|
||||||
|
icon: 'vn:addperson',
|
||||||
|
},
|
||||||
|
component: () => import('src/pages/Customer/CustomerCreate.vue'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
component: () => import('src/pages/Customer/Card/CustomerCard.vue'),
|
||||||
|
redirect: { name: 'CustomerBasicData' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'basic-data',
|
||||||
|
name: 'CustomerBasicData',
|
||||||
|
meta: {
|
||||||
|
title: 'basicData'
|
||||||
|
},
|
||||||
|
component: () => import('src/pages/Customer/Card/CustomerBasicData.vue'),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { RouterView } from 'vue-router';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
path: '/ticket',
|
||||||
|
name: 'Ticket',
|
||||||
|
meta: {
|
||||||
|
roles: ['developer'],
|
||||||
|
title: 'tickets',
|
||||||
|
icon: 'vn:ticket'
|
||||||
|
},
|
||||||
|
component: RouterView,
|
||||||
|
redirect: { name: 'TicketMain' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'TicketMain',
|
||||||
|
component: () => import('src/pages/Ticket/TicketMain.vue'),
|
||||||
|
redirect: { name: 'TicketList' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'list',
|
||||||
|
name: 'TicketList',
|
||||||
|
meta: {
|
||||||
|
title: 'list',
|
||||||
|
icon: 'view_list',
|
||||||
|
},
|
||||||
|
component: () => import('src/pages/Ticket/TicketList.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
name: 'TicketCreate',
|
||||||
|
meta: {
|
||||||
|
title: 'createTicket',
|
||||||
|
icon: 'vn:ticketAdd',
|
||||||
|
roles: ['salesPerson'],
|
||||||
|
},
|
||||||
|
component: () => import('src/pages/Ticket/TicketList.vue'),
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
component: () => import('src/pages/Ticket/Card/TicketCard.vue'),
|
||||||
|
redirect: { name: 'TicketBasicData' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'basic-data',
|
||||||
|
name: 'TicketBasicData',
|
||||||
|
meta: {
|
||||||
|
title: 'basicData'
|
||||||
|
},
|
||||||
|
component: () => import('src/pages/Ticket/Card/TicketBasicData.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'boxing',
|
||||||
|
name: 'TicketBoxing',
|
||||||
|
meta: {
|
||||||
|
title: 'boxing'
|
||||||
|
},
|
||||||
|
component: () => import('src/pages/Ticket/Card/TicketBoxing.vue'),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
|
@ -0,0 +1,35 @@
|
||||||
|
import customer from './modules/customer';
|
||||||
|
import ticket from './modules/ticket';
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
meta: { title: 'logIn' },
|
||||||
|
component: () => import('../pages/Login/Login.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Main',
|
||||||
|
component: () => import('../layouts/MainLayout.vue'),
|
||||||
|
redirect: { name: 'Dashboard' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/dashboard',
|
||||||
|
name: 'Dashboard',
|
||||||
|
meta: { title: 'dashboard', icon: 'dashboard' },
|
||||||
|
component: () => import('../pages/Dashboard/Dashboard.vue'),
|
||||||
|
},
|
||||||
|
// Module routes
|
||||||
|
customer,
|
||||||
|
ticket,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'NotFound',
|
||||||
|
component: () => import('../pages/NotFound.vue'),
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
|
@ -1,6 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
declare module '*.vue' {
|
|
||||||
import type { DefineComponent } from 'vue'
|
|
||||||
const component: DefineComponent<{}, {}, any>
|
|
||||||
export default component
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { createStore } from "vuex";
|
|
||||||
|
|
||||||
export default createStore({
|
|
||||||
state: {},
|
|
||||||
mutations: {},
|
|
||||||
actions: {},
|
|
||||||
modules: {},
|
|
||||||
});
|
|
|
@ -1,3 +0,0 @@
|
||||||
@import './quasar.variables.scss';
|
|
||||||
@import '~quasar-styl';
|
|
||||||
// @import '~quasar-addon-styl';
|
|
|
@ -1,15 +0,0 @@
|
||||||
// It's highly recommended to change the default colors
|
|
||||||
// to match your app's branding.
|
|
||||||
|
|
||||||
$primary : #027BE3;
|
|
||||||
$secondary : #26A69A;
|
|
||||||
$accent : #9C27B0;
|
|
||||||
|
|
||||||
$dark : #1D1D1D;
|
|
||||||
|
|
||||||
$positive : #21BA45;
|
|
||||||
$negative : #C10015;
|
|
||||||
$info : #31CCEC;
|
|
||||||
$warning : #F2C037;
|
|
||||||
|
|
||||||
@import '~quasar-variables-styl'
|
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="about">
|
|
||||||
<h1>This is an about page</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
|
@ -1,18 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="home">
|
|
||||||
<img alt="Vue logo" src="../assets/logo.png" />
|
|
||||||
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Options, Vue } from "vue-class-component";
|
|
||||||
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
|
|
||||||
|
|
||||||
@Options({
|
|
||||||
components: {
|
|
||||||
HelloWorld,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class Home extends Vue {}
|
|
||||||
</script>
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
videos/*
|
||||||
|
screenshots/*
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "Using fixtures to represent data",
|
||||||
|
"email": "hello@cypress.io",
|
||||||
|
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
describe('Login', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/#/login');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail to log in using wrong user', () => {
|
||||||
|
cy.get('input[aria-label="Username"]').type('incorrectUser');
|
||||||
|
cy.get('input[aria-label="Password"]').type('nightmare');
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.get('.q-notification__message').should('have.text', 'Invalid username or password');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail to log in using wrong password', () => {
|
||||||
|
cy.get('input[aria-label="Username"]').type('employee');
|
||||||
|
cy.get('input[aria-label="Password"]').type('wrongPassword');
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.get('.q-notification__message').should('have.text', 'Invalid username or password');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log in', () => {
|
||||||
|
cy.get('input[aria-label="Username"]').type('employee');
|
||||||
|
cy.get('input[aria-label="Password"]').type('nightmare');
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.get('.q-notification__message').should('have.text', 'You have successfully logged in');
|
||||||
|
cy.url().should('contain', '/dashboard');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log out', () => {
|
||||||
|
cy.get('input[aria-label="Username"]').type('employee');
|
||||||
|
cy.get('input[aria-label="Password"]').type('nightmare');
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.get('.q-notification__message').should('have.text', 'You have successfully logged in');
|
||||||
|
cy.url().should('contain', '/dashboard');
|
||||||
|
cy.get('#user').click();
|
||||||
|
cy.get('#logout').click();
|
||||||
|
cy.window().its('localStorage').invoke('getItem', 'token').should('not.exist');
|
||||||
|
cy.url().should('contain', '/login');
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`should get redirected to dashboard since employee can't create tickets`, () => {
|
||||||
|
cy.visit('/#/ticket/create', { failOnStatusCode: false });
|
||||||
|
cy.url().should('contain', '/#/login?redirect=/ticket/create');
|
||||||
|
cy.get('input[aria-label="Username"]').type('employee');
|
||||||
|
cy.get('input[aria-label="Password"]').type('nightmare');
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.url().should('contain', '/dashboard');
|
||||||
|
})
|
||||||
|
|
||||||
|
// ticket creation is not yet implemented, use this test once it is
|
||||||
|
// it(`should get redirected to ticket creation after login since salesPerson can do it`, () => {
|
||||||
|
// cy.visit('/#/ticket/create', { failOnStatusCode: false });
|
||||||
|
// cy.url().should('contain', '/#/login?redirect=/ticket/create');
|
||||||
|
// cy.get('input[aria-label="Username"]').type('salesPerson');
|
||||||
|
// cy.get('input[aria-label="Password"]').type('nightmare');
|
||||||
|
// cy.get('button[type="submit"]').click();
|
||||||
|
// cy.url().should('contain', '/#/ticket/create');
|
||||||
|
// })
|
||||||
|
});
|
|
@ -0,0 +1,38 @@
|
||||||
|
describe('TicketBoxing', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const ticketId = 1;
|
||||||
|
cy.viewport(1280, 720)
|
||||||
|
cy.login('developer')
|
||||||
|
cy.visit(`/#/ticket/${ticketId}/boxing`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load expeditions of ticket', () => {
|
||||||
|
cy.get('div[class="q-item__label text-h6"]').eq(0).should('have.text', '#1');
|
||||||
|
cy.get('div[class="q-item__label text-h6"]').eq(1).should('have.text', '#2');
|
||||||
|
cy.get('div[class="q-item__label text-h6"]').eq(2).should('have.text', '#3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error if not have video list', () => {
|
||||||
|
cy.get('div[class="q-item__label text-h6"]').eq(0).click();
|
||||||
|
cy.get('.q-notification__message').should('have.text', 'No videos available');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show select time and video if have video list', () => {
|
||||||
|
cy.intercept(
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/Boxings/*',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
"2022-01-01T01-01-00.mp4",
|
||||||
|
"2022-02-02T02-02-00.mp4",
|
||||||
|
"2022-03-03T03-03-00.mp4",
|
||||||
|
]
|
||||||
|
).as('getVideoList');
|
||||||
|
cy.get('.q-list > :nth-child(3)').click();
|
||||||
|
|
||||||
|
cy.get('.q-list > :nth-child(1)').should('be.visible');
|
||||||
|
cy.get('.q-list > :nth-child(2)').should('be.visible');
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,31 @@
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
/* eslint-env node */
|
||||||
|
// ***********************************************************
|
||||||
|
// This example plugins/index.js can be used to load plugins
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off loading
|
||||||
|
// the plugins file with the 'pluginsFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/plugins-guide
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// This function is called when a project is opened or re-opened (e.g. due to
|
||||||
|
// the project's config changing)
|
||||||
|
|
||||||
|
// cypress/plugins/index.js
|
||||||
|
|
||||||
|
// const {injectDevServer} = require('@quasar/quasar-app-extension-testing-e2e-cypress/cct-dev-server');
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * @type {Cypress.PluginConfig}
|
||||||
|
// */
|
||||||
|
module.exports = async (on, config) => {
|
||||||
|
// // Enable component testing, you can safely remove this
|
||||||
|
// // if you don't plan to use Cypress for unit tests
|
||||||
|
// if (config.testingType === 'component') {
|
||||||
|
// await injectDevServer(on, config);
|
||||||
|
// }
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|