Added project files
gitea/salix-front/pipeline/head There was a failure building this commit
Details
gitea/salix-front/pipeline/head There was a failure building this commit
Details
This commit is contained in:
parent
121b6390e1
commit
ebf4a791a1
|
@ -26,6 +26,7 @@
|
|||
"prettier": "^2.5.1",
|
||||
"@intlify/vite-plugin-vue-i18n": "^3.3.1",
|
||||
"@quasar/app-vite": "^1.0.0",
|
||||
"@pinia/testing": "^0.0.14",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"postcss": "^8.4.14"
|
||||
},
|
||||
|
|
|
@ -114,6 +114,7 @@ module.exports = configure(function (/* ctx */) {
|
|||
dark: 'auto',
|
||||
},
|
||||
},
|
||||
lang: 'en-GB',
|
||||
|
||||
// iconSet: 'material-icons', // Quasar icon set
|
||||
// lang: 'en-US', // Quasar language pack
|
||||
|
|
112
src/App.vue
112
src/App.vue
|
@ -1,11 +1,109 @@
|
|||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useSession } from 'src/composables/useSession';
|
||||
|
||||
const quasar = useQuasar();
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
const { t, availableLocales, locale, fallbackLocale } = useI18n();
|
||||
const { isLoggedIn } = session;
|
||||
|
||||
onMounted(() => {
|
||||
let userLang = window.navigator.language;
|
||||
if (userLang.includes('-')) {
|
||||
userLang = userLang.split('-')[0];
|
||||
}
|
||||
|
||||
if (availableLocales.includes(userLang)) {
|
||||
locale.value = userLang;
|
||||
} else {
|
||||
locale.value = fallbackLocale;
|
||||
}
|
||||
});
|
||||
|
||||
quasar.iconMapFn = (iconName) => {
|
||||
if (iconName.startsWith('vn:')) {
|
||||
const name = iconName.substring(3);
|
||||
|
||||
return {
|
||||
cls: `icon-${name}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
cls: 'material-symbols-outlined',
|
||||
content: iconName,
|
||||
};
|
||||
};
|
||||
|
||||
function responseError(error) {
|
||||
let message = error.message;
|
||||
let logOut = false;
|
||||
|
||||
switch (error.response?.status) {
|
||||
case 401:
|
||||
message = 'login.loginError';
|
||||
|
||||
if (isLoggedIn()) {
|
||||
message = 'errors.statusUnauthorized';
|
||||
logOut = true;
|
||||
}
|
||||
break;
|
||||
case 403:
|
||||
message = 'errors.statusUnauthorized';
|
||||
break;
|
||||
case 500:
|
||||
message = 'errors.statusInternalServerError';
|
||||
break;
|
||||
case 502:
|
||||
message = 'errors.statusBadGateway';
|
||||
break;
|
||||
case 504:
|
||||
message = 'errors.statusGatewayTimeout';
|
||||
break;
|
||||
}
|
||||
|
||||
let translatedMessage = t(message);
|
||||
if (!translatedMessage) translatedMessage = message;
|
||||
|
||||
quasar.notify({
|
||||
message: translatedMessage,
|
||||
type: 'negative',
|
||||
});
|
||||
|
||||
if (logOut) {
|
||||
session.destroy();
|
||||
router.push({ path: '/login' });
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
axios.interceptors.response.use((response) => {
|
||||
const { method } = response.config;
|
||||
|
||||
const isSaveRequest = method === 'patch';
|
||||
if (isSaveRequest) {
|
||||
quasar.notify({
|
||||
message: t('globals.dataSaved'),
|
||||
type: 'positive',
|
||||
icon: 'check',
|
||||
});
|
||||
}
|
||||
return response;
|
||||
}, responseError);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.body--light {
|
||||
background-color: #eee;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,77 @@
|
|||
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
|
||||
import { createWrapper } from 'app/tests/jest/jestHelpers';
|
||||
import App from '../App.vue';
|
||||
import { useSession } from 'src/composables/useSession';
|
||||
|
||||
const mockPush = jest.fn();
|
||||
const mockLoggedIn = jest.fn();
|
||||
const mockDestroy = jest.fn();
|
||||
const session = useSession();
|
||||
|
||||
jest.mock('vue-router', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
currentRoute: { value: 'myCurrentRoute' },
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('src/composables/useSession', () => ({
|
||||
useSession: () => ({
|
||||
isLoggedIn: mockLoggedIn,
|
||||
destroy: mockDestroy,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('App', () => {
|
||||
let vm;
|
||||
beforeAll(() => {
|
||||
const options = {
|
||||
global: {
|
||||
stubs: ['router-view'],
|
||||
},
|
||||
};
|
||||
vm = createWrapper(App, options).vm;
|
||||
});
|
||||
|
||||
it('should return a login error message', async () => {
|
||||
jest.spyOn(vm.quasar, 'notify');
|
||||
|
||||
session.isLoggedIn.mockReturnValue(false);
|
||||
|
||||
const response = {
|
||||
response: {
|
||||
status: 401,
|
||||
},
|
||||
};
|
||||
|
||||
expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response));
|
||||
expect(vm.quasar.notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Invalid username or password',
|
||||
type: 'negative',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return an unauthorized error message', async () => {
|
||||
jest.spyOn(vm.quasar, 'notify');
|
||||
|
||||
session.isLoggedIn.mockReturnValue(true);
|
||||
|
||||
const response = {
|
||||
response: {
|
||||
status: 401,
|
||||
},
|
||||
};
|
||||
|
||||
expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response));
|
||||
expect(vm.quasar.notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Access denied',
|
||||
type: 'negative',
|
||||
})
|
||||
);
|
||||
|
||||
expect(session.destroy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1"
|
||||
id="Capa_1" inkscape:version="0.91 r13725" sodipodi:docname="logo.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:ns="&ns_sfw;" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 400 168.6"
|
||||
style="enable-background:new 0 0 400 168.6;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#3D3D3F;}
|
||||
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#8EBB27;}
|
||||
.st2{fill:#8EBB27;}
|
||||
.st3{fill:#F19300;}
|
||||
</style>
|
||||
<sodipodi:namedview bordercolor="#666666" borderopacity="1" fit-margin-bottom="0" fit-margin-left="0" fit-margin-right="0" fit-margin-top="0" gridtolerance="10" guidetolerance="10" id="namedview41" inkscape:current-layer="Capa_1" inkscape:cx="200" inkscape:cy="84.28212" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="1016" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="1920" inkscape:window-y="27" inkscape:zoom="3.09" objecttolerance="10" pagecolor="#ffffff" showgrid="false">
|
||||
</sodipodi:namedview>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M106.1,40L92.3,0h10.9l5.6,20.6l0.5,1.7c0.7,2.5,1.2,4.5,1.6,6.2c0.2-0.8,0.4-1.8,0.7-2.9
|
||||
c0.3-1.1,0.7-2.6,1.2-4.3L118.7,0h10.8l-13.9,40H106.1z"/>
|
||||
<path class="st1" d="M386.1,40h-9.8c0-0.5,0.1-1,0.1-1.5l0.2-1.6c-1.7,1.4-3.5,2.4-5.2,3c-1.7,0.6-3.5,1-5.3,1
|
||||
c-2.8,0-4.9-0.8-6.1-2.3c-1.2-1.6-1.5-3.7-0.7-6.3c0.7-2.4,1.9-4.4,3.6-6c1.7-1.5,4-2.6,6.8-3.2c1.5-0.3,3.5-0.7,5.8-1.1
|
||||
c3.5-0.5,5.4-1.3,5.7-2.4l0.2-0.7c0.2-0.9,0.1-1.5-0.4-2c-0.5-0.4-1.4-0.7-2.7-0.7c-1.4,0-2.6,0.3-3.5,0.8c-1,0.6-1.7,1.4-2.2,2.5
|
||||
h-8.9c1.4-3.3,3.5-5.8,6.2-7.5c2.7-1.6,6.2-2.4,10.5-2.4c2.6,0,4.7,0.3,6.4,1c1.6,0.6,2.8,1.6,3.4,2.9c0.4,0.9,0.6,2,0.6,3.3
|
||||
c-0.1,1.3-0.5,3.3-1.3,6.2l-3.1,11.2c-0.4,1.3-0.5,2.4-0.5,3.2c0,0.8,0.2,1.3,0.7,1.5L386.1,40z M379.4,26.1
|
||||
c-0.9,0.5-2.3,0.9-4.3,1.3c-1,0.2-1.7,0.3-2.2,0.5c-1.3,0.3-2.2,0.7-2.8,1.2c-0.6,0.5-1.1,1.2-1.3,2c-0.3,1.1-0.2,1.9,0.3,2.5
|
||||
c0.5,0.6,1.2,1,2.3,1c1.7,0,3.1-0.5,4.4-1.4c1.3-1,2.2-2.2,2.6-3.7L379.4,26.1z"/>
|
||||
<path class="st1" d="M337.3,40l8.3-29.5h9.3l-1.4,5.2c1.6-2,3.3-3.5,5.1-4.4c1.8-0.9,3.9-1.4,6.3-1.5l-2.7,9.6
|
||||
c-0.4-0.1-0.8-0.1-1.2-0.1c-0.4,0-0.8,0-1.1,0c-1.5,0-2.8,0.2-3.9,0.7c-1.1,0.4-2.1,1.1-2.9,2.1c-0.5,0.6-1,1.5-1.5,2.6
|
||||
c-0.5,1.1-1.1,3-1.8,5.6l-2.8,9.9H337.3z"/>
|
||||
<path class="st1" d="M340.8,10.5L332.5,40h-9.5l1.1-4.1c-1.6,1.6-3.3,2.9-4.9,3.6c-1.7,0.8-3.5,1.2-5.4,1.2
|
||||
c-3.3,0-5.5-0.8-6.7-2.5c-1.2-1.7-1.3-4.2-0.4-7.4l5.7-20.3h9.7L317.6,27c-0.7,2.4-0.8,4.1-0.5,5c0.4,0.9,1.3,1.4,2.8,1.4
|
||||
c1.7,0,3.1-0.6,4.1-1.7c1.1-1.1,2-2.9,2.7-5.5l4.4-15.8H340.8z"/>
|
||||
<path class="st1" d="M290.1,16.3l1.6-5.8h4l2.3-8.3h9.7l-2.3,8.3h5l-1.6,5.8h-5l-3.6,12.8c-0.5,2-0.7,3.3-0.3,3.9
|
||||
c0.3,0.6,1.2,1,2.6,1l0.7,0l0.5,0l-1.7,6.2c-1.1,0.2-2.1,0.3-3.1,0.5c-1,0.1-2,0.2-2.9,0.2c-3.4,0-5.4-0.8-6.2-2.5
|
||||
c-0.8-1.6-0.4-5.1,1.1-10.5l3.2-11.4H290.1z"/>
|
||||
<path class="st1" d="M283.5,40h-9.8c0-0.5,0.1-1,0.1-1.5L274,37c-1.7,1.4-3.5,2.4-5.2,3c-1.7,0.6-3.5,1-5.3,1
|
||||
c-2.8,0-4.9-0.8-6.1-2.3c-1.2-1.6-1.5-3.7-0.7-6.3c0.7-2.4,1.9-4.4,3.6-6c1.7-1.5,4-2.6,6.8-3.2c1.5-0.3,3.5-0.7,5.8-1.1
|
||||
c3.5-0.5,5.4-1.3,5.7-2.4l0.2-0.7c0.2-0.9,0.1-1.5-0.4-2c-0.5-0.4-1.4-0.7-2.7-0.7c-1.4,0-2.6,0.3-3.5,0.8c-1,0.6-1.7,1.4-2.2,2.5
|
||||
H261c1.4-3.3,3.5-5.8,6.2-7.5c2.7-1.6,6.2-2.4,10.5-2.4c2.6,0,4.7,0.3,6.4,1c1.6,0.6,2.8,1.6,3.4,2.9c0.4,0.9,0.6,2,0.6,3.3
|
||||
c-0.1,1.3-0.5,3.3-1.3,6.2l-3.1,11.2c-0.4,1.3-0.5,2.4-0.5,3.2c0,0.8,0.2,1.3,0.7,1.5L283.5,40z M276.7,26.1
|
||||
c-0.9,0.5-2.3,0.9-4.3,1.3c-1,0.2-1.7,0.3-2.2,0.5c-1.3,0.3-2.2,0.7-2.8,1.2c-0.6,0.5-1.1,1.2-1.3,2c-0.3,1.1-0.2,1.9,0.3,2.5
|
||||
c0.5,0.6,1.2,1,2.3,1c1.7,0,3.1-0.5,4.4-1.4c1.3-1,2.2-2.2,2.6-3.7L276.7,26.1z"/>
|
||||
<path class="st0" d="M219.6,0l-11.2,40h-9.7l1.1-3.9c-1.5,1.6-3.1,2.8-4.8,3.6c-1.6,0.8-3.4,1.2-5.3,1.2c-3.7,0-6.3-1.4-7.8-4.3
|
||||
c-1.5-2.9-1.6-6.6-0.3-11.2c1.3-4.7,3.5-8.4,6.7-11.4c3.1-2.9,6.5-4.4,10.1-4.4c1.9,0,3.6,0.4,4.8,1.2c1.3,0.8,2.2,1.9,2.8,3.5
|
||||
L210,0H219.6z M189.8,24.9c-0.7,2.6-0.8,4.7-0.2,6.1c0.6,1.4,1.8,2.1,3.7,2.1c1.8,0,3.4-0.7,4.8-2.1c1.3-1.4,2.4-3.4,3.1-6.1
|
||||
c0.7-2.5,0.7-4.4,0.1-5.8c-0.6-1.4-1.8-2-3.7-2c-1.7,0-3.3,0.7-4.7,2.1C191.5,20.6,190.4,22.5,189.8,24.9z"/>
|
||||
<path class="st0" d="M153.6,40l8.3-29.5h9.3l-1.4,5.2c1.6-2,3.3-3.5,5.1-4.4c1.8-0.9,7.9-1.4,10.3-1.5l-2.7,9.6
|
||||
c-0.4-0.1-0.8-0.1-1.2-0.1c-0.4,0-0.8,0-1.1,0c-1.5,0-6.8,0.2-7.9,0.7c-1.1,0.4-2.1,1.1-2.9,2.1c-0.5,0.6-1,1.5-1.5,2.6
|
||||
c-0.5,1.1-1.1,3-1.8,5.6l-2.8,9.9H153.6z"/>
|
||||
<path class="st0" d="M143.5,30.7h9.3c-1.8,3.2-4.2,5.7-7.2,7.5c-3,1.8-6.4,2.7-10.2,2.7c-4.6,0-7.8-1.4-9.7-4.2
|
||||
c-1.9-2.8-2.2-6.6-0.8-11.4c1.4-4.9,3.8-8.8,7.3-11.6c3.5-2.9,7.5-4.3,12-4.3c4.7,0,8,1.5,9.8,4.3c1.9,2.9,2.1,6.9,0.7,12
|
||||
l-0.3,1.1l-0.2,0.6h-20c-0.6,2.1-0.6,3.7,0,4.8c0.6,1.1,1.8,1.6,3.5,1.6c1.3,0,2.4-0.3,3.4-0.8C142.1,32.6,142.9,31.8,143.5,30.7z
|
||||
M135.4,22.1l11,0c0.5-1.9,0.4-3.4-0.3-4.4c-0.7-1.1-1.8-1.6-3.5-1.6c-1.6,0-3,0.5-4.3,1.6C137.1,18.6,136.1,20.1,135.4,22.1z"/>
|
||||
<path class="st2" d="M241.2,40.4l-8.4-24.6l-8.5,24.6h-9.6l12.6-40h10.8L244,21l0.5,1.7c0.7,2.5,1.2,4.5,1.6,6.2l0.7-2.9
|
||||
c0.3-1.1,0.7-2.6,1.2-4.3l5.9-21.2h10.8l-13.9,40H241.2z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st3" d="M106.1,54.4h4.8l48.9,113.9h-5.9L137,129H79.9l-16.8,39.3H57L106.1,54.4z M135.3,124.2l-26.8-62.7l-26.9,62.7
|
||||
H135.3z"/>
|
||||
<path class="st3" d="M178.1,168.3V54.4h5.6v108.7h69.8v5.1H178.1z"/>
|
||||
<path class="st3" d="M271.1,168.3V54.4h5.6v113.9H271.1z"/>
|
||||
<path class="st3" d="M300.2,54.4l42,53.6l42-53.6h6.4l-45.4,57.7l44.1,56.1H383l-40.7-52l-40.7,52h-6.7l44.1-56.1l-45.4-57.7
|
||||
H300.2z"/>
|
||||
<g>
|
||||
<path class="st3" d="M5.8,168.3L5.3,163l0.2,2.7L5.3,163c0.4,0,10.4-1.1,18.9-11.8c10.5-13.1,14.1-35.2,10.5-63.9
|
||||
C31,57.7,35.4,34.8,47.6,19.1C60.3,3,76.6,0.9,77.3,0.8l0.6,5.3c-0.1,0-11.9,1.6-22.4,12.1c-14,14-19.3,37.7-15.5,68.4
|
||||
c3.8,30.7-0.1,53.6-11.8,68.1C18.3,167.1,6.3,168.2,5.8,168.3z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.2 KiB |
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="40mm" height="40mm" viewBox="0 0 40 40" version="1.1" id="svg823" inkscape:version="0.92.4 (5da689c313, 2019-01-14)" sodipodi:docname="logo.svg">
|
||||
<defs id="defs817"/>
|
||||
<sodipodi:namedview id="base" pagecolor="#2f2f2f" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:zoom="5.635625" inkscape:cx="70.551181" inkscape:cy="75.590551" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="false" inkscape:window-width="1920" inkscape:window-height="1043" inkscape:window-x="1920" inkscape:window-y="0" inkscape:window-maximized="1"/>
|
||||
<metadata id="metadata820">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(0,-257)">
|
||||
<path style="fill:#88bd32;stroke-width:0.83333331" inkscape:connector-curvature="0" id="path4" d="m 27.583333,261.08333 c 0.25,-0.0833 0.5,-0.16666 0.75,-0.16666 L 39,259.5 l -0.166667,6.08333 c -0.166666,5 -3.083333,9.5 -6.666666,10.5 -0.25,0.0833 -0.5,0.16667 -0.75,0.16667 L 20.75,277.66667 l 0.166667,-6.08334 c 0.166666,-5.08333 3.083333,-9.5 6.666666,-10.5 z" class="st0"/>
|
||||
<path style="fill:#88bd32;stroke-width:0.83333331" inkscape:connector-curvature="0" id="path6" d="m 5.9166667,281.91667 c 0.1666667,-0.0833 0.4166667,-0.0833 0.5833334,-0.0833 L 14.25,280.75 14.16667,285.08333 C 14.08334,288.75 11.91667,292 9.3333364,292.75 c -0.25,0.0833 -0.4166667,0.0833 -0.5833334,0.0833 L 1.0000001,293.91667 1.0833334,289.5 c 0.1666667,-3.58333 2.25,-6.91667 4.8333333,-7.58333 z" class="st0"/>
|
||||
<g id="g10" style="fill:#f7931e;fill-opacity:1;stroke:none;stroke-opacity:1" transform="matrix(0.83333333,0,0,0.83333333,8.0000002e-8,257)">
|
||||
<path style="fill:#f7931e;fill-opacity:1;stroke:none;stroke-opacity:1" inkscape:connector-curvature="0" id="path8" d="m 12,48 c -0.4,0 -0.7,-0.3 -0.7,-0.6 0,-0.4 0.2,-0.7 0.6,-0.8 0,0 3,-0.3 5.5,-3.4 3,-3.8 4.1,-10.1 3,-18.4 C 19.3,16.3 20.6,9.7 24.1,5.3 27.8,0.6 32.5,0 32.7,0 c 0.4,0 0.7,0.2 0.8,0.6 0,0.4 -0.2,0.7 -0.6,0.8 0,0 -4.3,0.6 -7.6,4.7 -3.3,4.2 -4.4,10.4 -3.4,18.5 1.1,8.8 0,15.4 -3.4,19.5 -2.8,3.5 -6.2,3.9 -6.5,3.9 0.1,0 0.1,0 0,0 z" class="st1"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -1,15 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 356 360">
|
||||
<path
|
||||
d="M43.4 303.4c0 3.8-2.3 6.3-7.1 6.3h-15v-22h14.4c4.3 0 6.2 2.2 6.2 5.2 0 2.6-1.5 4.4-3.4 5 2.8.4 4.9 2.5 4.9 5.5zm-8-13H24.1v6.9H35c2.1 0 4-1.3 4-3.8 0-2.2-1.3-3.1-3.7-3.1zm5.1 12.6c0-2.3-1.8-3.7-4-3.7H24.2v7.7h11.7c3.4 0 4.6-1.8 4.6-4zm36.3 4v2.7H56v-22h20.6v2.7H58.9v6.8h14.6v2.3H58.9v7.5h17.9zm23-5.8v8.5H97v-8.5l-11-13.4h3.4l8.9 11 8.8-11h3.4l-10.8 13.4zm19.1-1.8V298c0-7.9 5.2-10.7 12.7-10.7 7.5 0 13 2.8 13 10.7v1.4c0 7.9-5.5 10.8-13 10.8s-12.7-3-12.7-10.8zm22.7 0V298c0-5.7-3.9-8-10-8-6 0-9.8 2.3-9.8 8v1.4c0 5.8 3.8 8.1 9.8 8.1 6 0 10-2.3 10-8.1zm37.2-11.6v21.9h-2.9l-15.8-17.9v17.9h-2.8v-22h3l15.6 18v-18h2.9zm37.9 10.2v1.3c0 7.8-5.2 10.4-12.4 10.4H193v-22h11.2c7.2 0 12.4 2.8 12.4 10.3zm-3 0c0-5.3-3.3-7.6-9.4-7.6h-8.4V307h8.4c6 0 9.5-2 9.5-7.7V298zm50.8-7.6h-9.7v19.3h-3v-19.3h-9.7v-2.6h22.4v2.6zm34.4-2.6v21.9h-3v-10.1h-16.8v10h-2.8v-21.8h2.8v9.2H296v-9.2h2.9zm34.9 19.2v2.7h-20.7v-22h20.6v2.7H316v6.8h14.5v2.3H316v7.5h17.8zM24 340.2v7.3h13.9v2.4h-14v9.6H21v-22h20v2.7H24zm41.5 11.4h-9.8v7.9H53v-22h13.3c5.1 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6H66c3.1 0 5.3-1.5 5.3-4.7 0-3.3-2.2-4.1-5.3-4.1H55.7v8.8zm47.9 6.2H89l-2 4.3h-3.2l10.7-22.2H98l10.7 22.2h-3.2l-2-4.3zm-1-2.3l-6.3-13-6 13h12.2zm46.3-15.3v21.9H146v-17.2L135.7 358h-2.1l-10.2-15.6v17h-2.8v-21.8h3l11 16.9 11.3-17h3zm35 19.3v2.6h-20.7v-22h20.6v2.7H166v6.8h14.5v2.3H166v7.6h17.8zm47-19.3l-8.3 22h-3l-7.1-18.6-7 18.6h-3l-8.2-22h3.3L204 356l6.8-18.5h3.4L221 356l6.6-18.5h3.3zm10 11.6v-1.4c0-7.8 5.2-10.7 12.7-10.7 7.6 0 13 2.9 13 10.7v1.4c0 7.9-5.4 10.8-13 10.8-7.5 0-12.7-3-12.7-10.8zm22.8 0v-1.4c0-5.7-4-8-10-8s-9.9 2.3-9.9 8v1.4c0 5.8 3.8 8.2 9.8 8.2 6.1 0 10-2.4 10-8.2zm28.3 2.4h-9.8v7.9h-2.8v-22h13.2c5.2 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6h10.2c3 0 5.2-1.5 5.2-4.7 0-3.3-2.1-4.1-5.2-4.1h-10.2v8.8zm40.3-1.5l-6.8 5.6v6.4h-2.9v-22h2.9v12.3l15.2-12.2h3.7l-9.9 8.1 10.3 13.8h-3.6l-8.9-12z" />
|
||||
<path fill="#050A14"
|
||||
d="M188.4 71.7a10.4 10.4 0 01-20.8 0 10.4 10.4 0 1120.8 0zM224.2 45c-2.2-3.9-5-7.5-8.2-10.7l-12 7c-3.7-3.2-8-5.7-12.6-7.3a49.4 49.4 0 00-9.7 13.9 59 59 0 0140.1 14l7.6-4.4a57 57 0 00-5.2-12.5zM178 125.1c4.5 0 9-.6 13.4-1.7v-14a40 40 0 0012.5-7.2 47.7 47.7 0 00-7.1-15.3 59 59 0 01-32.2 27.7v8.7c4.4 1.2 8.9 1.8 13.4 1.8zM131.8 45c-2.3 4-4 8.1-5.2 12.5l12 7a40 40 0 000 14.4c5.7 1.5 11.3 2 16.9 1.5a59 59 0 01-8-41.7l-7.5-4.3c-3.2 3.2-6 6.7-8.2 10.6z" />
|
||||
<path fill="#00B4FF"
|
||||
d="M224.2 98.4c2.3-3.9 4-8 5.2-12.4l-12-7a40 40 0 000-14.5c-5.7-1.5-11.3-2-16.9-1.5a59 59 0 018 41.7l7.5 4.4c3.2-3.2 6-6.8 8.2-10.7zm-92.4 0c2.2 4 5 7.5 8.2 10.7l12-7a40 40 0 0012.6 7.3c4-4.1 7.3-8.8 9.7-13.8a59 59 0 01-40-14l-7.7 4.4c1.2 4.3 3 8.5 5.2 12.4zm46.2-80c-4.5 0-9 .5-13.4 1.7V34a40 40 0 00-12.5 7.2c1.5 5.7 4 10.8 7.1 15.4a59 59 0 0132.2-27.7V20a53.3 53.3 0 00-13.4-1.8z" />
|
||||
<path fill="#00B4FF"
|
||||
d="M178 9.2a62.6 62.6 0 11-.1 125.2A62.6 62.6 0 01178 9.2m0-9.2a71.7 71.7 0 100 143.5A71.7 71.7 0 00178 0z" />
|
||||
<path fill="#050A14"
|
||||
d="M96.6 212v4.3c-9.2-.8-15.4-5.8-15.4-17.8V180h4.6v18.4c0 8.6 4 12.6 10.8 13.5zm16-31.9v18.4c0 8.9-4.3 12.8-10.9 13.5v4.4c9.2-.7 15.5-5.6 15.5-18v-18.3h-4.7zM62.2 199v-2.2c0-12.7-8.8-17.4-21-17.4-12.1 0-20.7 4.7-20.7 17.4v2.2c0 12.8 8.6 17.6 20.7 17.6 1.5 0 3-.1 4.4-.3l11.8 6.2 2-3.3-8.2-4-6.4-3.1a32 32 0 01-3.6.2c-9.8 0-16-3.9-16-13.3v-2.2c0-9.3 6.2-13.1 16-13.1 9.9 0 16.3 3.8 16.3 13.1v2.2c0 5.3-2.1 8.7-5.6 10.8l4.8 2.4c3.4-2.8 5.5-7 5.5-13.2zM168 215.6h5.1L156 179.7h-4.8l17 36zM143 205l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.8-3.7H143zm133.7 10.7h5.2l-17.3-35.9h-4.8l17 36zm-25-10.7l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.7-3.7h-14.8zm73.8-2.5c6-1.2 9-5.4 9-11.4 0-8-4.5-10.9-12.9-10.9h-21.4v35.5h4.6v-31.3h16.5c5 0 8.5 1.4 8.5 6.7 0 5.2-3.5 7.7-8.5 7.7h-11.4v4.1h10.7l9.3 12.8h5.5l-9.9-13.2zm-117.4 9.9c-9.7 0-14.7-2.5-18.6-6.3l-2.2 3.8c5.1 5 11 6.7 21 6.7 1.6 0 3.1-.1 4.6-.3l-1.9-4h-3zm18.4-7c0-6.4-4.7-8.6-13.8-9.4l-10.1-1c-6.7-.7-9.3-2.2-9.3-5.6 0-2.5 1.4-4 4.6-5l-1.8-3.8c-4.7 1.4-7.5 4.2-7.5 8.9 0 5.2 3.4 8.7 13 9.6l11.3 1.2c6.4.6 8.9 2 8.9 5.4 0 2.7-2.1 4.7-6 5.8l1.8 3.9c5.3-1.6 8.9-4.7 8.9-10zm-20.3-21.9c7.9 0 13.3 1.8 18.1 5.7l1.8-3.9a30 30 0 00-19.6-5.9c-2 0-4 .1-5.7.3l1.9 4 3.5-.2z" />
|
||||
<path fill="#00B4FF"
|
||||
d="M.5 251.9c29.6-.5 59.2-.8 88.8-1l88.7-.3 88.7.3 44.4.4 44.4.6-44.4.6-44.4.4-88.7.3-88.7-.3a7981 7981 0 01-88.8-1z" />
|
||||
<path fill="none" d="M-565.2 324H-252v15.8h-313.2z" />
|
||||
</svg>
|
Before Width: | Height: | Size: 4.4 KiB |
|
@ -1,24 +1,24 @@
|
|||
import { boot } from 'quasar/wrappers';
|
||||
import axios from 'axios';
|
||||
import { useSession } from 'src/composables/useSession';
|
||||
|
||||
// Be careful when using SSR for cross-request state pollution
|
||||
// due to creating a Singleton instance here;
|
||||
// If any client changes this (global) instance, it might be a
|
||||
// good idea to move this instance creation inside of the
|
||||
// "export default () => {}" function below (which runs individually
|
||||
// for each client)
|
||||
const api = axios.create({ baseURL: 'https://api.example.com' });
|
||||
export default boot(() => {
|
||||
const { getToken } = useSession();
|
||||
|
||||
export default boot(({ app }) => {
|
||||
// for use inside Vue files (Options API) through this.$axios and this.$api
|
||||
axios.defaults.baseURL = '/api/';
|
||||
|
||||
app.config.globalProperties.$axios = axios;
|
||||
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
||||
// so you won't necessarily have to import axios in each vue file
|
||||
axios.interceptors.request.use(
|
||||
function (context) {
|
||||
const token = getToken();
|
||||
|
||||
app.config.globalProperties.$api = api;
|
||||
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
||||
// so you can easily perform requests against your app's API
|
||||
if (token.length && context.headers) {
|
||||
context.headers.Authorization = token;
|
||||
}
|
||||
|
||||
return context;
|
||||
},
|
||||
function (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export { api };
|
||||
|
|
|
@ -2,13 +2,17 @@ import { boot } from 'quasar/wrappers';
|
|||
import { createI18n } from 'vue-i18n';
|
||||
import messages from 'src/i18n';
|
||||
|
||||
export default boot(({ app }) => {
|
||||
const i18n = createI18n({
|
||||
locale: 'en-US',
|
||||
globalInjection: true,
|
||||
messages,
|
||||
});
|
||||
const i18n = createI18n({
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
globalInjection: true,
|
||||
messages,
|
||||
missingWarn: false,
|
||||
});
|
||||
|
||||
export default boot(({ app }) => {
|
||||
// Set i18n instance on app
|
||||
app.use(i18n);
|
||||
});
|
||||
|
||||
export { i18n };
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
<template>
|
||||
<q-item clickable tag="a" target="_blank" :href="link">
|
||||
<q-item-section v-if="icon" avatar>
|
||||
<q-icon :name="icon" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>{{ title }}</q-item-label>
|
||||
<q-item-label caption>{{ caption }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EssentialLink',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
caption: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
link: {
|
||||
type: String,
|
||||
default: '#',
|
||||
},
|
||||
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,60 @@
|
|||
<script setup>
|
||||
import { h, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
|
||||
const $props = defineProps({
|
||||
autoLoad: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
filter: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
where: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
sortBy: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
limit: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['onFetch']);
|
||||
defineExpose({ fetch });
|
||||
|
||||
onMounted(async () => {
|
||||
if ($props.autoLoad) {
|
||||
await fetch();
|
||||
}
|
||||
});
|
||||
|
||||
async function fetch() {
|
||||
const filter = Object.assign({}, $props.filter);
|
||||
if ($props.where) filter.where = $props.where;
|
||||
if ($props.sortBy) filter.order = $props.sortBy;
|
||||
if ($props.limit) filter.limit = $props.limit;
|
||||
|
||||
const { data } = await axios.get($props.url, {
|
||||
params: { filter },
|
||||
});
|
||||
|
||||
emit('onFetch', data);
|
||||
}
|
||||
|
||||
const render = () => {
|
||||
return h('div', []);
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<render />
|
||||
</template>
|
|
@ -0,0 +1,118 @@
|
|||
<script setup>
|
||||
import { onMounted, onUnmounted, computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useQuasar } from 'quasar';
|
||||
import axios from 'axios';
|
||||
|
||||
import { useState } from 'src/composables/useState';
|
||||
import { useValidator } from 'src/composables/useValidator';
|
||||
import SkeletonForm from 'components/ui/SkeletonForm.vue';
|
||||
|
||||
const quasar = useQuasar();
|
||||
const { t } = useI18n();
|
||||
const state = useState();
|
||||
const { validate } = useValidator();
|
||||
|
||||
const $props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
filter: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['onFetch']);
|
||||
|
||||
defineExpose({
|
||||
save,
|
||||
});
|
||||
|
||||
onMounted(async () => await fetch());
|
||||
|
||||
onUnmounted(() => {
|
||||
state.unset($props.model);
|
||||
});
|
||||
|
||||
const isLoading = ref(false);
|
||||
const hasChanges = ref(false);
|
||||
const formData = computed(() => state.get($props.model));
|
||||
const originalData = ref();
|
||||
|
||||
async function fetch() {
|
||||
const { data } = await axios.get($props.url, {
|
||||
params: { filter: $props.filter },
|
||||
});
|
||||
|
||||
state.set($props.model, data);
|
||||
originalData.value = Object.assign({}, data);
|
||||
|
||||
watch(formData.value, () => (hasChanges.value = true));
|
||||
|
||||
emit('onFetch', state.get($props.model));
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!hasChanges.value) {
|
||||
return quasar.notify({
|
||||
type: 'negative',
|
||||
message: t('globals.noChanges'),
|
||||
});
|
||||
}
|
||||
isLoading.value = true;
|
||||
await axios.patch($props.url, formData.value);
|
||||
|
||||
originalData.value = formData.value;
|
||||
hasChanges.value = false;
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
state.set($props.model, originalData.value);
|
||||
hasChanges.value = false;
|
||||
}
|
||||
|
||||
function filter(value, update, filterOptions) {
|
||||
update(
|
||||
() => {
|
||||
const { options, filterFn } = filterOptions;
|
||||
|
||||
options.value = filterFn(options, value);
|
||||
},
|
||||
(ref) => {
|
||||
ref.setOptionIndex(-1);
|
||||
ref.moveOptionSelection(1, true);
|
||||
}
|
||||
);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<q-banner v-if="hasChanges" class="text-white bg-warning">
|
||||
<q-icon name="warning" size="md" class="q-mr-md" />
|
||||
<span>{{ t('globals.changesToSave') }}</span>
|
||||
</q-banner>
|
||||
<q-form v-if="formData" @submit="save" @reset="reset" class="q-pa-md">
|
||||
<slot name="form" :data="formData" :validate="validate" :filter="filter"></slot>
|
||||
<div class="q-mt-lg">
|
||||
<slot name="actions">
|
||||
<q-btn :label="t('globals.save')" type="submit" color="primary" />
|
||||
<q-btn
|
||||
:label="t('globals.reset')"
|
||||
type="reset"
|
||||
class="q-ml-sm"
|
||||
color="primary"
|
||||
flat
|
||||
:disable="!hasChanges"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</q-form>
|
||||
<skeleton-form v-if="!formData" />
|
||||
<q-inner-loading :showing="isLoading" :label="t('globals.pleaseWait')" color="primary" />
|
||||
</template>
|
|
@ -0,0 +1,200 @@
|
|||
<script setup>
|
||||
import axios from 'axios';
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useNavigationStore } from 'src/stores/useNavigationStore';
|
||||
import { toLowerCamel } from 'src/filters';
|
||||
import routes from 'src/router/modules';
|
||||
import LeftMenuItem from './LeftMenuItem.vue';
|
||||
import LeftMenuItemGroup from './LeftMenuItemGroup.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const quasar = useQuasar();
|
||||
const navigation = useNavigationStore();
|
||||
|
||||
const props = defineProps({
|
||||
source: {
|
||||
type: String,
|
||||
default: 'main',
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await navigation.fetchPinned();
|
||||
getRoutes();
|
||||
});
|
||||
|
||||
function findMatches(search, item) {
|
||||
const matches = [];
|
||||
function findRoute(search, item) {
|
||||
for (const child of item.children) {
|
||||
if (search.indexOf(child.name) > -1) {
|
||||
matches.push(child);
|
||||
} else if (child.children) {
|
||||
findRoute(search, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findRoute(search, item);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function addChildren(module, route, parent) {
|
||||
if (route.menus) {
|
||||
const mainMenus = route.menus[props.source];
|
||||
const matches = findMatches(mainMenus, route);
|
||||
|
||||
for (const child of matches) {
|
||||
navigation.addMenuItem(module, child, parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pinnedItems = computed(() => {
|
||||
return items.value.filter((item) => item.isPinned);
|
||||
});
|
||||
|
||||
const items = ref([]);
|
||||
function getRoutes() {
|
||||
if (props.source === 'main') {
|
||||
const modules = Object.assign([], navigation.getModules().value);
|
||||
|
||||
for (const item of modules) {
|
||||
const moduleDef = routes.find((route) => toLowerCamel(route.name) === item.module);
|
||||
item.children = [];
|
||||
|
||||
if (!moduleDef) continue;
|
||||
|
||||
addChildren(item.module, moduleDef, item.children);
|
||||
}
|
||||
|
||||
items.value = modules;
|
||||
}
|
||||
|
||||
if (props.source === 'card') {
|
||||
const currentRoute = route.matched[1];
|
||||
const currentModule = toLowerCamel(currentRoute.name);
|
||||
const moduleDef = routes.find((route) => toLowerCamel(route.name) === currentModule);
|
||||
|
||||
if (!moduleDef) return;
|
||||
|
||||
addChildren(currentModule, moduleDef, items.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function togglePinned(item, event) {
|
||||
if (event.defaultPrevented) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const data = { moduleName: item.module };
|
||||
const response = await axios.post('StarredModules/toggleStarredModule', data);
|
||||
|
||||
item.isPinned = false;
|
||||
|
||||
if (response.data && response.data.id) {
|
||||
item.isPinned = true;
|
||||
}
|
||||
|
||||
navigation.togglePinned(item.module);
|
||||
|
||||
quasar.notify({
|
||||
message: t('globals.dataSaved'),
|
||||
type: 'positive',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-list padding>
|
||||
<template v-if="$props.source === 'main'">
|
||||
<q-item-label header>
|
||||
{{ t('globals.pinnedModules') }}
|
||||
</q-item-label>
|
||||
<template v-for="item in pinnedItems" :key="item.name">
|
||||
<template v-if="item.children">
|
||||
<left-menu-item-group :item="item" group="pinnedModules" class="pinned">
|
||||
<template #side>
|
||||
<q-btn
|
||||
v-if="item.isPinned === true"
|
||||
@click="togglePinned(item, $event)"
|
||||
icon="vn:pin_off"
|
||||
size="xs"
|
||||
flat
|
||||
round
|
||||
>
|
||||
<q-tooltip>{{ t('components.leftMenu.removeFromPinned') }}</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-if="item.isPinned === false"
|
||||
@click="togglePinned(item, $event)"
|
||||
icon="vn:pin"
|
||||
size="xs"
|
||||
flat
|
||||
round
|
||||
>
|
||||
<q-tooltip>{{ t('components.leftMenu.addToPinned') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</left-menu-item-group>
|
||||
</template>
|
||||
|
||||
<left-menu-item v-if="!item.children" :item="item" />
|
||||
</template>
|
||||
<q-separator />
|
||||
<q-expansion-item :label="t('moduleIndex.allModules')">
|
||||
<template v-for="item in items" :key="item.name">
|
||||
<template v-if="item.children">
|
||||
<left-menu-item-group :item="item" group="modules">
|
||||
<template #side>
|
||||
<q-btn
|
||||
v-if="item.isPinned === true"
|
||||
@click="togglePinned(item, $event)"
|
||||
icon="vn:pin_off"
|
||||
size="xs"
|
||||
flat
|
||||
round
|
||||
>
|
||||
<q-tooltip>{{ t('components.leftMenu.removeFromPinned') }}</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-if="item.isPinned === false"
|
||||
@click="togglePinned(item, $event)"
|
||||
icon="vn:pin"
|
||||
size="xs"
|
||||
flat
|
||||
round
|
||||
>
|
||||
<q-tooltip>{{ t('components.leftMenu.addToPinned') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</left-menu-item-group>
|
||||
</template>
|
||||
</template>
|
||||
</q-expansion-item>
|
||||
<q-separator />
|
||||
</template>
|
||||
<template v-if="$props.source === 'card'">
|
||||
<template v-for="item in items" :key="item.name">
|
||||
<left-menu-item v-if="!item.children" :item="item" />
|
||||
</template>
|
||||
</template>
|
||||
</q-list>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.pinned .icon-pin,
|
||||
.pinned .icon-pin_off {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.pinned:hover .icon-pin,
|
||||
.pinned:hover .icon-pin_off {
|
||||
visibility: visible;
|
||||
}
|
||||
</style>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,68 @@
|
|||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useState } from 'src/composables/useState';
|
||||
import { useSession } from 'src/composables/useSession';
|
||||
import UserPanel from 'components/UserPanel.vue';
|
||||
import PinnedModules from './PinnedModules.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const session = useSession();
|
||||
const state = useState();
|
||||
const user = state.getUser();
|
||||
const token = session.getToken();
|
||||
|
||||
onMounted(() => (state.headerMounted.value = true));
|
||||
|
||||
function onToggleDrawer() {
|
||||
state.drawer.value = !state.drawer.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-header class="bg-dark" color="white" elevated>
|
||||
<q-toolbar class="q-py-sm q-px-md">
|
||||
<q-btn flat @click="onToggleDrawer()" round dense icon="menu">
|
||||
<q-tooltip bottom anchor="bottom right">
|
||||
{{ t('globals.collapseMenu') }}
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<router-link to="/">
|
||||
<q-btn flat round class="q-ml-xs" v-if="$q.screen.gt.xs">
|
||||
<q-avatar square size="md">
|
||||
<q-img src="~/assets/logo_icon.svg" alt="Logo" />
|
||||
</q-avatar>
|
||||
<q-tooltip bottom>
|
||||
{{ t('globals.backToDashboard') }}
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</router-link>
|
||||
<q-toolbar-title shrink class="text-weight-bold">Salix</q-toolbar-title>
|
||||
<q-space></q-space>
|
||||
<div id="searchbar"></div>
|
||||
<q-space></q-space>
|
||||
<div class="q-pl-sm q-gutter-sm row items-center no-wrap">
|
||||
<div id="header-actions"></div>
|
||||
<q-btn id="pinnedModules" icon="apps" flat dense rounded>
|
||||
<q-tooltip bottom>
|
||||
{{ t('globals.pinnedModules') }}
|
||||
</q-tooltip>
|
||||
<PinnedModules />
|
||||
</q-btn>
|
||||
<q-btn rounded dense flat no-wrap id="user">
|
||||
<q-avatar size="lg">
|
||||
<q-img
|
||||
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
|
||||
spinner-color="white"
|
||||
>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
<q-tooltip bottom>
|
||||
{{ t('globals.userPanel') }}
|
||||
</q-tooltip>
|
||||
<UserPanel />
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
</template>
|
|
@ -0,0 +1,205 @@
|
|||
<script setup>
|
||||
import axios from 'axios';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const $props = defineProps({
|
||||
autoLoad: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
filter: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
where: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
sortBy: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
limit: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
rowsPerPage: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
offset: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['onFetch', 'onPaginate']);
|
||||
defineExpose({ refresh });
|
||||
|
||||
onMounted(() => {
|
||||
if ($props.autoLoad) paginate();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => $props.data,
|
||||
() => {
|
||||
rows.value = $props.data;
|
||||
}
|
||||
);
|
||||
|
||||
const isLoading = ref(false);
|
||||
const hasMoreData = ref(false);
|
||||
const pagination = ref({
|
||||
sortBy: $props.sortBy,
|
||||
rowsPerPage: $props.rowsPerPage,
|
||||
page: 1,
|
||||
});
|
||||
const rows = ref(null);
|
||||
|
||||
async function fetch() {
|
||||
const { page, rowsPerPage, sortBy } = pagination.value;
|
||||
|
||||
if (!$props.url) return;
|
||||
|
||||
const filter = {
|
||||
limit: rowsPerPage,
|
||||
skip: rowsPerPage * (page - 1),
|
||||
};
|
||||
|
||||
Object.assign(filter, $props.filter);
|
||||
|
||||
if ($props.where) filter.where = $props.where;
|
||||
if ($props.sortBy) filter.order = $props.sortBy;
|
||||
if ($props.limit) filter.limit = $props.limit;
|
||||
|
||||
if (sortBy) filter.order = sortBy;
|
||||
|
||||
const { data } = await axios.get($props.url, {
|
||||
params: { filter },
|
||||
});
|
||||
|
||||
isLoading.value = false;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function paginate() {
|
||||
const { page, rowsPerPage, sortBy, descending } = pagination.value;
|
||||
|
||||
const data = await fetch();
|
||||
|
||||
if (!data) {
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
hasMoreData.value = data.length === rowsPerPage;
|
||||
|
||||
if (!rows.value) rows.value = [];
|
||||
for (const row of data) rows.value.push(row);
|
||||
|
||||
pagination.value.rowsNumber = rows.value.length;
|
||||
pagination.value.page = page;
|
||||
pagination.value.rowsPerPage = rowsPerPage;
|
||||
pagination.value.sortBy = sortBy;
|
||||
pagination.value.descending = descending;
|
||||
|
||||
isLoading.value = false;
|
||||
|
||||
emit('onFetch', rows);
|
||||
emit('onPaginate', data);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const { rowsPerPage } = pagination.value;
|
||||
|
||||
const data = await fetch();
|
||||
|
||||
if (!data) {
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
hasMoreData.value = data.length === rowsPerPage;
|
||||
|
||||
if (!rows.value) rows.value = [];
|
||||
rows.value = data;
|
||||
|
||||
isLoading.value = false;
|
||||
|
||||
emit('onFetch', rows);
|
||||
}
|
||||
|
||||
async function onLoad(...params) {
|
||||
const done = params[1];
|
||||
if (!rows.value || rows.value.length === 0 || !$props.url) return done(false);
|
||||
|
||||
pagination.value.page = pagination.value.page + 1;
|
||||
|
||||
await paginate();
|
||||
|
||||
const endOfPages = !hasMoreData.value;
|
||||
done(endOfPages);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-infinite-scroll @load="onLoad" :offset="offset" class="column items-center">
|
||||
<div v-if="rows" class="card-list q-gutter-y-md">
|
||||
<slot name="body" :rows="rows"></slot>
|
||||
<div v-if="!rows.length && !isLoading" class="info-row q-pa-md text-center">
|
||||
<h5>
|
||||
{{ t('components.smartCard.noData') }}
|
||||
</h5>
|
||||
</div>
|
||||
<div v-if="isLoading" class="info-row q-pa-md text-center">
|
||||
<q-spinner color="orange" size="md" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!rows" class="card-list q-gutter-y-md">
|
||||
<q-card class="card" v-for="$index in $props.rowsPerPage" :key="$index">
|
||||
<q-item v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
|
||||
<q-item-section class="q-pa-md">
|
||||
<q-skeleton type="rect" class="q-mb-md" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" class="q-mb-md" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
</q-item-section>
|
||||
<q-separator vertical />
|
||||
<q-card-actions vertical class="justify-between">
|
||||
<q-skeleton type="circle" class="q-mb-md" size="40px" />
|
||||
<q-skeleton type="circle" class="q-mb-md" size="40px" />
|
||||
<q-skeleton type="circle" class="q-mb-md" size="40px" />
|
||||
</q-card-actions>
|
||||
</q-item>
|
||||
</q-card>
|
||||
</div>
|
||||
</q-infinite-scroll>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-list {
|
||||
width: 100%;
|
||||
max-width: 60em;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
width: 100%;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,57 @@
|
|||
<script setup>
|
||||
import { onMounted, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useNavigationStore } from 'src/stores/useNavigationStore';
|
||||
|
||||
const navigation = useNavigationStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
onMounted(() => {
|
||||
navigation.fetchPinned();
|
||||
});
|
||||
|
||||
const pinnedModules = computed(() => navigation.getPinnedModules());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-menu
|
||||
anchor="bottom left"
|
||||
class="row q-pa-md q-col-gutter-lg"
|
||||
max-width="350px"
|
||||
max-height="400px"
|
||||
v-if="pinnedModules.length"
|
||||
>
|
||||
<div v-for="item of pinnedModules" :key="item.title" class="row no-wrap q-pa-xs flex-item">
|
||||
<q-btn
|
||||
align="evenly"
|
||||
padding="16px"
|
||||
flat
|
||||
stack
|
||||
size="lg"
|
||||
:icon="item.icon"
|
||||
color="primary"
|
||||
class="col-4 button"
|
||||
:to="{ name: item.name }"
|
||||
>
|
||||
<div class="text-center text-primary button-text">
|
||||
{{ t(item.title) }}
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-menu>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.flex-item {
|
||||
width: 100px;
|
||||
}
|
||||
.button {
|
||||
width: 100%;
|
||||
line-height: normal;
|
||||
align-items: center;
|
||||
}
|
||||
.button-text {
|
||||
font-size: 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,137 @@
|
|||
<script setup>
|
||||
import { onMounted, computed } from 'vue';
|
||||
import { Dark, Quasar } from 'quasar';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
|
||||
import { useState } from 'src/composables/useState';
|
||||
import { useSession } from 'src/composables/useSession';
|
||||
|
||||
const state = useState();
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const userLocale = computed({
|
||||
get() {
|
||||
return locale.value;
|
||||
},
|
||||
set(value) {
|
||||
locale.value = value;
|
||||
|
||||
if (value === 'en') value = 'en-GB';
|
||||
|
||||
import(`quasar/lang/${value}`).then((language) => {
|
||||
Quasar.lang.set(language.default);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const darkMode = computed({
|
||||
get() {
|
||||
return Dark.isActive;
|
||||
},
|
||||
set(value) {
|
||||
Dark.set(value);
|
||||
},
|
||||
});
|
||||
|
||||
const user = state.getUser();
|
||||
const token = session.getToken();
|
||||
|
||||
onMounted(async () => {
|
||||
updatePreferences();
|
||||
});
|
||||
|
||||
function updatePreferences() {
|
||||
if (user.value.darkMode !== null) {
|
||||
darkMode.value = user.value.darkMode;
|
||||
}
|
||||
if (user.value.lang) {
|
||||
locale.value = user.value.lang;
|
||||
userLocale.value = user.value.lang;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDarkMode(value) {
|
||||
const query = `/UserConfigs/${user.value.id}`;
|
||||
await axios.patch(query, {
|
||||
darkMode: value,
|
||||
});
|
||||
user.value.darkMode = value;
|
||||
}
|
||||
|
||||
async function saveLanguage(value) {
|
||||
const query = `/Accounts/${user.value.id}`;
|
||||
await axios.patch(query, {
|
||||
lang: value,
|
||||
});
|
||||
user.value.lang = value;
|
||||
}
|
||||
|
||||
function logout() {
|
||||
session.destroy();
|
||||
router.push('/login');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-menu anchor="bottom left">
|
||||
<div class="row no-wrap q-pa-md">
|
||||
<div class="column panel">
|
||||
<div class="text-h6 q-mb-md">{{ t('components.userPanel.settings') }}</div>
|
||||
<q-toggle
|
||||
v-model="userLocale"
|
||||
@update:model-value="saveLanguage"
|
||||
:label="t(`globals.lang['${userLocale}']`)"
|
||||
icon="public"
|
||||
color="orange"
|
||||
false-value="es"
|
||||
true-value="en"
|
||||
/>
|
||||
<q-toggle
|
||||
v-model="darkMode"
|
||||
@update:model-value="saveDarkMode"
|
||||
:label="t(`globals.darkMode`)"
|
||||
checked-icon="dark_mode"
|
||||
color="orange"
|
||||
unchecked-icon="light_mode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-separator vertical inset class="q-mx-lg" />
|
||||
|
||||
<div class="column items-center panel">
|
||||
<q-avatar size="80px">
|
||||
<q-img
|
||||
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
|
||||
spinner-color="white"
|
||||
/>
|
||||
</q-avatar>
|
||||
|
||||
<div class="text-subtitle1 q-mt-md">
|
||||
<strong>{{ user.nickname }}</strong>
|
||||
</div>
|
||||
<div class="text-subtitle3 text-grey-7 q-mb-xs">@{{ user.name }}</div>
|
||||
|
||||
<q-btn
|
||||
id="logout"
|
||||
color="orange"
|
||||
flat
|
||||
:label="t('globals.logOut')"
|
||||
size="sm"
|
||||
icon="logout"
|
||||
@click="logout()"
|
||||
v-close-popup
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-menu>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel {
|
||||
width: 150px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,104 @@
|
|||
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
|
||||
import { createWrapper } from 'app/tests/jest/jestHelpers';
|
||||
import Leftmenu from '../LeftMenu.vue';
|
||||
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useNavigationStore } from 'src/stores/useNavigationStore';
|
||||
|
||||
const mockPush = jest.fn();
|
||||
|
||||
jest.mock('vue-router', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
currentRoute: { value: 'myCurrentRoute' },
|
||||
}),
|
||||
useRoute: () => ({
|
||||
matched: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('src/router/modules', () => [
|
||||
{
|
||||
path: '/customer',
|
||||
name: 'Customer',
|
||||
meta: {
|
||||
title: 'customers',
|
||||
icon: 'vn:client',
|
||||
},
|
||||
menus: {
|
||||
main: ['CustomerList', 'CustomerCreate'],
|
||||
card: ['CustomerBasicData'],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'CustomerMain',
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'CustomerList',
|
||||
meta: {
|
||||
title: 'list',
|
||||
icon: 'view_list',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
name: 'CustomerCreate',
|
||||
meta: {
|
||||
title: 'createCustomer',
|
||||
icon: 'vn:addperson',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
describe('Leftmenu', () => {
|
||||
let vm;
|
||||
let navigation;
|
||||
beforeAll(async () => {
|
||||
vm = createWrapper(Leftmenu, {
|
||||
propsData: {
|
||||
source: 'main',
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({ stubActions: false })],
|
||||
},
|
||||
}).vm;
|
||||
|
||||
navigation = useNavigationStore();
|
||||
navigation.modules = ['customer']; // I should mock to have just one module but isn´t working
|
||||
navigation.fetchPinned = jest.fn().mockReturnValue(Promise.resolve(true));
|
||||
navigation.getModules = jest.fn().mockReturnValue({
|
||||
value: [
|
||||
{
|
||||
name: 'customer',
|
||||
title: 'customer.pageTitles.customers',
|
||||
icon: 'vn:customer',
|
||||
module: 'customer',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a proper formated object with two child items', async () => {
|
||||
const expectedMenuItem = [
|
||||
{
|
||||
name: 'CustomerList',
|
||||
title: 'customer.pageTitles.list',
|
||||
icon: 'view_list',
|
||||
},
|
||||
{
|
||||
name: 'CustomerCreate',
|
||||
title: 'customer.pageTitles.createCustomer',
|
||||
icon: 'vn:addperson',
|
||||
},
|
||||
];
|
||||
|
||||
const firstMenuItem = vm.items[0];
|
||||
expect(firstMenuItem.children).toEqual(expect.arrayContaining(expectedMenuItem));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,151 @@
|
|||
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
|
||||
import { createWrapper, axios } from 'app/tests/jest/jestHelpers';
|
||||
import Paginate from '../PaginateData.vue';
|
||||
|
||||
const mockPush = jest.fn();
|
||||
|
||||
jest.mock('vue-router', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
currentRoute: { value: 'myCurrentRoute' }
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Paginate', () => {
|
||||
const expectedUrl = '/api/customers';
|
||||
let vm;
|
||||
beforeAll(() => {
|
||||
const options = {
|
||||
attrs: {
|
||||
url: expectedUrl,
|
||||
sortBy: 'id DESC',
|
||||
rowsPerPage: 3
|
||||
}
|
||||
};
|
||||
vm = createWrapper(Paginate, options).vm;
|
||||
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: [
|
||||
{ id: 1, name: 'Tony Stark' },
|
||||
{ id: 2, name: 'Jessica Jones' },
|
||||
{ id: 3, name: 'Bruce Wayne' },
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.rows = [];
|
||||
vm.pagination.page = 1;
|
||||
vm.hasMoreData = true;
|
||||
})
|
||||
|
||||
describe('paginate()', () => {
|
||||
it('should call to the paginate() method and set the data on the rows property', async () => {
|
||||
const expectedOptions = {
|
||||
params: {
|
||||
filter: {
|
||||
order: 'id DESC',
|
||||
limit: 3,
|
||||
skip: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await vm.paginate();
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(vm.rows.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should call to the paginate() method and then call it again to paginate', async () => {
|
||||
const expectedOptions = {
|
||||
params: {
|
||||
filter: {
|
||||
order: 'id DESC',
|
||||
limit: 3,
|
||||
skip: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await vm.paginate();
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(vm.rows.length).toEqual(3);
|
||||
|
||||
const expectedOptionsPaginated = {
|
||||
params: {
|
||||
filter: {
|
||||
order: 'id DESC',
|
||||
limit: 3,
|
||||
skip: 3
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
vm.pagination.page = 2;
|
||||
|
||||
await vm.paginate();
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptionsPaginated);
|
||||
expect(vm.rows.length).toEqual(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onLoad()', () => {
|
||||
it('should call to the done() callback and not increment the pagination', async () => {
|
||||
const index = 1;
|
||||
const done = jest.fn();
|
||||
|
||||
await vm.onLoad(index, done);
|
||||
|
||||
expect(vm.pagination.page).toEqual(1);
|
||||
expect(done).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should increment the pagination and then call to the done() callback', async () => {
|
||||
vm.rows = [
|
||||
{ id: 1, name: 'Tony Stark' },
|
||||
{ id: 2, name: 'Jessica Jones' },
|
||||
{ id: 3, name: 'Bruce Wayne' },
|
||||
];
|
||||
|
||||
expect(vm.pagination.page).toEqual(1);
|
||||
|
||||
const index = 1;
|
||||
const done = jest.fn();
|
||||
|
||||
await vm.onLoad(index, done);
|
||||
|
||||
expect(vm.pagination.page).toEqual(2);
|
||||
expect(done).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should call to the done() callback with true as argument to finish pagination', async () => {
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: [
|
||||
{ id: 1, name: 'Tony Stark' },
|
||||
{ id: 2, name: 'Jessica Jones' }
|
||||
]
|
||||
});
|
||||
|
||||
vm.rows = [
|
||||
{ id: 1, name: 'Tony Stark' },
|
||||
{ id: 2, name: 'Jessica Jones' },
|
||||
{ id: 3, name: 'Bruce Wayne' },
|
||||
];
|
||||
|
||||
expect(vm.pagination.page).toEqual(1);
|
||||
|
||||
const index = 1;
|
||||
const done = jest.fn();
|
||||
|
||||
vm.hasMoreData = false;
|
||||
|
||||
await vm.onLoad(index, done);
|
||||
|
||||
expect(vm.pagination.page).toEqual(2);
|
||||
expect(done).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
|
@ -0,0 +1,102 @@
|
|||
<script setup>
|
||||
import { useSlots } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineProps({
|
||||
module: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const slots = useSlots();
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="descriptor">
|
||||
<div class="header bg-primary q-pa-sm">
|
||||
<router-link :to="{ name: `${module}List` }">
|
||||
<q-btn round flat dense size="md" icon="view_list" color="white">
|
||||
<q-tooltip>{{ t('components.cardDescriptor.mainList') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</router-link>
|
||||
<router-link :to="{ name: `${module}Summary`, params: { id: data.id } }">
|
||||
<q-btn round flat dense size="md" icon="launch" color="white">
|
||||
<q-tooltip>{{ t('components.cardDescriptor.summary') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</router-link>
|
||||
|
||||
<q-btn v-if="slots.menu" size="md" icon="more_vert" color="white" round flat dense>
|
||||
<q-tooltip>{{ t('components.cardDescriptor.moreOptions') }}</q-tooltip>
|
||||
<q-menu>
|
||||
<q-list>
|
||||
<slot name="menu" />
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="$props.data" class="body q-py-sm">
|
||||
<q-list>
|
||||
<q-item-label header class="ellipsis text-h5" :lines="1">
|
||||
{{ $props.description }}
|
||||
<q-tooltip>{{ $props.description }}</q-tooltip>
|
||||
</q-item-label>
|
||||
<q-item dense>
|
||||
<q-item-label class="text-subtitle2" caption>#{{ data.id }}</q-item-label>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<slot name="body" />
|
||||
</div>
|
||||
|
||||
<!-- Skeleton -->
|
||||
<div id="descriptor-skeleton" v-if="!$props.data">
|
||||
<div class="col q-pl-sm q-pa-sm">
|
||||
<q-skeleton type="text" square height="45px" />
|
||||
<q-skeleton type="text" square height="18px" />
|
||||
<q-skeleton type="text" square height="18px" />
|
||||
<q-skeleton type="text" square height="18px" />
|
||||
</div>
|
||||
|
||||
<q-card-actions>
|
||||
<q-skeleton size="40px" />
|
||||
<q-skeleton size="40px" />
|
||||
<q-skeleton size="40px" />
|
||||
<q-skeleton size="40px" />
|
||||
<q-skeleton size="40px" />
|
||||
</q-card-actions>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.body {
|
||||
.q-card__actions {
|
||||
justify-content: center;
|
||||
}
|
||||
.text-h5 {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.descriptor {
|
||||
width: 256px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<div class="q-pa-md">
|
||||
<div class="row q-gutter-md q-mb-md">
|
||||
<div class="col">
|
||||
<q-skeleton type="QInput" square />
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-skeleton type="QInput" square />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-gutter-md q-mb-md">
|
||||
<div class="col">
|
||||
<q-skeleton type="QInput" square />
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-skeleton type="QInput" square />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-gutter-md q-mb-md">
|
||||
<div class="col">
|
||||
<q-skeleton type="QInput" square />
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-skeleton type="QInput" square />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-gutter-md">
|
||||
<q-skeleton type="QBtn" />
|
||||
<q-skeleton type="QBtn" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="header bg-primary q-pa-sm q-mb-md">
|
||||
<q-skeleton type="rect" square />
|
||||
</div>
|
||||
<div class="row q-pa-md q-col-gutter-md q-mb-md">
|
||||
<div class="col">
|
||||
<q-skeleton type="rect" class="q-mb-md" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-skeleton type="rect" class="q-mb-md" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-skeleton type="rect" class="q-mb-md" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-skeleton type="rect" class="q-mb-md" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-skeleton type="rect" class="q-mb-md" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
<q-skeleton type="text" square />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.row {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.col {
|
||||
min-width: 250px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -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>
|
|
@ -0,0 +1,76 @@
|
|||
import { describe, expect, it, jest } from '@jest/globals';
|
||||
import { axios, flushPromises } from 'app/tests/jest/jestHelpers';
|
||||
import { useRole } from '../useRole';
|
||||
const role = useRole();
|
||||
|
||||
describe('useRole', () => {
|
||||
|
||||
describe('fetch', () => {
|
||||
it('should call setUser and setRoles of the state with the expected data', async () => {
|
||||
const rolesData = [
|
||||
{
|
||||
role: {
|
||||
name: 'salesPerson'
|
||||
}
|
||||
},
|
||||
{
|
||||
role: {
|
||||
name: 'admin'
|
||||
}
|
||||
}
|
||||
];
|
||||
const fetchedUser = {
|
||||
id: 999,
|
||||
name: `T'Challa`,
|
||||
nickname: 'Black Panther',
|
||||
lang: 'en',
|
||||
userConfig: {
|
||||
darkMode: false,
|
||||
}
|
||||
}
|
||||
const expectedUser = {
|
||||
id: 999,
|
||||
name: `T'Challa`,
|
||||
nickname: 'Black Panther',
|
||||
lang: 'en',
|
||||
darkMode: false,
|
||||
}
|
||||
const expectedRoles = ['salesPerson', 'admin']
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: { roles: rolesData, user: fetchedUser }
|
||||
});
|
||||
|
||||
jest.spyOn(role.state, 'setUser');
|
||||
jest.spyOn(role.state, 'setRoles');
|
||||
|
||||
role.fetch();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(role.state.setUser).toHaveBeenCalledWith(expectedUser);
|
||||
expect(role.state.setRoles).toHaveBeenCalledWith(expectedRoles);
|
||||
|
||||
role.state.setRoles([])
|
||||
});
|
||||
});
|
||||
describe('hasAny', () => {
|
||||
it('should return true if a role matched', async () => {
|
||||
role.state.setRoles(['admin'])
|
||||
const hasRole = role.hasAny(['admin']);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(hasRole).toBe(true);
|
||||
|
||||
role.state.setRoles([])
|
||||
});
|
||||
|
||||
it('should return false if no roles matched', async () => {
|
||||
const hasRole = role.hasAny(['admin']);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(hasRole).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,133 @@
|
|||
import { describe, expect, it, jest } from '@jest/globals';
|
||||
import { useSession } from '../useSession';
|
||||
import { useState } from '../useState';
|
||||
import { axios } from 'app/tests/jest/jestHelpers';
|
||||
|
||||
const session = useSession();
|
||||
const state = useState();
|
||||
|
||||
describe('session', () => {
|
||||
describe('getToken / setToken', () => {
|
||||
it('should return an empty string if no token is found in local or session storage', async () => {
|
||||
const expectedToken = ''
|
||||
|
||||
const token = session.getToken();
|
||||
|
||||
expect(token).toEqual(expectedToken);
|
||||
});
|
||||
|
||||
it('should return the token stored in local or session storage', async () => {
|
||||
const expectedToken = 'myToken'
|
||||
const data = {
|
||||
token: expectedToken,
|
||||
keepLogin: false
|
||||
}
|
||||
session.setToken(data);
|
||||
|
||||
const token = session.getToken();
|
||||
|
||||
expect(token).toEqual(expectedToken);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
it('should remove the token from the local storage and set a blank user', async () => {
|
||||
const previousUser = {
|
||||
id: 999,
|
||||
name: `T'Challa`,
|
||||
nickname: 'Black Panther',
|
||||
lang: 'en',
|
||||
darkMode: false,
|
||||
}
|
||||
const expectedUser = {
|
||||
id: 0,
|
||||
name: '',
|
||||
nickname: '',
|
||||
lang: '',
|
||||
darkMode: null,
|
||||
}
|
||||
let user = state.getUser();
|
||||
|
||||
localStorage.setItem('token', 'tokenToBeGone');
|
||||
state.setUser(previousUser)
|
||||
|
||||
expect(localStorage.getItem('token')).toEqual('tokenToBeGone');
|
||||
expect(user.value).toEqual(previousUser);
|
||||
|
||||
|
||||
session.destroy();
|
||||
|
||||
user = state.getUser();
|
||||
expect(localStorage.getItem('token')).toBeNull();
|
||||
expect(user.value).toEqual(expectedUser);
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
const expectedUser = {
|
||||
id: 999,
|
||||
name: `T'Challa`,
|
||||
nickname: 'Black Panther',
|
||||
lang: 'en',
|
||||
userConfig: {
|
||||
darkMode: false,
|
||||
}
|
||||
}
|
||||
const rolesData = [
|
||||
{
|
||||
role: {
|
||||
name: 'salesPerson'
|
||||
}
|
||||
},
|
||||
{
|
||||
role: {
|
||||
name: 'admin'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
it('should fetch the user roles and then set token in the sessionStorage', async () => {
|
||||
const expectedRoles = ['salesPerson', 'admin']
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: { roles: rolesData, user: expectedUser }
|
||||
});
|
||||
|
||||
const expectedToken = 'mySessionToken'
|
||||
const keepLogin = false
|
||||
|
||||
await session.login(expectedToken, keepLogin);
|
||||
|
||||
const roles = state.getRoles();
|
||||
const localToken = localStorage.getItem('token');
|
||||
const sessionToken = sessionStorage.getItem('token');
|
||||
|
||||
expect(roles.value).toEqual(expectedRoles);
|
||||
expect(localToken).toBeNull();
|
||||
expect(sessionToken).toEqual(expectedToken);
|
||||
|
||||
session.destroy() // this clears token and user for any other test
|
||||
});
|
||||
|
||||
it('should fetch the user roles and then set token in the localStorage', async () => {
|
||||
const expectedRoles = ['salesPerson', 'admin']
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: { roles: rolesData, user: expectedUser }
|
||||
});
|
||||
|
||||
const expectedToken = 'myLocalToken'
|
||||
const keepLogin = true
|
||||
|
||||
await session.login(expectedToken, keepLogin);
|
||||
|
||||
const roles = state.getRoles();
|
||||
const localToken = localStorage.getItem('token');
|
||||
const sessionToken = sessionStorage.getItem('token');
|
||||
|
||||
expect(roles.value).toEqual(expectedRoles);
|
||||
expect(localToken).toEqual(expectedToken);
|
||||
expect(sessionToken).toBeNull();
|
||||
|
||||
session.destroy() // this clears token and user for any other test
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,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,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { useState } from './useState';
|
||||
import axios from 'axios';
|
||||
|
||||
export function useRole() {
|
||||
const state = useState();
|
||||
|
||||
async function fetch() {
|
||||
const { data } = await axios.get('Accounts/acl');
|
||||
const roles = data.roles.map((userRoles) => userRoles.role.name);
|
||||
|
||||
const userData = {
|
||||
id: data.user.id,
|
||||
name: data.user.name,
|
||||
nickname: data.user.nickname,
|
||||
lang: data.user.lang || 'es',
|
||||
darkMode: data.user.userConfig.darkMode,
|
||||
};
|
||||
state.setUser(userData);
|
||||
state.setRoles(roles);
|
||||
}
|
||||
|
||||
function hasAny(roles) {
|
||||
const roleStore = state.getRoles();
|
||||
|
||||
for (const role of roles) {
|
||||
if (roleStore.value.indexOf(role) !== -1) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
fetch,
|
||||
hasAny,
|
||||
state,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import { useState } from './useState';
|
||||
import { useRole } from './useRole';
|
||||
|
||||
export function useSession() {
|
||||
function getToken() {
|
||||
const localToken = localStorage.getItem('token');
|
||||
const sessionToken = sessionStorage.getItem('token');
|
||||
|
||||
return localToken || sessionToken || '';
|
||||
}
|
||||
|
||||
function setToken(data) {
|
||||
if (data.keepLogin) {
|
||||
localStorage.setItem('token', data.token);
|
||||
} else {
|
||||
sessionStorage.setItem('token', data.token);
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (localStorage.getItem('token'))
|
||||
localStorage.removeItem('token')
|
||||
|
||||
if (sessionStorage.getItem('token'))
|
||||
sessionStorage.removeItem('token');
|
||||
|
||||
const { setUser } = useState();
|
||||
|
||||
setUser({
|
||||
id: 0,
|
||||
name: '',
|
||||
nickname: '',
|
||||
lang: '',
|
||||
darkMode: null,
|
||||
});
|
||||
}
|
||||
|
||||
async function login(token, keepLogin) {
|
||||
const { fetch } = useRole();
|
||||
|
||||
setToken({ token, keepLogin });
|
||||
|
||||
await fetch();
|
||||
}
|
||||
|
||||
function isLoggedIn() {
|
||||
const localToken = localStorage.getItem('token');
|
||||
const sessionToken = sessionStorage.getItem('token');
|
||||
|
||||
return !!(localToken || sessionToken);
|
||||
}
|
||||
|
||||
return {
|
||||
getToken,
|
||||
setToken,
|
||||
destroy,
|
||||
login,
|
||||
isLoggedIn,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { ref, computed } from 'vue';
|
||||
|
||||
const state = ref({});
|
||||
|
||||
const user = ref({
|
||||
id: 0,
|
||||
name: '',
|
||||
nickname: '',
|
||||
lang: '',
|
||||
darkMode: null,
|
||||
});
|
||||
|
||||
const roles = ref([]);
|
||||
const drawer = ref(true);
|
||||
const headerMounted = ref(false);
|
||||
|
||||
export function useState() {
|
||||
function getUser() {
|
||||
return computed(() => {
|
||||
return {
|
||||
id: user.value.id,
|
||||
name: user.value.name,
|
||||
nickname: user.value.nickname,
|
||||
lang: user.value.lang,
|
||||
darkMode: user.value.darkMode,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function setUser(data) {
|
||||
user.value = {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
nickname: data.nickname,
|
||||
lang: data.lang,
|
||||
darkMode: data.darkMode,
|
||||
};
|
||||
}
|
||||
|
||||
function getRoles() {
|
||||
return computed(() => {
|
||||
return roles.value;
|
||||
});
|
||||
}
|
||||
|
||||
function setRoles(data) {
|
||||
roles.value = data;
|
||||
}
|
||||
|
||||
function set(name, data) {
|
||||
state.value[name] = ref(data);
|
||||
}
|
||||
|
||||
function get(name) {
|
||||
return state.value[name];
|
||||
}
|
||||
|
||||
function unset(name) {
|
||||
delete state.value[name];
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
getUser,
|
||||
setUser,
|
||||
getRoles,
|
||||
setRoles,
|
||||
set,
|
||||
get,
|
||||
unset,
|
||||
drawer,
|
||||
headerMounted
|
||||
};
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import axios from 'axios';
|
||||
import validator from 'validator';
|
||||
|
||||
|
||||
const models = ref(null);
|
||||
|
||||
export function useValidator() {
|
||||
if (!models.value) fetch();
|
||||
|
||||
function fetch() {
|
||||
axios.get('Schemas/ModelInfo')
|
||||
.then(response => models.value = response.data)
|
||||
}
|
||||
|
||||
function validate(propertyRule) {
|
||||
const modelInfo = models.value;
|
||||
if (!modelInfo || !propertyRule) return;
|
||||
|
||||
const rule = propertyRule.split('.');
|
||||
const model = rule[0];
|
||||
const property = rule[1];
|
||||
const modelName = model.charAt(0).toUpperCase() + model.slice(1);
|
||||
|
||||
if (!modelInfo[modelName]) return;
|
||||
|
||||
const modelValidations = modelInfo[modelName].validations;
|
||||
|
||||
if (!modelValidations[property]) return;
|
||||
|
||||
const rules = modelValidations[property].map((validation) => {
|
||||
return validations(validation)[validation.validation];
|
||||
});
|
||||
|
||||
return rules;
|
||||
}
|
||||
|
||||
const { t } = useI18n();
|
||||
const validations = function (validation) {
|
||||
|
||||
return {
|
||||
presence: (value) => {
|
||||
let message = `Value can't be empty`;
|
||||
if (validation.message)
|
||||
message = t(validation.message) || validation.message
|
||||
|
||||
return !validator.isEmpty(value ? String(value) : '') || message
|
||||
},
|
||||
length: (value) => {
|
||||
const options = {
|
||||
min: validation.min || validation.is,
|
||||
max: validation.max || validation.is
|
||||
};
|
||||
|
||||
value = String(value);
|
||||
|
||||
if (!value) value = '';
|
||||
|
||||
let message = `Value should have at most ${options.max} characters`;
|
||||
if (validation.is)
|
||||
message = `Value should be ${validation.is} characters long`;
|
||||
if (validation.min)
|
||||
message = `Value should have at least ${validation.min} characters`;
|
||||
if (validation.min && validation.max)
|
||||
message = `Value should have a length between ${validation.min} and ${validation.max}`;
|
||||
|
||||
return validator.isLength(value, options) || message;
|
||||
},
|
||||
numericality: (value) => {
|
||||
if (validation.int)
|
||||
return validator.isInt(value) || 'Value should be integer'
|
||||
return validator.isNumeric(value) || 'Value should be a number'
|
||||
},
|
||||
custom: (value) => validation.bindedFunction(value) || 'Invalid value'
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
validate
|
||||
};
|
||||
}
|
|
@ -1 +1,21 @@
|
|||
// app global css in SCSS form
|
||||
@import './icons.scss';
|
||||
|
||||
.body--dark {
|
||||
.q-card--dark {
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2), 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.q-layout__shadow::after {
|
||||
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.2), 0 0px 10px rgba(0, 0, 0, 0.24) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $primary;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: $orange-4;
|
||||
}
|
||||
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 159 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,399 @@
|
|||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src: url('fonts/icomoon.eot?g6kvgn');
|
||||
src: url('fonts/icomoon.eot?g6kvgn#iefix') format('embedded-opentype'),
|
||||
url('fonts/icomoon.ttf?g6kvgn') format('truetype'),
|
||||
url('fonts/icomoon.woff?g6kvgn') format('woff'),
|
||||
url('fonts/icomoon.svg?g6kvgn#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
[class^="icon-"], [class*=" icon-"] {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'icomoon' !important;
|
||||
speak: never;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-pin:before {
|
||||
content: "\e950";
|
||||
}
|
||||
.icon-pin_off:before {
|
||||
content: "\e95b";
|
||||
}
|
||||
.icon-frozen:before {
|
||||
content: "\e900";
|
||||
}
|
||||
.icon-Person:before {
|
||||
content: "\e901";
|
||||
}
|
||||
.icon-handmadeArtificial:before {
|
||||
content: "\e902";
|
||||
}
|
||||
.icon-fruit:before {
|
||||
content: "\e903";
|
||||
}
|
||||
.icon-funeral:before {
|
||||
content: "\e904";
|
||||
}
|
||||
.icon-noPayMethod:before {
|
||||
content: "\e905";
|
||||
}
|
||||
.icon-preserved:before {
|
||||
content: "\e906";
|
||||
}
|
||||
.icon-greenery:before {
|
||||
content: "\e907";
|
||||
}
|
||||
.icon-planta:before {
|
||||
content: "\e908";
|
||||
}
|
||||
.icon-handmade:before {
|
||||
content: "\e909";
|
||||
}
|
||||
.icon-accessory:before {
|
||||
content: "\e90a";
|
||||
}
|
||||
.icon-artificial:before {
|
||||
content: "\e90b";
|
||||
}
|
||||
.icon-flower:before {
|
||||
content: "\e90c";
|
||||
}
|
||||
.icon-fixedPrice:before {
|
||||
content: "\e90d";
|
||||
}
|
||||
.icon-addperson:before {
|
||||
content: "\e90e";
|
||||
}
|
||||
.icon-supplierfalse:before {
|
||||
content: "\e90f";
|
||||
}
|
||||
.icon-invoice-out:before {
|
||||
content: "\e910";
|
||||
}
|
||||
.icon-invoice-in:before {
|
||||
content: "\e911";
|
||||
}
|
||||
.icon-invoice-in-create:before {
|
||||
content: "\e912";
|
||||
}
|
||||
.icon-basketadd:before {
|
||||
content: "\e913";
|
||||
}
|
||||
.icon-basket:before {
|
||||
content: "\e914";
|
||||
}
|
||||
.icon-uniE915:before {
|
||||
content: "\e915";
|
||||
}
|
||||
.icon-uniE916:before {
|
||||
content: "\e916";
|
||||
}
|
||||
.icon-uniE917:before {
|
||||
content: "\e917";
|
||||
}
|
||||
.icon-uniE918:before {
|
||||
content: "\e918";
|
||||
}
|
||||
.icon-uniE919:before {
|
||||
content: "\e919";
|
||||
}
|
||||
.icon-uniE91A:before {
|
||||
content: "\e91a";
|
||||
}
|
||||
.icon-isTooLittle:before {
|
||||
content: "\e91b";
|
||||
}
|
||||
.icon-deliveryprices:before {
|
||||
content: "\e91c";
|
||||
}
|
||||
.icon-onlinepayment:before {
|
||||
content: "\e91d";
|
||||
}
|
||||
.icon-risk:before {
|
||||
content: "\e91e";
|
||||
}
|
||||
.icon-noweb:before {
|
||||
content: "\e91f";
|
||||
}
|
||||
.icon-no036:before {
|
||||
content: "\e920";
|
||||
}
|
||||
.icon-disabled:before {
|
||||
content: "\e921";
|
||||
}
|
||||
.icon-treatments:before {
|
||||
content: "\e922";
|
||||
}
|
||||
.icon-invoice:before {
|
||||
content: "\e923";
|
||||
}
|
||||
.icon-photo:before {
|
||||
content: "\e924";
|
||||
}
|
||||
.icon-supplier:before {
|
||||
content: "\e925";
|
||||
}
|
||||
.icon-languaje:before {
|
||||
content: "\e926";
|
||||
}
|
||||
.icon-credit:before {
|
||||
content: "\e927";
|
||||
}
|
||||
.icon-client:before {
|
||||
content: "\e928";
|
||||
}
|
||||
.icon-shipment-01:before {
|
||||
content: "\e929";
|
||||
}
|
||||
.icon-account:before {
|
||||
content: "\e92a";
|
||||
}
|
||||
.icon-inventory:before {
|
||||
content: "\e92b";
|
||||
}
|
||||
.icon-unavailable:before {
|
||||
content: "\e92c";
|
||||
}
|
||||
.icon-wiki:before {
|
||||
content: "\e92d";
|
||||
}
|
||||
.icon-attach:before {
|
||||
content: "\e92e";
|
||||
}
|
||||
.icon-exit:before {
|
||||
content: "\e92f";
|
||||
}
|
||||
.icon-anonymous:before {
|
||||
content: "\e930";
|
||||
}
|
||||
.icon-net:before {
|
||||
content: "\e931";
|
||||
}
|
||||
.icon-buyrequest:before {
|
||||
content: "\e932";
|
||||
}
|
||||
.icon-thermometer:before {
|
||||
content: "\e933";
|
||||
}
|
||||
.icon-entry:before {
|
||||
content: "\e934";
|
||||
}
|
||||
.icon-deletedTicket:before {
|
||||
content: "\e935";
|
||||
}
|
||||
.icon-logout:before {
|
||||
content: "\e936";
|
||||
}
|
||||
.icon-catalog:before {
|
||||
content: "\e937";
|
||||
}
|
||||
.icon-agency:before {
|
||||
content: "\e938";
|
||||
}
|
||||
.icon-delivery:before {
|
||||
content: "\e939";
|
||||
}
|
||||
.icon-wand:before {
|
||||
content: "\e93a";
|
||||
}
|
||||
.icon-buscaman:before {
|
||||
content: "\e93b";
|
||||
}
|
||||
.icon-pbx:before {
|
||||
content: "\e93c";
|
||||
}
|
||||
.icon-calendar:before {
|
||||
content: "\e93d";
|
||||
}
|
||||
.icon-splitline:before {
|
||||
content: "\e93e";
|
||||
}
|
||||
.icon-consignatarios:before {
|
||||
content: "\e93f";
|
||||
}
|
||||
.icon-tax:before {
|
||||
content: "\e940";
|
||||
}
|
||||
.icon-notes:before {
|
||||
content: "\e941";
|
||||
}
|
||||
.icon-lines:before {
|
||||
content: "\e942";
|
||||
}
|
||||
.icon-zone:before {
|
||||
content: "\e943";
|
||||
}
|
||||
.icon-greuge:before {
|
||||
content: "\e944";
|
||||
}
|
||||
.icon-ticketAdd:before {
|
||||
content: "\e945";
|
||||
}
|
||||
.icon-components:before {
|
||||
content: "\e946";
|
||||
}
|
||||
.icon-pets:before {
|
||||
content: "\e947";
|
||||
}
|
||||
.icon-linesprepaired:before {
|
||||
content: "\e948";
|
||||
}
|
||||
.icon-control:before {
|
||||
content: "\e949";
|
||||
}
|
||||
.icon-revision:before {
|
||||
content: "\e94a";
|
||||
}
|
||||
.icon-deaulter:before {
|
||||
content: "\e94b";
|
||||
}
|
||||
.icon-services:before {
|
||||
content: "\e94c";
|
||||
}
|
||||
.icon-albaran:before {
|
||||
content: "\e94d";
|
||||
}
|
||||
.icon-solunion:before {
|
||||
content: "\e94e";
|
||||
}
|
||||
.icon-stowaway:before {
|
||||
content: "\e94f";
|
||||
}
|
||||
.icon-apps:before {
|
||||
content: "\e951";
|
||||
}
|
||||
.icon-info:before {
|
||||
content: "\e952";
|
||||
}
|
||||
.icon-columndelete:before {
|
||||
content: "\e953";
|
||||
}
|
||||
.icon-columnadd:before {
|
||||
content: "\e954";
|
||||
}
|
||||
.icon-deleteline:before {
|
||||
content: "\e955";
|
||||
}
|
||||
.icon-item:before {
|
||||
content: "\e956";
|
||||
}
|
||||
.icon-worker:before {
|
||||
content: "\e957";
|
||||
}
|
||||
.icon-headercol:before {
|
||||
content: "\e958";
|
||||
}
|
||||
.icon-reserva:before {
|
||||
content: "\e959";
|
||||
}
|
||||
.icon-100:before {
|
||||
content: "\e95a";
|
||||
}
|
||||
.icon-sign:before {
|
||||
content: "\e95d";
|
||||
}
|
||||
.icon-polizon:before {
|
||||
content: "\e95e";
|
||||
}
|
||||
.icon-solclaim:before {
|
||||
content: "\e95f";
|
||||
}
|
||||
.icon-actions:before {
|
||||
content: "\e960";
|
||||
}
|
||||
.icon-details:before {
|
||||
content: "\e961";
|
||||
}
|
||||
.icon-traceability:before {
|
||||
content: "\e962";
|
||||
}
|
||||
.icon-claims:before {
|
||||
content: "\e963";
|
||||
}
|
||||
.icon-regentry:before {
|
||||
content: "\e964";
|
||||
}
|
||||
.icon-transaction:before {
|
||||
content: "\e966";
|
||||
}
|
||||
.icon-History:before {
|
||||
content: "\e968";
|
||||
}
|
||||
.icon-mana:before {
|
||||
content: "\e96a";
|
||||
}
|
||||
.icon-ticket:before {
|
||||
content: "\e96b";
|
||||
}
|
||||
.icon-niche:before {
|
||||
content: "\e96c";
|
||||
}
|
||||
.icon-tags:before {
|
||||
content: "\e96d";
|
||||
}
|
||||
.icon-volume:before {
|
||||
content: "\e96e";
|
||||
}
|
||||
.icon-bin:before {
|
||||
content: "\e96f";
|
||||
}
|
||||
.icon-splur:before {
|
||||
content: "\e970";
|
||||
}
|
||||
.icon-barcode:before {
|
||||
content: "\e971";
|
||||
}
|
||||
.icon-botanical:before {
|
||||
content: "\e972";
|
||||
}
|
||||
.icon-clone:before {
|
||||
content: "\e973";
|
||||
}
|
||||
.icon-sms:before {
|
||||
content: "\e975";
|
||||
}
|
||||
.icon-eye:before {
|
||||
content: "\e976";
|
||||
}
|
||||
.icon-doc:before {
|
||||
content: "\e977";
|
||||
}
|
||||
.icon-package:before {
|
||||
content: "\e978";
|
||||
}
|
||||
.icon-settings:before {
|
||||
content: "\e979";
|
||||
}
|
||||
.icon-bucket:before {
|
||||
content: "\e97a";
|
||||
}
|
||||
.icon-mandatory:before {
|
||||
content: "\e97b";
|
||||
}
|
||||
.icon-recovery:before {
|
||||
content: "\e97c";
|
||||
}
|
||||
.icon-payment:before {
|
||||
content: "\e97e";
|
||||
}
|
||||
.icon-grid:before {
|
||||
content: "\e980";
|
||||
}
|
||||
.icon-web:before {
|
||||
content: "\e982";
|
||||
}
|
||||
.icon-dfiscales:before {
|
||||
content: "\e984";
|
||||
}
|
|
@ -12,14 +12,19 @@
|
|||
// to match your app's branding.
|
||||
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
||||
|
||||
$primary: #1976d2;
|
||||
$primary: #ff9800;
|
||||
$secondary: #26a69a;
|
||||
$accent: #9c27b0;
|
||||
|
||||
$dark: #1d1d1d;
|
||||
$dark-page: #121212;
|
||||
|
||||
$positive: #21ba45;
|
||||
$negative: #c10015;
|
||||
$info: #31ccec;
|
||||
$warning: #f2c037;
|
||||
|
||||
$color-spacer-light: rgba(255, 255, 255, .12);
|
||||
$color-spacer:rgba(255, 255, 255, .3);
|
||||
$border-thin-light: 1px solid $color-spacer-light;
|
||||
|
||||
$spacing-md: 16px;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export default function (value) {
|
||||
if (value == null || value === '') return '-';
|
||||
return value;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import toLowerCase from './toLowerCase';
|
||||
import toDate from './toDate';
|
||||
import toCurrency from './toCurrency';
|
||||
import toPercentage from './toPercentage';
|
||||
import toLowerCamel from './toLowerCamel';
|
||||
import dashIfEmpty from './dashIfEmpty';
|
||||
|
||||
export {
|
||||
toLowerCase,
|
||||
toLowerCamel,
|
||||
toDate,
|
||||
toCurrency,
|
||||
toPercentage,
|
||||
dashIfEmpty,
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default function (value, symbol = 'EUR', fractionSize = 2) {
|
||||
if (value == null || value === '') value = 0;
|
||||
|
||||
const { locale } = useI18n();
|
||||
|
||||
const options = {
|
||||
style: 'currency',
|
||||
currency: symbol,
|
||||
minimumFractionDigits: fractionSize,
|
||||
maximumFractionDigits: fractionSize
|
||||
};
|
||||
|
||||
const lang = locale.value == 'es' ? 'de' : locale.value;
|
||||
|
||||
return new Intl.NumberFormat(lang, options)
|
||||
.format(value);
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default function (value, options = {}) {
|
||||
if (!value) return;
|
||||
|
||||
if (!options.dateStyle) options.dateStyle = 'short';
|
||||
|
||||
const { locale } = useI18n();
|
||||
const date = new Date(value);
|
||||
|
||||
return new Intl.DateTimeFormat(locale.value, options).format(date)
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default function toLowerCase(value) {
|
||||
return value.toLowerCase();
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default function (value, fractionSize = 2) {
|
||||
if (value == null || value === '') return;
|
||||
|
||||
const { locale } = useI18n();
|
||||
|
||||
const options = {
|
||||
style: 'percent',
|
||||
minimumFractionDigits: fractionSize,
|
||||
maximumFractionDigits: fractionSize
|
||||
};
|
||||
|
||||
return new Intl.NumberFormat(locale, options)
|
||||
.format(parseFloat(value));
|
||||
|
||||
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
// This is just an example,
|
||||
// so you can safely delete all default props below
|
||||
|
||||
export default {
|
||||
failed: 'Action failed',
|
||||
success: 'Action was successful',
|
||||
};
|
|
@ -0,0 +1,363 @@
|
|||
export default {
|
||||
globals: {
|
||||
lang: {
|
||||
es: 'Spanish',
|
||||
en: 'English',
|
||||
},
|
||||
language: 'Language',
|
||||
collapseMenu: 'Collapse left menu',
|
||||
backToDashboard: 'Return to dashboard',
|
||||
notifications: 'Notifications',
|
||||
userPanel: 'User panel',
|
||||
pinnedModules: 'Pinned modules',
|
||||
darkMode: 'Dark mode',
|
||||
logOut: 'Log out',
|
||||
dataSaved: 'Data saved',
|
||||
dataDeleted: 'Data deleted',
|
||||
add: 'Add',
|
||||
create: 'Create',
|
||||
save: 'Save',
|
||||
remove: 'Remove',
|
||||
reset: 'Reset',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
back: 'Back',
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
noChanges: 'No changes to save',
|
||||
changesToSave: 'You have changes pending to save',
|
||||
confirmRemove: 'You are about to delete this row. Are you sure?',
|
||||
rowAdded: 'Row added',
|
||||
rowRemoved: 'Row removed',
|
||||
pleaseWait: 'Please wait...',
|
||||
},
|
||||
moduleIndex: {
|
||||
allModules: 'All modules',
|
||||
},
|
||||
errors: {
|
||||
statusUnauthorized: 'Access denied',
|
||||
statusInternalServerError: 'An internal server error has ocurred',
|
||||
statusBadGateway: 'It seems that the server has fall down',
|
||||
statusGatewayTimeout: 'Could not contact the server',
|
||||
},
|
||||
login: {
|
||||
title: 'Login',
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
submit: 'Log in',
|
||||
keepLogin: 'Keep me logged in',
|
||||
loginSuccess: 'You have successfully logged in',
|
||||
loginError: 'Invalid username or password',
|
||||
fieldRequired: 'This field is required',
|
||||
},
|
||||
dashboard: {
|
||||
pageTitles: {
|
||||
dashboard: 'Dashboard',
|
||||
},
|
||||
},
|
||||
customer: {
|
||||
pageTitles: {
|
||||
customers: 'Customers',
|
||||
list: 'List',
|
||||
createCustomer: 'Create customer',
|
||||
summary: 'Summary',
|
||||
basicData: 'Basic Data',
|
||||
},
|
||||
list: {
|
||||
phone: 'Phone',
|
||||
email: 'Email',
|
||||
customerOrders: 'Display customer orders',
|
||||
moreOptions: 'More options',
|
||||
},
|
||||
card: {
|
||||
customerList: 'Customer list',
|
||||
customerId: 'Claim ID',
|
||||
salesPerson: 'Sales person',
|
||||
credit: 'Credit',
|
||||
securedCredit: 'Secured credit',
|
||||
payMethod: 'Pay method',
|
||||
debt: 'Debt',
|
||||
isDisabled: 'Customer is disabled',
|
||||
isFrozen: 'Customer is frozen',
|
||||
hasDebt: 'Customer has debt',
|
||||
notChecked: 'Customer not checked',
|
||||
noWebAccess: 'Web access is disabled',
|
||||
},
|
||||
summary: {
|
||||
basicData: 'Basic data',
|
||||
fiscalAddress: 'Fiscal address',
|
||||
fiscalData: 'Fiscal data',
|
||||
billingData: 'Billing data',
|
||||
consignee: 'Consignee',
|
||||
businessData: 'Business data',
|
||||
financialData: 'Financial data',
|
||||
customerId: 'Customer ID',
|
||||
name: 'Name',
|
||||
contact: 'Contact',
|
||||
phone: 'Phone',
|
||||
mobile: 'Mobile',
|
||||
email: 'Email',
|
||||
salesPerson: 'Sales person',
|
||||
contactChannel: 'Contact channel',
|
||||
socialName: 'Social name',
|
||||
fiscalId: 'Fiscal ID',
|
||||
postcode: 'Postcode',
|
||||
province: 'Province',
|
||||
country: 'Country',
|
||||
street: 'Address',
|
||||
isEqualizated: 'Is equalizated',
|
||||
isActive: 'Is active',
|
||||
invoiceByAddress: 'Invoice by address',
|
||||
verifiedData: 'Verified data',
|
||||
hasToInvoice: 'Has to invoice',
|
||||
notifyByEmail: 'Notify by email',
|
||||
vies: 'VIES',
|
||||
payMethod: 'Pay method',
|
||||
bankAccount: 'Bank account',
|
||||
dueDay: 'Due day',
|
||||
hasLcr: 'Has LCR',
|
||||
hasCoreVnl: 'Has core VNL',
|
||||
hasB2BVnl: 'Has B2B VNL',
|
||||
addressName: 'Address name',
|
||||
addressCity: 'City',
|
||||
addressStreet: 'Street',
|
||||
username: 'Username',
|
||||
webAccess: 'Web access',
|
||||
totalGreuge: 'Total greuge',
|
||||
mana: 'Mana',
|
||||
priceIncreasingRate: 'Price increasing rate',
|
||||
averageInvoiced: 'Average invoiced',
|
||||
claimRate: 'Claming rate',
|
||||
risk: 'Risk',
|
||||
riskInfo: 'Invoices minus payments plus orders not yet invoiced',
|
||||
credit: 'Credit',
|
||||
creditInfo: `Company's maximum risk`,
|
||||
securedCredit: 'Secured credit',
|
||||
securedCreditInfo: `Solunion's maximum risk`,
|
||||
balance: 'Balance',
|
||||
balanceInfo: 'Invoices minus payments',
|
||||
balanceDue: 'Balance due',
|
||||
balanceDueInfo: 'Deviated invoices minus payments',
|
||||
recoverySince: 'Recovery since',
|
||||
},
|
||||
basicData: {
|
||||
socialName: 'Fiscal name',
|
||||
businessType: 'Business type',
|
||||
contact: 'Contact',
|
||||
email: 'Email',
|
||||
phone: 'Phone',
|
||||
mobile: 'Mobile',
|
||||
salesPerson: 'Sales person',
|
||||
contactChannel: 'Contact channel',
|
||||
},
|
||||
},
|
||||
ticket: {
|
||||
pageTitles: {
|
||||
tickets: 'Tickets',
|
||||
list: 'List',
|
||||
createTicket: 'Create ticket',
|
||||
summary: 'Summary',
|
||||
basicData: 'Basic Data',
|
||||
boxing: 'Boxing',
|
||||
},
|
||||
list: {
|
||||
nickname: 'Nickname',
|
||||
state: 'State',
|
||||
shipped: 'Shipped',
|
||||
landed: 'Landed',
|
||||
salesPerson: 'Sales person',
|
||||
total: 'Total',
|
||||
},
|
||||
card: {
|
||||
ticketId: 'Ticket ID',
|
||||
state: 'State',
|
||||
customerId: 'Customer ID',
|
||||
salesPerson: 'Sales person',
|
||||
agency: 'Agency',
|
||||
shipped: 'Shipped',
|
||||
warehouse: 'Warehouse',
|
||||
customerCard: 'Customer card',
|
||||
},
|
||||
boxing: {
|
||||
expedition: 'Expedition',
|
||||
item: 'Item',
|
||||
created: 'Created',
|
||||
worker: 'Worker',
|
||||
selectTime: 'Select time:',
|
||||
selectVideo: 'Select video:',
|
||||
notFound: 'No videos available',
|
||||
},
|
||||
summary: {
|
||||
state: 'State',
|
||||
salesPerson: 'Sales person',
|
||||
agency: 'Agency',
|
||||
zone: 'Zone',
|
||||
warehouse: 'Warehouse',
|
||||
route: 'Route',
|
||||
invoice: 'Invoice',
|
||||
shipped: 'Shipped',
|
||||
landed: 'Landed',
|
||||
packages: 'Packages',
|
||||
consigneePhone: 'Consignee phone',
|
||||
consigneeMobile: 'Consignee mobile',
|
||||
clientPhone: 'Client phone',
|
||||
clientMobile: 'Client mobile',
|
||||
consignee: 'Consignee',
|
||||
subtotal: 'Subtotal',
|
||||
vat: 'VAT',
|
||||
total: 'Total',
|
||||
saleLines: 'Line items',
|
||||
item: 'Item',
|
||||
visible: 'Visible',
|
||||
available: 'Available',
|
||||
quantity: 'Quantity',
|
||||
description: 'Description',
|
||||
price: 'Price',
|
||||
discount: 'Discount',
|
||||
amount: 'Amount',
|
||||
packing: 'Packing',
|
||||
hasComponentLack: 'Component lack',
|
||||
itemShortage: 'Not visible',
|
||||
claim: 'Claim',
|
||||
reserved: 'Reserved',
|
||||
created: 'Created',
|
||||
package: 'Package',
|
||||
taxClass: 'Tax class',
|
||||
services: 'Services',
|
||||
changeState: 'Change state',
|
||||
requester: 'Requester',
|
||||
atender: 'Atender',
|
||||
request: 'Request',
|
||||
goTo: 'Go to'
|
||||
}
|
||||
},
|
||||
claim: {
|
||||
pageTitles: {
|
||||
claims: 'Claims',
|
||||
list: 'List',
|
||||
createClaim: 'Create claim',
|
||||
rmaList: 'RMA',
|
||||
summary: 'Summary',
|
||||
basicData: 'Basic Data',
|
||||
rma: 'RMA',
|
||||
},
|
||||
list: {
|
||||
customer: 'Customer',
|
||||
assignedTo: 'Assigned',
|
||||
created: 'Created',
|
||||
state: 'State',
|
||||
},
|
||||
rmaList: {
|
||||
code: 'Code',
|
||||
records: 'records',
|
||||
},
|
||||
rma: {
|
||||
user: 'User',
|
||||
created: 'Created',
|
||||
},
|
||||
card: {
|
||||
claimId: 'Claim ID',
|
||||
assignedTo: 'Assigned',
|
||||
created: 'Created',
|
||||
state: 'State',
|
||||
ticketId: 'Ticket ID',
|
||||
customerSummary: 'Customer summary',
|
||||
claimedTicket: 'Claimed ticket',
|
||||
},
|
||||
summary: {
|
||||
customer: 'Customer',
|
||||
assignedTo: 'Assigned',
|
||||
attendedBy: 'Attended by',
|
||||
created: 'Created',
|
||||
state: 'State',
|
||||
details: 'Details',
|
||||
item: 'Item',
|
||||
landed: 'Landed',
|
||||
quantity: 'Quantity',
|
||||
claimed: 'Claimed',
|
||||
description: 'Description',
|
||||
price: 'Price',
|
||||
discount: 'Discount',
|
||||
total: 'Total',
|
||||
actions: 'Actions',
|
||||
responsibility: 'Responsibility',
|
||||
company: 'Company',
|
||||
person: 'Employee/Customer',
|
||||
},
|
||||
basicData: {
|
||||
customer: 'Customer',
|
||||
assignedTo: 'Assigned',
|
||||
created: 'Created',
|
||||
state: 'State',
|
||||
packages: 'Packages',
|
||||
picked: 'Picked',
|
||||
returnOfMaterial: 'Return of material authorization (RMA)',
|
||||
},
|
||||
},
|
||||
invoiceOut: {
|
||||
pageTitles: {
|
||||
invoiceOuts: 'InvoiceOuts',
|
||||
list: 'List',
|
||||
createInvoiceOut: 'Create invoice out',
|
||||
summary: 'Summary',
|
||||
basicData: 'Basic Data'
|
||||
},
|
||||
list: {
|
||||
ref: 'Reference',
|
||||
issued: 'Issued',
|
||||
amount: 'Amount',
|
||||
client: 'Client',
|
||||
created: 'Created',
|
||||
company: 'Company',
|
||||
dued: 'Due date'
|
||||
},
|
||||
card: {
|
||||
issued: 'Issued',
|
||||
amount: 'Amount',
|
||||
client: 'Client',
|
||||
company: 'Company',
|
||||
customerCard: 'Customer card',
|
||||
ticketList: 'Ticket List'
|
||||
},
|
||||
summary: {
|
||||
issued: 'Issued',
|
||||
created: 'Created',
|
||||
dued: 'Due',
|
||||
booked: 'Booked',
|
||||
company: 'Company',
|
||||
taxBreakdown: 'Tax breakdown',
|
||||
type: 'Type',
|
||||
taxableBase: 'Taxable base',
|
||||
rate: 'Rate',
|
||||
fee: 'Fee',
|
||||
tickets: 'Tickets',
|
||||
ticketId: 'Ticket id',
|
||||
nickname: 'Alias',
|
||||
shipped: 'Shipped',
|
||||
totalWithVat: 'Amount',
|
||||
|
||||
}
|
||||
},
|
||||
components: {
|
||||
topbar: {},
|
||||
userPanel: {
|
||||
settings: 'Settings',
|
||||
logOut: 'Log Out',
|
||||
},
|
||||
smartCard: {
|
||||
noData: 'No data to display',
|
||||
openCard: 'View card',
|
||||
openSummary: 'Open summary',
|
||||
viewDescription: 'View description',
|
||||
},
|
||||
cardDescriptor: {
|
||||
mainList: 'Main list',
|
||||
summary: 'Summary',
|
||||
moreOptions: 'More options',
|
||||
},
|
||||
leftMenu: {
|
||||
addToPinned: 'Add to pinned',
|
||||
removeFromPinned: 'Remove from pinned',
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,362 @@
|
|||
export default {
|
||||
globals: {
|
||||
lang: {
|
||||
es: 'Español',
|
||||
en: 'Inglés',
|
||||
},
|
||||
language: 'Idioma',
|
||||
collapseMenu: 'Contraer menú lateral',
|
||||
backToDashboard: 'Volver al tablón',
|
||||
notifications: 'Notificaciones',
|
||||
userPanel: 'Panel de usuario',
|
||||
pinnedModules: 'Módulos fijados',
|
||||
darkMode: 'Modo oscuro',
|
||||
logOut: 'Cerrar sesión',
|
||||
dataSaved: 'Datos guardados',
|
||||
dataDeleted: 'Data deleted',
|
||||
add: 'Añadir',
|
||||
create: 'Crear',
|
||||
save: 'Guardar',
|
||||
remove: 'Eliminar',
|
||||
reset: 'Restaurar',
|
||||
cancel: 'Cancelar',
|
||||
confirm: 'Confirmar',
|
||||
back: 'Volver',
|
||||
yes: 'Si',
|
||||
no: 'No',
|
||||
noChanges: 'Sin cambios que guardar',
|
||||
changesToSave: 'Tienes cambios pendientes de guardar',
|
||||
confirmRemove: 'Vas a eliminar este registro. ¿Continuar?',
|
||||
rowAdded: 'Fila añadida',
|
||||
rowRemoved: 'Fila eliminada',
|
||||
pleaseWait: 'Por favor, espera...',
|
||||
},
|
||||
moduleIndex: {
|
||||
allModules: 'Todos los módulos',
|
||||
},
|
||||
errors: {
|
||||
statusUnauthorized: 'Acceso denegado',
|
||||
statusInternalServerError: 'Ha ocurrido un error interno del servidor',
|
||||
statusBadGateway: 'Parece ser que el servidor ha caído',
|
||||
statusGatewayTimeout: 'No se ha podido contactar con el servidor',
|
||||
},
|
||||
login: {
|
||||
title: 'Inicio de sesión',
|
||||
username: 'Nombre de usuario',
|
||||
password: 'Contraseña',
|
||||
submit: 'Iniciar sesión',
|
||||
keepLogin: 'Mantener sesión iniciada',
|
||||
loginSuccess: 'Inicio de sesión correcto',
|
||||
loginError: 'Nombre de usuario o contraseña incorrectos',
|
||||
fieldRequired: 'Este campo es obligatorio',
|
||||
},
|
||||
dashboard: {
|
||||
pageTitles: {
|
||||
dashboard: 'Tablón',
|
||||
},
|
||||
},
|
||||
customer: {
|
||||
pageTitles: {
|
||||
customers: 'Clientes',
|
||||
list: 'Listado',
|
||||
createCustomer: 'Crear cliente',
|
||||
summary: 'Resumen',
|
||||
basicData: 'Datos básicos',
|
||||
},
|
||||
list: {
|
||||
phone: 'Teléfono',
|
||||
email: 'Email',
|
||||
customerOrders: 'Mostrar órdenes del cliente',
|
||||
moreOptions: 'Más opciones',
|
||||
},
|
||||
card: {
|
||||
customerId: 'ID cliente',
|
||||
salesPerson: 'Comercial',
|
||||
credit: 'Crédito',
|
||||
securedCredit: 'Crédito asegurado',
|
||||
payMethod: 'Método de pago',
|
||||
debt: 'Riesgo',
|
||||
isDisabled: 'El cliente está desactivado',
|
||||
isFrozen: 'El cliente está congelado',
|
||||
hasDebt: 'El cliente tiene riesgo',
|
||||
notChecked: 'El cliente no está comprobado',
|
||||
noWebAccess: 'El acceso web está desactivado',
|
||||
},
|
||||
summary: {
|
||||
basicData: 'Datos básicos',
|
||||
fiscalAddress: 'Dirección fiscal',
|
||||
fiscalData: 'Datos fiscales',
|
||||
billingData: 'Datos de facturación',
|
||||
consignee: 'Consignatario',
|
||||
businessData: 'Datos comerciales',
|
||||
financialData: 'Datos financieros',
|
||||
customerId: 'ID cliente',
|
||||
name: 'Nombre',
|
||||
contact: 'Contacto',
|
||||
phone: 'Teléfono',
|
||||
mobile: 'Móvil',
|
||||
email: 'Email',
|
||||
salesPerson: 'Comercial',
|
||||
contactChannel: 'Canal de contacto',
|
||||
socialName: 'Razón social',
|
||||
fiscalId: 'NIF/CIF',
|
||||
postcode: 'Código postal',
|
||||
province: 'Provincia',
|
||||
country: 'País',
|
||||
street: 'Calle',
|
||||
isEqualizated: 'Equalizado',
|
||||
isActive: 'Activo',
|
||||
invoiceByAddress: 'Facturar por consignatario',
|
||||
verifiedData: 'Datos verificados',
|
||||
hasToInvoice: 'Facturar',
|
||||
notifyByEmail: 'Notificar por email',
|
||||
vies: 'VIES',
|
||||
payMethod: 'Método de pago',
|
||||
bankAccount: 'Cuenta bancaria',
|
||||
dueDay: 'Día de pago',
|
||||
hasLcr: 'Recibido LCR',
|
||||
hasCoreVnl: 'Recibido core VNL',
|
||||
hasB2BVnl: 'Recibido B2B VNL',
|
||||
addressName: 'Nombre de la dirección',
|
||||
addressCity: 'Ciudad',
|
||||
addressStreet: 'Calle',
|
||||
username: 'Usuario',
|
||||
webAccess: 'Acceso web',
|
||||
totalGreuge: 'Greuge total',
|
||||
mana: 'Maná',
|
||||
priceIncreasingRate: 'Ratio de incremento de precio',
|
||||
averageInvoiced: 'Facturación media',
|
||||
claimRate: 'Ratio de reclamaciones',
|
||||
risk: 'Riesgo',
|
||||
riskInfo: 'Facturas menos recibos mas pedidos sin facturar',
|
||||
credit: 'Crédito',
|
||||
creditInfo: `Riesgo máximo asumido por la empresa`,
|
||||
securedCredit: 'Crédito asegurado',
|
||||
securedCreditInfo: `Riesgo máximo asumido por Solunion`,
|
||||
balance: 'Balance',
|
||||
balanceInfo: 'Facturas menos recibos',
|
||||
balanceDue: 'Saldo vencido',
|
||||
balanceDueInfo: 'Facturas fuera de plazo menos recibos',
|
||||
recoverySince: 'Recobro desde',
|
||||
},
|
||||
basicData: {
|
||||
socialName: 'Nombre fiscal',
|
||||
businessType: 'Tipo de negocio',
|
||||
contact: 'Contacto',
|
||||
email: 'Email',
|
||||
phone: 'Teléfono',
|
||||
mobile: 'Móvil',
|
||||
salesPerson: 'Comercial',
|
||||
contactChannel: 'Canal de contacto',
|
||||
},
|
||||
},
|
||||
ticket: {
|
||||
pageTitles: {
|
||||
tickets: 'Tickets',
|
||||
list: 'Listado',
|
||||
createTicket: 'Crear ticket',
|
||||
summary: 'Resumen',
|
||||
basicData: 'Datos básicos',
|
||||
boxing: 'Encajado',
|
||||
},
|
||||
list: {
|
||||
nickname: 'Alias',
|
||||
state: 'Estado',
|
||||
shipped: 'Enviado',
|
||||
landed: 'Entregado',
|
||||
salesPerson: 'Comercial',
|
||||
total: 'Total',
|
||||
},
|
||||
card: {
|
||||
ticketId: 'ID ticket',
|
||||
state: 'Estado',
|
||||
customerId: 'ID cliente',
|
||||
salesPerson: 'Comercial',
|
||||
agency: 'Agencia',
|
||||
shipped: 'Enviado',
|
||||
warehouse: 'Almacén',
|
||||
customerCard: 'Ficha del cliente',
|
||||
},
|
||||
boxing: {
|
||||
expedition: 'Expedición',
|
||||
item: 'Artículo',
|
||||
created: 'Creado',
|
||||
worker: 'Trabajador',
|
||||
selectTime: 'Seleccionar hora:',
|
||||
selectVideo: 'Seleccionar vídeo:',
|
||||
notFound: 'No hay vídeos disponibles',
|
||||
},
|
||||
summary: {
|
||||
state: 'Estado',
|
||||
salesPerson: 'Comercial',
|
||||
agency: 'Agencia',
|
||||
zone: 'Zona',
|
||||
warehouse: 'Almacén',
|
||||
route: 'Ruta',
|
||||
invoice: 'Factura',
|
||||
shipped: 'Enviado',
|
||||
landed: 'Entregado',
|
||||
packages: 'Bultos',
|
||||
consigneePhone: 'Tel. consignatario',
|
||||
consigneeMobile: 'Móv. consignatario',
|
||||
clientPhone: 'Tel. cliente',
|
||||
clientMobile: 'Móv. cliente',
|
||||
consignee: 'Consignatario',
|
||||
subtotal: 'Subtotal',
|
||||
vat: 'IVA',
|
||||
total: 'Total',
|
||||
saleLines: 'Líneas del pedido',
|
||||
item: 'Artículo',
|
||||
visible: 'Visible',
|
||||
available: 'Disponible',
|
||||
quantity: 'Cantidad',
|
||||
description: 'Descripción',
|
||||
price: 'Precio',
|
||||
discount: 'Descuento',
|
||||
amount: 'Importe',
|
||||
packing: 'Encajado',
|
||||
hasComponentLack: 'Faltan componentes',
|
||||
itemShortage: 'No visible',
|
||||
claim: 'Reclamación',
|
||||
reserved: 'Reservado',
|
||||
created: 'Fecha creación',
|
||||
package: 'Embalaje',
|
||||
taxClass: 'Tipo IVA',
|
||||
services: 'Servicios',
|
||||
changeState: 'Cambiar estado',
|
||||
requester: 'Solicitante',
|
||||
atender: 'Comprador',
|
||||
request: 'Petición de compra',
|
||||
goTo: 'Ir a'
|
||||
}
|
||||
},
|
||||
claim: {
|
||||
pageTitles: {
|
||||
claims: 'Reclamaciones',
|
||||
list: 'Listado',
|
||||
createClaim: 'Crear reclamación',
|
||||
rmaList: 'RMA',
|
||||
summary: 'Resumen',
|
||||
basicData: 'Datos básicos',
|
||||
rma: 'RMA',
|
||||
},
|
||||
list: {
|
||||
customer: 'Cliente',
|
||||
assignedTo: 'Asignada a',
|
||||
created: 'Creada',
|
||||
state: 'Estado',
|
||||
},
|
||||
rmaList: {
|
||||
code: 'Código',
|
||||
records: 'registros',
|
||||
},
|
||||
rma: {
|
||||
user: 'Usuario',
|
||||
created: 'Creado',
|
||||
},
|
||||
card: {
|
||||
claimId: 'ID reclamación',
|
||||
assignedTo: 'Asignada a',
|
||||
created: 'Creada',
|
||||
state: 'Estado',
|
||||
ticketId: 'ID ticket',
|
||||
customerSummary: 'Resumen del cliente',
|
||||
claimedTicket: 'Ticket reclamado',
|
||||
},
|
||||
summary: {
|
||||
customer: 'Cliente',
|
||||
assignedTo: 'Asignada a',
|
||||
attendedBy: 'Atendida por',
|
||||
created: 'Creada',
|
||||
state: 'Estado',
|
||||
details: 'Detalles',
|
||||
item: 'Artículo',
|
||||
landed: 'Entregado',
|
||||
quantity: 'Cantidad',
|
||||
claimed: 'Reclamado',
|
||||
description: 'Descripción',
|
||||
price: 'Precio',
|
||||
discount: 'Descuento',
|
||||
total: 'Total',
|
||||
actions: 'Acciones',
|
||||
responsibility: 'Responsabilidad',
|
||||
company: 'Empresa',
|
||||
person: 'Comercial/Cliente',
|
||||
},
|
||||
basicData: {
|
||||
customer: 'Cliente',
|
||||
assignedTo: 'Asignada a',
|
||||
created: 'Creada',
|
||||
state: 'Estado',
|
||||
packages: 'Bultos',
|
||||
picked: 'Recogida',
|
||||
returnOfMaterial: 'Autorización de retorno de materiales (RMA)',
|
||||
},
|
||||
},
|
||||
invoiceOut: {
|
||||
pageTitles: {
|
||||
invoiceOuts: 'Fact. emitidas',
|
||||
list: 'Listado',
|
||||
createInvoiceOut: 'Crear fact. emitida',
|
||||
summary: 'Resumen',
|
||||
basicData: 'Datos básicos'
|
||||
},
|
||||
list: {
|
||||
ref: 'Referencia',
|
||||
issued: 'Fecha emisión',
|
||||
amount: 'Importe',
|
||||
client: 'Cliente',
|
||||
created: 'Fecha creación',
|
||||
company: 'Empresa',
|
||||
dued: 'Fecha vencimineto'
|
||||
},
|
||||
card: {
|
||||
issued: 'Fecha emisión',
|
||||
amount: 'Importe',
|
||||
client: 'Cliente',
|
||||
company: 'Empresa',
|
||||
customerCard: 'Ficha del cliente',
|
||||
ticketList: 'Listado de tickets'
|
||||
},
|
||||
summary: {
|
||||
issued: 'Fecha',
|
||||
created: 'Fecha creación',
|
||||
dued: 'Vencimiento',
|
||||
booked: 'Contabilizada',
|
||||
company: 'Empresa',
|
||||
taxBreakdown: 'Desglose impositivo',
|
||||
type: 'Tipo',
|
||||
taxableBase: 'Base imp.',
|
||||
rate: 'Tarifa',
|
||||
fee: 'Cuota',
|
||||
tickets: 'Tickets',
|
||||
ticketId: 'Id ticket',
|
||||
nickname: 'Alias',
|
||||
shipped: 'F. envío',
|
||||
totalWithVat: 'Importe',
|
||||
|
||||
}
|
||||
},
|
||||
components: {
|
||||
topbar: {},
|
||||
userPanel: {
|
||||
settings: 'Configuración',
|
||||
logOut: 'Cerrar sesión',
|
||||
},
|
||||
smartCard: {
|
||||
noData: 'Sin datos que mostrar',
|
||||
openCard: 'Ver ficha',
|
||||
openSummary: 'Abrir detalles',
|
||||
viewDescription: 'Ver descripción',
|
||||
},
|
||||
cardDescriptor: {
|
||||
mainList: 'Listado principal',
|
||||
summary: 'Resumen',
|
||||
moreOptions: 'Más opciones',
|
||||
},
|
||||
leftMenu: {
|
||||
addToPinned: 'Añadir a fijados',
|
||||
removeFromPinned: 'Eliminar de fijados',
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,5 +1,7 @@
|
|||
import enUS from './en-US';
|
||||
import en from './en';
|
||||
import es from './es';
|
||||
|
||||
export default {
|
||||
'en-US': enUS,
|
||||
en: en,
|
||||
es: es,
|
||||
};
|
||||
|
|
|
@ -1,95 +1,16 @@
|
|||
<script setup>
|
||||
import { useQuasar } from 'quasar';
|
||||
import Navbar from 'src/components/NavBar.vue';
|
||||
|
||||
const quasar = useQuasar();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-layout view="lHh Lpr lFf">
|
||||
<q-header elevated>
|
||||
<q-toolbar>
|
||||
<q-btn flat dense round icon="menu" aria-label="Menu" @click="toggleLeftDrawer" />
|
||||
|
||||
<q-toolbar-title> Quasar App </q-toolbar-title>
|
||||
|
||||
<div>Quasar v{{ $q.version }}</div>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-drawer v-model="leftDrawerOpen" show-if-above bordered>
|
||||
<q-list>
|
||||
<q-item-label header> Essential Links </q-item-label>
|
||||
|
||||
<EssentialLink v-for="link in essentialLinks" :key="link.title" v-bind="link" />
|
||||
</q-list>
|
||||
</q-drawer>
|
||||
|
||||
<q-page-container>
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
<q-layout view="hHh LpR fFf">
|
||||
<Navbar />
|
||||
<router-view></router-view>
|
||||
<q-footer v-if="quasar.platform.is.mobile"></q-footer>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import EssentialLink from 'components/EssentialLink.vue';
|
||||
|
||||
const linksList = [
|
||||
{
|
||||
title: 'Docs',
|
||||
caption: 'quasar.dev',
|
||||
icon: 'school',
|
||||
link: 'https://quasar.dev',
|
||||
},
|
||||
{
|
||||
title: 'Github',
|
||||
caption: 'github.com/quasarframework',
|
||||
icon: 'code',
|
||||
link: 'https://github.com/quasarframework',
|
||||
},
|
||||
{
|
||||
title: 'Discord Chat Channel',
|
||||
caption: 'chat.quasar.dev',
|
||||
icon: 'chat',
|
||||
link: 'https://chat.quasar.dev',
|
||||
},
|
||||
{
|
||||
title: 'Forum',
|
||||
caption: 'forum.quasar.dev',
|
||||
icon: 'record_voice_over',
|
||||
link: 'https://forum.quasar.dev',
|
||||
},
|
||||
{
|
||||
title: 'Twitter',
|
||||
caption: '@quasarframework',
|
||||
icon: 'rss_feed',
|
||||
link: 'https://twitter.quasar.dev',
|
||||
},
|
||||
{
|
||||
title: 'Facebook',
|
||||
caption: '@QuasarFramework',
|
||||
icon: 'public',
|
||||
link: 'https://facebook.quasar.dev',
|
||||
},
|
||||
{
|
||||
title: 'Quasar Awesome',
|
||||
caption: 'Community Quasar projects',
|
||||
icon: 'favorite',
|
||||
link: 'https://awesome.quasar.dev',
|
||||
},
|
||||
];
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MainLayout',
|
||||
|
||||
components: {
|
||||
EssentialLink,
|
||||
},
|
||||
|
||||
setup() {
|
||||
const leftDrawerOpen = ref(false);
|
||||
|
||||
return {
|
||||
essentialLinks: linksList,
|
||||
leftDrawerOpen,
|
||||
toggleLeftDrawer() {
|
||||
leftDrawerOpen.value = !leftDrawerOpen.value;
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useSession } from 'src/composables/useSession';
|
||||
import FetchData from 'components/FetchData.vue';
|
||||
import FormModel from 'components/FormModel.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const session = useSession();
|
||||
const token = session.getToken();
|
||||
|
||||
const claimFilter = {
|
||||
include: [
|
||||
{
|
||||
relation: 'client',
|
||||
scope: {
|
||||
fields: ['name'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const workers = ref([]);
|
||||
const workersCopy = ref([]);
|
||||
const claimStates = ref([]);
|
||||
const claimStatesCopy = ref([]);
|
||||
|
||||
function setWorkers(data) {
|
||||
workers.value = data;
|
||||
workersCopy.value = data;
|
||||
}
|
||||
|
||||
function setClaimStates(data) {
|
||||
claimStates.value = data;
|
||||
claimStatesCopy.value = data;
|
||||
}
|
||||
|
||||
const workerFilter = {
|
||||
options: workers,
|
||||
filterFn: (options, value) => {
|
||||
const search = value.toLowerCase();
|
||||
|
||||
if (value === '') return workersCopy.value;
|
||||
|
||||
return options.value.filter((row) => {
|
||||
const id = row.id;
|
||||
const name = row.name.toLowerCase();
|
||||
|
||||
const idMatches = id == search;
|
||||
const nameMatches = name.indexOf(search) > -1;
|
||||
|
||||
return idMatches || nameMatches;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const statesFilter = {
|
||||
options: claimStates,
|
||||
filterFn: (options, value) => {
|
||||
const search = value.toLowerCase();
|
||||
|
||||
if (value === '') return claimStatesCopy.value;
|
||||
|
||||
return options.value.filter((row) => {
|
||||
const description = row.description.toLowerCase();
|
||||
|
||||
return description.indexOf(search) > -1;
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<fetch-data
|
||||
url="Workers/activeWithInheritedRole"
|
||||
:filter="{ where: { role: 'salesPerson' } }"
|
||||
@on-fetch="setWorkers"
|
||||
auto-load
|
||||
/>
|
||||
<fetch-data url="ClaimStates" @on-fetch="setClaimStates" auto-load />
|
||||
|
||||
<div class="container">
|
||||
<q-card>
|
||||
<form-model :url="`Claims/${route.params.id}`" :filter="claimFilter" model="claim">
|
||||
<template #form="{ data, validate, filter }">
|
||||
<div class="row q-gutter-md q-mb-md">
|
||||
<div class="col">
|
||||
<q-input v-model="data.client.name" :label="t('claim.basicData.customer')" disable />
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-input v-model="data.created" mask="####-##-##" fill-mask="_" autofocus>
|
||||
<template #append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||
<q-date v-model="data.created" mask="YYYY-MM-DD">
|
||||
<div class="row items-center justify-end">
|
||||
<q-btn v-close-popup label="Close" color="primary" flat />
|
||||
</div>
|
||||
</q-date>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-gutter-md q-mb-md">
|
||||
<div class="col">
|
||||
<q-select
|
||||
v-model="data.workerFk"
|
||||
:options="workers"
|
||||
option-value="id"
|
||||
option-label="name"
|
||||
emit-value
|
||||
:label="t('claim.basicData.assignedTo')"
|
||||
map-options
|
||||
use-input
|
||||
@filter="(value, update) => filter(value, update, workerFilter)"
|
||||
:rules="validate('claim.claimStateFk')"
|
||||
:input-debounce="0"
|
||||
>
|
||||
<template #before>
|
||||
<q-avatar color="orange">
|
||||
<q-img
|
||||
v-if="data.workerFk"
|
||||
:src="`/api/Images/user/160x160/${data.workerFk}/download?access_token=${token}`"
|
||||
spinner-color="white"
|
||||
/>
|
||||
</q-avatar>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-select
|
||||
v-model="data.claimStateFk"
|
||||
:options="claimStates"
|
||||
option-value="id"
|
||||
option-label="description"
|
||||
emit-value
|
||||
:label="t('claim.basicData.state')"
|
||||
map-options
|
||||
use-input
|
||||
@filter="(value, update) => filter(value, update, statesFilter)"
|
||||
:rules="validate('claim.claimStateFk')"
|
||||
:input-debounce="0"
|
||||
>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-gutter-md q-mb-md">
|
||||
<div class="col">
|
||||
<q-input
|
||||
v-model="data.packages"
|
||||
:label="t('claim.basicData.packages')"
|
||||
:rules="validate('claim.packages')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-input
|
||||
v-model="data.rma"
|
||||
:label="t('claim.basicData.returnOfMaterial')"
|
||||
:rules="validate('claim.rma')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-gutter-md q-mb-md">
|
||||
<div class="col">
|
||||
<q-checkbox v-model="data.hasToPickUp" :label="t('claim.basicData.picked')" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</form-model>
|
||||
</q-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.q-card {
|
||||
width: 800px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,50 @@
|
|||
<script setup>
|
||||
import { useState } from 'composables/useState';
|
||||
import ClaimDescriptor from './ClaimDescriptor.vue';
|
||||
import LeftMenu from 'components/LeftMenu.vue';
|
||||
|
||||
const state = useState();
|
||||
</script>
|
||||
<template>
|
||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||
<q-scroll-area class="fit">
|
||||
<claim-descriptor />
|
||||
<q-separator />
|
||||
<left-menu source="card" />
|
||||
</q-scroll-area>
|
||||
</q-drawer>
|
||||
<q-page-container>
|
||||
<q-page class="q-pa-md">
|
||||
<router-view></router-view>
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.q-scrollarea__content {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.descriptor {
|
||||
max-width: 256px;
|
||||
|
||||
h5 {
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.q-card__actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#descriptor-skeleton .q-card__actions {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,120 @@
|
|||
<script setup>
|
||||
import { onMounted, computed, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { toDate } from 'src/filters';
|
||||
import axios from 'axios';
|
||||
import TicketDescriptorPopover from 'pages/Ticket/Card/TicketDescriptorPopover.vue';
|
||||
import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue';
|
||||
import CardDescriptor from 'components/ui/CardDescriptor.vue';
|
||||
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
|
||||
|
||||
const $props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await fetch();
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
const entityId = computed(() => {
|
||||
return $props.id || route.params.id;
|
||||
});
|
||||
|
||||
const claim = ref();
|
||||
async function fetch() {
|
||||
const filter = {
|
||||
include: [
|
||||
{ relation: 'client' },
|
||||
{ relation: 'claimState' },
|
||||
{
|
||||
relation: 'claimState',
|
||||
},
|
||||
{
|
||||
relation: 'worker',
|
||||
scope: {
|
||||
include: { relation: 'user' },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const options = { params: { filter } };
|
||||
const { data } = await axios.get(`Claims/${entityId.value}`, options);
|
||||
|
||||
if (data) claim.value = data;
|
||||
}
|
||||
|
||||
function stateColor(code) {
|
||||
if (code === 'pending') return 'green';
|
||||
if (code === 'managed') return 'orange';
|
||||
if (code === 'resolved') return 'red';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<skeleton-descriptor v-if="!claim" />
|
||||
<card-descriptor v-if="claim" module="Claim" :data="claim" :description="claim.client.name">
|
||||
<template #menu>
|
||||
<claim-descriptor-menu v-if="claim" :claim="claim" />
|
||||
</template>
|
||||
<template #body>
|
||||
<q-list>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.card.created') }}</q-item-label>
|
||||
<q-item-label>{{ toDate(claim.created) }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.card.state') }}</q-item-label>
|
||||
<q-item-label>
|
||||
<q-chip :color="stateColor(claim.claimState.code)" dense>
|
||||
{{ claim.claimState.description }}
|
||||
</q-chip>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.card.ticketId') }}</q-item-label>
|
||||
<q-item-label class="link">
|
||||
{{ claim.ticketFk }}
|
||||
<q-popup-proxy>
|
||||
<ticket-descriptor-popover :id="claim.ticketFk" />
|
||||
</q-popup-proxy>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.card.assignedTo') }}</q-item-label>
|
||||
<q-item-label>{{ claim.worker.user.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<q-card-actions>
|
||||
<q-btn
|
||||
size="md"
|
||||
icon="vn:client"
|
||||
color="primary"
|
||||
:to="{ name: 'CustomerCard', params: { id: claim.clientFk } }"
|
||||
>
|
||||
<q-tooltip>{{ t('claim.card.customerSummary') }}</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
size="md"
|
||||
icon="vn:ticket"
|
||||
color="primary"
|
||||
:to="{ name: 'TicketCard', params: { id: claim.ticketFk } }"
|
||||
>
|
||||
<q-tooltip>{{ t('claim.card.claimedTicket') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
</template>
|
||||
</card-descriptor>
|
||||
</template>
|
|
@ -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>
|
|
@ -0,0 +1,159 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useRoute } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import Paginate from 'src/components/PaginateData.vue';
|
||||
import FetchData from 'components/FetchData.vue';
|
||||
import TeleportSlot from 'components/ui/TeleportSlot';
|
||||
import { toDate } from 'src/filters';
|
||||
|
||||
const quasar = useQuasar();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
const claim = ref([]);
|
||||
const fetcher = ref();
|
||||
|
||||
const filter = {
|
||||
include: {
|
||||
relation: 'rmas',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'worker',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'user',
|
||||
},
|
||||
},
|
||||
},
|
||||
order: 'created DESC',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async function addRow() {
|
||||
const formData = {
|
||||
code: claim.value.rma,
|
||||
};
|
||||
|
||||
await axios.post(`ClaimRmas`, formData);
|
||||
await fetcher.value.fetch();
|
||||
|
||||
quasar.notify({
|
||||
type: 'positive',
|
||||
message: t('globals.rowAdded'),
|
||||
icon: 'check',
|
||||
});
|
||||
}
|
||||
|
||||
const confirmShown = ref(false);
|
||||
const rmaId = ref(null);
|
||||
function confirmRemove(id) {
|
||||
confirmShown.value = true;
|
||||
rmaId.value = id;
|
||||
}
|
||||
|
||||
async function remove() {
|
||||
const id = rmaId.value;
|
||||
|
||||
await axios.delete(`ClaimRmas/${id}`);
|
||||
await fetcher.value.fetch();
|
||||
confirmShown.value = false;
|
||||
|
||||
quasar.notify({
|
||||
type: 'positive',
|
||||
message: t('globals.rowRemoved'),
|
||||
icon: 'check',
|
||||
});
|
||||
}
|
||||
|
||||
function hide() {
|
||||
rmaId.value = null;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<fetch-data
|
||||
ref="fetcher"
|
||||
:url="`Claims/${route.params.id}`"
|
||||
:filter="filter"
|
||||
@on-fetch="(data) => (claim = data)"
|
||||
auto-load
|
||||
/>
|
||||
<paginate :data="claim.rmas">
|
||||
<template #body="{ rows }">
|
||||
<q-card class="card">
|
||||
<template v-for="row of rows" :key="row.id">
|
||||
<q-item class="q-pa-none items-start">
|
||||
<q-item-section class="q-pa-md">
|
||||
<q-list>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.rma.user') }}</q-item-label>
|
||||
<q-item-label>{{ row.worker.user.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.rma.created') }}</q-item-label>
|
||||
<q-item-label>
|
||||
{{ toDate(row.created, { timeStyle: 'medium' }) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-item-section>
|
||||
<q-card-actions vertical class="justify-between">
|
||||
<q-btn flat round color="orange" icon="vn:bin" @click="confirmRemove(row.id)">
|
||||
<q-tooltip>{{ t('globals.remove') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
</template>
|
||||
</q-card>
|
||||
</template>
|
||||
</paginate>
|
||||
|
||||
<q-dialog v-model="confirmShown" persistent @hide="hide">
|
||||
<q-card>
|
||||
<q-card-section class="row items-center">
|
||||
<q-avatar icon="warning" color="primary" text-color="white" />
|
||||
<span class="q-ml-sm">{{ t('globals.confirmRemove') }}</span>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat :label="t('globals.no')" color="primary" v-close-popup autofocus />
|
||||
<q-btn flat :label="t('globals.yes')" color="primary" @click="remove()" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<teleport-slot v-if="!quasar.platform.is.mobile" to="#header-actions">
|
||||
<div class="row q-gutter-x-sm">
|
||||
<q-btn @click="addRow()" icon="add" color="primary" dense rounded>
|
||||
<q-tooltip bottom> {{ t('globals.add') }} </q-tooltip>
|
||||
</q-btn>
|
||||
<q-separator vertical />
|
||||
</div>
|
||||
</teleport-slot>
|
||||
|
||||
<teleport-slot to=".q-footer">
|
||||
<q-tabs align="justify" inline-label narrow-indicator>
|
||||
<q-tab @click="addRow()" icon="add_circle" :label="t('globals.add')" />
|
||||
</q-tabs>
|
||||
</teleport-slot>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.q-toolbar {
|
||||
background-color: $grey-9;
|
||||
}
|
||||
.sticky-page {
|
||||
padding-top: 66px;
|
||||
}
|
||||
|
||||
.q-page-sticky {
|
||||
z-index: 2998;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,199 @@
|
|||
<script setup>
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import axios from 'axios';
|
||||
import { toDate, toCurrency } from 'src/filters';
|
||||
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
|
||||
|
||||
onMounted(() => fetch());
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
const $props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const entityId = computed(() => $props.id || route.params.id);
|
||||
|
||||
const claim = ref(null);
|
||||
const salesClaimed = ref(null);
|
||||
function fetch() {
|
||||
const id = entityId.value;
|
||||
axios.get(`Claims/${id}/getSummary`).then(({ data }) => {
|
||||
claim.value = data.claim;
|
||||
salesClaimed.value = data.salesClaimed;
|
||||
});
|
||||
}
|
||||
|
||||
const detailsColumns = ref([
|
||||
{
|
||||
name: 'item',
|
||||
label: 'claim.summary.item',
|
||||
field: (row) => row.sale.itemFk,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'landed',
|
||||
label: 'claim.summary.landed',
|
||||
field: (row) => row.sale.ticket.landed,
|
||||
format: (value) => toDate(value),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'quantity',
|
||||
label: 'claim.summary.quantity',
|
||||
field: (row) => row.sale.quantity,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'claimed',
|
||||
label: 'claim.summary.claimed',
|
||||
field: (row) => row.quantity,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'claim.summary.description',
|
||||
field: (row) => row.sale.concept,
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
label: 'claim.summary.price',
|
||||
field: (row) => row.sale.price,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'discount',
|
||||
label: 'claim.summary.discount',
|
||||
field: (row) => row.sale.discount,
|
||||
format: (value) => `${value} %`,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'total',
|
||||
label: 'claim.summary.total',
|
||||
field: ({ sale }) => toCurrency(sale.quantity * sale.price * ((100 - sale.discount) / 100)),
|
||||
sortable: true,
|
||||
},
|
||||
]);
|
||||
|
||||
function stateColor(code) {
|
||||
if (code === 'pending') return 'green';
|
||||
if (code === 'managed') return 'orange';
|
||||
if (code === 'resolved') return 'red';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="summary container">
|
||||
<q-card>
|
||||
<skeleton-summary v-if="!claim" />
|
||||
<template v-if="claim">
|
||||
<div class="header bg-primary q-pa-sm q-mb-md">{{ claim.id }} - {{ claim.client.name }}</div>
|
||||
<q-list>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.summary.created') }}</q-item-label>
|
||||
<q-item-label>{{ toDate(claim.created) }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.summary.state') }}</q-item-label>
|
||||
<q-item-label>
|
||||
<q-chip :color="stateColor(claim.claimState.code)" dense>
|
||||
{{ claim.claimState.description }}
|
||||
</q-chip>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.summary.assignedTo') }}</q-item-label>
|
||||
<q-item-label>{{ claim.worker.user.nickname }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.summary.attendedBy') }}</q-item-label>
|
||||
<q-item-label>{{ claim.client.salesPersonUser.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-card-section class="q-pa-md">
|
||||
<h6>{{ t('claim.summary.details') }}</h6>
|
||||
<q-table :columns="detailsColumns" :rows="salesClaimed" flat>
|
||||
<template #header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ t(col.label) }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-md">
|
||||
<h6>{{ t('claim.summary.actions') }}</h6>
|
||||
<q-separator />
|
||||
<div id="slider-container">
|
||||
<q-slider
|
||||
v-model="claim.responsibility"
|
||||
label
|
||||
:label-value="t('claim.summary.responsibility')"
|
||||
label-always
|
||||
color="primary"
|
||||
markers
|
||||
:marker-labels="[
|
||||
{ value: 1, label: t('claim.summary.company') },
|
||||
{ value: 5, label: t('claim.summary.person') },
|
||||
]"
|
||||
:min="1"
|
||||
:max="5"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</template>
|
||||
</q-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.q-card {
|
||||
width: 100%;
|
||||
max-width: 950px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
.header {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#slider-container {
|
||||
max-width: 80%;
|
||||
margin: 0 auto;
|
||||
|
||||
.q-slider {
|
||||
.q-slider__marker-labels:nth-child(1) {
|
||||
transform: none;
|
||||
}
|
||||
.q-slider__marker-labels:nth-child(2) {
|
||||
transform: none;
|
||||
left: auto !important;
|
||||
right: 0%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.q-dialog .summary {
|
||||
max-width: 1200px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,29 @@
|
|||
<script setup>
|
||||
import { useDialogPluginComponent } from 'quasar';
|
||||
import ClaimSummary from './ClaimSummary.vue';
|
||||
|
||||
const $props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||
<claim-summary v-if="$props.id" :id="$props.id" />
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.q-dialog .summary .header {
|
||||
position: sticky;
|
||||
z-index: $z-max;
|
||||
top: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,132 @@
|
|||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useQuasar } from 'quasar';
|
||||
import Paginate from 'src/components/PaginateData.vue';
|
||||
import { toDate } from 'src/filters/index';
|
||||
import ClaimSummaryDialog from './Card/ClaimSummaryDialog.vue';
|
||||
import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const quasar = useQuasar();
|
||||
const { t } = useI18n();
|
||||
|
||||
const filter = {
|
||||
include: [
|
||||
{
|
||||
relation: 'client',
|
||||
},
|
||||
{
|
||||
relation: 'claimState',
|
||||
},
|
||||
{
|
||||
relation: 'worker',
|
||||
scope: {
|
||||
include: { relation: 'user' },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function stateColor(code) {
|
||||
if (code === 'pending') return 'green';
|
||||
if (code === 'managed') return 'orange';
|
||||
if (code === 'resolved') return 'red';
|
||||
}
|
||||
|
||||
function navigate(id) {
|
||||
router.push({ path: `/claim/${id}` });
|
||||
}
|
||||
|
||||
function viewSummary(id) {
|
||||
quasar.dialog({
|
||||
component: ClaimSummaryDialog,
|
||||
componentProps: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page class="q-pa-md">
|
||||
<paginate url="/Claims" :filter="filter" sort-by="id DESC" auto-load>
|
||||
<template #body="{ rows }">
|
||||
<q-card class="card" v-for="row of rows" :key="row.id">
|
||||
<q-item class="q-pa-none items-start cursor-pointer q-hoverable" v-ripple clickable>
|
||||
<q-item-section class="q-pa-md" @click="navigate(row.id)">
|
||||
<div class="text-h6 link">
|
||||
{{ row.client.name }}
|
||||
<q-popup-proxy>
|
||||
<customer-descriptor-popover :customer="row.client" />
|
||||
</q-popup-proxy>
|
||||
</div>
|
||||
<q-item-label caption>#{{ row.id }}</q-item-label>
|
||||
<q-list>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.list.customer') }}</q-item-label>
|
||||
<q-item-label>{{ row.client.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.list.assignedTo') }}</q-item-label>
|
||||
<q-item-label>{{ row.worker.user.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.list.created') }}</q-item-label>
|
||||
<q-item-label>{{ toDate(row.created) }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.list.state') }}</q-item-label>
|
||||
<q-item-label>
|
||||
<q-chip :color="stateColor(row.claimState.code)" dense>
|
||||
{{ row.claimState.description }}
|
||||
</q-chip>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-item-section>
|
||||
<q-separator vertical />
|
||||
<q-card-actions vertical class="justify-between">
|
||||
<!-- <q-btn color="grey-7" round flat icon="more_vert">
|
||||
<q-tooltip>{{ t('customer.list.moreOptions') }}</q-tooltip>
|
||||
<q-menu cover auto-close>
|
||||
<q-list>
|
||||
<q-item clickable>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="add" />
|
||||
</q-item-section>
|
||||
<q-item-section>Add a note</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="logs" />
|
||||
</q-item-section>
|
||||
<q-item-section>Display claim logs</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn> -->
|
||||
|
||||
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
|
||||
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn flat round color="grey-7" icon="preview" @click="viewSummary(row.id)">
|
||||
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn flat round color="grey-7" icon="vn:client">
|
||||
<q-tooltip>{{ t('components.smartCard.viewDescription') }}</q-tooltip>
|
||||
<q-popup-proxy>
|
||||
<customer-descriptor-popover :customer="row.client" />
|
||||
</q-popup-proxy>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
</q-item>
|
||||
</q-card>
|
||||
</template>
|
||||
</paginate>
|
||||
</q-page>
|
||||
</template>
|
|
@ -0,0 +1,17 @@
|
|||
<script setup>
|
||||
import { useState } from 'src/composables/useState';
|
||||
import LeftMenu from 'components/LeftMenu.vue';
|
||||
|
||||
const state = useState();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||
<q-scroll-area class="fit text-grey-8">
|
||||
<LeftMenu />
|
||||
</q-scroll-area>
|
||||
</q-drawer>
|
||||
<q-page-container>
|
||||
<router-view></router-view>
|
||||
</q-page-container>
|
||||
</template>
|
|
@ -0,0 +1,143 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useQuasar } from 'quasar';
|
||||
import axios from 'axios';
|
||||
import Paginate from 'src/components/PaginateData.vue';
|
||||
|
||||
const quasar = useQuasar();
|
||||
const { t } = useI18n();
|
||||
|
||||
const rmas = ref([]);
|
||||
const card = ref(null);
|
||||
|
||||
function onFetch(data) {
|
||||
rmas.value = data.value;
|
||||
}
|
||||
|
||||
const newRma = ref({
|
||||
code: '',
|
||||
crated: new Date(),
|
||||
});
|
||||
|
||||
function onInputUpdate(value) {
|
||||
newRma.value.code = value.toUpperCase();
|
||||
}
|
||||
|
||||
function submit() {
|
||||
const formData = newRma.value;
|
||||
if (formData.code === '') return;
|
||||
|
||||
axios
|
||||
.post('ClaimRmas', formData)
|
||||
.then(() => {
|
||||
newRma.value = {
|
||||
code: '',
|
||||
crated: new Date(),
|
||||
};
|
||||
})
|
||||
.then(() => card.value.refresh());
|
||||
}
|
||||
|
||||
const confirmShown = ref(false);
|
||||
const rmaId = ref(null);
|
||||
function confirm(id) {
|
||||
confirmShown.value = true;
|
||||
rmaId.value = id;
|
||||
}
|
||||
|
||||
function remove() {
|
||||
const id = rmaId.value;
|
||||
axios
|
||||
.delete(`ClaimRmas/${id}`)
|
||||
.then(() => {
|
||||
confirmShown.value = false;
|
||||
|
||||
quasar.notify({
|
||||
type: 'positive',
|
||||
message: 'Entry deleted',
|
||||
icon: 'check',
|
||||
});
|
||||
})
|
||||
.then(() => card.value.refresh());
|
||||
}
|
||||
|
||||
function hide() {
|
||||
rmaId.value = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page class="q-pa-md sticky">
|
||||
<q-page-sticky expand position="top" :offset="[16, 16]">
|
||||
<q-card class="card q-pa-md">
|
||||
<q-form @submit="submit">
|
||||
<q-input
|
||||
v-model="newRma.code"
|
||||
:label="t('claim.rmaList.code')"
|
||||
@update:model-value="onInputUpdate"
|
||||
class="q-mb-md"
|
||||
autofocus
|
||||
/>
|
||||
<div class="text-caption">{{ rmas.length }} {{ t('claim.rmaList.records') }}</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-page-sticky>
|
||||
|
||||
<paginate ref="card" url="/ClaimRmas" @on-fetch="onFetch" sort-by="id DESC" auto-load>
|
||||
<template #body="{ rows }">
|
||||
<q-card class="card">
|
||||
<template v-for="row of rows" :key="row.code">
|
||||
<q-item class="q-pa-none items-start">
|
||||
<q-item-section class="q-pa-md">
|
||||
<q-list>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.rmaList.code') }}</q-item-label>
|
||||
<q-item-label>{{ row.code }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-item-section>
|
||||
<q-card-actions vertical class="justify-between">
|
||||
<q-btn flat round color="primary" icon="vn:bin" @click="confirm(row.id)">
|
||||
<q-tooltip>{{ t('globals.remove') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
</template>
|
||||
</q-card>
|
||||
</template>
|
||||
</paginate>
|
||||
</q-page>
|
||||
|
||||
<q-dialog v-model="confirmShown" persistent @hide="hide">
|
||||
<q-card>
|
||||
<q-card-section class="row items-center">
|
||||
<q-avatar icon="warning" color="primary" text-color="white" />
|
||||
<span class="q-ml-sm">{{ t('globals.confirmRemove') }}</span>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat :label="t('globals.no')" color="primary" v-close-popup autofocus />
|
||||
<q-btn flat :label="t('globals.yes')" color="primary" @click="remove()" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sticky {
|
||||
padding-top: 156px;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 60em;
|
||||
}
|
||||
|
||||
.q-page-sticky {
|
||||
z-index: 2998;
|
||||
}
|
||||
</style>
|
|
@ -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' }
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,172 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useSession } from 'src/composables/useSession';
|
||||
import FetchData from 'components/FetchData.vue';
|
||||
import FormModel from 'components/FormModel.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const session = useSession();
|
||||
const token = session.getToken();
|
||||
|
||||
const workers = ref([]);
|
||||
const workersCopy = ref([]);
|
||||
const businessTypes = ref([]);
|
||||
const contactChannels = ref([]);
|
||||
|
||||
function setWorkers(data) {
|
||||
workers.value = data;
|
||||
workersCopy.value = data;
|
||||
}
|
||||
|
||||
const filterOptions = {
|
||||
options: workers,
|
||||
filterFn: (options, value) => {
|
||||
const search = value.toLowerCase();
|
||||
|
||||
if (value === '') return workersCopy.value;
|
||||
|
||||
return options.value.filter((row) => {
|
||||
const id = row.id;
|
||||
const name = row.name.toLowerCase();
|
||||
|
||||
const idMatches = id === search;
|
||||
const nameMatches = name.indexOf(search) > -1;
|
||||
|
||||
return idMatches || nameMatches;
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<fetch-data
|
||||
url="Workers/activeWithInheritedRole"
|
||||
:filter="{ where: { role: 'salesPerson' } }"
|
||||
@on-fetch="setWorkers"
|
||||
auto-load
|
||||
/>
|
||||
<fetch-data url="ContactChannels" @on-fetch="(data) => contactChannels = data" auto-load />
|
||||
<fetch-data url="BusinessTypes" @on-fetch="(data) => businessTypes = data" auto-load />
|
||||
<div class="container">
|
||||
<q-card>
|
||||
<form-model :url="`Clients/${route.params.id}`" model="customer">
|
||||
<template #form="{ data, validate, filter }">
|
||||
<div class="row q-gutter-md q-mb-md">
|
||||
<div class="col">
|
||||
<q-input
|
||||
v-model="data.socialName"
|
||||
:label="t('customer.basicData.socialName')"
|
||||
:rules="validate('client.socialName')"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-select
|
||||
v-model="data.businessTypeFk"
|
||||
:options="businessTypes"
|
||||
option-value="code"
|
||||
option-label="description"
|
||||
emit-value
|
||||
:label="t('customer.basicData.businessType')"
|
||||
map-options
|
||||
:rules="validate('client.businessTypeFk')"
|
||||
:input-debounce="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-gutter-md q-mb-md">
|
||||
<div class="col">
|
||||
<q-input
|
||||
v-model="data.contact"
|
||||
:label="t('customer.basicData.contact')"
|
||||
:rules="validate('client.contact')"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-input
|
||||
v-model="data.email"
|
||||
type="email"
|
||||
:label="t('customer.basicData.email')"
|
||||
:rules="validate('client.email')"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-gutter-md q-mb-md">
|
||||
<div class="col">
|
||||
<q-input
|
||||
v-model="data.phone"
|
||||
:label="t('customer.basicData.phone')"
|
||||
:rules="validate('client.phone')"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-input
|
||||
v-model="data.mobile"
|
||||
:label="t('customer.basicData.mobile')"
|
||||
:rules="validate('client.mobile')"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-gutter-md q-mb-md">
|
||||
<div class="col">
|
||||
<q-select
|
||||
v-model="data.salesPersonFk"
|
||||
:options="workers"
|
||||
option-value="id"
|
||||
option-label="name"
|
||||
emit-value
|
||||
:label="t('customer.basicData.salesPerson')"
|
||||
map-options
|
||||
use-input
|
||||
@filter="(value, update) => filter(value, update, filterOptions)"
|
||||
:rules="validate('client.salesPersonFk')"
|
||||
:input-debounce="0"
|
||||
>
|
||||
<template #before>
|
||||
<q-avatar color="orange">
|
||||
<q-img
|
||||
v-if="data.salesPersonFk"
|
||||
:src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`"
|
||||
spinner-color="white"
|
||||
/>
|
||||
</q-avatar>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-select
|
||||
v-model="data.contactChannelFk"
|
||||
:options="contactChannels"
|
||||
option-value="id"
|
||||
option-label="name"
|
||||
emit-value
|
||||
:label="t('customer.basicData.contactChannel')"
|
||||
map-options
|
||||
:rules="validate('client.contactChannelFk')"
|
||||
:input-debounce="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</form-model>
|
||||
</q-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.q-card {
|
||||
width: 800px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,21 @@
|
|||
<script setup>
|
||||
import { useState } from 'src/composables/useState';
|
||||
import CustomerDescriptor from './CustomerDescriptor.vue';
|
||||
import LeftMenu from 'components/LeftMenu.vue';
|
||||
|
||||
const state = useState();
|
||||
</script>
|
||||
<template>
|
||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||
<q-scroll-area class="fit">
|
||||
<customer-descriptor />
|
||||
<q-separator />
|
||||
<left-menu source="card" />
|
||||
</q-scroll-area>
|
||||
</q-drawer>
|
||||
<q-page-container>
|
||||
<q-page class="q-pa-md">
|
||||
<router-view></router-view>
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
</template>
|
|
@ -0,0 +1,113 @@
|
|||
<script setup>
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { toCurrency } from 'src/filters';
|
||||
import axios from 'axios';
|
||||
import CardDescriptor from 'components/ui/CardDescriptor.vue';
|
||||
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
|
||||
|
||||
const $props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await fetch();
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
const entityId = computed(() => {
|
||||
return $props.id || route.params.id;
|
||||
});
|
||||
|
||||
const customer = ref();
|
||||
async function fetch() {
|
||||
const { data } = await axios.get(`Clients/${entityId.value}/getCard`);
|
||||
|
||||
if (data) customer.value = data;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<skeleton-descriptor v-if="!customer" />
|
||||
<card-descriptor v-if="customer" module="Customer" :data="customer" :description="customer.name">
|
||||
<!-- <template #menu>
|
||||
<q-item clickable v-ripple>Option 1</q-item>
|
||||
<q-item clickable v-ripple>Option 2</q-item>
|
||||
</template> -->
|
||||
<template #body>
|
||||
<q-list>
|
||||
<q-item v-if="customer.salesPersonUser">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.card.salesPerson') }}</q-item-label>
|
||||
<q-item-label>{{ customer.salesPersonUser.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.card.credit') }}</q-item-label>
|
||||
<q-item-label>{{ toCurrency(customer.credit) }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.card.securedCredit') }}</q-item-label>
|
||||
<q-item-label>{{ toCurrency(customer.creditInsurance) }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section v-if="customer.payMethod">
|
||||
<q-item-label caption>{{ t('customer.card.payMethod') }}</q-item-label>
|
||||
<q-item-label>{{ customer.payMethod.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.card.debt') }}</q-item-label>
|
||||
<q-item-label>{{ toCurrency(customer.debt) }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-card-actions class="q-gutter-md">
|
||||
<q-icon v-if="customer.isActive == false" name="vn:disabled" size="xs" color="primary">
|
||||
<q-tooltip>{{ t('customer.card.isDisabled') }}</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon v-if="customer.isFreezed == true" name="vn:frozen" size="xs" color="primary">
|
||||
<q-tooltip>{{ t('customer.card.isFrozen') }}</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon v-if="customer.debt > customer.credit" name="vn:risk" size="xs" color="primary">
|
||||
<q-tooltip>{{ t('customer.card.hasDebt') }}</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon v-if="customer.isTaxDataChecked == false" name="vn:no036" size="xs" color="primary">
|
||||
<q-tooltip>{{ t('customer.card.notChecked') }}</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon v-if="customer.account.active == false" name="vn:noweb" size="xs" color="primary">
|
||||
<q-tooltip>{{ t('customer.card.noWebAccess') }}</q-tooltip>
|
||||
</q-icon>
|
||||
</q-card-actions>
|
||||
<!-- <q-card-actions>
|
||||
<q-btn size="md" icon="vn:ticket" color="primary">
|
||||
<q-tooltip>Ticket list</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn size="md" icon="vn:invoice-out" color="primary">
|
||||
<q-tooltip>Invoice Out list</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn size="md" icon="vn:basketadd" color="primary">
|
||||
<q-tooltip>Order list</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn size="md" icon="face" color="primary">
|
||||
<q-tooltip>View user</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn size="md" icon="expand_more" color="primary">
|
||||
<q-tooltip>More options</q-tooltip>
|
||||
</q-btn>
|
||||
</q-card-actions> -->
|
||||
</template>
|
||||
</card-descriptor>
|
||||
</template>
|
|
@ -0,0 +1,15 @@
|
|||
<script setup>
|
||||
import CustomerDescriptor from './CustomerDescriptor.vue';
|
||||
|
||||
const $props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<q-card>
|
||||
<customer-descriptor v-if="$props.id" :id="$props.id" />
|
||||
</q-card>
|
||||
</template>
|
|
@ -0,0 +1,492 @@
|
|||
<script setup>
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import axios from 'axios';
|
||||
import { toCurrency, toPercentage, toDate } from 'src/filters';
|
||||
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
|
||||
|
||||
onMounted(() => fetch());
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
const $props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const entityId = computed(() => $props.id || route.params.id);
|
||||
|
||||
const customer = ref(null);
|
||||
function fetch() {
|
||||
const id = entityId.value;
|
||||
axios.get(`Clients/${id}/summary`).then(({ data }) => {
|
||||
customer.value = data;
|
||||
});
|
||||
}
|
||||
|
||||
const balanceDue = computed(() => {
|
||||
return customer.value.defaulters.length && customer.value.defaulters[0].amount;
|
||||
});
|
||||
|
||||
const balanceDueWarning = computed(() => (balanceDue.value ? 'negative' : ''));
|
||||
|
||||
const claimRate = computed(() => {
|
||||
const data = customer.value;
|
||||
|
||||
return data.claimsRatio.claimingRate * 100;
|
||||
});
|
||||
|
||||
const priceIncreasingRate = computed(() => {
|
||||
const data = customer.value;
|
||||
|
||||
return data.claimsRatio.priceIncreasing / 100;
|
||||
});
|
||||
|
||||
const debtWarning = computed(() => {
|
||||
const data = customer.value;
|
||||
|
||||
return data.debt.debt > data.credit ? 'negative' : '';
|
||||
});
|
||||
|
||||
const creditWarning = computed(() => {
|
||||
const data = customer.value;
|
||||
const tooMuchInsurance = data.credit > data.creditInsurance;
|
||||
const noCreditInsurance = data.credit && data.creditInsurance == null;
|
||||
|
||||
return tooMuchInsurance || noCreditInsurance ? 'negative' : '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="summary container">
|
||||
<q-card>
|
||||
<skeleton-summary v-if="!customer" />
|
||||
<template v-if="customer">
|
||||
<div class="header bg-primary q-pa-sm q-mb-md">{{ customer.id }} - {{ customer.name }}</div>
|
||||
<div class="row q-pa-md q-col-gutter-md q-mb-md">
|
||||
<div class="col">
|
||||
<q-list>
|
||||
<q-item-label header class="text-h6">
|
||||
{{ t('customer.summary.basicData') }}
|
||||
<router-link
|
||||
:to="{ name: 'CustomerBasicData', params: { id: entityId } }"
|
||||
target="_blank"
|
||||
>
|
||||
<q-icon name="open_in_new" />
|
||||
</router-link>
|
||||
</q-item-label>
|
||||
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.customerId') }}</q-item-label>
|
||||
<q-item-label>{{ customer.id }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.name') }}</q-item-label>
|
||||
<q-item-label>{{ customer.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.contact') }}</q-item-label>
|
||||
<q-item-label>{{ customer.contact }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="customer.salesPersonUser">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.salesPerson') }}</q-item-label>
|
||||
<q-item-label>{{ customer.salesPersonUser.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.phone') }}</q-item-label>
|
||||
<q-item-label>{{ customer.phone }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.mobile') }}</q-item-label>
|
||||
<q-item-label>{{ customer.mobile }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.email') }}</q-item-label>
|
||||
<q-item-label>{{ customer.email }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="customer.contactChannel">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.contactChannel') }}</q-item-label>
|
||||
<q-item-label>{{ customer.contactChannel.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-list>
|
||||
<q-item-label header class="text-h6">
|
||||
{{ t('customer.summary.fiscalAddress') }}
|
||||
</q-item-label>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.socialName') }}</q-item-label>
|
||||
<q-item-label>{{ customer.socialName }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.fiscalId') }}</q-item-label>
|
||||
<q-item-label>{{ customer.fi }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.postcode') }}</q-item-label>
|
||||
<q-item-label>{{ customer.postcode }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="customer.province">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.province') }}</q-item-label>
|
||||
<q-item-label>{{ customer.province.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="customer.country">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.country') }}</q-item-label>
|
||||
<q-item-label>{{ customer.country.country }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.street') }}</q-item-label>
|
||||
<q-item-label>{{ customer.street }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-list>
|
||||
<q-item-label header class="text-h6">
|
||||
{{ t('customer.summary.fiscalData') }}
|
||||
</q-item-label>
|
||||
<q-item dense>
|
||||
<q-checkbox
|
||||
v-model="customer.isEqualizated"
|
||||
:label="t('customer.summary.isEqualizated')"
|
||||
disable
|
||||
/>
|
||||
</q-item>
|
||||
<q-item dense>
|
||||
<q-checkbox
|
||||
v-model="customer.isActive"
|
||||
:label="t('customer.summary.isActive')"
|
||||
disable
|
||||
/>
|
||||
</q-item>
|
||||
<q-item dense>
|
||||
<q-checkbox
|
||||
v-model="customer.hasToInvoiceByAddress"
|
||||
:label="t('customer.summary.invoiceByAddress')"
|
||||
disable
|
||||
/>
|
||||
</q-item>
|
||||
<q-item dense>
|
||||
<q-checkbox
|
||||
v-model="customer.isTaxDataChecked"
|
||||
:label="t('customer.summary.verifiedData')"
|
||||
disable
|
||||
/>
|
||||
</q-item>
|
||||
<q-item dense>
|
||||
<q-checkbox
|
||||
v-model="customer.hasToInvoice"
|
||||
:label="t('customer.summary.hasToInvoice')"
|
||||
disable
|
||||
/>
|
||||
</q-item>
|
||||
<q-item dense>
|
||||
<q-checkbox
|
||||
v-model="customer.isToBeMailed"
|
||||
:label="t('customer.summary.notifyByEmail')"
|
||||
disable
|
||||
/>
|
||||
</q-item>
|
||||
<q-item dense>
|
||||
<q-checkbox v-model="customer.isVies" :label="t('customer.summary.vies')" disable />
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-list>
|
||||
<q-item-label header class="text-h6">
|
||||
{{ t('customer.summary.billingData') }}
|
||||
</q-item-label>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.payMethod') }}</q-item-label>
|
||||
<q-item-label>{{ customer.payMethod.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.bankAccount') }}</q-item-label>
|
||||
<q-item-label>{{ customer.iban }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.dueDay') }}</q-item-label>
|
||||
<q-item-label>{{ customer.dueDay }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item dense>
|
||||
<q-checkbox v-model="customer.hasLcr" :label="t('customer.summary.hasLcr')" disable />
|
||||
</q-item>
|
||||
<q-item dense>
|
||||
<q-checkbox
|
||||
v-model="customer.hasCoreVnl"
|
||||
:label="t('customer.summary.hasCoreVnl')"
|
||||
disable
|
||||
/>
|
||||
</q-item>
|
||||
<q-item dense>
|
||||
<q-checkbox
|
||||
v-model="customer.hasSepaVnl"
|
||||
:label="t('customer.summary.hasB2BVnl')"
|
||||
disable
|
||||
/>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
<div class="col" v-if="customer.defaultAddress">
|
||||
<q-list>
|
||||
<q-item-label header class="text-h6">
|
||||
{{ t('customer.summary.consignee') }}
|
||||
</q-item-label>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.addressName') }}</q-item-label>
|
||||
<q-item-label>{{ customer.defaultAddress.nickname }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.addressCity') }}</q-item-label>
|
||||
<q-item-label>{{ customer.defaultAddress.city }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.addressStreet') }}</q-item-label>
|
||||
<q-item-label>{{ customer.defaultAddress.street }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
<div class="col" v-if="customer.account">
|
||||
<q-list>
|
||||
<q-item-label header class="text-h6">
|
||||
{{ t('customer.summary.webAccess') }}
|
||||
</q-item-label>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.username') }}</q-item-label>
|
||||
<q-item-label>{{ customer.account.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item dense>
|
||||
<q-checkbox
|
||||
v-model="customer.account.active"
|
||||
:label="t('customer.summary.webAccess')"
|
||||
disable
|
||||
/>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-list>
|
||||
<q-item-label header class="text-h6">
|
||||
{{ t('customer.summary.businessData') }}
|
||||
</q-item-label>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.totalGreuge') }}</q-item-label>
|
||||
<q-item-label>{{ toCurrency(customer.totalGreuge) }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="customer.mana">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.mana') }}</q-item-label>
|
||||
<q-item-label>{{ toCurrency(customer.mana.mana) }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="customer.claimsRatio">
|
||||
<q-item-section>
|
||||
<q-item-label caption>
|
||||
{{ t('customer.summary.priceIncreasingRate') }}
|
||||
</q-item-label>
|
||||
<q-item-label>{{ toPercentage(priceIncreasingRate) }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="customer.averageInvoiced">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.averageInvoiced') }}</q-item-label>
|
||||
<q-item-label>{{ toCurrency(customer.averageInvoiced.invoiced) }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="customer.claimsRatio">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.claimRate') }}</q-item-label>
|
||||
<q-item-label>{{ toPercentage(claimRate) }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-list>
|
||||
<q-item-label header class="text-h6">
|
||||
{{ t('customer.summary.financialData') }}
|
||||
</q-item-label>
|
||||
<q-item v-if="customer.debt">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.risk') }}</q-item-label>
|
||||
<q-item-label :class="debtWarning">
|
||||
{{ toCurrency(customer.debt.debt) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="vn:info">
|
||||
<q-tooltip>{{ t('customer.summary.riskInfo') }}</q-tooltip>
|
||||
</q-icon>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.credit') }}</q-item-label>
|
||||
<q-item-label :class="creditWarning">
|
||||
{{ toCurrency(customer.credit) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="vn:info">
|
||||
<q-tooltip>{{ t('customer.summary.creditInfo') }}</q-tooltip>
|
||||
</q-icon>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="customer.creditInsurance">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.securedCredit') }}</q-item-label>
|
||||
<q-item-label>{{ toCurrency(customer.creditInsurance) }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="vn:info">
|
||||
<q-tooltip>{{ t('customer.summary.securedCreditInfo') }}</q-tooltip>
|
||||
</q-icon>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.balance') }}</q-item-label>
|
||||
<q-item-label>{{ toCurrency(customer.sumRisk) || toCurrency(0) }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="vn:info">
|
||||
<q-tooltip>{{ t('customer.summary.balanceInfo') }}</q-tooltip>
|
||||
</q-icon>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="customer.defaulters">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.balanceDue') }}</q-item-label>
|
||||
<q-item-label :class="balanceDueWarning">
|
||||
{{ toCurrency(balanceDue) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="vn:info">
|
||||
<q-tooltip>{{ t('customer.summary.balanceDueInfo') }}</q-tooltip>
|
||||
</q-icon>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="customer.recovery">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.summary.recoverySince') }}</q-item-label>
|
||||
<q-item-label>{{ toDate(customer.recovery.started) }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</q-card>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.q-card {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.summary {
|
||||
.q-list {
|
||||
.q-item__label--header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
a {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
.row {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.col {
|
||||
min-width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#slider-container {
|
||||
max-width: 80%;
|
||||
margin: 0 auto;
|
||||
|
||||
.q-slider {
|
||||
.q-slider__marker-labels:nth-child(1) {
|
||||
transform: none;
|
||||
}
|
||||
.q-slider__marker-labels:nth-child(2) {
|
||||
transform: none;
|
||||
left: auto !important;
|
||||
right: 0%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.q-dialog .summary {
|
||||
max-width: 1200px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,29 @@
|
|||
<script setup>
|
||||
import { useDialogPluginComponent } from 'quasar';
|
||||
import CustomerSummary from './CustomerSummary.vue';
|
||||
|
||||
const $props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||
<customer-summary v-if="$props.id" :id="$props.id" />
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.q-dialog .summary .header {
|
||||
position: sticky;
|
||||
z-index: $z-max;
|
||||
top: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,52 @@
|
|||
<script setup>
|
||||
import { reactive, watch } from 'vue'
|
||||
|
||||
const customer = reactive({
|
||||
name: '',
|
||||
});
|
||||
|
||||
watch(() => customer.name, () => {
|
||||
console.log('customer.name changed');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page class="q-pa-md">
|
||||
<q-card class="q-pa-md">
|
||||
<q-form @submit="onSubmit" @reset="onReset" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
v-model="customer.name"
|
||||
label="Your name *"
|
||||
hint="Name and surname"
|
||||
lazy-rules
|
||||
:rules="[val => val && val.length > 0 || 'Please type something']"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
v-model="age"
|
||||
label="Your age *"
|
||||
lazy-rules
|
||||
:rules="[
|
||||
val => val !== null && val !== '' || 'Please type your age',
|
||||
val => val > 0 && val < 100 || 'Please type a real age'
|
||||
]"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<q-btn label="Submit" type="submit" color="primary" />
|
||||
<q-btn label="Reset" type="reset" color="primary" flat class="q-ml-sm" />
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 60em;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,87 @@
|
|||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useQuasar } from 'quasar';
|
||||
import Paginate from 'src/components/PaginateData.vue';
|
||||
import CustomerSummaryDialog from './Card/CustomerSummaryDialog.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const quasar = useQuasar();
|
||||
const { t } = useI18n();
|
||||
|
||||
function navigate(id) {
|
||||
router.push({ path: `/customer/${id}` });
|
||||
}
|
||||
|
||||
function viewSummary(id) {
|
||||
quasar.dialog({
|
||||
component: CustomerSummaryDialog,
|
||||
componentProps: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page class="q-pa-md">
|
||||
<paginate url="/Clients" sort-by="id DESC" auto-load>
|
||||
<template #body="{ rows }">
|
||||
<q-card class="card" v-for="row of rows" :key="row.id">
|
||||
<q-item class="q-pa-none items-start cursor-pointer q-hoverable" v-ripple clickable>
|
||||
<q-item-section class="q-pa-md" @click="navigate(row.id)">
|
||||
<div class="text-h6">{{ row.name }}</div>
|
||||
<q-item-label caption>#{{ row.id }}</q-item-label>
|
||||
<q-list>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.list.email') }}</q-item-label>
|
||||
<q-item-label>{{ row.email }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.list.phone') }}</q-item-label>
|
||||
<q-item-label>{{ row.phone }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-item-section>
|
||||
<q-separator vertical />
|
||||
<q-card-actions vertical class="justify-between">
|
||||
<!-- <q-btn color="grey-7" round flat icon="more_vert">
|
||||
<q-tooltip>{{ t('customer.list.moreOptions') }}</q-tooltip>
|
||||
<q-menu cover auto-close>
|
||||
<q-list>
|
||||
<q-item clickable>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="add" />
|
||||
</q-item-section>
|
||||
<q-item-section>Add a note</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="history" />
|
||||
</q-item-section>
|
||||
<q-item-section>Display customer history</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn> -->
|
||||
|
||||
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
|
||||
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn flat round color="grey-7" icon="preview" @click="viewSummary(row.id)">
|
||||
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
|
||||
</q-btn>
|
||||
<!-- <q-btn flat round color="grey-7" icon="vn:ticket">
|
||||
<q-tooltip>{{ t('customer.list.customerOrders') }}</q-tooltip>
|
||||
</q-btn> -->
|
||||
</q-card-actions>
|
||||
</q-item>
|
||||
</q-card>
|
||||
</template>
|
||||
</paginate>
|
||||
</q-page>
|
||||
</template>
|
|
@ -0,0 +1,17 @@
|
|||
<script setup>
|
||||
import { useState } from 'src/composables/useState';
|
||||
import LeftMenu from 'components/LeftMenu.vue';
|
||||
|
||||
const state = useState();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||
<q-scroll-area class="fit text-grey-8">
|
||||
<LeftMenu />
|
||||
</q-scroll-area>
|
||||
</q-drawer>
|
||||
<q-page-container>
|
||||
<router-view></router-view>
|
||||
</q-page-container>
|
||||
</template>
|
|
@ -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>
|
|
@ -1,19 +0,0 @@
|
|||
<template>
|
||||
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
|
||||
<div>
|
||||
<div style="font-size: 30vh">404</div>
|
||||
|
||||
<div class="text-h2" style="opacity: 0.4">Oops. Nothing here...</div>
|
||||
|
||||
<q-btn class="q-mt-xl" color="white" text-color="blue" unelevated to="/" label="Go Home" no-caps />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ErrorNotFound',
|
||||
});
|
||||
</script>
|
|
@ -1,13 +0,0 @@
|
|||
<template>
|
||||
<q-page class="flex flex-center">
|
||||
<img alt="Quasar logo" src="~assets/quasar-logo-vertical.svg" style="width: 200px; height: 200px" />
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'IndexPage',
|
||||
});
|
||||
</script>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,142 @@
|
|||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { Dark, Quasar, useQuasar } from 'quasar';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
|
||||
import { useSession } from 'src/composables/useSession';
|
||||
|
||||
const quasar = useQuasar();
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const userLocale = computed({
|
||||
get() {
|
||||
return locale.value;
|
||||
},
|
||||
set(value) {
|
||||
locale.value = value;
|
||||
|
||||
if (value === 'en') value = 'en-GB';
|
||||
|
||||
import(`quasar/lang/${value}`).then((language) => {
|
||||
Quasar.lang.set(language.default);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const darkMode = computed({
|
||||
get() {
|
||||
return Dark.isActive;
|
||||
},
|
||||
set(value) {
|
||||
Dark.set(value);
|
||||
},
|
||||
});
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const keepLogin = ref(true);
|
||||
|
||||
async function onSubmit() {
|
||||
const { data } = await axios.post('Accounts/login', {
|
||||
user: username.value,
|
||||
password: password.value,
|
||||
});
|
||||
|
||||
if (!data) return;
|
||||
|
||||
await session.login(data.token, keepLogin.value);
|
||||
|
||||
quasar.notify({
|
||||
message: t('login.loginSuccess'),
|
||||
type: 'positive',
|
||||
});
|
||||
|
||||
const currentRoute = router.currentRoute.value;
|
||||
if (currentRoute.query && currentRoute.query.redirect) {
|
||||
router.push(currentRoute.query.redirect);
|
||||
} else {
|
||||
router.push({ name: 'Dashboard' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-layout>
|
||||
<q-page-container>
|
||||
<q-page id="login">
|
||||
<q-page-sticky position="top-right">
|
||||
<q-toolbar>
|
||||
<q-btn :label="t('globals.language')" icon="translate" color="primary" size="sm" flat rounded>
|
||||
<q-menu auto-close>
|
||||
<q-list dense>
|
||||
<q-item @click="userLocale = 'en'" :active="userLocale == 'en'" v-ripple clickable>
|
||||
{{ t('globals.lang.en') }}
|
||||
</q-item>
|
||||
<q-item @click="userLocale = 'es'" :active="userLocale == 'es'" v-ripple clickable>
|
||||
{{ t('globals.lang.es') }}
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
<q-list>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t(`globals.darkMode`) }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-toggle v-model="darkMode" checked-icon="dark_mode" unchecked-icon="light_mode" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-toolbar>
|
||||
</q-page-sticky>
|
||||
<div class="login-form q-pa-xl">
|
||||
<q-img src="~/assets/logo.svg" alt="Logo" fit="contain" :ratio="16 / 9" class="q-mb-md" />
|
||||
<q-form @submit="onSubmit" class="q-gutter-md">
|
||||
<q-input
|
||||
v-model="username"
|
||||
:label="t('login.username')"
|
||||
lazy-rules
|
||||
:rules="[(val) => (val && val.length > 0) || t('login.fieldRequired')]"
|
||||
/>
|
||||
<q-input
|
||||
type="password"
|
||||
v-model="password"
|
||||
:label="t('login.password')"
|
||||
lazy-rules
|
||||
:rules="[(val) => (val && val.length > 0) || t('login.fieldRequired')]"
|
||||
/>
|
||||
<q-toggle v-model="keepLogin" :label="t('login.keepLogin')" />
|
||||
|
||||
<div>
|
||||
<q-btn
|
||||
:label="t('login.submit')"
|
||||
type="submit"
|
||||
color="primary"
|
||||
class="full-width"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#login {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: inherit;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,57 @@
|
|||
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
|
||||
import { createWrapper, axios } from 'app/tests/jest/jestHelpers';
|
||||
import Login from '../LoginMain.vue';
|
||||
|
||||
const mockPush = jest.fn();
|
||||
|
||||
jest.mock('vue-router', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
currentRoute: { value: 'myCurrentRoute' }
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Login', () => {
|
||||
let vm;
|
||||
beforeAll(() => {
|
||||
vm = createWrapper(Login).vm;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should successfully set the token into session', async () => {
|
||||
const expectedUser = {
|
||||
id: 999,
|
||||
name: `T'Challa`,
|
||||
nickname: 'Black Panther',
|
||||
lang: 'en',
|
||||
userConfig: {
|
||||
darkMode: false,
|
||||
}
|
||||
}
|
||||
jest.spyOn(axios, 'post').mockResolvedValue({ data: { token: 'token' } });
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({ data: { roles: [], user: expectedUser } });
|
||||
jest.spyOn(vm.quasar, 'notify');
|
||||
|
||||
expect(vm.session.getToken()).toEqual('');
|
||||
|
||||
await vm.onSubmit();
|
||||
|
||||
expect(vm.session.getToken()).toEqual('token');
|
||||
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
|
||||
{ 'type': 'positive' }
|
||||
));
|
||||
vm.session.destroy();
|
||||
});
|
||||
|
||||
it('should not set the token into session if any error occurred', async () => {
|
||||
jest.spyOn(axios, 'post').mockReturnValue({ data: null });
|
||||
jest.spyOn(vm.quasar, 'notify');
|
||||
|
||||
await vm.onSubmit();
|
||||
|
||||
expect(vm.quasar.notify).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
<template>
|
||||
<div class="text-center q-pa-md flex flex-center">
|
||||
<div>
|
||||
<div class="text-grey-5" style="opacity: 0.4; font-size: 30vh">
|
||||
<q-icon name="vn:claims" />
|
||||
</div>
|
||||
|
||||
<div class="text-h2 text-grey-5" style="opacity: 0.4">{{ t('notFound') }}</div>
|
||||
|
||||
<q-btn
|
||||
@click="router.go(-1)"
|
||||
:label="t('globals.back')"
|
||||
class="q-mt-xl"
|
||||
color="primary"
|
||||
unelevated
|
||||
outline
|
||||
no-caps
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped></style>
|
||||
<i18n>
|
||||
{
|
||||
"en": {
|
||||
"notFound": "Oops. Nothing here..."
|
||||
},
|
||||
"es": {
|
||||
"notFound": "Vaya. Nada por aquí..."
|
||||
}
|
||||
}
|
||||
</i18n>
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<q-card>Basic Data</q-card>
|
||||
</template>
|
|
@ -0,0 +1,155 @@
|
|||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import { date, useQuasar } from 'quasar';
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const quasar = useQuasar();
|
||||
|
||||
onMounted(async () => {
|
||||
await fetch();
|
||||
});
|
||||
|
||||
const entityId = computed(function () {
|
||||
return router.currentRoute.value.params.id;
|
||||
});
|
||||
|
||||
const expeditions = ref({});
|
||||
const lastExpedition = ref();
|
||||
const slide = ref(null);
|
||||
const videoList = ref([]);
|
||||
const time = ref({
|
||||
min: 0,
|
||||
max: 24,
|
||||
});
|
||||
|
||||
async function fetch() {
|
||||
const filter = {
|
||||
where: {
|
||||
ticketFk: entityId.value,
|
||||
},
|
||||
};
|
||||
const { data } = await axios.get(`/Expeditions/filter`, {
|
||||
params: { filter },
|
||||
});
|
||||
|
||||
if (data) expeditions.value = data;
|
||||
}
|
||||
|
||||
async function getVideoList(expeditionId, timed) {
|
||||
lastExpedition.value = expeditionId;
|
||||
const params = {
|
||||
id: expeditionId,
|
||||
};
|
||||
|
||||
if (timed) {
|
||||
Object.assign(params, { from: timed.min, to: timed.max });
|
||||
}
|
||||
const { data } = await axios.get(`/Boxings/getVideoList`, { params: params });
|
||||
|
||||
const list = [];
|
||||
for (const video of data) {
|
||||
const videName = video.split('.')[0].split('T')[1].replaceAll('-', ':');
|
||||
list.push({
|
||||
label: videName,
|
||||
value: video,
|
||||
url: `api/Boxings/getVideo?id=${expeditionId}&filename=${video}`,
|
||||
});
|
||||
}
|
||||
|
||||
videoList.value = list.reverse();
|
||||
if (list[0]) {
|
||||
slide.value = list[0].value;
|
||||
time.value = {
|
||||
min: parseInt(list[0].label.split(':')[0]),
|
||||
max: parseInt(list[list.length - 1].label.split(':')[0]),
|
||||
};
|
||||
}
|
||||
|
||||
if (!data.length) {
|
||||
return quasar.notify({
|
||||
message: t('ticket.boxing.notFound'),
|
||||
type: 'negative',
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<teleport to=".q-layout">
|
||||
<q-drawer show-if-above side="right">
|
||||
<q-scroll-area class="fit">
|
||||
<q-list bordered separator style="max-width: 318px">
|
||||
<q-item v-if="lastExpedition && videoList.length">
|
||||
<q-item-section>
|
||||
<q-item-label class="text-h6">
|
||||
{{ t('ticket.boxing.selectTime') }} ({{ time.min }}-{{ time.max }})
|
||||
</q-item-label>
|
||||
<q-range
|
||||
v-model="time"
|
||||
@change="getVideoList(lastExpedition, time)"
|
||||
:min="0"
|
||||
:max="24"
|
||||
:step="1"
|
||||
:left-label-value="time.min + ':00'"
|
||||
:right-label-value="time.max + ':00'"
|
||||
label
|
||||
markers
|
||||
snap
|
||||
color="orange"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="lastExpedition && videoList.length">
|
||||
<q-item-section>
|
||||
<q-select
|
||||
color="orange"
|
||||
v-model="slide"
|
||||
:options="videoList"
|
||||
:label="t('ticket.boxing.selectVideo')"
|
||||
emit-value
|
||||
map-options
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="schedule" />
|
||||
</template>
|
||||
</q-select>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-for="expedition in expeditions"
|
||||
:key="expedition.id"
|
||||
@click="getVideoList(expedition.id)"
|
||||
clickable
|
||||
v-ripple
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-h6">#{{ expedition.id }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('ticket.boxing.created') }}</q-item-label>
|
||||
<q-item-label>
|
||||
{{ date.formatDate(expedition.created, 'YYYY-MM-DD HH:mm:ss') }}
|
||||
</q-item-label>
|
||||
<q-item-label caption>{{ t('ticket.boxing.item') }}</q-item-label>
|
||||
<q-item-label>{{ expedition.packagingItemFk }}</q-item-label>
|
||||
<q-item-label caption>{{ t('ticket.boxing.worker') }}</q-item-label>
|
||||
<q-item-label>{{ expedition.userName }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-scroll-area>
|
||||
</q-drawer>
|
||||
</teleport>
|
||||
|
||||
<q-card>
|
||||
<q-carousel animated v-model="slide" height="max-content">
|
||||
<q-carousel-slide v-for="video in videoList" :key="video.value" :name="video.value">
|
||||
<q-video :src="video.url" :ratio="16 / 9" />
|
||||
</q-carousel-slide>
|
||||
</q-carousel>
|
||||
</q-card>
|
||||
</template>
|
|
@ -0,0 +1,50 @@
|
|||
<script setup>
|
||||
import { useState } from 'src/composables/useState';
|
||||
import TicketDescriptor from './TicketDescriptor.vue';
|
||||
import LeftMenu from 'components/LeftMenu.vue';
|
||||
|
||||
const state = useState();
|
||||
</script>
|
||||
<template>
|
||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||
<q-scroll-area class="fit">
|
||||
<ticket-descriptor />
|
||||
<q-separator />
|
||||
<left-menu source="card" />
|
||||
</q-scroll-area>
|
||||
</q-drawer>
|
||||
<q-page-container>
|
||||
<q-page class="q-pa-md">
|
||||
<router-view></router-view>
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.q-scrollarea__content {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.descriptor {
|
||||
max-width: 256px;
|
||||
|
||||
h5 {
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.q-card__actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#descriptor-skeleton .q-card__actions {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,111 @@
|
|||
<script setup>
|
||||
import { onMounted, computed, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { toDate } from 'src/filters';
|
||||
import axios from 'axios';
|
||||
import 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({
|
||||
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 ticket = ref();
|
||||
async function fetch() {
|
||||
const { data } = await axios.get(`Tickets/${entityId.value}/summary`);
|
||||
|
||||
if (data) ticket.value = data;
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<skeleton-descriptor v-if="!ticket" />
|
||||
<card-descriptor v-if="ticket" module="Ticket" :data="ticket" :description="ticket.client.name">
|
||||
<!-- <template #menu>
|
||||
<q-item clickable v-ripple>Option 1</q-item>
|
||||
<q-item clickable v-ripple>Option 2</q-item>
|
||||
</template> -->
|
||||
<template #body>
|
||||
<q-list>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('ticket.card.ticketId') }}</q-item-label>
|
||||
<q-item-label>#{{ ticket.id }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('ticket.card.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.card.customerId') }}</q-item-label>
|
||||
<q-item-label class="link">
|
||||
{{ ticket.clientFk }}
|
||||
<q-popup-proxy>
|
||||
<customer-descriptor-popover :id="ticket.client.id" />
|
||||
</q-popup-proxy>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('ticket.card.salesPerson') }}</q-item-label>
|
||||
<q-item-label>{{ ticket.client.salesPersonUser.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('ticket.card.warehouse') }}</q-item-label>
|
||||
<q-item-label>{{ ticket.warehouse.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('ticket.card.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.card.agency') }}</q-item-label>
|
||||
<q-item-label>{{ ticket.agencyMode.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<q-card-actions>
|
||||
<q-btn
|
||||
size="md"
|
||||
icon="vn:client"
|
||||
color="primary"
|
||||
:to="{ name: 'CustomerCard', params: { id: ticket.clientFk } }"
|
||||
>
|
||||
<q-tooltip>{{ t('ticket.card.customerCard') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
</template>
|
||||
</card-descriptor>
|
||||
</template>
|
|
@ -0,0 +1,15 @@
|
|||
<script setup>
|
||||
import TicketDescriptor from './TicketDescriptor.vue';
|
||||
|
||||
const $props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<q-card>
|
||||
<ticket-descriptor v-if="$props.id" :id="$props.id" />
|
||||
</q-card>
|
||||
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,71 @@
|
|||
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
|
||||
import { createWrapper, axios } from 'app/tests/jest/jestHelpers';
|
||||
import TicketBoxing from '../TicketBoxing.vue';
|
||||
|
||||
const mockPush = jest.fn();
|
||||
|
||||
jest.mock('vue-router', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
currentRoute: {
|
||||
value: {
|
||||
params: {
|
||||
id: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
// #4836 - Investigate how to test q-drawer outside
|
||||
// q-layout or how to teleport q-drawer inside
|
||||
xdescribe('TicketBoxing', () => {
|
||||
let vm;
|
||||
beforeAll(() => {
|
||||
vm = createWrapper(TicketBoxing).vm;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getVideoList()', () => {
|
||||
it('should when response videoList use to list', async () => {
|
||||
const expeditionId = 1;
|
||||
const timed = {
|
||||
min: 1,
|
||||
max: 2
|
||||
}
|
||||
const videoList = [
|
||||
"2022-01-01T01-01-00.mp4",
|
||||
"2022-02-02T02-02-00.mp4",
|
||||
"2022-03-03T03-03-00.mp4",
|
||||
]
|
||||
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({ data: videoList });
|
||||
jest.spyOn(vm.quasar, 'notify');
|
||||
|
||||
await vm.getVideoList(expeditionId, timed);
|
||||
|
||||
expect(vm.videoList.length).toEqual(videoList.length);
|
||||
expect(vm.slide).toEqual(videoList.reverse()[0]);
|
||||
});
|
||||
|
||||
it('should if not have video show notify', async () => {
|
||||
const expeditionId = 1;
|
||||
const timed = {
|
||||
min: 1,
|
||||
max: 2
|
||||
}
|
||||
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({ data: [] });
|
||||
jest.spyOn(vm.quasar, 'notify')
|
||||
|
||||
await vm.getVideoList(expeditionId, timed);
|
||||
|
||||
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
|
||||
{ 'type': 'negative' }
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,122 @@
|
|||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useRouter } from 'vue-router';
|
||||
import Paginate from 'src/components/PaginateData.vue';
|
||||
import { toDate, toCurrency } from 'src/filters/index';
|
||||
import TicketSummaryDialog from './Card/TicketSummaryDialog.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const quasar = useQuasar();
|
||||
const { t } = useI18n();
|
||||
|
||||
const filter = {
|
||||
include: [
|
||||
{
|
||||
relation: 'client',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'salesPersonUser',
|
||||
scope: {
|
||||
fields: ['name'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
relation: 'ticketState',
|
||||
scope: {
|
||||
fields: ['stateFk', 'code', 'alertLevel'],
|
||||
include: {
|
||||
relation: 'state',
|
||||
scope: {
|
||||
fields: ['name'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function stateColor(state) {
|
||||
if (state.code === 'OK') return 'green';
|
||||
if (state.code === 'FREE') return 'blue-3';
|
||||
if (state.alertLevel === 1) return 'orange';
|
||||
if (state.alertLevel === 0) return 'red';
|
||||
}
|
||||
|
||||
function navigate(id) {
|
||||
router.push({ path: `/ticket/${id}` });
|
||||
}
|
||||
|
||||
function viewSummary(id) {
|
||||
quasar.dialog({
|
||||
component: TicketSummaryDialog,
|
||||
componentProps: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page class="q-pa-md">
|
||||
<paginate url="/Tickets" :filter="filter" sort-by="id DESC" auto-load>
|
||||
<template #body="{ rows }">
|
||||
<q-card class="card" v-for="row of rows" :key="row.id">
|
||||
<q-item class="q-pa-none items-start cursor-pointer q-hoverable" v-ripple clickable>
|
||||
<q-item-section class="q-pa-md" @click="navigate(row.id)">
|
||||
<div class="text-h6">{{ row.name }}</div>
|
||||
<q-item-label caption>#{{ row.id }}</q-item-label>
|
||||
<q-list>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('ticket.list.nickname') }}</q-item-label>
|
||||
<q-item-label>{{ row.nickname }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('ticket.list.state') }}</q-item-label>
|
||||
<q-item-label>
|
||||
<q-chip :color="stateColor(row.ticketState)" dense>
|
||||
{{ row.ticketState.state.name }}
|
||||
</q-chip>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('ticket.list.shipped') }}</q-item-label>
|
||||
<q-item-label>{{ toDate(row.shipped) }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('ticket.list.landed') }}</q-item-label>
|
||||
<q-item-label>{{ toDate(row.landed) }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section v-if="row.client.salesPersonUser">
|
||||
<q-item-label caption>{{ t('ticket.list.salesPerson') }}</q-item-label>
|
||||
<q-item-label>{{ row.client.salesPersonUser.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('ticket.list.total') }}</q-item-label>
|
||||
<q-item-label>{{ toCurrency(row.totalWithVat) }}</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>
|
|
@ -0,0 +1,17 @@
|
|||
<script setup>
|
||||
import { useState } from 'src/composables/useState';
|
||||
import LeftMenu from 'components/LeftMenu.vue';
|
||||
|
||||
const state = useState();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||
<q-scroll-area class="fit text-grey-8">
|
||||
<LeftMenu />
|
||||
</q-scroll-area>
|
||||
</q-drawer>
|
||||
<q-page-container>
|
||||
<router-view></router-view>
|
||||
</q-page-container>
|
||||
</template>
|
|
@ -1,6 +1,16 @@
|
|||
import { route } from 'quasar/wrappers';
|
||||
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router';
|
||||
import routes from './routes';
|
||||
import { i18n } from 'src/boot/i18n';
|
||||
import { useState } from 'src/composables/useState';
|
||||
import { useSession } from 'src/composables/useSession';
|
||||
import { useRole } from 'src/composables/useRole';
|
||||
import { toLowerCamel } from 'src/filters';
|
||||
|
||||
const state = useState();
|
||||
const session = useSession();
|
||||
const role = useRole();
|
||||
const { t } = i18n.global;
|
||||
|
||||
/*
|
||||
* If not building with SSR mode, you can
|
||||
|
@ -28,5 +38,62 @@ export default route(function (/* { store, ssrContext } */) {
|
|||
history: createHistory(process.env.VUE_ROUTER_BASE),
|
||||
});
|
||||
|
||||
Router.beforeEach(async (to, from, next) => {
|
||||
const { isLoggedIn } = session;
|
||||
|
||||
if (!isLoggedIn() && to.name !== 'Login') {
|
||||
return next({ name: 'Login', query: { redirect: to.fullPath } });
|
||||
}
|
||||
|
||||
if (isLoggedIn()) {
|
||||
const stateRoles = state.getRoles().value;
|
||||
if (stateRoles.length === 0) {
|
||||
await role.fetch();
|
||||
}
|
||||
const matches = to.matched;
|
||||
const hasRequiredRoles = matches.every((route) => {
|
||||
const meta = route.meta;
|
||||
if (meta && meta.roles) return role.hasAny(meta.roles);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!hasRequiredRoles) {
|
||||
return next({ path: '/' });
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
Router.afterEach((to) => {
|
||||
let title = t(`login.title`);
|
||||
|
||||
const matches = to.matched;
|
||||
let moduleName;
|
||||
if (matches && matches.length > 1) {
|
||||
const module = matches[1];
|
||||
const moduleTitle = module.meta && module.meta.title;
|
||||
moduleName = toLowerCamel(module.name);
|
||||
if (moduleTitle) {
|
||||
title = t(`${moduleName}.pageTitles.${moduleTitle}`);
|
||||
}
|
||||
}
|
||||
|
||||
const childPage = to.meta;
|
||||
const childPageTitle = childPage && childPage.title;
|
||||
if (childPageTitle && matches.length > 2) {
|
||||
if (title != '') title += ': ';
|
||||
|
||||
const pageTitle = t(`${moduleName}.pageTitles.${childPageTitle}`);
|
||||
const idParam = to.params && to.params.id;
|
||||
const idPageTitle = `${idParam} - ${pageTitle}`;
|
||||
const builtTitle = idParam ? idPageTitle : pageTitle;
|
||||
|
||||
title += builtTitle;
|
||||
}
|
||||
|
||||
document.title = title;
|
||||
});
|
||||
|
||||
return Router;
|
||||
});
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import { RouterView } from 'vue-router';
|
||||
|
||||
export default {
|
||||
name: 'Claim',
|
||||
path: '/claim',
|
||||
meta: {
|
||||
title: 'claims',
|
||||
icon: 'vn:claims',
|
||||
},
|
||||
component: RouterView,
|
||||
redirect: { name: 'ClaimMain' },
|
||||
menus: {
|
||||
main: ['ClaimList', 'ClaimRmaList'],
|
||||
card: ['ClaimBasicData', 'ClaimRma'],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'ClaimMain',
|
||||
path: '',
|
||||
component: () => import('src/pages/Claim/ClaimMain.vue'),
|
||||
redirect: { name: 'ClaimList' },
|
||||
children: [
|
||||
{
|
||||
name: 'ClaimList',
|
||||
path: 'list',
|
||||
meta: {
|
||||
title: 'list',
|
||||
icon: 'view_list',
|
||||
},
|
||||
component: () => import('src/pages/Claim/ClaimList.vue'),
|
||||
},
|
||||
{
|
||||
name: 'ClaimRmaList',
|
||||
path: 'rma',
|
||||
meta: {
|
||||
title: 'rmaList',
|
||||
icon: 'vn:barcode',
|
||||
roles: ['claimManager'],
|
||||
},
|
||||
component: () => import('src/pages/Claim/ClaimRmaList.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ClaimCard',
|
||||
path: ':id',
|
||||
component: () => import('src/pages/Claim/Card/ClaimCard.vue'),
|
||||
redirect: { name: 'ClaimSummary' },
|
||||
children: [
|
||||
{
|
||||
name: 'ClaimSummary',
|
||||
path: 'summary',
|
||||
meta: {
|
||||
title: 'summary',
|
||||
icon: 'launch',
|
||||
},
|
||||
component: () => import('src/pages/Claim/Card/ClaimSummary.vue'),
|
||||
},
|
||||
{
|
||||
name: 'ClaimBasicData',
|
||||
path: 'basic-data',
|
||||
meta: {
|
||||
title: 'basicData',
|
||||
icon: 'vn:settings',
|
||||
roles: ['salesPerson'],
|
||||
},
|
||||
component: () => import('src/pages/Claim/Card/ClaimBasicData.vue'),
|
||||
},
|
||||
{
|
||||
name: 'ClaimRma',
|
||||
path: 'rma',
|
||||
meta: {
|
||||
title: 'rma',
|
||||
icon: 'vn:barcode',
|
||||
roles: ['claimManager'],
|
||||
},
|
||||
component: () => import('src/pages/Claim/Card/ClaimRma.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue