Removed old folder
gitea/salix-front/pipeline/head There was a failure building this commit Details

This commit is contained in:
joan 2022-12-20 12:50:57 +01:00
parent ebf4a791a1
commit 7df523e2ae
145 changed files with 63 additions and 46028 deletions

View File

@ -1,19 +0,0 @@
{
"plugins": ["@babel/plugin-syntax-dynamic-import"],
"env": {
"test": {
"plugins": ["dynamic-import-node"],
"presets": [
[
"@babel/preset-env",
{
"modules": "commonjs",
"targets": {
"node": "current"
}
}
]
]
}
}
}

View File

@ -1,9 +0,0 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

View File

@ -1,8 +0,0 @@
/dist
/src-bex/www
/src-capacitor
/src-cordova
/.quasar
/node_modules
.eslintrc.js
babel.config.js

View File

@ -1,78 +0,0 @@
module.exports = {
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
// This option interrupts the configuration hierarchy at this file
// Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
root: true,
parserOptions: {
parser: '@babel/eslint-parser',
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
},
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',
],
},
],
};

34
__OLD__/.gitignore vendored
View File

@ -1,34 +0,0 @@
.DS_Store
.thumbs.db
node_modules
junit.xml
# Quasar core related directories
.quasar
/dist
# Cordova related directories and files
/src-cordova/node_modules
/src-cordova/platforms
/src-cordova/plugins
/src-cordova/www
# Capacitor related directories and files
/src-capacitor/www
/src-capacitor/node_modules
# BEX related directories and files
/src-bex/www
/src-bex/js/core
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln

View File

@ -1,9 +0,0 @@
/* 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'),
],
};

View File

@ -1,7 +0,0 @@
module.exports = {
singleQuote: true,
printWidth: 120,
tabWidth: 4,
semi: true,
endOfLine: 'auto',
};

View File

@ -1,15 +0,0 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"editorconfig.editorconfig",
"Vue.volar",
"wayou.vscode-todo-highlight"
],
"unwantedRecommendations": [
"octref.vetur",
"hookyqr.beautify",
"dbaeumer.jshint",
"ms-vscode.vscode-typescript-tslint-plugin"
]
}

View File

@ -1,17 +0,0 @@
{
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": ["source.fixAll.eslint"],
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
"json.schemas": [
{
"fileMatch": ["cypress.json"],
"url": "https://on.cypress.io/cypress.schema.json"
}
],
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View File

@ -1,5 +0,0 @@
FROM node:stretch-slim
RUN npm install -g @quasar/cli
WORKDIR /app
COPY dist/spa ./
CMD ["quasar", "serve", "./", "--history", "--hostname", "0.0.0.0"]

100
__OLD__/Jenkinsfile vendored
View File

@ -1,100 +0,0 @@
#!/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()
}
}
}
}
}
}

View File

@ -1,49 +0,0 @@
# Salix (salix-front)
Salix front-end
## Install the dependencies
```bash
yarn
# or
npm install
```
### Install quasar cli
```bash
sudo npm install -g @quasar/cli
```
### Start the app in development mode (hot-code reloading, error reporting, etc.)
```bash
quasar dev
```
### Lint the files
```bash
yarn lint
# or
npm run lint
```
### Format the files
```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).

View File

@ -1,18 +0,0 @@
/* 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 = {
presets: ['@quasar/babel-preset-app'],
extends: extend,
};

View File

@ -1,15 +0,0 @@
{
"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"
}
}

View File

@ -1,59 +0,0 @@
const esModules = ['quasar', 'quasar/lang', 'lodash-es'].join('|');
module.exports = {
globals: {
__DEV__: true,
// 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',
'^composables/(.*)$': '<rootDir>/src/composables/$1',
'^filters/(.*)$': '<rootDir>/src/filters/$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'],
};

View File

@ -1,21 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"src/*": ["src/*"],
"app/*": ["*"],
"components/*": ["src/components/*"],
"composables/*": ["src/composables/*"],
"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
}
}

36668
__OLD__/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,63 +0,0 @@
{
"name": "salix-front",
"version": "0.0.1",
"description": "Salix front-end",
"productName": "Salix",
"author": "Verdnatura",
"private": true,
"scripts": {
"lint": "eslint --ext .js,.vue ./",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:unit": "jest --reporters=default --watchAll",
"test:unit:ci": "jest --ci --reporters=default --reporters=jest-junit --maxWorkers=2",
"test:unit:coverage": "jest --coverage",
"serve:test:coverage": "quasar serve test/jest/coverage/lcov-report/ --port 8788",
"concurrently:dev:jest": "concurrently \"quasar dev\" \"jest --watch\"",
"test:e2e": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress open\"",
"test:e2e:ci": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress run\""
},
"dependencies": {
"@quasar/extras": "^1.15.8",
"axios": "^1.2.1",
"core-js": "^3.6.5",
"pinia": "^2.0.28",
"quasar": "^2.11.1",
"validator": "^13.7.0",
"vue": "^3.2.45",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@babel/eslint-parser": "^7.13.14",
"@intlify/vue-i18n-loader": "^4.1.0",
"@pinia/testing": "^0.0.14",
"@quasar/app-webpack": "^3.6.2",
"@quasar/quasar-app-extension-testing-e2e-cypress": "^4.2.2",
"@quasar/quasar-app-extension-testing-unit-jest": "^3.0.0-beta.5",
"eslint": "^8.30.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-jest": "^27.1.7",
"eslint-plugin-vue": "^8.7.1",
"eslint-webpack-plugin": "^3.2.0",
"jest-junit": "^13.0.0",
"prettier": "^2.5.1"
},
"browserslist": [
"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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1,251 +0,0 @@
/* 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');
const path = require('path');
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', 'pinia'],
// 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',
'material-icons-outlined',
'material-symbols-outlined',
],
// 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'] }]);
},
extendWebpack(cfg) {
cfg.resolve.alias = {
...cfg.resolve.alias, // This adds the existing alias
// Add your own alias like this
composables: path.resolve(__dirname, './src/composables'),
filters: path.resolve(__dirname, './src/filters'),
};
},
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-devServer
devServer: {
server: {
type: 'http',
},
port: 8080,
proxy: {
'/api': {
target: 'http://0.0.0.0:3000',
logLevel: 'debug',
changeOrigin: true,
secure: false,
},
},
},
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-framework
framework: {
config: {
brand: {
primary: 'orange',
},
dark: 'auto',
},
lang: 'es',
// 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'] }]);
},
},
};
});

View File

@ -1,9 +0,0 @@
{
"@quasar/testing-unit-jest": {
"babel": "babelrc",
"options": ["scripts"]
},
"@quasar/testing-e2e-cypress": {
"options": ["scripts"]
}
}

View File

@ -1,11 +0,0 @@
{
"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"
}
}

View File

@ -1,109 +0,0 @@
<script setup>
import { onMounted } from 'vue';
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, availableLocales, locale, fallbackLocale } = useI18n();
const { isLoggedIn } = session;
onMounted(() => {
let userLang = window.navigator.language;
if (userLang.includes('-')) {
userLang = userLang.split('-')[0];
}
if (availableLocales.includes(userLang)) {
locale.value = userLang;
} else {
locale.value = fallbackLocale;
}
});
quasar.iconMapFn = (iconName) => {
if (iconName.startsWith('vn:')) {
const name = iconName.substring(3);
return {
cls: `icon-${name}`,
};
}
return {
cls: 'material-symbols-outlined',
content: iconName,
};
};
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.reject(error);
}
axios.interceptors.response.use((response) => {
const { method } = response.config;
const isSaveRequest = method === 'patch';
if (isSaveRequest) {
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
icon: 'check',
});
}
return response;
}, responseError);
</script>
<template>
<router-view />
</template>
<style lang="scss">
.body--light {
background-color: #eee;
}
</style>

View File

@ -1,77 +0,0 @@
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,
}),
}));
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,
},
};
expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response));
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Invalid username or password',
type: 'negative',
})
);
});
it('should return an unauthorized error message', async () => {
jest.spyOn(vm.quasar, 'notify');
session.isLoggedIn.mockReturnValue(true);
const response = {
response: {
status: 401,
},
};
expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response));
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Access denied',
type: 'negative',
})
);
expect(session.destroy).toHaveBeenCalled();
});
});

View File

@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
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="&#38;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"
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"
style="enable-background:new 0 0 400 168.6;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#3D3D3F;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#8EBB27;}
.st2{fill:#8EBB27;}
.st3{fill:#F19300;}
</style>
<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">
</sodipodi:namedview>
<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: 6.2 KiB

View File

@ -1,22 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,21 +0,0 @@
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);
}
);

View File

@ -1,18 +0,0 @@
import { boot } from 'quasar/wrappers';
import { createI18n } from 'vue-i18n';
import messages from 'src/i18n';
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages,
legacy: false,
missingWarn: false
});
export default boot(({ app }) => {
// Set i18n instance on app
app.use(i18n);
});
export { i18n };

View File

@ -1,8 +0,0 @@
import { boot } from 'quasar/wrappers';
import { createPinia } from 'pinia';
export default boot(({ app }) => {
const pinia = createPinia();
app.use(pinia);
});

View File

@ -1,60 +0,0 @@
<script setup>
import { h, onMounted } from 'vue';
import axios from 'axios';
const $props = defineProps({
autoLoad: {
type: Boolean,
default: false,
},
url: {
type: String,
default: '',
},
filter: {
type: Object,
default: null,
},
where: {
type: Object,
default: null,
},
sortBy: {
type: String,
default: '',
},
limit: {
type: String,
default: '',
},
});
const emit = defineEmits(['onFetch']);
defineExpose({ fetch });
onMounted(async () => {
if ($props.autoLoad) {
await fetch();
}
});
async function fetch() {
const filter = Object.assign({}, $props.filter);
if ($props.where) filter.where = $props.where;
if ($props.sortBy) filter.order = $props.sortBy;
if ($props.limit) filter.limit = $props.limit;
const { data } = await axios.get($props.url, {
params: { filter },
});
emit('onFetch', data);
}
const render = () => {
return h('div', []);
};
</script>
<template>
<render />
</template>

View File

@ -1,118 +0,0 @@
<script setup>
import { onMounted, onUnmounted, computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios';
import { useState } from 'src/composables/useState';
import { useValidator } from 'src/composables/useValidator';
import SkeletonForm from 'components/ui/SkeletonForm.vue';
const quasar = useQuasar();
const { t } = useI18n();
const state = useState();
const { validate } = useValidator();
const $props = defineProps({
url: {
type: String,
default: '',
},
model: {
type: String,
default: '',
},
filter: {
type: Object,
default: null,
},
});
const emit = defineEmits(['onFetch']);
defineExpose({
save,
});
onMounted(async () => await fetch());
onUnmounted(() => {
state.unset($props.model);
});
const isLoading = ref(false);
const hasChanges = ref(false);
const formData = computed(() => state.get($props.model));
const originalData = ref();
async function fetch() {
const { data } = await axios.get($props.url, {
params: { filter: $props.filter },
});
state.set($props.model, data);
originalData.value = Object.assign({}, data);
watch(formData.value, () => (hasChanges.value = true));
emit('onFetch', state.get($props.model));
}
async function save() {
if (!hasChanges.value) {
return quasar.notify({
type: 'negative',
message: t('globals.noChanges'),
});
}
isLoading.value = true;
await axios.patch($props.url, formData.value);
originalData.value = formData.value;
hasChanges.value = false;
isLoading.value = false;
}
function reset() {
state.set($props.model, originalData.value);
hasChanges.value = false;
}
function filter(value, update, filterOptions) {
update(
() => {
const { options, filterFn } = filterOptions;
options.value = filterFn(options, value);
},
(ref) => {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
);
}
</script>
<template>
<q-banner v-if="hasChanges" class="text-white bg-warning">
<q-icon name="warning" size="md" class="q-mr-md" />
<span>{{ t('globals.changesToSave') }}</span>
</q-banner>
<q-form v-if="formData" @submit="save" @reset="reset" class="q-pa-md">
<slot name="form" :data="formData" :validate="validate" :filter="filter"></slot>
<div class="q-mt-lg">
<slot name="actions">
<q-btn :label="t('globals.save')" type="submit" color="primary" />
<q-btn
:label="t('globals.reset')"
type="reset"
class="q-ml-sm"
color="primary"
flat
:disable="!hasChanges"
/>
</slot>
</div>
</q-form>
<skeleton-form v-if="!formData" />
<q-inner-loading :showing="isLoading" :label="t('globals.pleaseWait')" color="primary" />
</template>

View File

@ -1,200 +0,0 @@
<script setup>
import axios from 'axios';
import { onMounted, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useRoute } from 'vue-router';
import { useNavigationStore } from 'src/stores/useNavigationStore';
import { toLowerCamel } from 'src/filters';
import routes from 'src/router/modules';
import LeftMenuItem from './LeftMenuItem.vue';
import LeftMenuItemGroup from './LeftMenuItemGroup.vue';
const { t } = useI18n();
const route = useRoute();
const quasar = useQuasar();
const navigation = useNavigationStore();
const props = defineProps({
source: {
type: String,
default: 'main',
},
});
onMounted(async () => {
await navigation.fetchPinned();
getRoutes();
});
function findMatches(search, item) {
const matches = [];
function findRoute(search, item) {
for (const child of item.children) {
if (search.indexOf(child.name) > -1) {
matches.push(child);
} else if (child.children) {
findRoute(search, child);
}
}
}
findRoute(search, item);
return matches;
}
function addChildren(module, route, parent) {
if (route.menus) {
const mainMenus = route.menus[props.source];
const matches = findMatches(mainMenus, route);
for (const child of matches) {
navigation.addMenuItem(module, child, parent);
}
}
}
const pinnedItems = computed(() => {
return items.value.filter((item) => item.isPinned);
});
const items = ref([]);
function getRoutes() {
if (props.source === 'main') {
const modules = Object.assign([], navigation.getModules().value);
for (const item of modules) {
const moduleDef = routes.find((route) => toLowerCamel(route.name) === item.module);
item.children = [];
if (!moduleDef) continue;
addChildren(item.module, moduleDef, item.children);
}
items.value = modules;
}
if (props.source === 'card') {
const currentRoute = route.matched[1];
const currentModule = toLowerCamel(currentRoute.name);
const moduleDef = routes.find((route) => toLowerCamel(route.name) === currentModule);
if (!moduleDef) return;
addChildren(currentModule, moduleDef, items.value);
}
}
async function togglePinned(item, event) {
if (event.defaultPrevented) return;
event.preventDefault();
event.stopPropagation();
const data = { moduleName: item.module };
const response = await axios.post('StarredModules/toggleStarredModule', data);
item.isPinned = false;
if (response.data && response.data.id) {
item.isPinned = true;
}
navigation.togglePinned(item.module);
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
}
</script>
<template>
<q-list padding>
<template v-if="$props.source === 'main'">
<q-item-label header>
{{ t('globals.pinnedModules') }}
</q-item-label>
<template v-for="item in pinnedItems" :key="item.name">
<template v-if="item.children">
<left-menu-item-group :item="item" group="pinnedModules" class="pinned">
<template #side>
<q-btn
v-if="item.isPinned === true"
@click="togglePinned(item, $event)"
icon="vn:pin_off"
size="xs"
flat
round
>
<q-tooltip>{{ t('components.leftMenu.removeFromPinned') }}</q-tooltip>
</q-btn>
<q-btn
v-if="item.isPinned === false"
@click="togglePinned(item, $event)"
icon="vn:pin"
size="xs"
flat
round
>
<q-tooltip>{{ t('components.leftMenu.addToPinned') }}</q-tooltip>
</q-btn>
</template>
</left-menu-item-group>
</template>
<left-menu-item v-if="!item.children" :item="item" />
</template>
<q-separator />
<q-expansion-item :label="t('moduleIndex.allModules')">
<template v-for="item in items" :key="item.name">
<template v-if="item.children">
<left-menu-item-group :item="item" group="modules">
<template #side>
<q-btn
v-if="item.isPinned === true"
@click="togglePinned(item, $event)"
icon="vn:pin_off"
size="xs"
flat
round
>
<q-tooltip>{{ t('components.leftMenu.removeFromPinned') }}</q-tooltip>
</q-btn>
<q-btn
v-if="item.isPinned === false"
@click="togglePinned(item, $event)"
icon="vn:pin"
size="xs"
flat
round
>
<q-tooltip>{{ t('components.leftMenu.addToPinned') }}</q-tooltip>
</q-btn>
</template>
</left-menu-item-group>
</template>
</template>
</q-expansion-item>
<q-separator />
</template>
<template v-if="$props.source === 'card'">
<template v-for="item in items" :key="item.name">
<left-menu-item v-if="!item.children" :item="item" />
</template>
</template>
</q-list>
</template>
<style>
.pinned .icon-pin,
.pinned .icon-pin_off {
visibility: hidden;
}
.pinned:hover .icon-pin,
.pinned:hover .icon-pin_off {
visibility: visible;
}
</style>

View File

@ -1,26 +0,0 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
item: {
type: Object,
required: true,
},
});
const item = computed(() => props.item);
</script>
<template>
<q-item active-class="text-primary" :to="{ name: item.name }" clickable v-ripple>
<q-item-section avatar v-if="item.icon">
<q-icon :name="item.icon" />
</q-item-section>
<q-item-section avatar v-if="!item.icon">
<q-icon name="disabled_by_default" />
</q-item-section>
<q-item-section>{{ t(item.title) }}</q-item-section>
</q-item>
</template>

View File

@ -1,51 +0,0 @@
<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import LeftMenuItem from './LeftMenuItem.vue';
const route = useRoute();
const { t } = useI18n();
const props = defineProps({
item: {
type: Object,
required: true,
},
group: {
type: String,
default: '',
},
});
const item = computed(() => props.item);
const isOpened = computed(() => {
const { matched } = route;
const { name } = item.value;
return matched.some((item) => item.name === name);
});
</script>
<template>
<q-expansion-item
:group="props.group"
active-class="text-primary"
:label="item.title"
:to="{ name: item.name }"
expand-separator
:default-opened="isOpened"
>
<template #header>
<q-item-section avatar>
<q-icon :name="item.icon"></q-icon>
</q-item-section>
<q-item-section>{{ t(item.title) }}</q-item-section>
<q-item-section side>
<slot name="side" :item="item" />
</q-item-section>
</template>
<template v-for="section in item.children" :key="section.name">
<left-menu-item :item="section" />
</template>
</q-expansion-item>
</template>

View File

@ -1,68 +0,0 @@
<script setup>
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState';
import { useSession } from 'src/composables/useSession';
import UserPanel from 'components/UserPanel.vue';
import PinnedModules from './PinnedModules.vue';
const { t } = useI18n();
const session = useSession();
const state = useState();
const user = state.getUser();
const token = session.getToken();
onMounted(() => (state.headerMounted.value = true));
function onToggleDrawer() {
state.drawer.value = !state.drawer.value;
}
</script>
<template>
<q-header class="bg-dark" color="white" elevated>
<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 id="searchbar"></div>
<q-space></q-space>
<div class="q-pl-sm q-gutter-sm row items-center no-wrap">
<div id="header-actions"></div>
<q-btn id="pinnedModules" icon="apps" flat dense rounded>
<q-tooltip bottom>
{{ t('globals.pinnedModules') }}
</q-tooltip>
<PinnedModules />
</q-btn>
<q-btn rounded 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-tooltip bottom>
{{ t('globals.userPanel') }}
</q-tooltip>
<UserPanel />
</q-btn>
</div>
</q-toolbar>
</q-header>
</template>

View File

@ -1,205 +0,0 @@
<script setup>
import axios from 'axios';
import { onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const $props = defineProps({
autoLoad: {
type: Boolean,
default: false,
},
url: {
type: String,
default: '',
},
data: {
type: Array,
default: null,
},
filter: {
type: Object,
default: null,
},
where: {
type: Object,
default: null,
},
sortBy: {
type: String,
default: '',
},
limit: {
type: String,
default: '',
},
rowsPerPage: {
type: Number,
default: 10,
},
offset: {
type: Number,
default: 500,
},
});
const emit = defineEmits(['onFetch', 'onPaginate']);
defineExpose({ refresh });
onMounted(() => {
if ($props.autoLoad) paginate();
});
watch(
() => $props.data,
() => {
rows.value = $props.data;
}
);
const isLoading = ref(false);
const hasMoreData = ref(false);
const pagination = ref({
sortBy: $props.sortBy,
rowsPerPage: $props.rowsPerPage,
page: 1,
});
const rows = ref(null);
async function fetch() {
const { page, rowsPerPage, sortBy } = pagination.value;
if (!$props.url) return;
const filter = {
limit: rowsPerPage,
skip: rowsPerPage * (page - 1),
};
Object.assign(filter, $props.filter);
if ($props.where) filter.where = $props.where;
if ($props.sortBy) filter.order = $props.sortBy;
if ($props.limit) filter.limit = $props.limit;
if (sortBy) filter.order = sortBy;
const { data } = await axios.get($props.url, {
params: { filter },
});
isLoading.value = false;
return data;
}
async function paginate() {
const { page, rowsPerPage, sortBy, descending } = pagination.value;
const data = await fetch();
if (!data) {
isLoading.value = false;
return;
}
hasMoreData.value = data.length === rowsPerPage;
if (!rows.value) rows.value = [];
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;
emit('onFetch', rows);
emit('onPaginate', data);
}
async function refresh() {
const { rowsPerPage } = pagination.value;
const data = await fetch();
if (!data) {
isLoading.value = false;
return;
}
hasMoreData.value = data.length === rowsPerPage;
if (!rows.value) rows.value = [];
rows.value = data;
isLoading.value = false;
emit('onFetch', rows);
}
async function onLoad(...params) {
const done = params[1];
if (!rows.value || rows.value.length === 0 || !$props.url) return done(false);
pagination.value.page = pagination.value.page + 1;
await paginate();
const endOfPages = !hasMoreData.value;
done(endOfPages);
}
</script>
<template>
<q-infinite-scroll @load="onLoad" :offset="offset" class="column items-center">
<div v-if="rows" class="card-list q-gutter-y-md">
<slot name="body" :rows="rows"></slot>
<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>
<div v-if="!rows" class="card-list q-gutter-y-md">
<q-card class="card" v-for="$index in $props.rowsPerPage" :key="$index">
<q-item v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
<q-item-section class="q-pa-md">
<q-skeleton type="rect" class="q-mb-md" square />
<q-skeleton type="text" square />
<q-skeleton type="text" class="q-mb-md" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
</q-item-section>
<q-separator vertical />
<q-card-actions vertical class="justify-between">
<q-skeleton type="circle" class="q-mb-md" size="40px" />
<q-skeleton type="circle" class="q-mb-md" size="40px" />
<q-skeleton type="circle" class="q-mb-md" size="40px" />
</q-card-actions>
</q-item>
</q-card>
</div>
</q-infinite-scroll>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
.info-row {
width: 100%;
h5 {
margin: 0;
}
}
</style>

View File

@ -1,57 +0,0 @@
<script setup>
import { onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useNavigationStore } from 'src/stores/useNavigationStore';
const navigation = useNavigationStore();
const { t } = useI18n();
onMounted(() => {
navigation.fetchPinned();
});
const pinnedModules = computed(() => navigation.getPinnedModules());
</script>
<template>
<q-menu
anchor="bottom left"
class="row q-pa-md q-col-gutter-lg"
max-width="350px"
max-height="400px"
v-if="pinnedModules.length"
>
<div v-for="item of pinnedModules" :key="item.title" class="row no-wrap q-pa-xs flex-item">
<q-btn
align="evenly"
padding="16px"
flat
stack
size="lg"
:icon="item.icon"
color="primary"
class="col-4 button"
:to="{ name: item.name }"
>
<div class="text-center text-primary button-text">
{{ t(item.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>

View File

@ -1,137 +0,0 @@
<script setup>
import { onMounted, computed } from 'vue';
import { Dark, Quasar } 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 userLocale = computed({
get() {
return locale.value;
},
set(value) {
locale.value = value;
if (value === 'en') value = 'en-GB';
import(`quasar/lang/${value}`).then((language) => {
Quasar.lang.set(language.default);
});
},
});
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 !== null) {
darkMode.value = user.value.darkMode;
}
if (user.value.lang) {
locale.value = user.value.lang;
userLocale.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="userLocale"
@update:model-value="saveLanguage"
:label="t(`globals.lang['${userLocale}']`)"
icon="public"
color="orange"
false-value="es"
true-value="en"
/>
<q-toggle
v-model="darkMode"
@update:model-value="saveDarkMode"
:label="t(`globals.darkMode`)"
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>

View File

@ -1,104 +0,0 @@
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
import { createWrapper } from 'app/tests/jest/jestHelpers';
import Leftmenu from '../LeftMenu.vue';
import { createTestingPinia } from '@pinia/testing';
import { useNavigationStore } from 'src/stores/useNavigationStore';
const mockPush = jest.fn();
jest.mock('vue-router', () => ({
useRouter: () => ({
push: mockPush,
currentRoute: { value: 'myCurrentRoute' },
}),
useRoute: () => ({
matched: [],
}),
}));
jest.mock('src/router/modules', () => [
{
path: '/customer',
name: 'Customer',
meta: {
title: 'customers',
icon: 'vn:client',
},
menus: {
main: ['CustomerList', 'CustomerCreate'],
card: ['CustomerBasicData'],
},
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;
let navigation;
beforeAll(async () => {
vm = createWrapper(Leftmenu, {
propsData: {
source: 'main',
},
global: {
plugins: [createTestingPinia({ stubActions: false })],
},
}).vm;
navigation = useNavigationStore();
navigation.modules = ['customer']; // I should mock to have just one module but isn´t working
navigation.fetchPinned = jest.fn().mockReturnValue(Promise.resolve(true));
navigation.getModules = jest.fn().mockReturnValue({
value: [
{
name: 'customer',
title: 'customer.pageTitles.customers',
icon: 'vn:customer',
module: 'customer',
},
],
});
});
it('should return a proper formated object with two child items', async () => {
const expectedMenuItem = [
{
name: 'CustomerList',
title: 'customer.pageTitles.list',
icon: 'view_list',
},
{
name: 'CustomerCreate',
title: 'customer.pageTitles.createCustomer',
icon: 'vn:addperson',
},
];
const firstMenuItem = vm.items[0];
expect(firstMenuItem.children).toEqual(expect.arrayContaining(expectedMenuItem));
});
});

View File

@ -1,151 +0,0 @@
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
import { createWrapper, axios } from 'app/tests/jest/jestHelpers';
import Paginate from '../PaginateData.vue';
const mockPush = jest.fn();
jest.mock('vue-router', () => ({
useRouter: () => ({
push: mockPush,
currentRoute: { value: 'myCurrentRoute' }
}),
}));
describe('Paginate', () => {
const expectedUrl = '/api/customers';
let vm;
beforeAll(() => {
const options = {
attrs: {
url: expectedUrl,
sortBy: 'id DESC',
rowsPerPage: 3
}
};
vm = createWrapper(Paginate, 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('paginate()', () => {
it('should call to the paginate() method and set the data on the rows property', async () => {
const expectedOptions = {
params: {
filter: {
order: 'id DESC',
limit: 3,
skip: 0
}
}
};
await vm.paginate();
expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptions);
expect(vm.rows.length).toEqual(3);
});
it('should call to the paginate() method and then call it again to paginate', async () => {
const expectedOptions = {
params: {
filter: {
order: 'id DESC',
limit: 3,
skip: 0
}
}
};
await vm.paginate();
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.paginate();
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);
});
});
});

View File

@ -1,72 +0,0 @@
<script setup>
import { ref } from 'vue';
import { useDialogPluginComponent } from 'quasar';
import { useI18n } from 'vue-i18n';
const $props = defineProps({
address: {
type: String,
default: '',
},
send: {
type: Function,
required: true,
},
});
defineEmits(['confirm', ...useDialogPluginComponent.emits]);
const { dialogRef, onDialogOK } = useDialogPluginComponent();
const { t } = useI18n();
const address = ref($props.address);
const isLoading = ref(false);
async function confirm() {
isLoading.value = true;
await $props.send(address.value);
isLoading.value = false;
onDialogOK();
}
</script>
<template>
<q-dialog ref="dialogRef" persistent>
<q-card class="q-pa-sm">
<q-card-section class="row items-center q-pb-none">
<span class="text-h6 text-grey">{{ t('sendEmailNotification') }}</span>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-card-section class="row items-center">
{{ t('notifyAddress') }}
</q-card-section>
<q-card-section class="q-pt-none">
<q-input dense v-model="address" rounded outlined autofocus />
</q-card-section>
<q-card-actions align="right">
<q-btn :label="t('globals.cancel')" color="primary" flat v-close-popup />
<q-btn :label="t('globals.confirm')" color="primary" :loading="isLoading" @click="confirm" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<style lang="scss" scoped>
.q-card {
min-width: 350px;
}
</style>
<i18n>
{
"en": {
"sendEmailNotification": "Send email notification",
"notifyAddress": "The notification will be sent to the following address"
},
"es": {
"sendEmailNotification": "Enviar notificación por correo",
"notifyAddress": "La notificación se enviará a la siguiente dirección"
}
}
</i18n>

View File

@ -1,102 +0,0 @@
<script setup>
import { useSlots } from 'vue';
import { useI18n } from 'vue-i18n';
defineProps({
module: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
description: {
type: String,
required: true,
},
});
const slots = useSlots();
const { t } = useI18n();
</script>
<template>
<div class="descriptor">
<div class="header bg-primary q-pa-sm">
<router-link :to="{ name: `${module}List` }">
<q-btn round flat dense size="md" icon="view_list" color="white">
<q-tooltip>{{ t('components.cardDescriptor.mainList') }}</q-tooltip>
</q-btn>
</router-link>
<router-link :to="{ name: `${module}Summary`, params: { id: data.id } }">
<q-btn round flat dense size="md" icon="launch" color="white">
<q-tooltip>{{ t('components.cardDescriptor.summary') }}</q-tooltip>
</q-btn>
</router-link>
<q-btn v-if="slots.menu" size="md" icon="more_vert" color="white" round flat dense>
<q-tooltip>{{ t('components.cardDescriptor.moreOptions') }}</q-tooltip>
<q-menu>
<q-list>
<slot name="menu" />
</q-list>
</q-menu>
</q-btn>
</div>
<div v-if="$props.data" class="body q-py-sm">
<q-list>
<q-item-label header class="ellipsis text-h5" :lines="1">
{{ $props.description }}
<q-tooltip>{{ $props.description }}</q-tooltip>
</q-item-label>
<q-item dense>
<q-item-label class="text-subtitle2" caption>#{{ data.id }}</q-item-label>
</q-item>
</q-list>
<slot name="body" />
</div>
<!-- Skeleton -->
<div id="descriptor-skeleton" v-if="!$props.data">
<div class="col q-pl-sm q-pa-sm">
<q-skeleton type="text" square height="45px" />
<q-skeleton type="text" square height="18px" />
<q-skeleton type="text" square height="18px" />
<q-skeleton type="text" square height="18px" />
</div>
<q-card-actions>
<q-skeleton size="40px" />
<q-skeleton size="40px" />
<q-skeleton size="40px" />
<q-skeleton size="40px" />
<q-skeleton size="40px" />
</q-card-actions>
</div>
</div>
</template>
<style lang="scss">
.body {
.q-card__actions {
justify-content: center;
}
.text-h5 {
padding-top: 5px;
padding-bottom: 5px;
}
}
</style>
<style lang="scss" scoped>
.descriptor {
width: 256px;
.header {
display: flex;
justify-content: space-between;
align-items: stretch;
}
}
</style>

View File

@ -1,54 +0,0 @@
<script setup>
defineProps({
maxLength: {
type: Number,
required: true,
},
item: {
type: Object,
required: true,
},
});
</script>
<template>
<div class="fetchedTags">
<div class="wrap">
<div class="inline-tag" :class="{ empty: !$props.item.value5 }">{{ $props.item.value5 }}</div>
<div class="inline-tag" :class="{ empty: !$props.item.value6 }">{{ $props.item.value6 }}</div>
<div class="inline-tag" :class="{ empty: !$props.item.value7 }">{{ $props.item.value7 }}</div>
<div class="inline-tag" :class="{ empty: !$props.item.value8 }">{{ $props.item.value8 }}</div>
<div class="inline-tag" :class="{ empty: !$props.item.value9 }">{{ $props.item.value9 }}</div>
<div class="inline-tag" :class="{ empty: !$props.item.value10 }">{{ $props.item.value10 }}</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.fetchedTags {
align-items: center;
.wrap {
width: 100%;
flex-wrap: wrap;
display: flex;
}
.inline-tag {
height: 1rem;
margin: 0.05rem;
color: $secondary;
text-align: center;
font-size: smaller;
padding: 1px;
flex: 1;
border: 1px solid $color-spacer;
text-overflow: ellipsis;
overflow: hidden;
min-width: 4rem;
max-width: 4rem;
}
.empty {
border: 1px solid $color-spacer-light;
}
}
</style>

View File

@ -1,24 +0,0 @@
<template>
<div id="descriptor-skeleton">
<div class="col q-pl-sm q-pa-sm">
<q-skeleton type="text" square height="45px" />
<q-skeleton type="text" square height="18px" />
<q-skeleton type="text" square height="18px" />
<q-skeleton type="text" square height="18px" />
</div>
<q-card-actions>
<q-skeleton size="40px" />
<q-skeleton size="40px" />
<q-skeleton size="40px" />
<q-skeleton size="40px" />
<q-skeleton size="40px" />
</q-card-actions>
</div>
</template>
<style lang="scss" scoped>
#descriptor-skeleton .q-card__actions {
justify-content: space-between;
}
</style>

View File

@ -1,32 +0,0 @@
<template>
<div class="q-pa-md">
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-skeleton type="QInput" square />
</div>
<div class="col">
<q-skeleton type="QInput" square />
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-skeleton type="QInput" square />
</div>
<div class="col">
<q-skeleton type="QInput" square />
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-skeleton type="QInput" square />
</div>
<div class="col">
<q-skeleton type="QInput" square />
</div>
</div>
<div class="row q-gutter-md">
<q-skeleton type="QBtn" />
<q-skeleton type="QBtn" />
</div>
</div>
</template>

View File

@ -1,57 +0,0 @@
<template>
<div class="header bg-primary q-pa-sm q-mb-md">
<q-skeleton type="rect" square />
</div>
<div class="row q-pa-md q-col-gutter-md q-mb-md">
<div class="col">
<q-skeleton type="rect" class="q-mb-md" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
</div>
<div class="col">
<q-skeleton type="rect" class="q-mb-md" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
</div>
<div class="col">
<q-skeleton type="rect" class="q-mb-md" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
</div>
<div class="col">
<q-skeleton type="rect" class="q-mb-md" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
</div>
<div class="col">
<q-skeleton type="rect" class="q-mb-md" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
</div>
</div>
</template>
<style lang="scss" scoped>
.row {
flex-wrap: wrap;
.col {
min-width: 250px;
}
}
</style>

View File

@ -1,20 +0,0 @@
<script setup>
import { nextTick, ref } from 'vue';
const $props = defineProps({
to: {
type: String,
required: true,
},
});
const isHeaderMounted = ref(false);
nextTick(() => {
isHeaderMounted.value = document.querySelector($props.to) !== null;
});
</script>
<template>
<teleport v-if="isHeaderMounted" :to="$props.to">
<slot />
</teleport>
</template>

View File

@ -1,76 +0,0 @@
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);
});
});
});

View File

@ -1,133 +0,0 @@
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
});
});
});

View File

@ -1,36 +0,0 @@
import { useSession } from './useSession';
import axios from 'axios';
import { useQuasar } from 'quasar';
export function usePrintService() {
const quasar = useQuasar();
const { getToken } = useSession();
function sendEmail(path, params) {
return axios.post(path, params).then(() =>
quasar.notify({
message: 'Notification sent',
type: 'positive',
icon: 'check',
})
);
}
function openReport(path, params) {
params = Object.assign(
{
access_token: getToken(),
},
params
);
const query = new URLSearchParams(params).toString();
window.open(`api/${path}?${query}`);
}
return {
sendEmail,
openReport,
};
}

View File

@ -1,37 +0,0 @@
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,
};
}

View File

@ -1,60 +0,0 @@
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,
};
}

View File

@ -1,74 +0,0 @@
import { ref, computed } from 'vue';
const state = ref({});
const user = ref({
id: 0,
name: '',
nickname: '',
lang: '',
darkMode: null,
});
const roles = ref([]);
const drawer = ref(true);
const headerMounted = ref(false);
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;
}
function set(name, data) {
state.value[name] = ref(data);
}
function get(name) {
return state.value[name];
}
function unset(name) {
delete state.value[name];
}
return {
getUser,
setUser,
getRoles,
setRoles,
set,
get,
unset,
drawer,
headerMounted
};
}

View File

@ -1,82 +0,0 @@
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import validator from 'validator';
const models = ref(null);
export function useValidator() {
if (!models.value) fetch();
function fetch() {
axios.get('Schemas/ModelInfo')
.then(response => models.value = response.data)
}
function validate(propertyRule) {
const modelInfo = models.value;
if (!modelInfo || !propertyRule) return;
const rule = propertyRule.split('.');
const model = rule[0];
const property = rule[1];
const modelName = model.charAt(0).toUpperCase() + model.slice(1);
if (!modelInfo[modelName]) return;
const modelValidations = modelInfo[modelName].validations;
if (!modelValidations[property]) return;
const rules = modelValidations[property].map((validation) => {
return validations(validation)[validation.validation];
});
return rules;
}
const { t } = useI18n();
const validations = function (validation) {
return {
presence: (value) => {
let message = `Value can't be empty`;
if (validation.message)
message = t(validation.message) || validation.message
return !validator.isEmpty(value ? String(value) : '') || message
},
length: (value) => {
const options = {
min: validation.min || validation.is,
max: validation.max || validation.is
};
value = String(value);
if (!value) value = '';
let message = `Value should have at most ${options.max} characters`;
if (validation.is)
message = `Value should be ${validation.is} characters long`;
if (validation.min)
message = `Value should have at least ${validation.min} characters`;
if (validation.min && validation.max)
message = `Value should have a length between ${validation.min} and ${validation.max}`;
return validator.isLength(value, options) || message;
},
numericality: (value) => {
if (validation.int)
return validator.isInt(value) || 'Value should be integer'
return validator.isNumeric(value) || 'Value should be a number'
},
custom: (value) => validation.bindedFunction(value) || 'Invalid value'
};
};
return {
validate
};
}

View File

@ -1,112 +0,0 @@
import * as validator from 'validator';
export const validators = {
presence: ($translate, value) => {
if (validator.isEmpty(value ? String(value) : ''))
throw new Error(_($translate, `Value can't be empty`));
},
absence: ($translate, value) => {
if (!validator.isEmpty(value))
throw new Error(_($translate, `Value should be empty`));
},
length: ($translate, value, conf) => {
let options = {
min: conf.min || conf.is,
max: conf.max || conf.is
};
let val = value ? String(value) : '';
if (!validator.isLength(val, options)) {
if (conf.is) {
throw new Error(_($translate,
`Value should be %s characters long`, [conf.is]));
} else if (conf.min && conf.max) {
throw new Error(_($translate,
`Value should have a length between %s and %s`, [conf.min, conf.max]));
} else if (conf.min) {
throw new Error(_($translate,
`Value should have at least %s characters`, [conf.min]));
} else {
throw new Error(_($translate,
`Value should have at most %s characters`, [conf.max]));
}
}
},
numericality: ($translate, value, conf) => {
if (conf.int) {
if (!validator.isInt(value))
throw new Error(_($translate, `Value should be integer`));
} else if (!validator.isNumeric(value))
throw new Error(_($translate, `Value should be a number`));
},
inclusion: ($translate, value, conf) => {
if (!validator.isIn(value, conf.in))
throw new Error(_($translate, `Invalid value`));
},
exclusion: ($translate, value, conf) => {
if (validator.isIn(value, conf.in))
throw new Error(_($translate, `Invalid value`));
},
format: ($translate, value, conf) => {
if (!validator.matches(value, conf.with))
throw new Error(_($translate, `Invalid value`));
},
custom: ($translate, value, conf) => {
if (!conf.bindedFunction(value))
throw new Error(_($translate, `Invalid value`));
}
};
/**
* Checks if value satisfies a set of validations.
*
* @param {*} value The value
* @param {Array} validations Array with validations
*/
export function validateAll($translate, value, validations) {
for (let conf of validations)
validate($translate, value, conf);
}
/**
* Checks if value satisfies a validation.
*
* @param {*} value The value
* @param {Object} conf The validation configuration
*/
export function validate($translate, value, conf) {
let validator = validators[conf.validation];
try {
let isEmpty = value == null || value === '';
if (isEmpty)
checkNull($translate, value, conf);
if (validator && (!isEmpty || conf.validation == 'presence'))
validator($translate, value, conf);
} catch (e) {
let message = conf.message ? conf.message : e.message;
throw new Error(_($translate, message));
}
}
/**
* Checks if value satisfies a blank or not null validation.
*
* @param {*} value The value
* @param {Object} conf The validation configuration
*/
export function checkNull($translate, value, conf) {
if (conf.allowBlank === false && value === '')
throw new Error(_($translate, `Value can't be blank`));
else if (conf.allowNull === false && value == null)
throw new Error(_($translate, `Value can't be null`));
}
export function _($translate, text, params = []) {
text = $translate.instant(text);
for (let i = 0; i < params.length; i++) {
text = text.replace('%s', params[i]);
}
return text;
}

View File

@ -1,21 +0,0 @@
// app global css in SCSS form
@import './icons.scss';
.body--dark {
.q-card--dark {
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2), 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12);
}
.q-layout__shadow::after {
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.2), 0 0px 10px rgba(0, 0, 0, 0.24) !important;
}
}
.link {
color: $primary;
cursor: pointer;
}
.link:hover {
color: $orange-4;
}

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -1,399 +0,0 @@
@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";
}

View File

@ -1,30 +0,0 @@
// 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: #ff9800;
$secondary: #26a69a;
$accent: #9c27b0;
$dark: #1d1d1d;
$positive: #21ba45;
$negative: #c10015;
$info: #31ccec;
$warning: #f2c037;
$color-spacer-light: rgba(255, 255, 255, .12);
$color-spacer:rgba(255, 255, 255, .3);
$border-thin-light: 1px solid $color-spacer-light;
$spacing-md: 16px;

View File

@ -1,4 +0,0 @@
export default function (value) {
if (value == null || value === '') return '-';
return value;
}

View File

@ -1,15 +0,0 @@
import toLowerCase from './toLowerCase';
import toDate from './toDate';
import toCurrency from './toCurrency';
import toPercentage from './toPercentage';
import toLowerCamel from './toLowerCamel';
import dashIfEmpty from './dashIfEmpty';
export {
toLowerCase,
toLowerCamel,
toDate,
toCurrency,
toPercentage,
dashIfEmpty,
};

View File

@ -1,21 +0,0 @@
import { useI18n } from 'vue-i18n';
export default function (value, symbol = 'EUR', fractionSize = 2) {
if (value == null || value === '') value = 0;
const { locale } = useI18n();
const options = {
style: 'currency',
currency: symbol,
minimumFractionDigits: fractionSize,
maximumFractionDigits: fractionSize
};
const lang = locale.value == 'es' ? 'de' : locale.value;
return new Intl.NumberFormat(lang, options)
.format(value);
}

View File

@ -1,12 +0,0 @@
import { useI18n } from 'vue-i18n';
export default function (value, options = {}) {
if (!value) return;
if (!options.dateStyle) options.dateStyle = 'short';
const { locale } = useI18n();
const date = new Date(value);
return new Intl.DateTimeFormat(locale.value, options).format(date)
}

View File

@ -1,5 +0,0 @@
export default function toLowerCamel(value) {
if (!value) return;
if (typeof (value) !== 'string') return value;
return value.charAt(0).toLowerCase() + value.slice(1);
}

View File

@ -1,3 +0,0 @@
export default function toLowerCase(value) {
return value.toLowerCase();
}

View File

@ -1,18 +0,0 @@
import { useI18n } from 'vue-i18n';
export default function (value, fractionSize = 2) {
if (value == null || value === '') return;
const { locale } = useI18n();
const options = {
style: 'percent',
minimumFractionDigits: fractionSize,
maximumFractionDigits: fractionSize
};
return new Intl.NumberFormat(locale, options)
.format(parseFloat(value));
}

View File

@ -1,363 +0,0 @@
export default {
globals: {
lang: {
es: 'Spanish',
en: 'English',
},
language: 'Language',
collapseMenu: 'Collapse left menu',
backToDashboard: 'Return to dashboard',
notifications: 'Notifications',
userPanel: 'User panel',
pinnedModules: 'Pinned modules',
darkMode: 'Dark mode',
logOut: 'Log out',
dataSaved: 'Data saved',
dataDeleted: 'Data deleted',
add: 'Add',
create: 'Create',
save: 'Save',
remove: 'Remove',
reset: 'Reset',
cancel: 'Cancel',
confirm: 'Confirm',
back: 'Back',
yes: 'Yes',
no: 'No',
noChanges: 'No changes to save',
changesToSave: 'You have changes pending to save',
confirmRemove: 'You are about to delete this row. Are you sure?',
rowAdded: 'Row added',
rowRemoved: 'Row removed',
pleaseWait: 'Please wait...',
},
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',
fieldRequired: 'This field is required',
},
dashboard: {
pageTitles: {
dashboard: 'Dashboard',
},
},
customer: {
pageTitles: {
customers: 'Customers',
list: 'List',
createCustomer: 'Create customer',
summary: 'Summary',
basicData: 'Basic Data',
},
list: {
phone: 'Phone',
email: 'Email',
customerOrders: 'Display customer orders',
moreOptions: 'More options',
},
card: {
customerList: 'Customer list',
customerId: 'Claim ID',
salesPerson: 'Sales person',
credit: 'Credit',
securedCredit: 'Secured credit',
payMethod: 'Pay method',
debt: 'Debt',
isDisabled: 'Customer is disabled',
isFrozen: 'Customer is frozen',
hasDebt: 'Customer has debt',
notChecked: 'Customer not checked',
noWebAccess: 'Web access is disabled',
},
summary: {
basicData: 'Basic data',
fiscalAddress: 'Fiscal address',
fiscalData: 'Fiscal data',
billingData: 'Billing data',
consignee: 'Consignee',
businessData: 'Business data',
financialData: 'Financial data',
customerId: 'Customer ID',
name: 'Name',
contact: 'Contact',
phone: 'Phone',
mobile: 'Mobile',
email: 'Email',
salesPerson: 'Sales person',
contactChannel: 'Contact channel',
socialName: 'Social name',
fiscalId: 'Fiscal ID',
postcode: 'Postcode',
province: 'Province',
country: 'Country',
street: 'Address',
isEqualizated: 'Is equalizated',
isActive: 'Is active',
invoiceByAddress: 'Invoice by address',
verifiedData: 'Verified data',
hasToInvoice: 'Has to invoice',
notifyByEmail: 'Notify by email',
vies: 'VIES',
payMethod: 'Pay method',
bankAccount: 'Bank account',
dueDay: 'Due day',
hasLcr: 'Has LCR',
hasCoreVnl: 'Has core VNL',
hasB2BVnl: 'Has B2B VNL',
addressName: 'Address name',
addressCity: 'City',
addressStreet: 'Street',
username: 'Username',
webAccess: 'Web access',
totalGreuge: 'Total greuge',
mana: 'Mana',
priceIncreasingRate: 'Price increasing rate',
averageInvoiced: 'Average invoiced',
claimRate: 'Claming rate',
risk: 'Risk',
riskInfo: 'Invoices minus payments plus orders not yet invoiced',
credit: 'Credit',
creditInfo: `Company's maximum risk`,
securedCredit: 'Secured credit',
securedCreditInfo: `Solunion's maximum risk`,
balance: 'Balance',
balanceInfo: 'Invoices minus payments',
balanceDue: 'Balance due',
balanceDueInfo: 'Deviated invoices minus payments',
recoverySince: 'Recovery since',
},
basicData: {
socialName: 'Fiscal name',
businessType: 'Business type',
contact: 'Contact',
email: 'Email',
phone: 'Phone',
mobile: 'Mobile',
salesPerson: 'Sales person',
contactChannel: 'Contact channel',
},
},
ticket: {
pageTitles: {
tickets: 'Tickets',
list: 'List',
createTicket: 'Create ticket',
summary: 'Summary',
basicData: 'Basic Data',
boxing: 'Boxing',
},
list: {
nickname: 'Nickname',
state: 'State',
shipped: 'Shipped',
landed: 'Landed',
salesPerson: 'Sales person',
total: 'Total',
},
card: {
ticketId: 'Ticket ID',
state: 'State',
customerId: 'Customer ID',
salesPerson: 'Sales person',
agency: 'Agency',
shipped: 'Shipped',
warehouse: 'Warehouse',
customerCard: 'Customer card',
},
boxing: {
expedition: 'Expedition',
item: 'Item',
created: 'Created',
worker: 'Worker',
selectTime: 'Select time:',
selectVideo: 'Select video:',
notFound: 'No videos available',
},
summary: {
state: 'State',
salesPerson: 'Sales person',
agency: 'Agency',
zone: 'Zone',
warehouse: 'Warehouse',
route: 'Route',
invoice: 'Invoice',
shipped: 'Shipped',
landed: 'Landed',
packages: 'Packages',
consigneePhone: 'Consignee phone',
consigneeMobile: 'Consignee mobile',
clientPhone: 'Client phone',
clientMobile: 'Client mobile',
consignee: 'Consignee',
subtotal: 'Subtotal',
vat: 'VAT',
total: 'Total',
saleLines: 'Line items',
item: 'Item',
visible: 'Visible',
available: 'Available',
quantity: 'Quantity',
description: 'Description',
price: 'Price',
discount: 'Discount',
amount: 'Amount',
packing: 'Packing',
hasComponentLack: 'Component lack',
itemShortage: 'Not visible',
claim: 'Claim',
reserved: 'Reserved',
created: 'Created',
package: 'Package',
taxClass: 'Tax class',
services: 'Services',
changeState: 'Change state',
requester: 'Requester',
atender: 'Atender',
request: 'Request',
goTo: 'Go to'
}
},
claim: {
pageTitles: {
claims: 'Claims',
list: 'List',
createClaim: 'Create claim',
rmaList: 'RMA',
summary: 'Summary',
basicData: 'Basic Data',
rma: 'RMA',
},
list: {
customer: 'Customer',
assignedTo: 'Assigned',
created: 'Created',
state: 'State',
},
rmaList: {
code: 'Code',
records: 'records',
},
rma: {
user: 'User',
created: 'Created',
},
card: {
claimId: 'Claim ID',
assignedTo: 'Assigned',
created: 'Created',
state: 'State',
ticketId: 'Ticket ID',
customerSummary: 'Customer summary',
claimedTicket: 'Claimed ticket',
},
summary: {
customer: 'Customer',
assignedTo: 'Assigned',
attendedBy: 'Attended by',
created: 'Created',
state: 'State',
details: 'Details',
item: 'Item',
landed: 'Landed',
quantity: 'Quantity',
claimed: 'Claimed',
description: 'Description',
price: 'Price',
discount: 'Discount',
total: 'Total',
actions: 'Actions',
responsibility: 'Responsibility',
company: 'Company',
person: 'Employee/Customer',
},
basicData: {
customer: 'Customer',
assignedTo: 'Assigned',
created: 'Created',
state: 'State',
packages: 'Packages',
picked: 'Picked',
returnOfMaterial: 'Return of material authorization (RMA)',
},
},
invoiceOut: {
pageTitles: {
invoiceOuts: 'InvoiceOuts',
list: 'List',
createInvoiceOut: 'Create invoice out',
summary: 'Summary',
basicData: 'Basic Data'
},
list: {
ref: 'Reference',
issued: 'Issued',
amount: 'Amount',
client: 'Client',
created: 'Created',
company: 'Company',
dued: 'Due date'
},
card: {
issued: 'Issued',
amount: 'Amount',
client: 'Client',
company: 'Company',
customerCard: 'Customer card',
ticketList: 'Ticket List'
},
summary: {
issued: 'Issued',
created: 'Created',
dued: 'Due',
booked: 'Booked',
company: 'Company',
taxBreakdown: 'Tax breakdown',
type: 'Type',
taxableBase: 'Taxable base',
rate: 'Rate',
fee: 'Fee',
tickets: 'Tickets',
ticketId: 'Ticket id',
nickname: 'Alias',
shipped: 'Shipped',
totalWithVat: 'Amount',
}
},
components: {
topbar: {},
userPanel: {
settings: 'Settings',
logOut: 'Log Out',
},
smartCard: {
noData: 'No data to display',
openCard: 'View card',
openSummary: 'Open summary',
viewDescription: 'View description',
},
cardDescriptor: {
mainList: 'Main list',
summary: 'Summary',
moreOptions: 'More options',
},
leftMenu: {
addToPinned: 'Add to pinned',
removeFromPinned: 'Remove from pinned',
},
},
};

View File

@ -1,362 +0,0 @@
export default {
globals: {
lang: {
es: 'Español',
en: 'Inglés',
},
language: 'Idioma',
collapseMenu: 'Contraer menú lateral',
backToDashboard: 'Volver al tablón',
notifications: 'Notificaciones',
userPanel: 'Panel de usuario',
pinnedModules: 'Módulos fijados',
darkMode: 'Modo oscuro',
logOut: 'Cerrar sesión',
dataSaved: 'Datos guardados',
dataDeleted: 'Data deleted',
add: 'Añadir',
create: 'Crear',
save: 'Guardar',
remove: 'Eliminar',
reset: 'Restaurar',
cancel: 'Cancelar',
confirm: 'Confirmar',
back: 'Volver',
yes: 'Si',
no: 'No',
noChanges: 'Sin cambios que guardar',
changesToSave: 'Tienes cambios pendientes de guardar',
confirmRemove: 'Vas a eliminar este registro. ¿Continuar?',
rowAdded: 'Fila añadida',
rowRemoved: 'Fila eliminada',
pleaseWait: 'Por favor, espera...',
},
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',
fieldRequired: 'Este campo es obligatorio',
},
dashboard: {
pageTitles: {
dashboard: 'Tablón',
},
},
customer: {
pageTitles: {
customers: 'Clientes',
list: 'Listado',
createCustomer: 'Crear cliente',
summary: 'Resumen',
basicData: 'Datos básicos',
},
list: {
phone: 'Teléfono',
email: 'Email',
customerOrders: 'Mostrar órdenes del cliente',
moreOptions: 'Más opciones',
},
card: {
customerId: 'ID cliente',
salesPerson: 'Comercial',
credit: 'Crédito',
securedCredit: 'Crédito asegurado',
payMethod: 'Método de pago',
debt: 'Riesgo',
isDisabled: 'El cliente está desactivado',
isFrozen: 'El cliente está congelado',
hasDebt: 'El cliente tiene riesgo',
notChecked: 'El cliente no está comprobado',
noWebAccess: 'El acceso web está desactivado',
},
summary: {
basicData: 'Datos básicos',
fiscalAddress: 'Dirección fiscal',
fiscalData: 'Datos fiscales',
billingData: 'Datos de facturación',
consignee: 'Consignatario',
businessData: 'Datos comerciales',
financialData: 'Datos financieros',
customerId: 'ID cliente',
name: 'Nombre',
contact: 'Contacto',
phone: 'Teléfono',
mobile: 'Móvil',
email: 'Email',
salesPerson: 'Comercial',
contactChannel: 'Canal de contacto',
socialName: 'Razón social',
fiscalId: 'NIF/CIF',
postcode: 'Código postal',
province: 'Provincia',
country: 'País',
street: 'Calle',
isEqualizated: 'Equalizado',
isActive: 'Activo',
invoiceByAddress: 'Facturar por consignatario',
verifiedData: 'Datos verificados',
hasToInvoice: 'Facturar',
notifyByEmail: 'Notificar por email',
vies: 'VIES',
payMethod: 'Método de pago',
bankAccount: 'Cuenta bancaria',
dueDay: 'Día de pago',
hasLcr: 'Recibido LCR',
hasCoreVnl: 'Recibido core VNL',
hasB2BVnl: 'Recibido B2B VNL',
addressName: 'Nombre de la dirección',
addressCity: 'Ciudad',
addressStreet: 'Calle',
username: 'Usuario',
webAccess: 'Acceso web',
totalGreuge: 'Greuge total',
mana: 'Maná',
priceIncreasingRate: 'Ratio de incremento de precio',
averageInvoiced: 'Facturación media',
claimRate: 'Ratio de reclamaciones',
risk: 'Riesgo',
riskInfo: 'Facturas menos recibos mas pedidos sin facturar',
credit: 'Crédito',
creditInfo: `Riesgo máximo asumido por la empresa`,
securedCredit: 'Crédito asegurado',
securedCreditInfo: `Riesgo máximo asumido por Solunion`,
balance: 'Balance',
balanceInfo: 'Facturas menos recibos',
balanceDue: 'Saldo vencido',
balanceDueInfo: 'Facturas fuera de plazo menos recibos',
recoverySince: 'Recobro desde',
},
basicData: {
socialName: 'Nombre fiscal',
businessType: 'Tipo de negocio',
contact: 'Contacto',
email: 'Email',
phone: 'Teléfono',
mobile: 'Móvil',
salesPerson: 'Comercial',
contactChannel: 'Canal de contacto',
},
},
ticket: {
pageTitles: {
tickets: 'Tickets',
list: 'Listado',
createTicket: 'Crear ticket',
summary: 'Resumen',
basicData: 'Datos básicos',
boxing: 'Encajado',
},
list: {
nickname: 'Alias',
state: 'Estado',
shipped: 'Enviado',
landed: 'Entregado',
salesPerson: 'Comercial',
total: 'Total',
},
card: {
ticketId: 'ID ticket',
state: 'Estado',
customerId: 'ID cliente',
salesPerson: 'Comercial',
agency: 'Agencia',
shipped: 'Enviado',
warehouse: 'Almacén',
customerCard: 'Ficha del cliente',
},
boxing: {
expedition: 'Expedición',
item: 'Artículo',
created: 'Creado',
worker: 'Trabajador',
selectTime: 'Seleccionar hora:',
selectVideo: 'Seleccionar vídeo:',
notFound: 'No hay vídeos disponibles',
},
summary: {
state: 'Estado',
salesPerson: 'Comercial',
agency: 'Agencia',
zone: 'Zona',
warehouse: 'Almacén',
route: 'Ruta',
invoice: 'Factura',
shipped: 'Enviado',
landed: 'Entregado',
packages: 'Bultos',
consigneePhone: 'Tel. consignatario',
consigneeMobile: 'Móv. consignatario',
clientPhone: 'Tel. cliente',
clientMobile: 'Móv. cliente',
consignee: 'Consignatario',
subtotal: 'Subtotal',
vat: 'IVA',
total: 'Total',
saleLines: 'Líneas del pedido',
item: 'Artículo',
visible: 'Visible',
available: 'Disponible',
quantity: 'Cantidad',
description: 'Descripción',
price: 'Precio',
discount: 'Descuento',
amount: 'Importe',
packing: 'Encajado',
hasComponentLack: 'Faltan componentes',
itemShortage: 'No visible',
claim: 'Reclamación',
reserved: 'Reservado',
created: 'Fecha creación',
package: 'Embalaje',
taxClass: 'Tipo IVA',
services: 'Servicios',
changeState: 'Cambiar estado',
requester: 'Solicitante',
atender: 'Comprador',
request: 'Petición de compra',
goTo: 'Ir a'
}
},
claim: {
pageTitles: {
claims: 'Reclamaciones',
list: 'Listado',
createClaim: 'Crear reclamación',
rmaList: 'RMA',
summary: 'Resumen',
basicData: 'Datos básicos',
rma: 'RMA',
},
list: {
customer: 'Cliente',
assignedTo: 'Asignada a',
created: 'Creada',
state: 'Estado',
},
rmaList: {
code: 'Código',
records: 'registros',
},
rma: {
user: 'Usuario',
created: 'Creado',
},
card: {
claimId: 'ID reclamación',
assignedTo: 'Asignada a',
created: 'Creada',
state: 'Estado',
ticketId: 'ID ticket',
customerSummary: 'Resumen del cliente',
claimedTicket: 'Ticket reclamado',
},
summary: {
customer: 'Cliente',
assignedTo: 'Asignada a',
attendedBy: 'Atendida por',
created: 'Creada',
state: 'Estado',
details: 'Detalles',
item: 'Artículo',
landed: 'Entregado',
quantity: 'Cantidad',
claimed: 'Reclamado',
description: 'Descripción',
price: 'Precio',
discount: 'Descuento',
total: 'Total',
actions: 'Acciones',
responsibility: 'Responsabilidad',
company: 'Empresa',
person: 'Comercial/Cliente',
},
basicData: {
customer: 'Cliente',
assignedTo: 'Asignada a',
created: 'Creada',
state: 'Estado',
packages: 'Bultos',
picked: 'Recogida',
returnOfMaterial: 'Autorización de retorno de materiales (RMA)',
},
},
invoiceOut: {
pageTitles: {
invoiceOuts: 'Fact. emitidas',
list: 'Listado',
createInvoiceOut: 'Crear fact. emitida',
summary: 'Resumen',
basicData: 'Datos básicos'
},
list: {
ref: 'Referencia',
issued: 'Fecha emisión',
amount: 'Importe',
client: 'Cliente',
created: 'Fecha creación',
company: 'Empresa',
dued: 'Fecha vencimineto'
},
card: {
issued: 'Fecha emisión',
amount: 'Importe',
client: 'Cliente',
company: 'Empresa',
customerCard: 'Ficha del cliente',
ticketList: 'Listado de tickets'
},
summary: {
issued: 'Fecha',
created: 'Fecha creación',
dued: 'Vencimiento',
booked: 'Contabilizada',
company: 'Empresa',
taxBreakdown: 'Desglose impositivo',
type: 'Tipo',
taxableBase: 'Base imp.',
rate: 'Tarifa',
fee: 'Cuota',
tickets: 'Tickets',
ticketId: 'Id ticket',
nickname: 'Alias',
shipped: 'F. envío',
totalWithVat: 'Importe',
}
},
components: {
topbar: {},
userPanel: {
settings: 'Configuración',
logOut: 'Cerrar sesión',
},
smartCard: {
noData: 'Sin datos que mostrar',
openCard: 'Ver ficha',
openSummary: 'Abrir detalles',
viewDescription: 'Ver descripción',
},
cardDescriptor: {
mainList: 'Listado principal',
summary: 'Resumen',
moreOptions: 'Más opciones',
},
leftMenu: {
addToPinned: 'Añadir a fijados',
removeFromPinned: 'Eliminar de fijados',
},
},
};

View File

@ -1,7 +0,0 @@
import en from './en';
import es from './es';
export default {
en: en,
es: es,
};

View File

@ -1,25 +0,0 @@
<!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>

View File

@ -1,16 +0,0 @@
<script setup>
import { useQuasar } from 'quasar';
import Navbar from 'src/components/NavBar.vue';
const quasar = useQuasar();
</script>
<template>
<q-layout view="hHh LpR fFf">
<Navbar />
<router-view></router-view>
<q-footer v-if="quasar.platform.is.mobile"></q-footer>
</q-layout>
</template>
<style lang="scss" scoped></style>

View File

@ -1,187 +0,0 @@
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
const route = useRoute();
const { t } = useI18n();
const session = useSession();
const token = session.getToken();
const claimFilter = {
include: [
{
relation: 'client',
scope: {
fields: ['name'],
},
},
],
};
const workers = ref([]);
const workersCopy = ref([]);
const claimStates = ref([]);
const claimStatesCopy = ref([]);
function setWorkers(data) {
workers.value = data;
workersCopy.value = data;
}
function setClaimStates(data) {
claimStates.value = data;
claimStatesCopy.value = data;
}
const workerFilter = {
options: workers,
filterFn: (options, value) => {
const search = value.toLowerCase();
if (value === '') return workersCopy.value;
return options.value.filter((row) => {
const id = row.id;
const name = row.name.toLowerCase();
const idMatches = id == search;
const nameMatches = name.indexOf(search) > -1;
return idMatches || nameMatches;
});
},
};
const statesFilter = {
options: claimStates,
filterFn: (options, value) => {
const search = value.toLowerCase();
if (value === '') return claimStatesCopy.value;
return options.value.filter((row) => {
const description = row.description.toLowerCase();
return description.indexOf(search) > -1;
});
},
};
</script>
<template>
<fetch-data
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="setWorkers"
auto-load
/>
<fetch-data url="ClaimStates" @on-fetch="setClaimStates" auto-load />
<div class="container">
<q-card>
<form-model :url="`Claims/${route.params.id}`" :filter="claimFilter" model="claim">
<template #form="{ data, validate, filter }">
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-input v-model="data.client.name" :label="t('claim.basicData.customer')" disable />
</div>
<div class="col">
<q-input v-model="data.created" mask="####-##-##" fill-mask="_" autofocus>
<template #append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="data.created" mask="YYYY-MM-DD">
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
</div>
</q-date>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-select
v-model="data.workerFk"
:options="workers"
option-value="id"
option-label="name"
emit-value
:label="t('claim.basicData.assignedTo')"
map-options
use-input
@filter="(value, update) => filter(value, update, workerFilter)"
:rules="validate('claim.claimStateFk')"
:input-debounce="0"
>
<template #before>
<q-avatar color="orange">
<q-img
v-if="data.workerFk"
:src="`/api/Images/user/160x160/${data.workerFk}/download?access_token=${token}`"
spinner-color="white"
/>
</q-avatar>
</template>
</q-select>
</div>
<div class="col">
<q-select
v-model="data.claimStateFk"
:options="claimStates"
option-value="id"
option-label="description"
emit-value
:label="t('claim.basicData.state')"
map-options
use-input
@filter="(value, update) => filter(value, update, statesFilter)"
:rules="validate('claim.claimStateFk')"
:input-debounce="0"
>
</q-select>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-input
v-model="data.packages"
:label="t('claim.basicData.packages')"
:rules="validate('claim.packages')"
/>
</div>
<div class="col">
<q-input
v-model="data.rma"
:label="t('claim.basicData.returnOfMaterial')"
:rules="validate('claim.rma')"
/>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-checkbox v-model="data.hasToPickUp" :label="t('claim.basicData.picked')" />
</div>
</div>
</template>
</form-model>
</q-card>
</div>
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 800px;
}
</style>

View File

@ -1,50 +0,0 @@
<script setup>
import { useState } from 'composables/useState';
import ClaimDescriptor from './ClaimDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
const state = useState();
</script>
<template>
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
<q-scroll-area class="fit">
<claim-descriptor />
<q-separator />
<left-menu source="card" />
</q-scroll-area>
</q-drawer>
<q-page-container>
<q-page class="q-pa-md">
<router-view></router-view>
</q-page>
</q-page-container>
</template>
<style lang="scss">
.q-scrollarea__content {
max-width: 100%;
}
</style>
<style lang="scss" scoped>
.descriptor {
max-width: 256px;
h5 {
margin: 0 15px;
}
.header {
display: flex;
justify-content: space-between;
}
.q-card__actions {
justify-content: center;
}
#descriptor-skeleton .q-card__actions {
justify-content: space-between;
}
}
</style>

View File

@ -1,120 +0,0 @@
<script setup>
import { onMounted, computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters';
import axios from 'axios';
import TicketDescriptorPopover from 'pages/Ticket/Card/TicketDescriptorPopover.vue';
import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
onMounted(async () => {
await fetch();
});
const route = useRoute();
const { t } = useI18n();
const entityId = computed(() => {
return $props.id || route.params.id;
});
const claim = ref();
async function fetch() {
const filter = {
include: [
{ relation: 'client' },
{ relation: 'claimState' },
{
relation: 'claimState',
},
{
relation: 'worker',
scope: {
include: { relation: 'user' },
},
},
],
};
const options = { params: { filter } };
const { data } = await axios.get(`Claims/${entityId.value}`, options);
if (data) claim.value = data;
}
function stateColor(code) {
if (code === 'pending') return 'green';
if (code === 'managed') return 'orange';
if (code === 'resolved') return 'red';
}
</script>
<template>
<skeleton-descriptor v-if="!claim" />
<card-descriptor v-if="claim" module="Claim" :data="claim" :description="claim.client.name">
<template #menu>
<claim-descriptor-menu v-if="claim" :claim="claim" />
</template>
<template #body>
<q-list>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('claim.card.created') }}</q-item-label>
<q-item-label>{{ toDate(claim.created) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('claim.card.state') }}</q-item-label>
<q-item-label>
<q-chip :color="stateColor(claim.claimState.code)" dense>
{{ claim.claimState.description }}
</q-chip>
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('claim.card.ticketId') }}</q-item-label>
<q-item-label class="link">
{{ claim.ticketFk }}
<q-popup-proxy>
<ticket-descriptor-popover :id="claim.ticketFk" />
</q-popup-proxy>
</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('claim.card.assignedTo') }}</q-item-label>
<q-item-label>{{ claim.worker.user.name }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<q-card-actions>
<q-btn
size="md"
icon="vn:client"
color="primary"
:to="{ name: 'CustomerCard', params: { id: claim.clientFk } }"
>
<q-tooltip>{{ t('claim.card.customerSummary') }}</q-tooltip>
</q-btn>
<q-btn
size="md"
icon="vn:ticket"
color="primary"
:to="{ name: 'TicketCard', params: { id: claim.ticketFk } }"
>
<q-tooltip>{{ t('claim.card.claimedTicket') }}</q-tooltip>
</q-btn>
</q-card-actions>
</template>
</card-descriptor>
</template>

View File

@ -1,132 +0,0 @@
<script setup>
import axios from 'axios';
import { ref } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { usePrintService } from 'composables/usePrintService';
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
const $props = defineProps({
claim: {
type: Object,
required: true,
},
});
const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
const { openReport, sendEmail } = usePrintService();
const claim = ref($props.claim);
function openPickupOrder() {
const id = claim.value.id;
openReport(`Claims/${id}/claim-pickup-pdf`, {
recipientId: claim.value.clientFk,
});
}
function confirmPickupOrder() {
const customer = claim.value.client;
quasar.dialog({
component: SendEmailDialog,
componentProps: {
address: customer.email,
send: sendPickupOrder,
},
});
}
function sendPickupOrder(address) {
const id = claim.value.id;
const customer = claim.value.client;
return sendEmail(`Claims/${id}/claim-pickup-email`, {
recipientId: customer.id,
recipient: address,
});
}
const showConfirmDialog = ref(false);
async function deleteClaim() {
const id = claim.value.id;
await axios.delete(`Claims/${id}`);
quasar.notify({
message: t('globals.dataDeleted'),
type: 'positive',
icon: 'check',
});
await router.push({ name: 'ClaimList' });
}
</script>
<template>
<q-item v-ripple clickable>
<q-item-section avatar>
<q-icon name="summarize" />
</q-item-section>
<q-item-section>{{ t('pickupOrder') }}</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu anchor="top end" self="top start" auto-close>
<q-list>
<q-item @click="openPickupOrder" v-ripple clickable>
<q-item-section avatar>
<q-icon name="picture_as_pdf" />
</q-item-section>
<q-item-section>{{ t('openPickupOrder') }}</q-item-section>
</q-item>
<q-item @click="confirmPickupOrder" v-ripple clickable>
<q-item-section avatar>
<q-icon name="send" />
</q-item-section>
<q-item-section>{{ t('sendPickupOrder') }}</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<q-separator />
<q-item @click="showConfirmDialog = true" v-ripple clickable>
<q-item-section avatar>
<q-icon name="delete" />
</q-item-section>
<q-item-section>{{ t('deleteClaim') }}</q-item-section>
</q-item>
<q-dialog v-model="showConfirmDialog">
<q-card class="q-pa-sm">
<q-card-section class="row items-center q-pb-none">
<span class="text-h6 text-grey">{{ t('confirmDeletion') }}</span>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-card-section class="row items-center">{{ t('confirmDeletionMessage') }}</q-card-section>
<q-card-actions align="right">
<q-btn :label="t('globals.cancel')" color="primary" flat v-close-popup />
<q-btn :label="t('globals.confirm')" color="primary" @click="deleteClaim" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<i18n>
{
"en": {
"pickupOrder": "Pickup order",
"openPickupOrder": "Open pickup order",
"sendPickupOrder": "Send pickup order",
"deleteClaim": "Delete claim",
"confirmDeletion": "Confirm deletion",
"confirmDeletionMessage": "Are you sure you want to delete this claim?"
},
"es": {
"pickupOrder": "Orden de recogida",
"openPickupOrder": "Abrir orden de recogida",
"sendPickupOrder": "Enviar orden de recogida",
"deleteClaim": "Eliminar reclamación",
"confirmDeletion": "Confirmar eliminación",
"confirmDeletionMessage": "Seguro que quieres eliminar esta reclamación?"
}
}
</i18n>

View File

@ -1,159 +0,0 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useRoute } from 'vue-router';
import axios from 'axios';
import Paginate from 'src/components/PaginateData.vue';
import FetchData from 'components/FetchData.vue';
import TeleportSlot from 'components/ui/TeleportSlot';
import { toDate } from 'src/filters';
const quasar = useQuasar();
const route = useRoute();
const { t } = useI18n();
const claim = ref([]);
const fetcher = ref();
const filter = {
include: {
relation: 'rmas',
scope: {
include: {
relation: 'worker',
scope: {
include: {
relation: 'user',
},
},
},
order: 'created DESC',
},
},
};
async function addRow() {
const formData = {
code: claim.value.rma,
};
await axios.post(`ClaimRmas`, formData);
await fetcher.value.fetch();
quasar.notify({
type: 'positive',
message: t('globals.rowAdded'),
icon: 'check',
});
}
const confirmShown = ref(false);
const rmaId = ref(null);
function confirmRemove(id) {
confirmShown.value = true;
rmaId.value = id;
}
async function remove() {
const id = rmaId.value;
await axios.delete(`ClaimRmas/${id}`);
await fetcher.value.fetch();
confirmShown.value = false;
quasar.notify({
type: 'positive',
message: t('globals.rowRemoved'),
icon: 'check',
});
}
function hide() {
rmaId.value = null;
}
</script>
<template>
<fetch-data
ref="fetcher"
:url="`Claims/${route.params.id}`"
:filter="filter"
@on-fetch="(data) => (claim = data)"
auto-load
/>
<paginate :data="claim.rmas">
<template #body="{ rows }">
<q-card class="card">
<template v-for="row of rows" :key="row.id">
<q-item class="q-pa-none items-start">
<q-item-section class="q-pa-md">
<q-list>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('claim.rma.user') }}</q-item-label>
<q-item-label>{{ row.worker.user.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('claim.rma.created') }}</q-item-label>
<q-item-label>
{{ toDate(row.created, { timeStyle: 'medium' }) }}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-item-section>
<q-card-actions vertical class="justify-between">
<q-btn flat round color="orange" icon="vn:bin" @click="confirmRemove(row.id)">
<q-tooltip>{{ t('globals.remove') }}</q-tooltip>
</q-btn>
</q-card-actions>
</q-item>
<q-separator />
</template>
</q-card>
</template>
</paginate>
<q-dialog v-model="confirmShown" persistent @hide="hide">
<q-card>
<q-card-section class="row items-center">
<q-avatar icon="warning" color="primary" text-color="white" />
<span class="q-ml-sm">{{ t('globals.confirmRemove') }}</span>
</q-card-section>
<q-card-actions align="right">
<q-btn flat :label="t('globals.no')" color="primary" v-close-popup autofocus />
<q-btn flat :label="t('globals.yes')" color="primary" @click="remove()" />
</q-card-actions>
</q-card>
</q-dialog>
<teleport-slot v-if="!quasar.platform.is.mobile" to="#header-actions">
<div class="row q-gutter-x-sm">
<q-btn @click="addRow()" icon="add" color="primary" dense rounded>
<q-tooltip bottom> {{ t('globals.add') }} </q-tooltip>
</q-btn>
<q-separator vertical />
</div>
</teleport-slot>
<teleport-slot to=".q-footer">
<q-tabs align="justify" inline-label narrow-indicator>
<q-tab @click="addRow()" icon="add_circle" :label="t('globals.add')" />
</q-tabs>
</teleport-slot>
</template>
<style lang="scss" scoped>
.q-toolbar {
background-color: $grey-9;
}
.sticky-page {
padding-top: 66px;
}
.q-page-sticky {
z-index: 2998;
}
</style>

View File

@ -1,199 +0,0 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { toDate, toCurrency } from 'src/filters';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
onMounted(() => fetch());
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
default: 0,
},
});
const entityId = computed(() => $props.id || route.params.id);
const claim = ref(null);
const salesClaimed = ref(null);
function fetch() {
const id = entityId.value;
axios.get(`Claims/${id}/getSummary`).then(({ data }) => {
claim.value = data.claim;
salesClaimed.value = data.salesClaimed;
});
}
const detailsColumns = ref([
{
name: 'item',
label: 'claim.summary.item',
field: (row) => row.sale.itemFk,
sortable: true,
},
{
name: 'landed',
label: 'claim.summary.landed',
field: (row) => row.sale.ticket.landed,
format: (value) => toDate(value),
sortable: true,
},
{
name: 'quantity',
label: 'claim.summary.quantity',
field: (row) => row.sale.quantity,
sortable: true,
},
{
name: 'claimed',
label: 'claim.summary.claimed',
field: (row) => row.quantity,
sortable: true,
},
{
name: 'description',
label: 'claim.summary.description',
field: (row) => row.sale.concept,
},
{
name: 'price',
label: 'claim.summary.price',
field: (row) => row.sale.price,
sortable: true,
},
{
name: 'discount',
label: 'claim.summary.discount',
field: (row) => row.sale.discount,
format: (value) => `${value} %`,
sortable: true,
},
{
name: 'total',
label: 'claim.summary.total',
field: ({ sale }) => toCurrency(sale.quantity * sale.price * ((100 - sale.discount) / 100)),
sortable: true,
},
]);
function stateColor(code) {
if (code === 'pending') return 'green';
if (code === 'managed') return 'orange';
if (code === 'resolved') return 'red';
}
</script>
<template>
<div class="summary container">
<q-card>
<skeleton-summary v-if="!claim" />
<template v-if="claim">
<div class="header bg-primary q-pa-sm q-mb-md">{{ claim.id }} - {{ claim.client.name }}</div>
<q-list>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('claim.summary.created') }}</q-item-label>
<q-item-label>{{ toDate(claim.created) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('claim.summary.state') }}</q-item-label>
<q-item-label>
<q-chip :color="stateColor(claim.claimState.code)" dense>
{{ claim.claimState.description }}
</q-chip>
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('claim.summary.assignedTo') }}</q-item-label>
<q-item-label>{{ claim.worker.user.nickname }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('claim.summary.attendedBy') }}</q-item-label>
<q-item-label>{{ claim.client.salesPersonUser.name }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<q-card-section class="q-pa-md">
<h6>{{ t('claim.summary.details') }}</h6>
<q-table :columns="detailsColumns" :rows="salesClaimed" flat>
<template #header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
</q-th>
</q-tr>
</template>
</q-table>
</q-card-section>
<q-card-section class="q-pa-md">
<h6>{{ t('claim.summary.actions') }}</h6>
<q-separator />
<div id="slider-container">
<q-slider
v-model="claim.responsibility"
label
:label-value="t('claim.summary.responsibility')"
label-always
color="primary"
markers
:marker-labels="[
{ value: 1, label: t('claim.summary.company') },
{ value: 5, label: t('claim.summary.person') },
]"
:min="1"
:max="5"
readonly
/>
</div>
</q-card-section>
</template>
</q-card>
</div>
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 100%;
max-width: 950px;
}
.summary {
.header {
text-align: center;
font-size: 18px;
}
#slider-container {
max-width: 80%;
margin: 0 auto;
.q-slider {
.q-slider__marker-labels:nth-child(1) {
transform: none;
}
.q-slider__marker-labels:nth-child(2) {
transform: none;
left: auto !important;
right: 0%;
}
}
}
}
.q-dialog .summary {
max-width: 1200px;
}
</style>

View File

@ -1,29 +0,0 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import ClaimSummary from './ClaimSummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
</script>
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<claim-summary v-if="$props.id" :id="$props.id" />
</q-dialog>
</template>
<style lang="scss">
.q-dialog .summary .header {
position: sticky;
z-index: $z-max;
top: 0;
}
</style>

View File

@ -1,132 +0,0 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import Paginate from 'src/components/PaginateData.vue';
import { toDate } from 'src/filters/index';
import ClaimSummaryDialog from './Card/ClaimSummaryDialog.vue';
import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue';
const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
const filter = {
include: [
{
relation: 'client',
},
{
relation: 'claimState',
},
{
relation: 'worker',
scope: {
include: { relation: 'user' },
},
},
],
};
function stateColor(code) {
if (code === 'pending') return 'green';
if (code === 'managed') return 'orange';
if (code === 'resolved') return 'red';
}
function navigate(id) {
router.push({ path: `/claim/${id}` });
}
function viewSummary(id) {
quasar.dialog({
component: ClaimSummaryDialog,
componentProps: {
id,
},
});
}
</script>
<template>
<q-page class="q-pa-md">
<paginate url="/Claims" :filter="filter" sort-by="id DESC" auto-load>
<template #body="{ rows }">
<q-card class="card" v-for="row of rows" :key="row.id">
<q-item class="q-pa-none items-start cursor-pointer q-hoverable" v-ripple clickable>
<q-item-section class="q-pa-md" @click="navigate(row.id)">
<div class="text-h6 link">
{{ row.client.name }}
<q-popup-proxy>
<customer-descriptor-popover :customer="row.client" />
</q-popup-proxy>
</div>
<q-item-label caption>#{{ row.id }}</q-item-label>
<q-list>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('claim.list.customer') }}</q-item-label>
<q-item-label>{{ row.client.name }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('claim.list.assignedTo') }}</q-item-label>
<q-item-label>{{ row.worker.user.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('claim.list.created') }}</q-item-label>
<q-item-label>{{ toDate(row.created) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('claim.list.state') }}</q-item-label>
<q-item-label>
<q-chip :color="stateColor(row.claimState.code)" dense>
{{ row.claimState.description }}
</q-chip>
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-item-section>
<q-separator vertical />
<q-card-actions vertical class="justify-between">
<!-- <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="logs" />
</q-item-section>
<q-item-section>Display claim logs</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" @click="viewSummary(row.id)">
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
</q-btn>
<q-btn flat round color="grey-7" icon="vn:client">
<q-tooltip>{{ t('components.smartCard.viewDescription') }}</q-tooltip>
<q-popup-proxy>
<customer-descriptor-popover :customer="row.client" />
</q-popup-proxy>
</q-btn>
</q-card-actions>
</q-item>
</q-card>
</template>
</paginate>
</q-page>
</template>

View File

@ -1,17 +0,0 @@
<script setup>
import { useState } from 'src/composables/useState';
import LeftMenu from 'components/LeftMenu.vue';
const state = useState();
</script>
<template>
<q-drawer v-model="state.drawer.value" show-if-above :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>

View File

@ -1,143 +0,0 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios';
import Paginate from 'src/components/PaginateData.vue';
const quasar = useQuasar();
const { t } = useI18n();
const rmas = ref([]);
const card = ref(null);
function onFetch(data) {
rmas.value = data.value;
}
const newRma = ref({
code: '',
crated: new Date(),
});
function onInputUpdate(value) {
newRma.value.code = value.toUpperCase();
}
function submit() {
const formData = newRma.value;
if (formData.code === '') return;
axios
.post('ClaimRmas', formData)
.then(() => {
newRma.value = {
code: '',
crated: new Date(),
};
})
.then(() => card.value.refresh());
}
const confirmShown = ref(false);
const rmaId = ref(null);
function confirm(id) {
confirmShown.value = true;
rmaId.value = id;
}
function remove() {
const id = rmaId.value;
axios
.delete(`ClaimRmas/${id}`)
.then(() => {
confirmShown.value = false;
quasar.notify({
type: 'positive',
message: 'Entry deleted',
icon: 'check',
});
})
.then(() => card.value.refresh());
}
function hide() {
rmaId.value = null;
}
</script>
<template>
<q-page class="q-pa-md sticky">
<q-page-sticky expand position="top" :offset="[16, 16]">
<q-card class="card q-pa-md">
<q-form @submit="submit">
<q-input
v-model="newRma.code"
:label="t('claim.rmaList.code')"
@update:model-value="onInputUpdate"
class="q-mb-md"
autofocus
/>
<div class="text-caption">{{ rmas.length }} {{ t('claim.rmaList.records') }}</div>
</q-form>
</q-card>
</q-page-sticky>
<paginate ref="card" url="/ClaimRmas" @on-fetch="onFetch" sort-by="id DESC" auto-load>
<template #body="{ rows }">
<q-card class="card">
<template v-for="row of rows" :key="row.code">
<q-item class="q-pa-none items-start">
<q-item-section class="q-pa-md">
<q-list>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('claim.rmaList.code') }}</q-item-label>
<q-item-label>{{ row.code }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-item-section>
<q-card-actions vertical class="justify-between">
<q-btn flat round color="primary" icon="vn:bin" @click="confirm(row.id)">
<q-tooltip>{{ t('globals.remove') }}</q-tooltip>
</q-btn>
</q-card-actions>
</q-item>
<q-separator />
</template>
</q-card>
</template>
</paginate>
</q-page>
<q-dialog v-model="confirmShown" persistent @hide="hide">
<q-card>
<q-card-section class="row items-center">
<q-avatar icon="warning" color="primary" text-color="white" />
<span class="q-ml-sm">{{ t('globals.confirmRemove') }}</span>
</q-card-section>
<q-card-actions align="right">
<q-btn flat :label="t('globals.no')" color="primary" v-close-popup autofocus />
<q-btn flat :label="t('globals.yes')" color="primary" @click="remove()" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<style lang="scss" scoped>
.sticky {
padding-top: 156px;
}
.card {
width: 100%;
max-width: 60em;
}
.q-page-sticky {
z-index: 2998;
}
</style>

View File

@ -1,48 +0,0 @@
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
import { createWrapper, axios } from 'app/tests/jest/jestHelpers';
import ClaimDescriptorMenu from '../Card/ClaimDescriptorMenu.vue';
const mockPush = jest.fn();
jest.mock('vue-router', () => ({
useRouter: () => ({
push: mockPush,
currentRoute: {
value: {
params: {
id: 1,
},
},
},
}),
}));
describe('ClaimDescriptorMenu', () => {
let vm;
beforeAll(() => {
vm = createWrapper(ClaimDescriptorMenu, {
propsData: {
claim: {
id: 1
}
}
}).vm;
});
afterEach(() => {
jest.clearAllMocks();
});
describe('deleteClaim()', () => {
it('should delete the claim', async () => {
jest.spyOn(axios, 'delete').mockResolvedValue({ data: true });
jest.spyOn(vm.quasar, 'notify');
await vm.deleteClaim();
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
{ 'type': 'positive' }
));
});
});
});

View File

@ -1,172 +0,0 @@
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
const route = useRoute();
const { t } = useI18n();
const session = useSession();
const token = session.getToken();
const workers = ref([]);
const workersCopy = ref([]);
const businessTypes = ref([]);
const contactChannels = ref([]);
function setWorkers(data) {
workers.value = data;
workersCopy.value = data;
}
const filterOptions = {
options: workers,
filterFn: (options, value) => {
const search = value.toLowerCase();
if (value === '') return workersCopy.value;
return options.value.filter((row) => {
const id = row.id;
const name = row.name.toLowerCase();
const idMatches = id === search;
const nameMatches = name.indexOf(search) > -1;
return idMatches || nameMatches;
});
},
};
</script>
<template>
<fetch-data
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="setWorkers"
auto-load
/>
<fetch-data url="ContactChannels" @on-fetch="(data) => contactChannels = data" auto-load />
<fetch-data url="BusinessTypes" @on-fetch="(data) => businessTypes = data" auto-load />
<div class="container">
<q-card>
<form-model :url="`Clients/${route.params.id}`" model="customer">
<template #form="{ data, validate, filter }">
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-input
v-model="data.socialName"
:label="t('customer.basicData.socialName')"
:rules="validate('client.socialName')"
autofocus
/>
</div>
<div class="col">
<q-select
v-model="data.businessTypeFk"
:options="businessTypes"
option-value="code"
option-label="description"
emit-value
:label="t('customer.basicData.businessType')"
map-options
:rules="validate('client.businessTypeFk')"
:input-debounce="0"
/>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-input
v-model="data.contact"
:label="t('customer.basicData.contact')"
:rules="validate('client.contact')"
clearable
/>
</div>
<div class="col">
<q-input
v-model="data.email"
type="email"
:label="t('customer.basicData.email')"
:rules="validate('client.email')"
clearable
/>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-input
v-model="data.phone"
:label="t('customer.basicData.phone')"
:rules="validate('client.phone')"
clearable
/>
</div>
<div class="col">
<q-input
v-model="data.mobile"
:label="t('customer.basicData.mobile')"
:rules="validate('client.mobile')"
clearable
/>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-select
v-model="data.salesPersonFk"
:options="workers"
option-value="id"
option-label="name"
emit-value
:label="t('customer.basicData.salesPerson')"
map-options
use-input
@filter="(value, update) => filter(value, update, filterOptions)"
:rules="validate('client.salesPersonFk')"
:input-debounce="0"
>
<template #before>
<q-avatar color="orange">
<q-img
v-if="data.salesPersonFk"
:src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`"
spinner-color="white"
/>
</q-avatar>
</template>
</q-select>
</div>
<div class="col">
<q-select
v-model="data.contactChannelFk"
:options="contactChannels"
option-value="id"
option-label="name"
emit-value
:label="t('customer.basicData.contactChannel')"
map-options
:rules="validate('client.contactChannelFk')"
:input-debounce="0"
/>
</div>
</div>
</template>
</form-model>
</q-card>
</div>
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 800px;
}
</style>

View File

@ -1,21 +0,0 @@
<script setup>
import { useState } from 'src/composables/useState';
import CustomerDescriptor from './CustomerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
const state = useState();
</script>
<template>
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
<q-scroll-area class="fit">
<customer-descriptor />
<q-separator />
<left-menu source="card" />
</q-scroll-area>
</q-drawer>
<q-page-container>
<q-page class="q-pa-md">
<router-view></router-view>
</q-page>
</q-page-container>
</template>

View File

@ -1,113 +0,0 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toCurrency } from 'src/filters';
import axios from 'axios';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
onMounted(async () => {
await fetch();
});
const route = useRoute();
const { t } = useI18n();
const entityId = computed(() => {
return $props.id || route.params.id;
});
const customer = ref();
async function fetch() {
const { data } = await axios.get(`Clients/${entityId.value}/getCard`);
if (data) customer.value = data;
}
</script>
<template>
<skeleton-descriptor v-if="!customer" />
<card-descriptor v-if="customer" module="Customer" :data="customer" :description="customer.name">
<!-- <template #menu>
<q-item clickable v-ripple>Option 1</q-item>
<q-item clickable v-ripple>Option 2</q-item>
</template> -->
<template #body>
<q-list>
<q-item v-if="customer.salesPersonUser">
<q-item-section>
<q-item-label caption>{{ t('customer.card.salesPerson') }}</q-item-label>
<q-item-label>{{ customer.salesPersonUser.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.card.credit') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.credit) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('customer.card.securedCredit') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.creditInsurance) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section v-if="customer.payMethod">
<q-item-label caption>{{ t('customer.card.payMethod') }}</q-item-label>
<q-item-label>{{ customer.payMethod.name }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('customer.card.debt') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.debt) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<q-card-actions class="q-gutter-md">
<q-icon v-if="customer.isActive == false" name="vn:disabled" size="xs" color="primary">
<q-tooltip>{{ t('customer.card.isDisabled') }}</q-tooltip>
</q-icon>
<q-icon v-if="customer.isFreezed == true" name="vn:frozen" size="xs" color="primary">
<q-tooltip>{{ t('customer.card.isFrozen') }}</q-tooltip>
</q-icon>
<q-icon v-if="customer.debt > customer.credit" name="vn:risk" size="xs" color="primary">
<q-tooltip>{{ t('customer.card.hasDebt') }}</q-tooltip>
</q-icon>
<q-icon v-if="customer.isTaxDataChecked == false" name="vn:no036" size="xs" color="primary">
<q-tooltip>{{ t('customer.card.notChecked') }}</q-tooltip>
</q-icon>
<q-icon v-if="customer.account.active == false" name="vn:noweb" size="xs" color="primary">
<q-tooltip>{{ t('customer.card.noWebAccess') }}</q-tooltip>
</q-icon>
</q-card-actions>
<!-- <q-card-actions>
<q-btn size="md" icon="vn:ticket" color="primary">
<q-tooltip>Ticket list</q-tooltip>
</q-btn>
<q-btn size="md" icon="vn:invoice-out" color="primary">
<q-tooltip>Invoice Out list</q-tooltip>
</q-btn>
<q-btn size="md" icon="vn:basketadd" color="primary">
<q-tooltip>Order list</q-tooltip>
</q-btn>
<q-btn size="md" icon="face" color="primary">
<q-tooltip>View user</q-tooltip>
</q-btn>
<q-btn size="md" icon="expand_more" color="primary">
<q-tooltip>More options</q-tooltip>
</q-btn>
</q-card-actions> -->
</template>
</card-descriptor>
</template>

View File

@ -1,15 +0,0 @@
<script setup>
import CustomerDescriptor from './CustomerDescriptor.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
</script>
<template>
<q-card>
<customer-descriptor v-if="$props.id" :id="$props.id" />
</q-card>
</template>

View File

@ -1,492 +0,0 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { toCurrency, toPercentage, toDate } from 'src/filters';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
onMounted(() => fetch());
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
default: 0,
},
});
const entityId = computed(() => $props.id || route.params.id);
const customer = ref(null);
function fetch() {
const id = entityId.value;
axios.get(`Clients/${id}/summary`).then(({ data }) => {
customer.value = data;
});
}
const balanceDue = computed(() => {
return customer.value.defaulters.length && customer.value.defaulters[0].amount;
});
const balanceDueWarning = computed(() => (balanceDue.value ? 'negative' : ''));
const claimRate = computed(() => {
const data = customer.value;
return data.claimsRatio.claimingRate * 100;
});
const priceIncreasingRate = computed(() => {
const data = customer.value;
return data.claimsRatio.priceIncreasing / 100;
});
const debtWarning = computed(() => {
const data = customer.value;
return data.debt.debt > data.credit ? 'negative' : '';
});
const creditWarning = computed(() => {
const data = customer.value;
const tooMuchInsurance = data.credit > data.creditInsurance;
const noCreditInsurance = data.credit && data.creditInsurance == null;
return tooMuchInsurance || noCreditInsurance ? 'negative' : '';
});
</script>
<template>
<div class="summary container">
<q-card>
<skeleton-summary v-if="!customer" />
<template v-if="customer">
<div class="header bg-primary q-pa-sm q-mb-md">{{ customer.id }} - {{ customer.name }}</div>
<div class="row q-pa-md q-col-gutter-md q-mb-md">
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.basicData') }}
<router-link
:to="{ name: 'CustomerBasicData', params: { id: entityId } }"
target="_blank"
>
<q-icon name="open_in_new" />
</router-link>
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.customerId') }}</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>{{ t('customer.summary.name') }}</q-item-label>
<q-item-label>{{ customer.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.contact') }}</q-item-label>
<q-item-label>{{ customer.contact }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.salesPersonUser">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.salesPerson') }}</q-item-label>
<q-item-label>{{ customer.salesPersonUser.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.phone') }}</q-item-label>
<q-item-label>{{ customer.phone }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.mobile') }}</q-item-label>
<q-item-label>{{ customer.mobile }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.email') }}</q-item-label>
<q-item-label>{{ customer.email }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.contactChannel">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.contactChannel') }}</q-item-label>
<q-item-label>{{ customer.contactChannel.name }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.fiscalAddress') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.socialName') }}</q-item-label>
<q-item-label>{{ customer.socialName }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.fiscalId') }}</q-item-label>
<q-item-label>{{ customer.fi }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.postcode') }}</q-item-label>
<q-item-label>{{ customer.postcode }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.province">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.province') }}</q-item-label>
<q-item-label>{{ customer.province.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.country">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.country') }}</q-item-label>
<q-item-label>{{ customer.country.country }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.street') }}</q-item-label>
<q-item-label>{{ customer.street }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.fiscalData') }}
</q-item-label>
<q-item dense>
<q-checkbox
v-model="customer.isEqualizated"
:label="t('customer.summary.isEqualizated')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.isActive"
:label="t('customer.summary.isActive')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasToInvoiceByAddress"
:label="t('customer.summary.invoiceByAddress')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.isTaxDataChecked"
:label="t('customer.summary.verifiedData')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasToInvoice"
:label="t('customer.summary.hasToInvoice')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.isToBeMailed"
:label="t('customer.summary.notifyByEmail')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox v-model="customer.isVies" :label="t('customer.summary.vies')" disable />
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.billingData') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.payMethod') }}</q-item-label>
<q-item-label>{{ customer.payMethod.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.bankAccount') }}</q-item-label>
<q-item-label>{{ customer.iban }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.dueDay') }}</q-item-label>
<q-item-label>{{ customer.dueDay }}</q-item-label>
</q-item-section>
</q-item>
<q-item dense>
<q-checkbox v-model="customer.hasLcr" :label="t('customer.summary.hasLcr')" disable />
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasCoreVnl"
:label="t('customer.summary.hasCoreVnl')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasSepaVnl"
:label="t('customer.summary.hasB2BVnl')"
disable
/>
</q-item>
</q-list>
</div>
<div class="col" v-if="customer.defaultAddress">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.consignee') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.addressName') }}</q-item-label>
<q-item-label>{{ customer.defaultAddress.nickname }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.addressCity') }}</q-item-label>
<q-item-label>{{ customer.defaultAddress.city }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.addressStreet') }}</q-item-label>
<q-item-label>{{ customer.defaultAddress.street }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col" v-if="customer.account">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.webAccess') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.username') }}</q-item-label>
<q-item-label>{{ customer.account.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.account.active"
:label="t('customer.summary.webAccess')"
disable
/>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.businessData') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.totalGreuge') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.totalGreuge) }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.mana">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.mana') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.mana.mana) }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.claimsRatio">
<q-item-section>
<q-item-label caption>
{{ t('customer.summary.priceIncreasingRate') }}
</q-item-label>
<q-item-label>{{ toPercentage(priceIncreasingRate) }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.averageInvoiced">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.averageInvoiced') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.averageInvoiced.invoiced) }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.claimsRatio">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.claimRate') }}</q-item-label>
<q-item-label>{{ toPercentage(claimRate) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.financialData') }}
</q-item-label>
<q-item v-if="customer.debt">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.risk') }}</q-item-label>
<q-item-label :class="debtWarning">
{{ toCurrency(customer.debt.debt) }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.riskInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.credit') }}</q-item-label>
<q-item-label :class="creditWarning">
{{ toCurrency(customer.credit) }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.creditInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item v-if="customer.creditInsurance">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.securedCredit') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.creditInsurance) }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.securedCreditInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.balance') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.sumRisk) || toCurrency(0) }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.balanceInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item v-if="customer.defaulters">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.balanceDue') }}</q-item-label>
<q-item-label :class="balanceDueWarning">
{{ toCurrency(balanceDue) }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.balanceDueInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item v-if="customer.recovery">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.recoverySince') }}</q-item-label>
<q-item-label>{{ toDate(customer.recovery.started) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</div>
</template>
</q-card>
</div>
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 100%;
max-width: 1200px;
}
.negative {
color: red;
}
.summary {
.q-list {
.q-item__label--header {
display: flex;
justify-content: space-between;
a {
color: $primary;
}
}
}
.row {
flex-wrap: wrap;
.col {
min-width: 250px;
}
}
.header {
text-align: center;
font-size: 18px;
}
#slider-container {
max-width: 80%;
margin: 0 auto;
.q-slider {
.q-slider__marker-labels:nth-child(1) {
transform: none;
}
.q-slider__marker-labels:nth-child(2) {
transform: none;
left: auto !important;
right: 0%;
}
}
}
}
.q-dialog .summary {
max-width: 1200px;
}
</style>

View File

@ -1,29 +0,0 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import CustomerSummary from './CustomerSummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
</script>
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<customer-summary v-if="$props.id" :id="$props.id" />
</q-dialog>
</template>
<style lang="scss">
.q-dialog .summary .header {
position: sticky;
z-index: $z-max;
top: 0;
}
</style>

View File

@ -1,52 +0,0 @@
<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>

View File

@ -1,87 +0,0 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import Paginate from 'src/components/PaginateData.vue';
import CustomerSummaryDialog from './Card/CustomerSummaryDialog.vue';
const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
function navigate(id) {
router.push({ path: `/customer/${id}` });
}
function viewSummary(id) {
quasar.dialog({
component: CustomerSummaryDialog,
componentProps: {
id,
},
});
}
</script>
<template>
<q-page class="q-pa-md">
<paginate url="/Clients" sort-by="id DESC" auto-load>
<template #body="{ rows }">
<q-card class="card" v-for="row of rows" :key="row.id">
<q-item class="q-pa-none items-start cursor-pointer q-hoverable" v-ripple clickable>
<q-item-section class="q-pa-md" @click="navigate(row.id)">
<div class="text-h6">{{ row.name }}</div>
<q-item-label caption>#{{ row.id }}</q-item-label>
<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>
</q-item-section>
<q-separator vertical />
<q-card-actions vertical class="justify-between">
<!-- <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" @click="viewSummary(row.id)">
<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> -->
</q-card-actions>
</q-item>
</q-card>
</template>
</paginate>
</q-page>
</template>

View File

@ -1,17 +0,0 @@
<script setup>
import { useState } from 'src/composables/useState';
import LeftMenu from 'components/LeftMenu.vue';
const state = useState();
</script>
<template>
<q-drawer v-model="state.drawer.value" show-if-above :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>

Some files were not shown because too many files have changed in this diff Show More