Merge branch 'dev' into 4834-create-worker-module
gitea/salix-front/pipeline/head There was a failure building this commit Details

This commit is contained in:
Pau 2022-12-20 08:49:02 +01:00
commit 6e2425fd5a
89 changed files with 7978 additions and 6272 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
.DS_Store .DS_Store
.thumbs.db .thumbs.db
node_modules node_modules
junit.xml
# Quasar core related directories # Quasar core related directories
.quasar .quasar

View File

@ -3,7 +3,7 @@
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"editorconfig.editorconfig", "editorconfig.editorconfig",
"johnsoncodehk.volar", "Vue.volar",
"wayou.vscode-todo-highlight" "wayou.vscode-todo-highlight"
], ],
"unwantedRecommendations": [ "unwantedRecommendations": [

View File

@ -2,7 +2,7 @@
"editor.bracketPairColorization.enabled": true, "editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true, "editor.guides.bracketPairs": true,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.defaultFormatter": "johnsoncodehk.volar", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": ["source.fixAll.eslint"], "editor.codeActionsOnSave": ["source.fixAll.eslint"],
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"], "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
"json.schemas": [ "json.schemas": [
@ -11,9 +11,6 @@
"url": "https://on.cypress.io/cypress.schema.json" "url": "https://on.cypress.io/cypress.schema.json"
} }
], ],
"[javascript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
} }

View File

@ -38,6 +38,8 @@ module.exports = {
'^src/(.*)$': '<rootDir>/src/$1', '^src/(.*)$': '<rootDir>/src/$1',
'^app/(.*)$': '<rootDir>/$1', '^app/(.*)$': '<rootDir>/$1',
'^components/(.*)$': '<rootDir>/src/components/$1', '^components/(.*)$': '<rootDir>/src/components/$1',
'^composables/(.*)$': '<rootDir>/src/composables/$1',
'^filters/(.*)$': '<rootDir>/src/filters/$1',
'^layouts/(.*)$': '<rootDir>/src/layouts/$1', '^layouts/(.*)$': '<rootDir>/src/layouts/$1',
'^pages/(.*)$': '<rootDir>/src/pages/$1', '^pages/(.*)$': '<rootDir>/src/pages/$1',
'^assets/(.*)$': '<rootDir>/src/assets/$1', '^assets/(.*)$': '<rootDir>/src/assets/$1',

View File

@ -5,6 +5,7 @@
"src/*": ["src/*"], "src/*": ["src/*"],
"app/*": ["*"], "app/*": ["*"],
"components/*": ["src/components/*"], "components/*": ["src/components/*"],
"composables/*": ["src/composables/*"],
"layouts/*": ["src/layouts/*"], "layouts/*": ["src/layouts/*"],
"pages/*": ["src/pages/*"], "pages/*": ["src/pages/*"],
"assets/*": ["src/assets/*"], "assets/*": ["src/assets/*"],

10706
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,27 +18,29 @@
"test:e2e:ci": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress run\"" "test:e2e:ci": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress run\""
}, },
"dependencies": { "dependencies": {
"@quasar/extras": "^1.15.5", "@quasar/extras": "^1.15.8",
"axios": "^0.21.1", "axios": "^1.2.1",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"quasar": "^2.10.0", "pinia": "^2.0.28",
"quasar": "^2.11.1",
"validator": "^13.7.0", "validator": "^13.7.0",
"vue": "^3.0.0", "vue": "^3.2.45",
"vue-i18n": "^9.0.0", "vue-i18n": "^9.2.2",
"vue-router": "^4.0.0" "vue-router": "^4.1.6"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.13.14", "@babel/eslint-parser": "^7.13.14",
"@intlify/vue-i18n-loader": "^4.1.0", "@intlify/vue-i18n-loader": "^4.1.0",
"@pinia/testing": "^0.0.14",
"@quasar/app-webpack": "^3.6.2", "@quasar/app-webpack": "^3.6.2",
"@quasar/quasar-app-extension-testing-e2e-cypress": "^4.2.2", "@quasar/quasar-app-extension-testing-e2e-cypress": "^4.2.2",
"@quasar/quasar-app-extension-testing-unit-jest": "^3.0.0-alpha.10", "@quasar/quasar-app-extension-testing-unit-jest": "^3.0.0-beta.5",
"eslint": "^8.10.0", "eslint": "^8.30.0",
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-cypress": "^2.11.3", "eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-jest": "^25.2.2", "eslint-plugin-jest": "^27.1.7",
"eslint-plugin-vue": "^8.5.0", "eslint-plugin-vue": "^8.7.1",
"eslint-webpack-plugin": "^3.1.1", "eslint-webpack-plugin": "^3.2.0",
"jest-junit": "^13.0.0", "jest-junit": "^13.0.0",
"prettier": "^2.5.1" "prettier": "^2.5.1"
}, },

View File

@ -9,8 +9,8 @@
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js
const ESLintPlugin = require('eslint-webpack-plugin'); const ESLintPlugin = require('eslint-webpack-plugin');
const { configure } = require('quasar/wrappers'); const { configure } = require('quasar/wrappers');
const path = require('path');
module.exports = configure(function (ctx) { module.exports = configure(function (ctx) {
return { return {
@ -23,7 +23,7 @@ module.exports = configure(function (ctx) {
// app boot file (/src/boot) // app boot file (/src/boot)
// --> boot files are part of "main.js" // --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-webpack/boot-files // https://v2.quasar.dev/quasar-cli-webpack/boot-files
boot: ['i18n', 'axios'], boot: ['i18n', 'axios', 'pinia'],
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
css: ['app.scss'], css: ['app.scss'],
@ -38,8 +38,9 @@ module.exports = configure(function (ctx) {
// 'line-awesome', // 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
'roboto-font', // optional, you are not bound to it 'roboto-font',
'material-icons', // optional, you are not bound to it 'material-icons-outlined',
'material-symbols-outlined',
], ],
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-build // Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-build
@ -68,15 +69,23 @@ module.exports = configure(function (ctx) {
chainWebpack(chain) { chainWebpack(chain) {
chain.module chain.module
.rule("i18n") .rule('i18n')
.resourceQuery(/blockType=i18n/) .resourceQuery(/blockType=i18n/)
.type('javascript/auto') .type('javascript/auto')
.use("i18n") .use('i18n')
.loader("@intlify/vue-i18n-loader") .loader('@intlify/vue-i18n-loader')
.end(); .end();
chain.plugin('eslint-webpack-plugin') chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [{ extensions: ['js', 'vue'] }]);
.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'),
};
}, },
}, },
@ -91,7 +100,7 @@ module.exports = configure(function (ctx) {
target: 'http://0.0.0.0:3000', target: 'http://0.0.0.0:3000',
logLevel: 'debug', logLevel: 'debug',
changeOrigin: true, changeOrigin: true,
secure: false secure: false,
}, },
}, },
}, },
@ -100,9 +109,9 @@ module.exports = configure(function (ctx) {
framework: { framework: {
config: { config: {
brand: { brand: {
primary: 'orange' primary: 'orange',
}, },
dark: 'auto' dark: 'auto',
}, },
lang: 'es', lang: 'es',

View File

@ -1,4 +1,5 @@
<script setup> <script setup>
import { onMounted } from 'vue';
import axios from 'axios'; import axios from 'axios';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -8,9 +9,22 @@ import { useSession } from 'src/composables/useSession';
const quasar = useQuasar(); const quasar = useQuasar();
const router = useRouter(); const router = useRouter();
const session = useSession(); const session = useSession();
const { t } = useI18n(); const { t, availableLocales, locale, fallbackLocale } = useI18n();
const { isLoggedIn } = session; 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) => { quasar.iconMapFn = (iconName) => {
if (iconName.startsWith('vn:')) { if (iconName.startsWith('vn:')) {
const name = iconName.substring(3); const name = iconName.substring(3);
@ -19,6 +33,11 @@ quasar.iconMapFn = (iconName) => {
cls: `icon-${name}`, cls: `icon-${name}`,
}; };
} }
return {
cls: 'material-symbols-outlined',
content: iconName,
};
}; };
function responseError(error) { function responseError(error) {

View File

@ -11,21 +11,14 @@ const session = useSession();
jest.mock('vue-router', () => ({ jest.mock('vue-router', () => ({
useRouter: () => ({ useRouter: () => ({
push: mockPush, push: mockPush,
currentRoute: { value: 'myCurrentRoute' } currentRoute: { value: 'myCurrentRoute' },
}), }),
})); }));
jest.mock('src/composables/useSession', () => ({ jest.mock('src/composables/useSession', () => ({
useSession: () => ({ useSession: () => ({
isLoggedIn: mockLoggedIn, isLoggedIn: mockLoggedIn,
destroy: mockDestroy destroy: mockDestroy,
}),
}));
jest.mock('vue-i18n', () => ({
createI18n: () => { },
useI18n: () => ({
t: () => { }
}), }),
})); }));
@ -34,11 +27,10 @@ describe('App', () => {
beforeAll(() => { beforeAll(() => {
const options = { const options = {
global: { global: {
stubs: ['router-view'] stubs: ['router-view'],
} },
}; };
vm = createWrapper(App, options).vm; vm = createWrapper(App, options).vm;
}); });
it('should return a login error message', async () => { it('should return a login error message', async () => {
@ -48,17 +40,17 @@ describe('App', () => {
const response = { const response = {
response: { response: {
status: 401 status: 401,
} },
}; };
expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response)); expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response));
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining( expect(vm.quasar.notify).toHaveBeenCalledWith(
{ expect.objectContaining({
message: 'Invalid username or password',
type: 'negative', type: 'negative',
message: 'login.loginError' })
} );
));
}); });
it('should return an unauthorized error message', async () => { it('should return an unauthorized error message', async () => {
@ -68,17 +60,17 @@ describe('App', () => {
const response = { const response = {
response: { response: {
status: 401 status: 401,
} },
}; };
expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response)); expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response));
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining( expect(vm.quasar.notify).toHaveBeenCalledWith(
{ expect.objectContaining({
message: 'Access denied',
type: 'negative', type: 'negative',
message: 'errors.statusUnauthorized' })
} );
));
expect(session.destroy).toHaveBeenCalled(); expect(session.destroy).toHaveBeenCalled();
}); });

View File

@ -3,9 +3,11 @@ import { createI18n } from 'vue-i18n';
import messages from 'src/i18n'; import messages from 'src/i18n';
const i18n = createI18n({ const i18n = createI18n({
locale: 'es', locale: 'en',
fallbackLocale: 'en',
messages, messages,
legacy: false legacy: false,
missingWarn: false
}); });
export default boot(({ app }) => { export default boot(({ app }) => {

8
src/boot/pinia.js Normal file
View File

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

View File

@ -6,7 +6,7 @@ import axios from 'axios';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useValidator } from 'src/composables/useValidator'; import { useValidator } from 'src/composables/useValidator';
import SkeletonForm from 'src/components/SkeletonForm.vue'; import SkeletonForm from 'components/ui/SkeletonForm.vue';
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();

View File

@ -1,17 +1,107 @@
<script setup> <script setup>
import axios from 'axios';
import { onMounted, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRole } from 'src/composables/useRole';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useNavigation } from 'src/composables/useNavigation'; 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 { t } = useI18n();
const { hasAny } = useRole(); const route = useRoute();
const navigation = useNavigation();
const quasar = useQuasar(); const quasar = useQuasar();
const navigation = useNavigationStore();
async function onToggleFavoriteModule(moduleName, event) { const props = defineProps({
await navigation.toggleFavorite(moduleName, event); 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({ quasar.notify({
message: t('globals.dataSaved'), message: t('globals.dataSaved'),
@ -22,149 +112,89 @@ async function onToggleFavoriteModule(moduleName, event) {
<template> <template>
<q-list padding> <q-list padding>
<q-item-label header>{{ t('globals.favoriteModules') }}</q-item-label> <template v-if="$props.source === 'main'">
<template v-for="module in navigation.favorites.value" :key="module.title"> <q-item-label header>
<div class="module" v-if="!module.children"> {{ t('globals.pinnedModules') }}
<q-item </q-item-label>
clickable <template v-for="item in pinnedItems" :key="item.name">
v-ripple <template v-if="item.children">
active-class="text-primary" <left-menu-item-group :item="item" group="pinnedModules" class="pinned">
:key="module.title" <template #side>
:to="{ name: module.stateName }" <q-btn
v-if="!module.roles || !module.roles.length || hasAny(module.roles)" v-if="item.isPinned === true"
> @click="togglePinned(item, $event)"
<q-item-section avatar :if="module.icon"> icon="vn:pin_off"
<q-icon :name="module.icon" /> size="xs"
</q-item-section> flat
<q-item-section>{{ t(`${module.name}.pageTitles.${module.title}`) }}</q-item-section> round
<q-item-section side> >
<div @click="onToggleFavoriteModule(module.name, $event)" class="row items-center"> <q-tooltip>{{ t('components.leftMenu.removeFromPinned') }}</q-tooltip>
<q-icon name="vn:pin_off"></q-icon> </q-btn>
</div> <q-btn
</q-item-section> v-if="item.isPinned === false"
</q-item> @click="togglePinned(item, $event)"
</div> icon="vn:pin"
size="xs"
flat
round
>
<q-tooltip>{{ t('components.leftMenu.addToPinned') }}</q-tooltip>
</q-btn>
</template>
</left-menu-item-group>
</template>
<template v-if="module.children"> <left-menu-item v-if="!item.children" :item="item" />
<q-expansion-item </template>
class="module" <q-separator />
active-class="text-primary" <q-expansion-item :label="t('moduleIndex.allModules')">
:label="t(`${module.name}.pageTitles.${module.title}`)" <template v-for="item in items" :key="item.name">
v-if="!module.roles || !module.roles.length || hasAny(module.roles)" <template v-if="item.children">
:to="{ name: module.stateName }" <left-menu-item-group :item="item" group="modules">
> <template #side>
<template #header> <q-btn
<q-item-section avatar> v-if="item.isPinned === true"
<q-icon :name="module.icon"></q-icon> @click="togglePinned(item, $event)"
</q-item-section> icon="vn:pin_off"
<q-item-section>{{ t(`${module.name}.pageTitles.${module.title}`) }}</q-item-section> size="xs"
<q-item-section side> flat
<div @click="onToggleFavoriteModule(module.name, $event)" class="row items-center"> round
<q-icon name="vn:pin_off"></q-icon> >
</div> <q-tooltip>{{ t('components.leftMenu.removeFromPinned') }}</q-tooltip>
</q-item-section> </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>
<template v-for="section in module.children" :key="section.title"> </template>
<q-item </q-expansion-item>
clickable <q-separator />
v-ripple </template>
active-class="text-primary" <template v-if="$props.source === 'card'">
:to="{ name: section.stateName }" <template v-for="item in items" :key="item.name">
v-if="!section.roles || !section.roles.length || hasAny(section.roles)" <left-menu-item v-if="!item.children" :item="item" />
>
<q-item-section avatar :if="section.icon">
<q-icon :name="section.icon" />
</q-item-section>
<q-item-section>{{ t(`${module.name}.pageTitles.${section.title}`) }}</q-item-section>
</q-item>
</template>
</q-expansion-item>
</template> </template>
</template> </template>
</q-list> </q-list>
<q-separator />
<q-expansion-item :label="t('moduleIndex.allModules')">
<q-list padding>
<template v-for="module in navigation.modules.value" :key="module.title">
<div class="module" v-if="!module.children">
<q-item
class="module"
clickable
v-ripple
active-class="text-primary"
:key="module.title"
:to="{ name: module.stateName }"
v-if="!module.roles || !module.roles.length || hasAny(module.roles)"
>
<q-item-section avatar :if="module.icon">
<q-icon :name="module.icon" />
</q-item-section>
<q-item-section>{{ t(`${module.name}.pageTitles.${module.title}`) }}</q-item-section>
<q-item-section side>
<div
@click="onToggleFavoriteModule(module.name, $event)"
class="row items-center"
v-if="module.name != 'dashboard'"
>
<q-icon name="vn:pin"></q-icon>
</div>
</q-item-section>
</q-item>
</div>
<template v-if="module.children">
<q-expansion-item
class="module"
active-class="text-primary"
:label="t(`${module.name}.pageTitles.${module.title}`)"
v-if="!module.roles || !module.roles.length || hasAny(module.roles)"
:to="{ name: module.stateName }"
>
<template #header>
<q-item-section avatar>
<q-icon :name="module.icon"></q-icon>
</q-item-section>
<q-item-section>{{ t(`${module.name}.pageTitles.${module.title}`) }}</q-item-section>
<q-item-section side>
<div
@click="onToggleFavoriteModule(module.name, $event)"
class="row items-center"
v-if="module.name != 'dashboard'"
>
<q-icon name="vn:pin"></q-icon>
</div>
</q-item-section>
</template>
<template v-for="section in module.children" :key="section.title">
<q-item
clickable
v-ripple
active-class="text-primary"
:to="{ name: section.stateName }"
v-if="!section.roles || !section.roles.length || hasAny(section.roles)"
>
<q-item-section avatar :if="section.icon">
<q-icon :name="section.icon" />
</q-item-section>
<q-item-section>{{ t(`${module.name}.pageTitles.${section.title}`) }}</q-item-section>
</q-item>
</template>
</q-expansion-item>
</template>
</template>
</q-list>
</q-expansion-item>
</template> </template>
<style> <style>
.module .icon-pin, .pinned .icon-pin,
.module .icon-pin_off { .pinned .icon-pin_off {
visibility: hidden; visibility: hidden;
} }
.module:hover .icon-pin,
.module:hover .icon-pin_off { .pinned:hover .icon-pin,
.pinned:hover .icon-pin_off {
visibility: visible; visibility: visible;
} }
</style> </style>

View File

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

@ -0,0 +1,51 @@
<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,9 +1,10 @@
<script setup> <script setup>
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import UserPanel from 'src/components/UserPanel.vue'; import UserPanel from 'components/UserPanel.vue';
import FavoriteModules from './FavoriteModules.vue'; import PinnedModules from './PinnedModules.vue';
const { t } = useI18n(); const { t } = useI18n();
const session = useSession(); const session = useSession();
@ -11,6 +12,8 @@ const state = useState();
const user = state.getUser(); const user = state.getUser();
const token = session.getToken(); const token = session.getToken();
onMounted(() => (state.headerMounted.value = true));
function onToggleDrawer() { function onToggleDrawer() {
state.drawer.value = !state.drawer.value; state.drawer.value = !state.drawer.value;
} }
@ -36,42 +39,17 @@ function onToggleDrawer() {
</router-link> </router-link>
<q-toolbar-title shrink class="text-weight-bold">Salix</q-toolbar-title> <q-toolbar-title shrink class="text-weight-bold">Salix</q-toolbar-title>
<q-space></q-space> <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 class="q-pl-sm q-gutter-sm row items-center no-wrap">
<!-- <q-btn v-if="$q.screen.gt.xs" dense flat size="md" icon="add"> <div id="header-actions"></div>
<q-menu> <q-btn id="pinnedModules" icon="apps" flat dense rounded>
<q-list style="min-width: 150px">
<q-item :to="{ path: '/customer/create' }" clickable>
<q-item-section>New customer</q-item-section>
</q-item>
<q-item clickable>
<q-item-section>New ticket</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn> -->
<!-- <q-btn v-if="$q.screen.gt.xs" dense flat round size="md" icon="notifications">
<q-badge color="red" text-color="white" floating> 2 </q-badge>
<q-tooltip bottom> <q-tooltip bottom>
{{ t('globals.notifications') }} {{ t('globals.pinnedModules') }}
</q-tooltip> </q-tooltip>
<q-menu class="q-pa-md" style="min-width: 250px"> <PinnedModules />
<strong>Notifications</strong>
<q-separator />
<div style="text-align: center; font-size: 2em">
<q-spinner-puff color="orange" />
</div>
</q-menu>
</q-btn> -->
<q-btn dense flat no-wrap id="favoriteModules">
<q-avatar size="lg">
<q-icon name="apps" size="s" />
</q-avatar>
<q-tooltip bottom>
{{ t('globals.favoriteModules') }}
</q-tooltip>
<FavoriteModules />
</q-btn> </q-btn>
<q-btn dense flat no-wrap id="user"> <q-btn rounded dense flat no-wrap id="user">
<q-avatar size="lg"> <q-avatar size="lg">
<q-img <q-img
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`" :src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
@ -79,7 +57,6 @@ function onToggleDrawer() {
> >
</q-img> </q-img>
</q-avatar> </q-avatar>
<q-icon name="arrow_drop_down" size="s" />
<q-tooltip bottom> <q-tooltip bottom>
{{ t('globals.userPanel') }} {{ t('globals.userPanel') }}
</q-tooltip> </q-tooltip>

View File

@ -1,14 +1,16 @@
<script setup> <script setup>
import { onMounted } from 'vue'; import { onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useNavigation } from 'src/composables/useNavigation'; import { useNavigationStore } from 'src/stores/useNavigationStore';
const navigation = useNavigationStore();
const { t } = useI18n(); const { t } = useI18n();
const navigation = useNavigation();
onMounted(() => { onMounted(() => {
navigation.fetchFavorites(); navigation.fetchPinned();
}); });
const pinnedModules = computed(() => navigation.getPinnedModules());
</script> </script>
<template> <template>
@ -17,22 +19,22 @@ onMounted(() => {
class="row q-pa-md q-col-gutter-lg" class="row q-pa-md q-col-gutter-lg"
max-width="350px" max-width="350px"
max-height="400px" max-height="400px"
v-if="navigation.favorites.value.length" v-if="pinnedModules.length"
> >
<div v-for="module of navigation.favorites.value" :key="module.title" class="row no-wrap q-pa-xs flex-item"> <div v-for="item of pinnedModules" :key="item.title" class="row no-wrap q-pa-xs flex-item">
<q-btn <q-btn
align="evenly" align="evenly"
padding="16px" padding="16px"
flat flat
stack stack
size="lg" size="lg"
:icon="module.icon" :icon="item.icon"
color="primary" color="primary"
class="col-4 button" class="col-4 button"
:to="{ name: module.stateName }" :to="{ name: item.name }"
> >
<div class="text-center text-primary button-text"> <div class="text-center text-primary button-text">
{{ t(`${module.name}.pageTitles.${module.title}`) }} {{ t(item.title) }}
</div> </div>
</q-btn> </q-btn>
</div> </div>

View File

@ -45,7 +45,7 @@ onMounted(async () => {
}); });
function updatePreferences() { function updatePreferences() {
if (user.value.darkMode) { if (user.value.darkMode !== null) {
darkMode.value = user.value.darkMode; darkMode.value = user.value.darkMode;
} }
if (user.value.lang) { if (user.value.lang) {

View File

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

View File

@ -1,6 +1,6 @@
import { jest, describe, expect, it, beforeAll } from '@jest/globals'; import { jest, describe, expect, it, beforeAll } from '@jest/globals';
import { createWrapper, axios } from 'app/tests/jest/jestHelpers'; import { createWrapper, axios } from 'app/tests/jest/jestHelpers';
import Paginate from '../Paginate.vue'; import Paginate from '../PaginateData.vue';
const mockPush = jest.fn(); const mockPush = jest.fn();

View File

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

@ -98,9 +98,5 @@ const { t } = useI18n();
justify-content: space-between; justify-content: space-between;
align-items: stretch; align-items: stretch;
} }
#descriptor-skeleton .q-card__actions {
justify-content: space-between;
}
} }
</style> </style>

View File

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

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

@ -0,0 +1,20 @@
<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,43 +0,0 @@
import { describe, expect, it } from '@jest/globals';
import { useNavigation } from '../useNavigation';
const navigation = useNavigation();
describe('useNavigation', () => {
it('should return the routes for all modules', async () => {
expect(navigation.modules.value.length).toBeGreaterThan(1);
});
it('should return a proper formated object without the children property', async () => {
const expectedMenuItem = {
stateName: 'Dashboard',
name: 'dashboard',
roles: [],
icon: 'dashboard',
title: 'dashboard'
}
const firstMenuItem = navigation.modules.value[0]
expect(firstMenuItem.children).toBeUndefined();
expect(firstMenuItem).toEqual(expect.objectContaining(expectedMenuItem));
});
it('should return a proper formated object with two child items', async () => {
const expectedMenuItem = [{
name: 'CustomerList',
title: 'list',
icon: 'view_list',
stateName: 'CustomerList'
},
{
name: 'CustomerCreate',
title: 'createCustomer',
icon: 'vn:addperson',
stateName: 'CustomerCreate',
roles: ['developer']
}];
const secondMenuItem = navigation.modules.value[1]
expect(secondMenuItem.children).toEqual(expect.arrayContaining(expectedMenuItem));
expect(secondMenuItem.children.length).toEqual(2)
});
});

View File

@ -1,92 +0,0 @@
import routes from 'src/router/routes';
import { ref } from 'vue';
import axios from 'axios';
const favorites = ref([]);
const modules = ref([]);
const mainRoute = routes.find((route) => route.path === '/');
const moduleRoutes = (mainRoute && mainRoute.children) || [];
for (const route of moduleRoutes) {
const module = {
stateName: route.name,
name: route.name.toLowerCase(),
roles: [],
};
if (route.meta) {
Object.assign(module, route.meta);
}
if (route.children && route.children.length) {
const [moduleMain] = route.children;
const routes = moduleMain.children;
module.children = routes.map((route) => {
const submodule = {
stateName: route.name,
name: route.name,
};
Object.assign(submodule, route.meta);
return submodule;
});
}
modules.value.push(module);
}
export function useNavigation() {
const salixModules = {
customer: 'Clients',
claim: 'Claims',
entry: 'Entries',
invoiceIn: 'Invoices In',
invoiceOut: 'Invoices Out',
item: 'Items',
monitor: 'Monitors',
order: 'Orders',
route: 'Routes',
supplier: 'Suppliers',
ticket: 'Tickets',
travel: 'Travels',
user: 'Users',
worker: 'Workers',
zone: 'Zones',
};
async function fetchFavorites() {
const response = await axios.get('StarredModules/getStarredModules');
const filteredModules = modules.value.filter((module) => {
return response.data.find((element) => element.moduleFk == salixModules[module.name]);
});
return (favorites.value = filteredModules);
}
async function toggleFavorite(moduleName, event) {
if (event.defaultPrevented) return;
event.preventDefault();
event.stopPropagation();
const params = { moduleName: salixModules[moduleName] };
const query = 'StarredModules/toggleStarredModule';
await axios.post(query, params);
updateFavorites(moduleName);
}
function updateFavorites(name) {
if (!favorites.value.find((module) => module.name == name)) {
const newStarreModule = modules.value.find((module) => module.name == name);
favorites.value.push(newStarreModule);
} else {
const moduleToRemove = favorites.value.find((module) => module.name == name);
favorites.value.splice(favorites.value.indexOf(moduleToRemove), 1);
}
}
return { modules, favorites, toggleFavorite, fetchFavorites, updateFavorites };
}

View File

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

@ -6,7 +6,7 @@ export function useRole() {
async function fetch() { async function fetch() {
const { data } = await axios.get('Accounts/acl'); const { data } = await axios.get('Accounts/acl');
const roles = data.roles.map(userRoles => userRoles.role.name); const roles = data.roles.map((userRoles) => userRoles.role.name);
const userData = { const userData = {
id: data.user.id, id: data.user.id,
@ -14,7 +14,7 @@ export function useRole() {
nickname: data.user.nickname, nickname: data.user.nickname,
lang: data.user.lang || 'es', lang: data.user.lang || 'es',
darkMode: data.user.userConfig.darkMode, darkMode: data.user.userConfig.darkMode,
} };
state.setUser(userData); state.setUser(userData);
state.setRoles(roles); state.setRoles(roles);
} }
@ -32,6 +32,6 @@ export function useRole() {
return { return {
fetch, fetch,
hasAny, hasAny,
state state,
}; };
} }

View File

@ -12,6 +12,7 @@ const user = ref({
const roles = ref([]); const roles = ref([]);
const drawer = ref(true); const drawer = ref(true);
const headerMounted = ref(false);
export function useState() { export function useState() {
function getUser() { function getUser() {
@ -67,6 +68,7 @@ export function useState() {
set, set,
get, get,
unset, unset,
drawer drawer,
headerMounted
}; };
} }

View File

@ -1,9 +1,19 @@
// app global css in SCSS form // app global css in SCSS form
@import './icons.scss'; @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 { .link {
color: $primary; color: $primary;
cursor: pointer cursor: pointer;
} }
.link:hover { .link:hover {

View File

@ -22,3 +22,9 @@ $positive: #21ba45;
$negative: #c10015; $negative: #c10015;
$info: #31ccec; $info: #31ccec;
$warning: #f2c037; $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

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

View File

@ -2,10 +2,14 @@ import toLowerCase from './toLowerCase';
import toDate from './toDate'; import toDate from './toDate';
import toCurrency from './toCurrency'; import toCurrency from './toCurrency';
import toPercentage from './toPercentage'; import toPercentage from './toPercentage';
import toLowerCamel from './toLowerCamel';
import dashIfEmpty from './dashIfEmpty';
export { export {
toLowerCase, toLowerCase,
toLowerCamel,
toDate, toDate,
toCurrency, toCurrency,
toPercentage, toPercentage,
dashIfEmpty,
}; };

View File

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

View File

@ -9,16 +9,18 @@ export default {
backToDashboard: 'Return to dashboard', backToDashboard: 'Return to dashboard',
notifications: 'Notifications', notifications: 'Notifications',
userPanel: 'User panel', userPanel: 'User panel',
favoriteModules: 'Favorite modules', pinnedModules: 'Pinned modules',
darkMode: 'Dark mode', darkMode: 'Dark mode',
logOut: 'Log out', logOut: 'Log out',
dataSaved: 'Data saved', dataSaved: 'Data saved',
dataDeleted: 'Data deleted',
add: 'Add', add: 'Add',
create: 'Create', create: 'Create',
save: 'Save', save: 'Save',
remove: 'Remove', remove: 'Remove',
reset: 'Reset', reset: 'Reset',
cancel: 'Cancel', cancel: 'Cancel',
confirm: 'Confirm',
back: 'Back', back: 'Back',
yes: 'Yes', yes: 'Yes',
no: 'No', no: 'No',
@ -27,10 +29,10 @@ export default {
confirmRemove: 'You are about to delete this row. Are you sure?', confirmRemove: 'You are about to delete this row. Are you sure?',
rowAdded: 'Row added', rowAdded: 'Row added',
rowRemoved: 'Row removed', rowRemoved: 'Row removed',
pleaseWait: 'Please wait...' pleaseWait: 'Please wait...',
}, },
moduleIndex: { moduleIndex: {
allModules: 'All modules' allModules: 'All modules',
}, },
errors: { errors: {
statusUnauthorized: 'Access denied', statusUnauthorized: 'Access denied',
@ -46,12 +48,12 @@ export default {
keepLogin: 'Keep me logged in', keepLogin: 'Keep me logged in',
loginSuccess: 'You have successfully logged in', loginSuccess: 'You have successfully logged in',
loginError: 'Invalid username or password', loginError: 'Invalid username or password',
fieldRequired: 'This field is required' fieldRequired: 'This field is required',
}, },
dashboard: { dashboard: {
pageTitles: { pageTitles: {
dashboard: 'Dashboard', dashboard: 'Dashboard',
} },
}, },
customer: { customer: {
pageTitles: { pageTitles: {
@ -59,13 +61,13 @@ export default {
list: 'List', list: 'List',
createCustomer: 'Create customer', createCustomer: 'Create customer',
summary: 'Summary', summary: 'Summary',
basicData: 'Basic Data' basicData: 'Basic Data',
}, },
list: { list: {
phone: 'Phone', phone: 'Phone',
email: 'Email', email: 'Email',
customerOrders: 'Display customer orders', customerOrders: 'Display customer orders',
moreOptions: 'More options' moreOptions: 'More options',
}, },
card: { card: {
customerList: 'Customer list', customerList: 'Customer list',
@ -79,7 +81,7 @@ export default {
isFrozen: 'Customer is frozen', isFrozen: 'Customer is frozen',
hasDebt: 'Customer has debt', hasDebt: 'Customer has debt',
notChecked: 'Customer not checked', notChecked: 'Customer not checked',
noWebAccess: 'Web access is disabled' noWebAccess: 'Web access is disabled',
}, },
summary: { summary: {
basicData: 'Basic data', basicData: 'Basic data',
@ -146,8 +148,8 @@ export default {
phone: 'Phone', phone: 'Phone',
mobile: 'Mobile', mobile: 'Mobile',
salesPerson: 'Sales person', salesPerson: 'Sales person',
contactChannel: 'Contact channel' contactChannel: 'Contact channel',
} },
}, },
ticket: { ticket: {
pageTitles: { pageTitles: {
@ -156,7 +158,7 @@ export default {
createTicket: 'Create ticket', createTicket: 'Create ticket',
summary: 'Summary', summary: 'Summary',
basicData: 'Basic Data', basicData: 'Basic Data',
boxing: 'Boxing' boxing: 'Boxing',
}, },
list: { list: {
nickname: 'Nickname', nickname: 'Nickname',
@ -164,7 +166,7 @@ export default {
shipped: 'Shipped', shipped: 'Shipped',
landed: 'Landed', landed: 'Landed',
salesPerson: 'Sales person', salesPerson: 'Sales person',
total: 'Total' total: 'Total',
}, },
card: { card: {
ticketId: 'Ticket ID', ticketId: 'Ticket ID',
@ -174,7 +176,7 @@ export default {
agency: 'Agency', agency: 'Agency',
shipped: 'Shipped', shipped: 'Shipped',
warehouse: 'Warehouse', warehouse: 'Warehouse',
customerCard: 'Customer card' customerCard: 'Customer card',
}, },
boxing: { boxing: {
expedition: 'Expedition', expedition: 'Expedition',
@ -183,8 +185,51 @@ export default {
worker: 'Worker', worker: 'Worker',
selectTime: 'Select time:', selectTime: 'Select time:',
selectVideo: 'Select video:', selectVideo: 'Select video:',
notFound: 'No videos available' 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: { claim: {
pageTitles: { pageTitles: {
@ -194,21 +239,21 @@ export default {
rmaList: 'RMA', rmaList: 'RMA',
summary: 'Summary', summary: 'Summary',
basicData: 'Basic Data', basicData: 'Basic Data',
rma: 'RMA' rma: 'RMA',
}, },
list: { list: {
customer: 'Customer', customer: 'Customer',
assignedTo: 'Assigned', assignedTo: 'Assigned',
created: 'Created', created: 'Created',
state: 'State' state: 'State',
}, },
rmaList: { rmaList: {
code: 'Code', code: 'Code',
records: 'records' records: 'records',
}, },
rma: { rma: {
user: 'User', user: 'User',
created: 'Created' created: 'Created',
}, },
card: { card: {
claimId: 'Claim ID', claimId: 'Claim ID',
@ -217,7 +262,7 @@ export default {
state: 'State', state: 'State',
ticketId: 'Ticket ID', ticketId: 'Ticket ID',
customerSummary: 'Customer summary', customerSummary: 'Customer summary',
claimedTicket: 'Claimed ticket' claimedTicket: 'Claimed ticket',
}, },
summary: { summary: {
customer: 'Customer', customer: 'Customer',
@ -237,7 +282,7 @@ export default {
actions: 'Actions', actions: 'Actions',
responsibility: 'Responsibility', responsibility: 'Responsibility',
company: 'Company', company: 'Company',
person: 'Employee/Customer' person: 'Employee/Customer',
}, },
basicData: { basicData: {
customer: 'Customer', customer: 'Customer',
@ -246,7 +291,50 @@ export default {
state: 'State', state: 'State',
packages: 'Packages', packages: 'Packages',
picked: 'Picked', picked: 'Picked',
returnOfMaterial: 'Return of material authorization (RMA)' 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',
}, },
}, },
worker: { worker: {
@ -299,12 +387,16 @@ export default {
noData: 'No data to display', noData: 'No data to display',
openCard: 'View card', openCard: 'View card',
openSummary: 'Open summary', openSummary: 'Open summary',
viewDescription: 'View description' viewDescription: 'View description',
}, },
cardDescriptor: { cardDescriptor: {
mainList: 'Main list', mainList: 'Main list',
summary: 'Summary', summary: 'Summary',
moreOptions: 'More options' moreOptions: 'More options',
} },
} leftMenu: {
addToPinned: 'Add to pinned',
removeFromPinned: 'Remove from pinned',
},
},
}; };

View File

@ -9,16 +9,18 @@ export default {
backToDashboard: 'Volver al tablón', backToDashboard: 'Volver al tablón',
notifications: 'Notificaciones', notifications: 'Notificaciones',
userPanel: 'Panel de usuario', userPanel: 'Panel de usuario',
favoriteModules: 'Módulos favoritos', pinnedModules: 'Módulos fijados',
darkMode: 'Modo oscuro', darkMode: 'Modo oscuro',
logOut: 'Cerrar sesión', logOut: 'Cerrar sesión',
dataSaved: 'Datos guardados', dataSaved: 'Datos guardados',
dataDeleted: 'Data deleted',
add: 'Añadir', add: 'Añadir',
create: 'Crear', create: 'Crear',
save: 'Guardar', save: 'Guardar',
remove: 'Eliminar', remove: 'Eliminar',
reset: 'Restaurar', reset: 'Restaurar',
cancel: 'Cancelar', cancel: 'Cancelar',
confirm: 'Confirmar',
back: 'Volver', back: 'Volver',
yes: 'Si', yes: 'Si',
no: 'No', no: 'No',
@ -27,10 +29,10 @@ export default {
confirmRemove: 'Vas a eliminar este registro. ¿Continuar?', confirmRemove: 'Vas a eliminar este registro. ¿Continuar?',
rowAdded: 'Fila añadida', rowAdded: 'Fila añadida',
rowRemoved: 'Fila eliminada', rowRemoved: 'Fila eliminada',
pleaseWait: 'Por favor, espera...' pleaseWait: 'Por favor, espera...',
}, },
moduleIndex: { moduleIndex: {
allModules: 'Todos los módulos' allModules: 'Todos los módulos',
}, },
errors: { errors: {
statusUnauthorized: 'Acceso denegado', statusUnauthorized: 'Acceso denegado',
@ -46,12 +48,12 @@ export default {
keepLogin: 'Mantener sesión iniciada', keepLogin: 'Mantener sesión iniciada',
loginSuccess: 'Inicio de sesión correcto', loginSuccess: 'Inicio de sesión correcto',
loginError: 'Nombre de usuario o contraseña incorrectos', loginError: 'Nombre de usuario o contraseña incorrectos',
fieldRequired: 'Este campo es obligatorio' fieldRequired: 'Este campo es obligatorio',
}, },
dashboard: { dashboard: {
pageTitles: { pageTitles: {
dashboard: 'Tablón', dashboard: 'Tablón',
} },
}, },
customer: { customer: {
pageTitles: { pageTitles: {
@ -59,13 +61,13 @@ export default {
list: 'Listado', list: 'Listado',
createCustomer: 'Crear cliente', createCustomer: 'Crear cliente',
summary: 'Resumen', summary: 'Resumen',
basicData: 'Datos básicos' basicData: 'Datos básicos',
}, },
list: { list: {
phone: 'Teléfono', phone: 'Teléfono',
email: 'Email', email: 'Email',
customerOrders: 'Mostrar órdenes del cliente', customerOrders: 'Mostrar órdenes del cliente',
moreOptions: 'Más opciones' moreOptions: 'Más opciones',
}, },
card: { card: {
customerId: 'ID cliente', customerId: 'ID cliente',
@ -78,7 +80,7 @@ export default {
isFrozen: 'El cliente está congelado', isFrozen: 'El cliente está congelado',
hasDebt: 'El cliente tiene riesgo', hasDebt: 'El cliente tiene riesgo',
notChecked: 'El cliente no está comprobado', notChecked: 'El cliente no está comprobado',
noWebAccess: 'El acceso web está desactivado' noWebAccess: 'El acceso web está desactivado',
}, },
summary: { summary: {
basicData: 'Datos básicos', basicData: 'Datos básicos',
@ -145,8 +147,8 @@ export default {
phone: 'Teléfono', phone: 'Teléfono',
mobile: 'Móvil', mobile: 'Móvil',
salesPerson: 'Comercial', salesPerson: 'Comercial',
contactChannel: 'Canal de contacto' contactChannel: 'Canal de contacto',
} },
}, },
ticket: { ticket: {
pageTitles: { pageTitles: {
@ -155,7 +157,7 @@ export default {
createTicket: 'Crear ticket', createTicket: 'Crear ticket',
summary: 'Resumen', summary: 'Resumen',
basicData: 'Datos básicos', basicData: 'Datos básicos',
boxing: 'Encajado' boxing: 'Encajado',
}, },
list: { list: {
nickname: 'Alias', nickname: 'Alias',
@ -163,7 +165,7 @@ export default {
shipped: 'Enviado', shipped: 'Enviado',
landed: 'Entregado', landed: 'Entregado',
salesPerson: 'Comercial', salesPerson: 'Comercial',
total: 'Total' total: 'Total',
}, },
card: { card: {
ticketId: 'ID ticket', ticketId: 'ID ticket',
@ -173,7 +175,7 @@ export default {
agency: 'Agencia', agency: 'Agencia',
shipped: 'Enviado', shipped: 'Enviado',
warehouse: 'Almacén', warehouse: 'Almacén',
customerCard: 'Ficha del cliente' customerCard: 'Ficha del cliente',
}, },
boxing: { boxing: {
expedition: 'Expedición', expedition: 'Expedición',
@ -182,8 +184,51 @@ export default {
worker: 'Trabajador', worker: 'Trabajador',
selectTime: 'Seleccionar hora:', selectTime: 'Seleccionar hora:',
selectVideo: 'Seleccionar vídeo:', selectVideo: 'Seleccionar vídeo:',
notFound: 'No hay vídeos disponibles' 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: { claim: {
pageTitles: { pageTitles: {
@ -193,21 +238,21 @@ export default {
rmaList: 'RMA', rmaList: 'RMA',
summary: 'Resumen', summary: 'Resumen',
basicData: 'Datos básicos', basicData: 'Datos básicos',
rma: 'RMA' rma: 'RMA',
}, },
list: { list: {
customer: 'Cliente', customer: 'Cliente',
assignedTo: 'Asignada a', assignedTo: 'Asignada a',
created: 'Creada', created: 'Creada',
state: 'Estado' state: 'Estado',
}, },
rmaList: { rmaList: {
code: 'Código', code: 'Código',
records: 'registros' records: 'registros',
}, },
rma: { rma: {
user: 'Usuario', user: 'Usuario',
created: 'Creado' created: 'Creado',
}, },
card: { card: {
claimId: 'ID reclamación', claimId: 'ID reclamación',
@ -216,7 +261,7 @@ export default {
state: 'Estado', state: 'Estado',
ticketId: 'ID ticket', ticketId: 'ID ticket',
customerSummary: 'Resumen del cliente', customerSummary: 'Resumen del cliente',
claimedTicket: 'Ticket reclamado' claimedTicket: 'Ticket reclamado',
}, },
summary: { summary: {
customer: 'Cliente', customer: 'Cliente',
@ -236,7 +281,7 @@ export default {
actions: 'Acciones', actions: 'Acciones',
responsibility: 'Responsabilidad', responsibility: 'Responsabilidad',
company: 'Empresa', company: 'Empresa',
person: 'Comercial/Cliente' person: 'Comercial/Cliente',
}, },
basicData: { basicData: {
customer: 'Cliente', customer: 'Cliente',
@ -245,9 +290,53 @@ export default {
state: 'Estado', state: 'Estado',
packages: 'Bultos', packages: 'Bultos',
picked: 'Recogida', picked: 'Recogida',
returnOfMaterial: 'Autorización de retorno de materiales (RMA)' returnOfMaterial: 'Autorización de retorno de materiales (RMA)',
} },
}, worker: { },
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',
},
},
worker: {
pageTitles: { pageTitles: {
workers: 'Trabajadores', workers: 'Trabajadores',
list: 'Listado', list: 'Listado',
@ -297,12 +386,16 @@ export default {
noData: 'Sin datos que mostrar', noData: 'Sin datos que mostrar',
openCard: 'Ver ficha', openCard: 'Ver ficha',
openSummary: 'Abrir detalles', openSummary: 'Abrir detalles',
viewDescription: 'Ver descripción' viewDescription: 'Ver descripción',
}, },
cardDescriptor: { cardDescriptor: {
mainList: 'Listado principal', mainList: 'Listado principal',
summary: 'Resumen', summary: 'Resumen',
moreOptions: 'Más opciones', moreOptions: 'Más opciones',
} },
} leftMenu: {
addToPinned: 'Añadir a fijados',
removeFromPinned: 'Eliminar de fijados',
},
},
}; };

View File

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

View File

@ -4,8 +4,8 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'src/components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();

View File

@ -1,9 +1,8 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import { useState } from 'composables/useState';
import { useState } from 'src/composables/useState';
import ClaimDescriptor from './ClaimDescriptor.vue'; import ClaimDescriptor from './ClaimDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
const { t } = useI18n();
const state = useState(); const state = useState();
</script> </script>
<template> <template>
@ -11,20 +10,7 @@ const state = useState();
<q-scroll-area class="fit"> <q-scroll-area class="fit">
<claim-descriptor /> <claim-descriptor />
<q-separator /> <q-separator />
<q-list> <left-menu source="card" />
<q-item :to="{ name: 'ClaimBasicData' }" clickable v-ripple>
<q-item-section avatar>
<q-icon name="vn:settings" />
</q-item-section>
<q-item-section>{{ t('claim.pageTitles.basicData') }}</q-item-section>
</q-item>
<q-item :to="{ name: 'ClaimRma' }" clickable v-ripple>
<q-item-section avatar>
<q-icon name="vn:barcode" />
</q-item-section>
<q-item-section>{{ t('claim.pageTitles.rma') }}</q-item-section>
</q-item>
</q-list>
</q-scroll-area> </q-scroll-area>
</q-drawer> </q-drawer>
<q-page-container> <q-page-container>

View File

@ -4,8 +4,10 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import axios from 'axios'; import axios from 'axios';
import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; import TicketDescriptorPopover from 'pages/Ticket/Card/TicketDescriptorPopover.vue';
import TicketDescriptorPopover from 'src/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({ const $props = defineProps({
id: { id: {
@ -57,7 +59,11 @@ function stateColor(code) {
</script> </script>
<template> <template>
<skeleton-descriptor v-if="!claim" />
<card-descriptor v-if="claim" module="Claim" :data="claim" :description="claim.client.name"> <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> <template #body>
<q-list> <q-list>
<q-item> <q-item>

View File

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

@ -4,8 +4,9 @@ import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import Paginate from 'src/components/Paginate.vue'; import Paginate from 'src/components/PaginateData.vue';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import TeleportSlot from 'components/ui/TeleportSlot';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
const quasar = useQuasar(); const quasar = useQuasar();
@ -77,54 +78,44 @@ function hide() {
ref="fetcher" ref="fetcher"
:url="`Claims/${route.params.id}`" :url="`Claims/${route.params.id}`"
:filter="filter" :filter="filter"
@on-fetch="($data) => (claim = $data)" @on-fetch="(data) => (claim = data)"
auto-load auto-load
/> />
<div class="sticky-page"> <paginate :data="claim.rmas">
<q-page-sticky expand position="top"> <template #body="{ rows }">
<q-toolbar class="bg-grey-9"> <q-card class="card">
<q-space /> <template v-for="row of rows" :key="row.id">
<div class="q-gutter-md"> <q-item class="q-pa-none items-start">
<q-btn icon="add" :label="t('globals.add')" color="primary" @click="addRow()" /> <q-item-section class="q-pa-md">
</div> <q-list>
</q-toolbar> <q-item class="q-pa-none">
</q-page-sticky> <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>
<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>
</div>
<q-dialog v-model="confirmShown" persistent @hide="hide"> <q-dialog v-model="confirmShown" persistent @hide="hide">
<q-card> <q-card>
<q-card-section class="row items-center"> <q-card-section class="row items-center">
@ -138,6 +129,20 @@ function hide() {
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</q-dialog> </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> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -4,7 +4,7 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios'; import axios from 'axios';
import { toDate, toCurrency } from 'src/filters'; import { toDate, toCurrency } from 'src/filters';
import SkeletonSummary from 'src/components/SkeletonSummary'; import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
onMounted(() => fetch()); onMounted(() => fetch());

View File

@ -19,3 +19,11 @@ const { dialogRef, onDialogHide } = useDialogPluginComponent();
<claim-summary v-if="$props.id" :id="$props.id" /> <claim-summary v-if="$props.id" :id="$props.id" />
</q-dialog> </q-dialog>
</template> </template>
<style lang="scss">
.q-dialog .summary .header {
position: sticky;
z-index: $z-max;
top: 0;
}
</style>

View File

@ -2,7 +2,7 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import Paginate from 'src/components/Paginate.vue'; import Paginate from 'src/components/PaginateData.vue';
import { toDate } from 'src/filters/index'; import { toDate } from 'src/filters/index';
import ClaimSummaryDialog from './Card/ClaimSummaryDialog.vue'; import ClaimSummaryDialog from './Card/ClaimSummaryDialog.vue';
import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue'; import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue';

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import LeftMenu from 'src/components/LeftMenu.vue'; import LeftMenu from 'components/LeftMenu.vue';
const state = useState(); const state = useState();
</script> </script>

View File

@ -3,7 +3,7 @@ import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import axios from 'axios'; import axios from 'axios';
import Paginate from 'src/components/Paginate.vue'; import Paginate from 'src/components/PaginateData.vue';
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();

View File

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

@ -4,8 +4,8 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'src/components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -33,7 +33,7 @@ const filterOptions = {
const id = row.id; const id = row.id;
const name = row.name.toLowerCase(); const name = row.name.toLowerCase();
const idMatches = id == search; const idMatches = id === search;
const nameMatches = name.indexOf(search) > -1; const nameMatches = name.indexOf(search) > -1;
return idMatches || nameMatches; return idMatches || nameMatches;
@ -48,9 +48,8 @@ const filterOptions = {
@on-fetch="setWorkers" @on-fetch="setWorkers"
auto-load auto-load
/> />
<fetch-data url="ContactChannels" @on-fetch="($data) => (contactChannels = $data)" auto-load /> <fetch-data url="ContactChannels" @on-fetch="(data) => contactChannels = data" auto-load />
<fetch-data url="BusinessTypes" @on-fetch="($data) => (businessTypes = $data)" auto-load /> <fetch-data url="BusinessTypes" @on-fetch="(data) => businessTypes = data" auto-load />
<div class="container"> <div class="container">
<q-card> <q-card>
<form-model :url="`Clients/${route.params.id}`" model="customer"> <form-model :url="`Clients/${route.params.id}`" model="customer">

View File

@ -1,40 +1,16 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import CustomerDescriptor from './CustomerDescriptor.vue'; import CustomerDescriptor from './CustomerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
const state = useState(); const state = useState();
const { t } = useI18n();
</script> </script>
<template> <template>
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500"> <q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
<q-scroll-area class="fit"> <q-scroll-area class="fit">
<customer-descriptor /> <customer-descriptor />
<q-separator /> <q-separator />
<q-list> <left-menu source="card" />
<q-item :to="{ name: 'CustomerBasicData' }" clickable v-ripple>
<q-item-section avatar>
<q-icon name="vn:settings" />
</q-item-section>
<q-item-section>{{ t('customer.pageTitles.basicData') }}</q-item-section>
</q-item>
<!-- <q-item clickable v-ripple>
<q-item-section avatar>
<q-icon name="notes" />
</q-item-section>
<q-item-section>Notes</q-item-section>
</q-item>
<q-expansion-item icon="more" label="More options" expand-icon-toggle expand-separator>
<q-list>
<q-item clickable v-ripple>
<q-item-section avatar>
<q-icon name="person" />
</q-item-section>
<q-item-section>Option</q-item-section>
</q-item>
</q-list>
</q-expansion-item> -->
</q-list>
</q-scroll-area> </q-scroll-area>
</q-drawer> </q-drawer>
<q-page-container> <q-page-container>

View File

@ -4,7 +4,8 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import axios from 'axios'; import axios from 'axios';
import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue';
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -34,6 +35,7 @@ async function fetch() {
</script> </script>
<template> <template>
<skeleton-descriptor v-if="!customer" />
<card-descriptor v-if="customer" module="Customer" :data="customer" :description="customer.name"> <card-descriptor v-if="customer" module="Customer" :data="customer" :description="customer.name">
<!-- <template #menu> <!-- <template #menu>
<q-item clickable v-ripple>Option 1</q-item> <q-item clickable v-ripple>Option 1</q-item>

View File

@ -4,7 +4,7 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios'; import axios from 'axios';
import { toCurrency, toPercentage, toDate } from 'src/filters'; import { toCurrency, toPercentage, toDate } from 'src/filters';
import SkeletonSummary from 'src/components/SkeletonSummary'; import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
onMounted(() => fetch()); onMounted(() => fetch());

View File

@ -19,3 +19,11 @@ const { dialogRef, onDialogHide } = useDialogPluginComponent();
<customer-summary v-if="$props.id" :id="$props.id" /> <customer-summary v-if="$props.id" :id="$props.id" />
</q-dialog> </q-dialog>
</template> </template>
<style lang="scss">
.q-dialog .summary .header {
position: sticky;
z-index: $z-max;
top: 0;
}
</style>

View File

@ -2,7 +2,7 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import Paginate from 'src/components/Paginate.vue'; import Paginate from 'src/components/PaginateData.vue';
import CustomerSummaryDialog from './Card/CustomerSummaryDialog.vue'; import CustomerSummaryDialog from './Card/CustomerSummaryDialog.vue';
const router = useRouter(); const router = useRouter();

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import LeftMenu from 'src/components/LeftMenu.vue'; import LeftMenu from 'components/LeftMenu.vue';
const state = useState(); const state = useState();
</script> </script>

View File

@ -1,79 +0,0 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState';
import LeftMenu from 'src/components/LeftMenu.vue';
import { useNavigation } from 'src/composables/useNavigation';
const { t } = useI18n();
const state = useState();
const modules = useNavigation();
</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>
<q-page class="q-pa-md">
<!-- <q-banner v-if="$q.screen.gt.xs" inline-actions rounded class="bg-orange text-white q-mb-lg">
Employee notification message
<template #action>
<q-btn flat label="Dismiss" />
</template>
</q-banner> -->
<div class="row items-start wrap q-col-gutter-md q-mb-lg">
<div class="col-12 col-md" v-if="modules.favorites.value.length">
<div class="text-h6 text-grey-8 q-mb-sm">{{ t('globals.favoriteModules') }}</div>
<q-card class="row flex-container">
<div
v-for="module of modules.favorites.value"
:key="module.title"
class="row no-wrap q-pa-xs flex-item"
>
<q-btn
align="evenly"
padding="16px"
flat
stack
size="lg"
:icon="module.icon"
color="orange-6"
class="col-4 button"
:to="{ name: module.stateName }"
>
<div class="text-center text-primary button-text">
{{ t(`${module.name}.pageTitles.${module.title}`) }}
</div>
</q-btn>
</div>
</q-card>
</div>
</div>
</q-page>
</q-page-container>
</template>
<style lang="scss" scoped>
.flex-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
max-height: 300px;
overflow: auto;
}
.flex-item {
width: 100px;
}
.button {
width: 100%;
line-height: normal;
align-items: center;
}
.button-text {
font-size: 10px;
margin-top: 5px;
}
</style>

View File

@ -0,0 +1,91 @@
<script setup>
import { onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState';
import LeftMenu from 'components/LeftMenu.vue';
import { useNavigationStore } from 'src/stores/useNavigationStore';
const state = useState();
const navigation = useNavigationStore();
const { t } = useI18n();
onMounted(() => {
navigation.fetchPinned();
});
const pinnedModules = computed(() => navigation.getPinnedModules());
</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>
<q-page class="q-pa-md">
<div class="row items-start wrap q-col-gutter-md q-mb-lg">
<div class="col-12 col-md">
<div class="text-h6 text-grey-8 q-mb-sm">{{ t('globals.pinnedModules') }}</div>
<q-card class="row flex-container q-pa-md">
<div class="text-grey-5" v-if="pinnedModules.length === 0">
{{ t('pinnedInfo') }}
</div>
<template 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="orange-6"
class="col-4 button"
:to="{ name: item.name }"
>
<div class="text-center text-primary button-text">
{{ t(item.title) }}
</div>
</q-btn>
</div>
</template>
</q-card>
</div>
</div>
</q-page>
</q-page-container>
</template>
<style lang="scss" scoped>
.flex-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
max-height: 300px;
overflow: auto;
}
.flex-item {
width: 100px;
}
.button {
width: 100%;
line-height: normal;
align-items: center;
}
.button-text {
font-size: 10px;
margin-top: 5px;
}
</style>
<i18n>
{
"en": {
"pinnedInfo": "Your pinned modules will be shown here..."
},
"es": {
"pinnedInfo": "Tus módulos fijados aparecerán aquí..."
}
}
</i18n>

View File

@ -0,0 +1,18 @@
<script setup>
import { useState } from 'src/composables/useState';
import InvoiceOutDescriptor from './InvoiceOutDescriptor.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">
<InvoiceOutDescriptor />
</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

@ -0,0 +1,103 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toCurrency, toDate } from 'src/filters';
import axios from 'axios';
import CardDescriptor from 'src/components/ui/CardDescriptor.vue';
import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.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 invoiceOut = ref();
async function fetch() {
const filter = {
include: [
{
relation: 'company',
scope: {
fields: ['id', 'code'],
},
},
{
relation: 'client',
scope: {
fields: ['id', 'name', 'email'],
},
},
],
};
const options = { params: { filter } };
const { data } = await axios.get(`InvoiceOuts/${entityId.value}`, options);
if (data) invoiceOut.value = data;
}
const filter = computed(() => {
return invoiceOut.value ? JSON.stringify({ refFk: invoiceOut.value.ref }) : null;
});
</script>
<template>
<card-descriptor v-if="invoiceOut" module="InvoiceOut" :data="invoiceOut" :description="invoiceOut.ref">
<template #body>
<q-list>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.card.issued') }}</q-item-label>
<q-item-label>{{ toDate(invoiceOut.issued) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.card.amount') }}</q-item-label>
<q-item-label>{{ toCurrency(invoiceOut.amount) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section v-if="invoiceOut.company">
<q-item-label caption>{{ t('invoiceOut.card.client') }}</q-item-label>
<q-item-label class="link">
{{ invoiceOut.client.name }}
<q-popup-proxy>
<customer-descriptor-popover :id="invoiceOut.client.id" />
</q-popup-proxy>
</q-item-label>
</q-item-section>
<q-item-section v-if="invoiceOut.company">
<q-item-label caption>{{ t('invoiceOut.card.company') }}</q-item-label>
<q-item-label>{{ invoiceOut.company.code }}</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: invoiceOut.client.id } }"
>
<q-tooltip>{{ t('invoiceOut.card.customerCard') }}</q-tooltip>
</q-btn>
<q-btn size="md" icon="vn:ticket" color="primary" :to="{ name: 'TicketList', params: { q: filter } }">
<q-tooltip>{{ t('invoiceOut.card.ticketList') }}</q-tooltip>
</q-btn>
</q-card-actions>
</template>
</card-descriptor>
</template>

View File

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

View File

@ -0,0 +1,200 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { toCurrency, 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 invoiceOut = ref(null);
const tax = ref(null);
const tikets = ref(null);
function fetch() {
const id = entityId.value;
axios.get(`InvoiceOuts/${id}/summary`).then(({ data }) => {
invoiceOut.value = data.invoiceOut;
tax.value = data.invoiceOut.taxesBreakdown;
});
axios.get(`InvoiceOuts/${id}/getTickets`).then(({ data }) => {
tikets.value = data;
});
}
const taxColumns = ref([
{
name: 'item',
label: 'invoiceOut.summary.type',
field: (row) => row.name,
sortable: true,
},
{
name: 'landed',
label: 'invoiceOut.summary.taxableBase',
field: (row) => row.taxableBase,
format: (value) => toCurrency(value),
sortable: true,
},
{
name: 'quantity',
label: 'invoiceOut.summary.rate',
field: (row) => row.rate,
sortable: true,
},
{
name: 'invoiceOuted',
label: 'invoiceOut.summary.fee',
field: (row) => row.vat,
sortable: true,
},
]);
const ticketsColumns = ref([
{
name: 'item',
label: 'invoiceOut.summary.ticketId',
field: (row) => row.id,
sortable: true,
},
{
name: 'quantity',
label: 'invoiceOut.summary.nickname',
field: (row) => row.nickname,
sortable: true,
},
{
name: 'landed',
label: 'invoiceOut.summary.shipped',
field: (row) => row.shipped,
format: (value) => toDate(value),
sortable: true,
},
{
name: 'landed',
label: 'invoiceOut.summary.totalWithVat',
field: (row) => row.totalWithVat,
format: (value) => toCurrency(value),
sortable: true,
},
]);
</script>
<template>
<div class="summary container">
<q-card>
<skeleton-summary v-if="!invoiceOut" />
<template v-if="invoiceOut">
<div class="header bg-primary q-pa-sm q-mb-md">
{{ invoiceOut.ref }} - {{ invoiceOut.client.socialName }}
</div>
<q-list>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.summary.issued') }}</q-item-label>
<q-item-label>{{ toDate(invoiceOut.issued) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.summary.dued') }}</q-item-label>
<q-item-label>{{ toDate(invoiceOut.dued) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.summary.created') }}</q-item-label>
<q-item-label>{{ toDate(invoiceOut.created) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.summary.booked') }}</q-item-label>
<q-item-label>{{ toDate(invoiceOut.booked) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.summary.company') }}</q-item-label>
<q-item-label>{{ invoiceOut.company.code }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<q-card-section class="q-pa-md">
<h6>{{ t('invoiceOut.summary.taxBreakdown') }}</h6>
<q-table :columns="taxColumns" :rows="tax" 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('invoiceOut.summary.tickets') }}</h6>
<q-table :columns="ticketsColumns" :rows="tikets" 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>
</template>
</q-card>
</div>
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 100%;
min-width: 950px;
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

@ -0,0 +1,29 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import InvoiceOutSummary from './InvoiceOutSummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
</script>
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<invoiceOut-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

@ -0,0 +1,83 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import Paginate from 'src/components/PaginateData.vue';
import InvoiceOutSummaryDialog from './Card/InvoiceOutSummaryDialog.vue';
import { toDate, toCurrency } from 'src/filters/index';
const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
function navigate(id) {
router.push({ path: `/invoiceOut/${id}` });
}
function viewSummary(id) {
quasar.dialog({
component: InvoiceOutSummaryDialog,
componentProps: {
id,
},
});
}
</script>
<template>
<q-page class="q-pa-md">
<paginate url="/InvoiceOuts/filter" sort-by="issued DESC, 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.ref }}</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('invoiceOut.list.issued') }}</q-item-label>
<q-item-label>{{ toDate(row.issued) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.list.amount') }}</q-item-label>
<q-item-label>{{ toCurrency(row.amount) }}</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.list.client') }}</q-item-label>
<q-item-label>{{ row.clientSocialName }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.list.created') }}</q-item-label>
<q-item-label>{{ toDate(row.created) }}</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.list.company') }}</q-item-label>
<q-item-label>{{ row.companyCode }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.list.dued') }}</q-item-label>
<q-item-label>{{ toDate(row.dued) }}</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 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-card-actions>
</q-item>
</q-card>
</template>
</paginate>
</q-page>
</template>

View File

@ -0,0 +1,17 @@
<script setup>
import { useState } from 'src/composables/useState';
import LeftMenu from 'src/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,6 +1,6 @@
import { jest, describe, expect, it, beforeAll } from '@jest/globals'; import { jest, describe, expect, it, beforeAll } from '@jest/globals';
import { createWrapper, axios } from 'app/tests/jest/jestHelpers'; import { createWrapper, axios } from 'app/tests/jest/jestHelpers';
import Login from '../Login.vue'; import Login from '../LoginMain.vue';
const mockPush = jest.fn(); const mockPush = jest.fn();

View File

@ -29,11 +29,11 @@ const { t } = useI18n();
<style lang="scss" scoped></style> <style lang="scss" scoped></style>
<i18n> <i18n>
{ {
'en': { "en": {
'notFound': 'Oops. Nothing here...' "notFound": "Oops. Nothing here..."
}, },
'es': { "es": {
'notFound': 'Vaya. Nada por aquí...' "notFound": "Vaya. Nada por aquí..."
} }
} }
</i18n> </i18n>

View File

@ -79,69 +79,71 @@ async function getVideoList(expeditionId, timed) {
</script> </script>
<template> <template>
<q-drawer show-if-above side="right"> <teleport to=".q-layout">
<q-scroll-area class="fit"> <q-drawer show-if-above side="right">
<q-list bordered separator style="max-width: 318px"> <q-scroll-area class="fit">
<q-item v-if="lastExpedition && videoList.length"> <q-list bordered separator style="max-width: 318px">
<q-item-section> <q-item v-if="lastExpedition && videoList.length">
<q-item-label class="text-h6"> <q-item-section>
{{ t('ticket.boxing.selectTime') }} ({{ time.min }}-{{ time.max }}) <q-item-label class="text-h6">
</q-item-label> {{ t('ticket.boxing.selectTime') }} ({{ time.min }}-{{ time.max }})
<q-range </q-item-label>
v-model="time" <q-range
@change="getVideoList(lastExpedition, time)" v-model="time"
:min="0" @change="getVideoList(lastExpedition, time)"
:max="24" :min="0"
:step="1" :max="24"
:left-label-value="time.min + ':00'" :step="1"
:right-label-value="time.max + ':00'" :left-label-value="time.min + ':00'"
label :right-label-value="time.max + ':00'"
markers label
snap markers
color="orange" snap
/> color="orange"
</q-item-section> />
</q-item> </q-item-section>
<q-item v-if="lastExpedition && videoList.length"> </q-item>
<q-item-section> <q-item v-if="lastExpedition && videoList.length">
<q-select <q-item-section>
color="orange" <q-select
v-model="slide" color="orange"
:options="videoList" v-model="slide"
:label="t('ticket.boxing.selectVideo')" :options="videoList"
emit-value :label="t('ticket.boxing.selectVideo')"
map-options emit-value
> map-options
<template #prepend> >
<q-icon name="schedule" /> <template #prepend>
</template> <q-icon name="schedule" />
</q-select> </template>
</q-item-section> </q-select>
</q-item> </q-item-section>
<q-item </q-item>
v-for="expedition in expeditions" <q-item
:key="expedition.id" v-for="expedition in expeditions"
@click="getVideoList(expedition.id)" :key="expedition.id"
clickable @click="getVideoList(expedition.id)"
v-ripple clickable
> v-ripple
<q-item-section> >
<q-item-label class="text-h6">#{{ expedition.id }}</q-item-label> <q-item-section>
</q-item-section> <q-item-label class="text-h6">#{{ expedition.id }}</q-item-label>
<q-item-section> </q-item-section>
<q-item-label caption>{{ t('ticket.boxing.created') }}</q-item-label> <q-item-section>
<q-item-label> <q-item-label caption>{{ t('ticket.boxing.created') }}</q-item-label>
{{ date.formatDate(expedition.created, 'YYYY-MM-DD HH:mm:ss') }} <q-item-label>
</q-item-label> {{ date.formatDate(expedition.created, 'YYYY-MM-DD HH:mm:ss') }}
<q-item-label caption>{{ t('ticket.boxing.item') }}</q-item-label> </q-item-label>
<q-item-label>{{ expedition.packagingItemFk }}</q-item-label> <q-item-label caption>{{ t('ticket.boxing.item') }}</q-item-label>
<q-item-label caption>{{ t('ticket.boxing.worker') }}</q-item-label> <q-item-label>{{ expedition.packagingItemFk }}</q-item-label>
<q-item-label>{{ expedition.userName }}</q-item-label> <q-item-label caption>{{ t('ticket.boxing.worker') }}</q-item-label>
</q-item-section> <q-item-label>{{ expedition.userName }}</q-item-label>
</q-item> </q-item-section>
</q-list> </q-item>
</q-scroll-area> </q-list>
</q-drawer> </q-scroll-area>
</q-drawer>
</teleport>
<q-card> <q-card>
<q-carousel animated v-model="slide" height="max-content"> <q-carousel animated v-model="slide" height="max-content">

View File

@ -1,24 +1,16 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import TicketDescriptor from './TicketDescriptor.vue'; import TicketDescriptor from './TicketDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
const state = useState(); const state = useState();
const { t } = useI18n();
</script> </script>
<template> <template>
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500"> <q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
<q-scroll-area class="fit"> <q-scroll-area class="fit">
<ticket-descriptor /> <ticket-descriptor />
<q-separator /> <q-separator />
<q-list> <left-menu source="card" />
<q-item :to="{ name: 'TicketBoxing' }" clickable v-ripple>
<q-item-section avatar>
<q-icon name="vn:package" />
</q-item-section>
<q-item-section>{{ t('ticket.pageTitles.boxing') }}</q-item-section>
</q-item>
</q-list>
</q-scroll-area> </q-scroll-area>
</q-drawer> </q-drawer>
<q-page-container> <q-page-container>

View File

@ -4,8 +4,9 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import axios from 'axios'; import axios from 'axios';
import CardDescriptor from 'src/components/ui/CardDescriptor.vue';
import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue'; import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -42,6 +43,7 @@ function stateColor(state) {
</script> </script>
<template> <template>
<skeleton-descriptor v-if="!ticket" />
<card-descriptor v-if="ticket" module="Ticket" :data="ticket" :description="ticket.client.name"> <card-descriptor v-if="ticket" module="Ticket" :data="ticket" :description="ticket.client.name">
<!-- <template #menu> <!-- <template #menu>
<q-item clickable v-ripple>Option 1</q-item> <q-item clickable v-ripple>Option 1</q-item>

View File

@ -0,0 +1,553 @@
<script setup>
import { onMounted, ref, computed, onUpdated } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { dashIfEmpty, toDate, toCurrency } from 'src/filters';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
import FetchData from 'components/FetchData.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
onMounted(() => fetch());
onUpdated(() => fetch());
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const entityId = computed(() => $props.id || route.params.id);
const ticket = ref();
const salesLines = ref(null);
const editableStates = ref([]);
async function fetch() {
const { data } = await axios.get(`Tickets/${entityId.value}/summary`);
if (data) {
ticket.value = data;
salesLines.value = data.sales;
}
}
function stateColor(state) {
if (state.code === 'OK') return 'text-green';
if (state.code === 'FREE') return 'text-blue-3';
if (state.alertLevel === 1) return 'text-primary';
if (state.alertLevel === 0) return 'text-red';
}
function formattedAddress() {
if (!ticket.value) return '';
const address = this.ticket.address;
const postcode = address.postalCode;
const province = address.province ? `(${address.province.name})` : '';
return `${address.street} - ${postcode} - ${address.city} ${province}`;
}
function isEditable() {
try {
return !this.ticket.ticketState.state.alertLevel;
} catch (e) {
console.error(e);
}
return true;
}
async function changeState(value) {
if (!this.ticket.id) return;
const formData = {
ticketFk: this.ticket.id,
code: value,
};
await axios.post(`TicketTrackings/changeState`, formData);
await router.go(route.fullPath);
}
</script>
<template>
<fetch-data url="States/editableStates" @on-fetch="(data) => (editableStates = data)" auto-load />
<div class="summary container">
<q-card>
<skeleton-summary v-if="!ticket" />
<template v-if="ticket">
<div class="header bg-primary q-pa-sm q-mb-md">
<span>
Ticket #{{ ticket.id }} - {{ ticket.client.name }} ({{ ticket.client.id }}) -
{{ ticket.nickname }}
</span>
<q-btn-dropdown
side
top
color="orange-11"
text-color="black"
:label="t('ticket.summary.changeState')"
:disable="!isEditable()"
>
<q-list>
<q-virtual-scroll
style="max-height: 300px"
:items="editableStates"
separator
v-slot="{ item, index }"
>
<q-item :key="index" dense clickable v-close-popup @click="changeState(item.code)">
<q-item-section>
<q-item-label>{{ item.name }}</q-item-label>
</q-item-section>
</q-item>
</q-virtual-scroll>
</q-list>
</q-btn-dropdown>
</div>
<div class="row q-pa-md q-col-gutter-md q-mb-md">
<div class="col">
<q-list>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.state') }}</q-item-label>
<q-item-label :class="stateColor(ticket.ticketState.state)">
{{ ticket.ticketState.state.name }}
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.salesPerson') }}</q-item-label>
<q-item-label class="link">{{ ticket.client.salesPersonUser.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.agency') }}</q-item-label>
<q-item-label>{{ ticket.agencyMode.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.zone') }}</q-item-label>
<q-item-label class="link">{{ ticket.routeFk }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.warehouse') }}</q-item-label>
<q-item-label>{{ ticket.warehouse.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.invoice') }}</q-item-label>
<q-item-label v-if="ticket.refFk" class="link">{{ ticket.refFk }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.shipped') }}</q-item-label>
<q-item-label>{{ toDate(ticket.shipped) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.landed') }}</q-item-label>
<q-item-label>{{ toDate(ticket.landed) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.packages') }}</q-item-label>
<q-item-label>{{ ticket.packages }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.consigneePhone') }}</q-item-label>
<q-item-label>{{ ticket.address.phone }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.consigneeMobile') }}</q-item-label>
<q-item-label>{{ ticket.address.mobile }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.clientPhone') }}</q-item-label>
<q-item-label>{{ ticket.client.phone }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.clientMobile') }}</q-item-label>
<q-item-label>{{ ticket.client.mobile }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.consignee') }}</q-item-label>
<q-item-label>{{ formattedAddress() }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item v-for="note in ticket.notes" :key="note.id">
<q-item-section>
<q-item-label caption>
{{ note.observationType.description }}
</q-item-label>
<q-item-label>
{{ note.description }}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list class="taxes">
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.subtotal') }}</q-item-label>
<q-item-label>{{ toCurrency(ticket.totalWithoutVat) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.vat') }}</q-item-label>
<q-item-label>{{
toCurrency(ticket.totalWithVat - ticket.totalWithoutVat)
}}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.total') }}</q-item-label>
<q-item-label>{{ toCurrency(ticket.totalWithVat) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</div>
<div class="row q-pa-md" v-if="salesLines.length > 0">
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('ticket.summary.saleLines') }}
<router-link
:to="{ name: 'TicketBasicData', params: { id: entityId } }"
target="_blank"
>
<q-icon name="open_in_new" />
</router-link>
</q-item-label>
<q-table :rows="ticket.sales" flat>
<template #header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th auto-width>{{ t('ticket.summary.item') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.visible') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.available') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.quantity') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.description') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.price') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.discount') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.amount') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.packing') }}</q-th>
</q-tr>
</template>
<template #body="props">
<q-tr :props="props">
<q-td>
<q-btn
flat
round
size="xs"
icon="vn:claims"
v-if="props.row.claim"
color="primary"
:to="{ name: 'ClaimCard', params: { id: props.row.claim.claimFk } }"
>
<q-tooltip
>{{ t('ticket.summary.claim') }}:
{{ props.row.claim.claimFk }}</q-tooltip
>
</q-btn>
<q-btn
flat
round
size="xs"
icon="vn:claims"
v-if="props.row.claimBeginning"
color="primary"
:to="{
name: 'ClaimCard',
params: { id: props.row.claimBeginning.claimFk },
}"
>
<q-tooltip
>{{ t('ticket.summary.claim') }}:
{{ props.row.claimBeginning.claimFk }}</q-tooltip
>
</q-btn>
<q-icon
name="warning"
v-show="props.row.visible < 0"
size="xs"
color="primary"
>
<q-tooltip
>{{ t('ticket.summary.visible') }}:
{{ props.row.visible }}</q-tooltip
>
</q-icon>
<q-icon
name="vn:reserva"
v-show="props.row.reserved"
size="xs"
color="primary"
>
<q-tooltip>{{ t('ticket.summary.reserved') }}</q-tooltip>
</q-icon>
<q-icon
name="vn:unavailable"
v-show="props.row.itemShortage"
size="xs"
color="primary"
>
<q-tooltip>{{ t('ticket.summary.itemShortage') }}</q-tooltip>
</q-icon>
<q-icon
name="vn:components"
v-show="props.row.hasComponentLack"
size="xs"
color="primary"
>
<q-tooltip>{{ t('ticket.summary.hasComponentLack') }}</q-tooltip>
</q-icon>
</q-td>
<q-td class="link">{{ props.row.itemFk }}</q-td>
<q-td>{{ props.row.visible }}</q-td>
<q-td>{{ props.row.available }}</q-td>
<q-td>{{ props.row.quantity }}</q-td>
<q-td>
<div class="fetched-tags">
<span>{{ props.row.item.name }}</span>
<span v-if="props.row.item.subName" class="subName">{{
props.row.item.subName
}}</span>
</div>
<fetched-tags :item="props.row.item" :max-length="5"></fetched-tags>
</q-td>
<q-td>{{ props.row.price }}</q-td>
<q-td>{{ props.row.discount }} %</q-td>
<q-td
>{{
toCurrency(
props.row.quantity *
props.row.price *
((100 - props.row.discount) / 100)
)
}}
</q-td>
<q-td>{{ dashIfEmpty(props.row.item.itemPackingTypeFk) }}</q-td>
</q-tr>
</template>
</q-table>
</q-list>
</div>
</div>
<div class="row q-pa-md" v-if="ticket.packagings.length > 0 || ticket.services.length > 0">
<div class="col" v-if="ticket.packagings.length > 0">
<q-list>
<q-item-label header class="text-h6">
{{ t('ticket.summary.packages') }}
<q-icon name="open_in_new" />
</q-item-label>
<q-table :rows="ticket.packagings" flat>
<template #header="props">
<q-tr :props="props">
<q-th auto-width>{{ t('ticket.summary.created') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.package') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.quantity') }}</q-th>
</q-tr>
</template>
<template #body="props">
<q-tr :props="props">
<q-td>{{ toDate(props.row.created) }}</q-td>
<q-td>{{ props.row.packaging.item.name }}</q-td>
<q-td>{{ props.row.quantity }}</q-td>
</q-tr>
</template>
</q-table>
</q-list>
</div>
<div class="col" v-if="ticket.services.length > 0">
<q-list>
<q-item-label header class="text-h6">
{{ t('ticket.summary.services') }}
<q-icon name="open_in_new" />
</q-item-label>
<q-table :rows="ticket.services" flat>
<template #header="props">
<q-tr :props="props">
<q-th auto-width>{{ t('ticket.summary.quantity') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.description') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.price') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.taxClass') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.amount') }}</q-th>
</q-tr>
</template>
<template #body="props">
<q-tr :props="props">
<q-td>{{ props.row.quantity }}</q-td>
<q-td>{{ props.row.description }}</q-td>
<q-td>{{ toCurrency(props.row.price) }}</q-td>
<q-td>{{ props.row.taxClass.description }}</q-td>
<q-td>{{ toCurrency(props.row.quantity * props.row.price) }}</q-td>
</q-tr>
</template>
</q-table>
</q-list>
</div>
</div>
<div class="row q-pa-md" v-if="ticket.requests.length > 0">
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('ticket.summary.request') }}
<q-icon name="open_in_new" />
</q-item-label>
<q-table :rows="ticket.requests" flat>
<template #header="props">
<q-tr :props="props">
<q-th auto-width>{{ t('ticket.summary.description') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.created') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.requester') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.atender') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.quantity') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.price') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.item') }}</q-th>
<q-th auto-width>Ok</q-th>
</q-tr>
</template>
<template #body="props">
<q-tr :props="props">
<q-td>{{ props.row.description }}</q-td>
<q-td>{{ toDate(props.row.created) }}</q-td>
<q-td>{{ props.row.requester.user.name }}</q-td>
<q-td>{{ props.row.atender.user.name }}</q-td>
<q-td>{{ props.row.quantity }}</q-td>
<q-td>{{ toCurrency(props.row.price) }}</q-td>
<q-td v-if="!props.row.sale">-</q-td>
<q-td v-if="props.row.sale" class="link">{{ props.row.sale.itemFk }}</q-td>
<q-td><q-checkbox v-model="props.row.isOk" :disable="true" /></q-td>
</q-tr>
</template>
</q-table>
</q-list>
</div>
</div>
</template>
</q-card>
</div>
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 100%;
height: 100%;
max-width: 1200px;
}
.summary {
.q-list {
.q-item__label--header {
display: flex;
justify-content: space-between;
a {
color: $primary;
}
}
}
.fetched-tags {
display: flex;
flex-wrap: wrap;
align-items: center;
& span {
flex-basis: 50%;
}
& span.subName {
flex-basis: 50%;
color: $secondary;
text-transform: uppercase;
font-size: 0.75rem;
}
}
.q-table__container {
text-align: left;
.q-icon {
padding: 2%;
}
}
.taxes {
border: $border-thin-light;
text-align: right;
padding: 8px;
}
.row {
flex-wrap: wrap;
.col {
min-width: 250px;
padding-left: 1.5%;
padding-right: 1.5%;
}
}
.header {
font-size: 18px;
display: flex;
justify-content: space-between;
align-items: center;
align-content: center;
margin: 0;
text-align: center;
span {
flex: 1;
}
.q-btn {
flex: none;
}
}
}
.q-dialog .summary {
max-width: 1200px;
}
</style>

View File

@ -0,0 +1,29 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import TicketSummary from './TicketSummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
</script>
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<ticket-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,12 +1,13 @@
<script setup> <script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import Paginate from 'src/components/Paginate.vue'; import Paginate from 'src/components/PaginateData.vue';
import { toDate, toCurrency } from 'src/filters/index'; import { toDate, toCurrency } from 'src/filters/index';
// import TicketSummary from './Card/TicketSummary.vue'; import TicketSummaryDialog from './Card/TicketSummaryDialog.vue';
const router = useRouter(); const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const filter = { const filter = {
@ -48,15 +49,13 @@ function navigate(id) {
router.push({ path: `/ticket/${id}` }); router.push({ path: `/ticket/${id}` });
} }
const preview = ref({ function viewSummary(id) {
shown: false, quasar.dialog({
}); component: TicketSummaryDialog,
componentProps: {
function showPreview(id) { id,
preview.value.shown = true; },
preview.value.data = { });
customerId: id,
};
} }
</script> </script>
@ -108,42 +107,16 @@ function showPreview(id) {
</q-item-section> </q-item-section>
<q-separator vertical /> <q-separator vertical />
<q-card-actions vertical class="justify-between"> <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-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip> <q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
</q-btn> </q-btn>
<q-btn flat round color="grey-7" icon="preview" @click="showPreview(row.id)"> <q-btn flat round color="grey-7" icon="preview" @click="viewSummary(row.id)">
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip> <q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
</q-btn> </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-card-actions>
</q-item> </q-item>
</q-card> </q-card>
</template> </template>
</paginate> </paginate>
</q-page> </q-page>
<!-- <q-dialog v-model="preview.shown">
<customer-summary :customer-id="preview.data.customerId" />
</q-dialog> -->
</template> </template>

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import LeftMenu from 'src/components/LeftMenu.vue'; import LeftMenu from 'components/LeftMenu.vue';
const state = useState(); const state = useState();
</script> </script>

View File

@ -1,11 +1,11 @@
import { route } from 'quasar/wrappers'; import { route } from 'quasar/wrappers';
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'; import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router';
// import { Notify } from 'quasar';
import routes from './routes'; import routes from './routes';
import { i18n } from 'src/boot/i18n'; import { i18n } from 'src/boot/i18n';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { useRole } from 'src/composables/useRole'; import { useRole } from 'src/composables/useRole';
import { toLowerCamel } from 'src/filters';
const state = useState(); const state = useState();
const session = useSession(); const session = useSession();
@ -25,8 +25,8 @@ export default route(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER const createHistory = process.env.SERVER
? createMemoryHistory ? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history' : process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory ? createWebHistory
: createWebHashHistory; : createWebHashHistory;
const Router = createRouter({ const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }), scrollBehavior: () => ({ left: 0, top: 0 }),
@ -46,26 +46,14 @@ export default route(function (/* { store, ssrContext } */) {
} }
if (isLoggedIn()) { if (isLoggedIn()) {
// try {
const stateRoles = state.getRoles().value; const stateRoles = state.getRoles().value;
if (stateRoles.length === 0) { if (stateRoles.length === 0) {
await role.fetch(); await role.fetch();
} }
// } catch (error) {
// Notify.create({
// message: t('errors.statusUnauthorized'),
// type: 'negative',
// });
// session.destroy();
// return next({ path: '/login' });
// }
const matches = to.matched; const matches = to.matched;
const hasRequiredRoles = matches.every(route => { const hasRequiredRoles = matches.every((route) => {
const meta = route.meta; const meta = route.meta;
if (meta && meta.roles) if (meta && meta.roles) return role.hasAny(meta.roles);
return role.hasAny(meta.roles)
return true; return true;
}); });
@ -85,7 +73,7 @@ export default route(function (/* { store, ssrContext } */) {
if (matches && matches.length > 1) { if (matches && matches.length > 1) {
const module = matches[1]; const module = matches[1];
const moduleTitle = module.meta && module.meta.title; const moduleTitle = module.meta && module.meta.title;
moduleName = module.name.toLowerCase(); moduleName = toLowerCamel(module.name);
if (moduleTitle) { if (moduleTitle) {
title = t(`${moduleName}.pageTitles.${moduleTitle}`); title = t(`${moduleName}.pageTitles.${moduleTitle}`);
} }

View File

@ -5,10 +5,14 @@ export default {
path: '/claim', path: '/claim',
meta: { meta: {
title: 'claims', title: 'claims',
icon: 'vn:claims' icon: 'vn:claims',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'ClaimMain' }, redirect: { name: 'ClaimMain' },
menus: {
main: ['ClaimList', 'ClaimRmaList'],
card: ['ClaimBasicData', 'ClaimRma'],
},
children: [ children: [
{ {
name: 'ClaimMain', name: 'ClaimMain',
@ -31,11 +35,11 @@ export default {
meta: { meta: {
title: 'rmaList', title: 'rmaList',
icon: 'vn:barcode', icon: 'vn:barcode',
roles: ['claimManager'] roles: ['claimManager'],
}, },
component: () => import('src/pages/Claim/ClaimRmaList.vue'), component: () => import('src/pages/Claim/ClaimRmaList.vue'),
} },
] ],
}, },
{ {
name: 'ClaimCard', name: 'ClaimCard',
@ -47,7 +51,8 @@ export default {
name: 'ClaimSummary', name: 'ClaimSummary',
path: 'summary', path: 'summary',
meta: { meta: {
title: 'summary' title: 'summary',
icon: 'launch',
}, },
component: () => import('src/pages/Claim/Card/ClaimSummary.vue'), component: () => import('src/pages/Claim/Card/ClaimSummary.vue'),
}, },
@ -56,7 +61,8 @@ export default {
path: 'basic-data', path: 'basic-data',
meta: { meta: {
title: 'basicData', title: 'basicData',
roles: ['salesPerson'] icon: 'vn:settings',
roles: ['salesPerson'],
}, },
component: () => import('src/pages/Claim/Card/ClaimBasicData.vue'), component: () => import('src/pages/Claim/Card/ClaimBasicData.vue'),
}, },
@ -65,11 +71,12 @@ export default {
path: 'rma', path: 'rma',
meta: { meta: {
title: 'rma', title: 'rma',
roles: ['claimManager'] icon: 'vn:barcode',
roles: ['claimManager'],
}, },
component: () => import('src/pages/Claim/Card/ClaimRma.vue') component: () => import('src/pages/Claim/Card/ClaimRma.vue'),
} },
] ],
}, },
] ],
}; };

View File

@ -5,10 +5,14 @@ export default {
name: 'Customer', name: 'Customer',
meta: { meta: {
title: 'customers', title: 'customers',
icon: 'vn:client' icon: 'vn:client',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'CustomerMain' }, redirect: { name: 'CustomerMain' },
menus: {
main: ['CustomerList', 'CustomerCreate'],
card: ['CustomerBasicData'],
},
children: [ children: [
{ {
path: '', path: '',
@ -35,7 +39,7 @@ export default {
}, },
component: () => import('src/pages/Customer/CustomerCreate.vue'), component: () => import('src/pages/Customer/CustomerCreate.vue'),
}, },
] ],
}, },
{ {
name: 'CustomerCard', name: 'CustomerCard',
@ -47,7 +51,8 @@ export default {
name: 'CustomerSummary', name: 'CustomerSummary',
path: 'summary', path: 'summary',
meta: { meta: {
title: 'summary' title: 'summary',
icon: 'launch',
}, },
component: () => import('src/pages/Customer/Card/CustomerSummary.vue'), component: () => import('src/pages/Customer/Card/CustomerSummary.vue'),
}, },
@ -55,11 +60,12 @@ export default {
path: 'basic-data', path: 'basic-data',
name: 'CustomerBasicData', name: 'CustomerBasicData',
meta: { meta: {
title: 'basicData' title: 'basicData',
icon: 'vn:settings',
}, },
component: () => import('src/pages/Customer/Card/CustomerBasicData.vue'), component: () => import('src/pages/Customer/Card/CustomerBasicData.vue'),
} },
] ],
}, },
] ],
}; };

View File

@ -0,0 +1,11 @@
import Customer from './customer';
import Ticket from './ticket';
import Claim from './claim';
import InvoiceOut from './invoiceOut';
export default [
Customer,
Ticket,
Claim,
InvoiceOut
]

View File

@ -0,0 +1,50 @@
import { RouterView } from 'vue-router';
export default {
path: '/invoiceOut',
name: 'InvoiceOut',
meta: {
title: 'invoiceOuts',
icon: 'vn:invoice-out'
},
component: RouterView,
redirect: { name: 'InvoiceOutMain' },
menus: {
main: ['InvoiceOutList']
},
children: [
{
path: '',
name: 'InvoiceOutMain',
component: () => import('src/pages/InvoiceOut/InvoiceOutMain.vue'),
redirect: { name: 'InvoiceOutList' },
children: [
{
path: 'list',
name: 'InvoiceOutList',
meta: {
title: 'list',
icon: 'view_list',
},
component: () => import('src/pages/InvoiceOut/InvoiceOutList.vue'),
}
]
},
{
name: 'InvoiceOutCard',
path: ':id',
component: () => import('src/pages/InvoiceOut/Card/InvoiceOutCard.vue'),
redirect: { name: 'InvoiceOutSummary' },
children: [
{
name: 'InvoiceOutSummary',
path: 'summary',
meta: {
title: 'summary'
},
component: () => import('src/pages/InvoiceOut/Card/InvoiceOutSummary.vue'),
}
]
},
]
};

View File

@ -5,10 +5,14 @@ export default {
path: '/ticket', path: '/ticket',
meta: { meta: {
title: 'tickets', title: 'tickets',
icon: 'vn:ticket' icon: 'vn:ticket',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'TicketMain' }, redirect: { name: 'TicketMain' },
menus: {
main: ['TicketList'],
card: ['TicketBoxing'],
},
children: [ children: [
{ {
name: 'TicketMain', name: 'TicketMain',
@ -35,8 +39,7 @@ export default {
}, },
component: () => import('src/pages/Ticket/TicketList.vue'), component: () => import('src/pages/Ticket/TicketList.vue'),
}, },
],
]
}, },
{ {
name: 'TicketCard', name: 'TicketCard',
@ -48,7 +51,8 @@ export default {
name: 'TicketSummary', name: 'TicketSummary',
path: 'summary', path: 'summary',
meta: { meta: {
title: 'summary' title: 'summary',
icon: 'launch',
}, },
component: () => import('src/pages/Ticket/Card/TicketSummary.vue'), component: () => import('src/pages/Ticket/Card/TicketSummary.vue'),
}, },
@ -56,7 +60,8 @@ export default {
name: 'TicketBasicData', name: 'TicketBasicData',
path: 'basic-data', path: 'basic-data',
meta: { meta: {
title: 'basicData' title: 'basicData',
icon: 'vn:settings',
}, },
component: () => import('src/pages/Ticket/Card/TicketBasicData.vue'), component: () => import('src/pages/Ticket/Card/TicketBasicData.vue'),
}, },
@ -64,11 +69,12 @@ export default {
path: 'boxing', path: 'boxing',
name: 'TicketBoxing', name: 'TicketBoxing',
meta: { meta: {
title: 'boxing' title: 'boxing',
icon: 'vn:package',
}, },
component: () => import('src/pages/Ticket/Card/TicketBoxing.vue'), component: () => import('src/pages/Ticket/Card/TicketBoxing.vue'),
} },
] ],
}, },
] ],
}; };

View File

@ -2,13 +2,14 @@ import customer from './modules/customer';
import ticket from './modules/ticket'; import ticket from './modules/ticket';
import claim from './modules/claim'; import claim from './modules/claim';
import worker from './modules/worker'; import worker from './modules/worker';
import invoiceOut from './modules/invoiceOut';
const routes = [ const routes = [
{ {
path: '/login', path: '/login',
name: 'Login', name: 'Login',
meta: { title: 'logIn' }, meta: { title: 'logIn' },
component: () => import('../pages/Login/Login.vue') component: () => import('../pages/Login/LoginMain.vue'),
}, },
{ {
path: '/', path: '/',
@ -20,20 +21,21 @@ const routes = [
path: '/dashboard', path: '/dashboard',
name: 'Dashboard', name: 'Dashboard',
meta: { title: 'dashboard', icon: 'dashboard' }, meta: { title: 'dashboard', icon: 'dashboard' },
component: () => import('../pages/Dashboard/Dashboard.vue'), component: () => import('../pages/Dashboard/DashboardMain.vue'),
}, },
// Module routes // Module routes
customer, customer,
ticket, ticket,
claim, claim,
worker, worker,
invoiceOut,
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'NotFound', name: 'NotFound',
component: () => import('../pages/NotFound.vue'), component: () => import('../pages/NotFound.vue'),
} },
], ],
} },
]; ];
export default routes; export default routes;

View File

@ -0,0 +1,85 @@
import axios from 'axios';
import { ref } from 'vue';
import { defineStore } from 'pinia';
import { toLowerCamel } from 'src/filters';
import { useRole } from 'src/composables/useRole';
import routes from 'src/router/modules';
export const useNavigationStore = defineStore('navigationStore', () => {
const modules = ['customer', 'claim', 'ticket', 'invoiceOut'];
const pinnedModules = ref([]);
const role = useRole();
function getModules() {
const modulesRoutes = ref([]);
for (const module of modules) {
const moduleDef = routes.find((route) => toLowerCamel(route.name) === module);
if (!moduleDef) continue;
const item = addMenuItem(module, moduleDef, modulesRoutes.value);
if (!item) continue;
item.module = module;
item.isPinned = false;
if (pinnedModules.value.includes(module)) {
item.isPinned = true;
}
}
return modulesRoutes;
}
function getPinnedModules() {
const modules = getModules();
return modules && modules.value.filter((item) => item.isPinned);
}
function addMenuItem(module, route, parent) {
const { meta } = route;
if (meta && meta.roles && role.hasAny(meta.roles) === false) return;
const item = {
name: route.name,
};
if (meta) {
item.title = `${module}.pageTitles.${meta.title}`;
item.icon = meta.icon;
}
parent.push(item);
return item;
}
async function fetchPinned() {
if (pinnedModules.value.length) return;
const response = await axios.get('StarredModules/getStarredModules');
pinnedModules.value = response.data.map((row) => row.moduleFk);
}
function togglePinned(module) {
if (pinnedModules.value.includes(module)) {
const index = pinnedModules.value.indexOf(module);
pinnedModules.value.splice(index, 1);
return;
}
pinnedModules.value.push(module);
}
return {
modules,
pinnedModules,
getModules,
getPinnedModules,
fetchPinned,
togglePinned,
addMenuItem,
};
});

View File

@ -1,25 +1,27 @@
import { mount, flushPromises } from '@vue/test-utils'; import { mount, flushPromises } from '@vue/test-utils';
import { installQuasarPlugin } from '@quasar/quasar-app-extension-testing-unit-jest'; import { installQuasarPlugin } from '@quasar/quasar-app-extension-testing-unit-jest';
import { i18n } from 'src/boot/i18n'; import { i18n } from 'src/boot/i18n';
import { Notify } from 'quasar'; import { Notify, Dialog } from 'quasar';
import axios from 'axios'; import axios from 'axios';
// Specify here Quasar config you'll need to test your component
installQuasarPlugin({ installQuasarPlugin({
plugins: { plugins: {
Notify Notify,
} Dialog,
},
}); });
export function createWrapper(component, options) { export function createWrapper(component, options) {
const mountOptions = { const mountOptions = {};
global: {
plugins: [i18n]
}
};
if (options instanceof Object) if (options instanceof Object) Object.assign(mountOptions, options);
Object.assign(mountOptions, options)
if (mountOptions.global && mountOptions.global.plugins) {
mountOptions.global.plugins.push(i18n);
} else {
if (!mountOptions.global) mountOptions.global = {};
mountOptions.global.plugins = [i18n];
}
const wrapper = mount(component, mountOptions); const wrapper = mount(component, mountOptions);
const vm = wrapper.vm; const vm = wrapper.vm;
@ -27,7 +29,4 @@ export function createWrapper(component, options) {
return { vm, wrapper }; return { vm, wrapper };
} }
export { export { axios, flushPromises };
axios,
flushPromises
}