Merge branch 'dev' into 8246-ZoneAddressFk2
gitea/salix-front/pipeline/pr-dev There was a failure building this commit Details

This commit is contained in:
Alex Moreno 2025-02-07 12:04:55 +00:00
commit 2d65811360
18 changed files with 846 additions and 451 deletions

View File

@ -0,0 +1,2 @@
export const langs = ['en', 'es'];
export const decimalPlaces = 2;

View File

@ -41,7 +41,6 @@ const filteredItems = computed(() => {
return locale.includes(normalizedSearch);
});
});
const filteredPinnedModules = computed(() => {
if (!search.value) return pinnedModules.value;
const normalizedSearch = search.value
@ -72,7 +71,7 @@ watch(
items.value = [];
getRoutes();
},
{ deep: true }
{ deep: true },
);
function findMatches(search, item) {
@ -104,33 +103,40 @@ function addChildren(module, route, parent) {
}
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
);
if (!moduleDef) continue;
item.children = [];
addChildren(item.module, moduleDef, item.children);
}
items.value = modules;
const handleRoutes = {
main: getMainRoutes,
card: getCardRoutes,
};
try {
handleRoutes[props.source]();
} catch (error) {
throw new Error(`Method is not defined`);
}
}
function getMainRoutes() {
const modules = Object.assign([], navigation.getModules().value);
if (props.source === 'card') {
const currentRoute = route.matched[1];
const currentModule = toLowerCamel(currentRoute.name);
let moduleDef = routes.find(
(route) => toLowerCamel(route.name) === currentModule
for (const item of modules) {
const moduleDef = routes.find(
(route) => toLowerCamel(route.name) === item.module,
);
if (!moduleDef) continue;
item.children = [];
if (!moduleDef) return;
if (!moduleDef?.menus) moduleDef = betaGetRoutes();
addChildren(currentModule, moduleDef, items.value);
addChildren(item.module, moduleDef, item.children);
}
items.value = modules;
}
function getCardRoutes() {
const currentRoute = route.matched[1];
const currentModule = toLowerCamel(currentRoute.name);
let moduleDef = routes.find((route) => toLowerCamel(route.name) === currentModule);
if (!moduleDef) return;
if (!moduleDef?.menus) moduleDef = betaGetRoutes();
addChildren(currentModule, moduleDef, items.value);
}
function betaGetRoutes() {
@ -223,9 +229,16 @@ const searchModule = () => {
</template>
<template v-for="(item, index) in filteredItems" :key="item.name">
<template
v-if="search ||item.children && !filteredPinnedModules.has(item.name)"
v-if="
search ||
(item.children && !filteredPinnedModules.has(item.name))
"
>
<LeftMenuItem :item="item" group="modules" :class="search && index === 0 ? 'searched' : ''">
<LeftMenuItem
:item="item"
group="modules"
:class="search && index === 0 ? 'searched' : ''"
>
<template #side>
<QBtn
v-if="item.isPinned === true"
@ -342,7 +355,7 @@ const searchModule = () => {
.header {
color: var(--vn-label-color);
}
.searched{
.searched {
background-color: var(--vn-section-hover-color);
}
</style>

View File

@ -27,7 +27,7 @@ function columnName(col) {
</script>
<template>
<VnFilterPanel v-bind="$attrs" :search-button="true" :disable-submit-event="true">
<template #body="{ params, orders }">
<template #body="{ params, orders, searchFn }">
<div
class="row no-wrap flex-center"
v-for="col of columns.filter((c) => c.columnFilter ?? true)"
@ -52,6 +52,7 @@ function columnName(col) {
<slot
name="moreFilterPanel"
:params="params"
:search-fn="searchFn"
:orders="orders"
:columns="columns"
/>

View File

@ -1,9 +1,12 @@
import { vi, describe, expect, it, beforeAll } from 'vitest';
import { vi, describe, expect, it, beforeAll, beforeEach, afterEach } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper';
import Leftmenu from 'components/LeftMenu.vue';
import * as vueRouter from 'vue-router';
import { useNavigationStore } from 'src/stores/useNavigationStore';
let vm;
let navigation;
vi.mock('src/router/modules', () => ({
default: [
{
@ -21,6 +24,16 @@ vi.mock('src/router/modules', () => ({
{
path: '',
name: 'CustomerMain',
meta: {
menu: 'Customer',
menuChildren: [
{
name: 'CustomerCreditContracts',
title: 'creditContracts',
icon: 'vn:solunion',
},
],
},
children: [
{
path: 'list',
@ -28,6 +41,13 @@ vi.mock('src/router/modules', () => ({
meta: {
title: 'list',
icon: 'view_list',
menuChildren: [
{
name: 'CustomerCreditContracts',
title: 'creditContracts',
icon: 'vn:solunion',
},
],
},
},
{
@ -44,51 +64,325 @@ vi.mock('src/router/modules', () => ({
},
],
}));
describe('Leftmenu', () => {
let vm;
let navigation;
beforeAll(() => {
vi.spyOn(axios, 'get').mockResolvedValue({
data: [],
});
vm = createWrapper(Leftmenu, {
propsData: {
source: 'main',
vi.spyOn(vueRouter, 'useRoute').mockReturnValue({
matched: [
{
path: '/',
redirect: {
name: 'Dashboard',
},
}).vm;
navigation = useNavigationStore();
navigation.fetchPinned = vi.fn().mockReturnValue(Promise.resolve(true));
navigation.getModules = vi.fn().mockReturnValue({
value: [
name: 'Main',
meta: {},
props: {
default: false,
},
children: [
{
name: 'customer',
title: 'customer.pageTitles.customers',
icon: 'vn:customer',
module: 'customer',
path: '/dashboard',
name: 'Dashboard',
meta: {
title: 'dashboard',
icon: 'dashboard',
},
},
],
},
{
path: '/customer',
redirect: {
name: 'CustomerMain',
},
name: 'Customer',
meta: {
title: 'customers',
icon: 'vn:client',
moduleName: 'Customer',
keyBinding: 'c',
menu: 'customer',
},
},
],
query: {},
params: {},
meta: { moduleName: 'mockName' },
path: 'mockName/1',
name: 'Customer',
});
function mount(source = 'main') {
vi.spyOn(axios, 'get').mockResolvedValue({
data: [],
});
const wrapper = createWrapper(Leftmenu, {
propsData: {
source,
},
});
navigation = useNavigationStore();
navigation.fetchPinned = vi.fn().mockReturnValue(Promise.resolve(true));
navigation.getModules = vi.fn().mockReturnValue({
value: [
{
name: 'customer',
title: 'customer.pageTitles.customers',
icon: 'vn:customer',
module: 'customer',
},
],
});
return wrapper;
}
describe('getRoutes', () => {
afterEach(() => vi.clearAllMocks());
const getRoutes = vi.fn().mockImplementation((props, getMethodA, getMethodB) => {
const handleRoutes = {
methodA: getMethodA,
methodB: getMethodB,
};
try {
handleRoutes[props.source]();
} catch (error) {
throw Error('Method not defined');
}
});
const getMethodA = vi.fn();
const getMethodB = vi.fn();
const fn = (props) => getRoutes(props, getMethodA, getMethodB);
it('should call getMethodB when source is card', () => {
let props = { source: 'methodB' };
fn(props);
expect(getMethodB).toHaveBeenCalled();
expect(getMethodA).not.toHaveBeenCalled();
});
it('should call getMethodA when source is main', () => {
let props = { source: 'methodA' };
fn(props);
expect(getMethodA).toHaveBeenCalled();
expect(getMethodB).not.toHaveBeenCalled();
});
it('should call getMethodA when source is not exists or undefined', () => {
let props = { source: 'methodC' };
expect(() => fn(props)).toThrowError('Method not defined');
expect(getMethodA).not.toHaveBeenCalled();
expect(getMethodB).not.toHaveBeenCalled();
});
});
describe('Leftmenu as card', () => {
beforeAll(() => {
vm = mount('card').vm;
});
it('should get routes for card source', async () => {
vm.getRoutes();
});
});
describe('Leftmenu as main', () => {
beforeEach(() => {
vm = mount().vm;
});
it('should initialize with default props', () => {
expect(vm.source).toBe('main');
});
it('should filter items based on search input', async () => {
vm.search = 'cust';
await vm.$nextTick();
expect(vm.filteredItems[0].name).toEqual('customer');
expect(vm.filteredItems[0].module).toEqual('customer');
});
it('should filter items based on search input', async () => {
vm.search = 'Rou';
await vm.$nextTick();
expect(vm.filteredItems).toEqual([]);
});
it('should return pinned items', () => {
vm.items = [
{ name: 'Item 1', isPinned: false },
{ name: 'Item 2', isPinned: true },
];
expect(vm.pinnedModules).toEqual(
new Map([['Item 2', { name: 'Item 2', isPinned: true }]]),
);
});
it('should find matches in routes', () => {
const search = 'child1';
const item = {
children: [
{ name: 'child1', children: [] },
{ name: 'child2', children: [] },
],
};
const matches = vm.findMatches(search, item);
expect(matches).toEqual([{ name: 'child1', children: [] }]);
});
it('should not proceed if event is already prevented', async () => {
const item = { module: 'testModule', isPinned: false };
const event = {
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
defaultPrevented: true,
};
await vm.togglePinned(item, event);
expect(event.preventDefault).not.toHaveBeenCalled();
expect(event.stopPropagation).not.toHaveBeenCalled();
});
it('should call quasar.notify with success message', async () => {
const item = { module: 'testModule', isPinned: false };
const event = {
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
defaultPrevented: false,
};
const response = { data: { id: 1 } };
vi.spyOn(axios, 'post').mockResolvedValue(response);
vi.spyOn(vm.quasar, 'notify');
await vm.togglePinned(item, event);
expect(vm.quasar.notify).toHaveBeenCalledWith({
message: 'Data saved',
type: 'positive',
});
});
it('should return a proper formated object with two child items', async () => {
const expectedMenuItem = [
{
children: null,
name: 'CustomerList',
title: 'globals.pageTitles.list',
icon: 'view_list',
},
{
children: null,
name: 'CustomerCreate',
title: 'globals.pageTitles.createCustomer',
icon: 'vn:addperson',
},
];
const firstMenuItem = vm.items[0];
expect(firstMenuItem.children).toEqual(expect.arrayContaining(expectedMenuItem));
it('should handle a single matched route with a menu', () => {
const route = {
matched: [{ meta: { menu: 'customer' } }],
};
const result = vm.betaGetRoutes();
expect(result.meta.menu).toEqual(route.matched[0].meta.menu);
});
it('should get routes for main source', () => {
vm.props.source = 'main';
vm.getRoutes();
expect(navigation.getModules).toHaveBeenCalled();
});
it('should find direct child matches', () => {
const search = 'child1';
const item = {
children: [{ name: 'child1' }, { name: 'child2' }],
};
const result = vm.findMatches(search, item);
expect(result).toEqual([{ name: 'child1' }]);
});
it('should find nested child matches', () => {
const search = 'child3';
const item = {
children: [
{ name: 'child1' },
{
name: 'child2',
children: [{ name: 'child3' }],
},
],
};
const result = vm.findMatches(search, item);
expect(result).toEqual([{ name: 'child3' }]);
});
});
describe('normalize', () => {
beforeAll(() => {
vm = mount('card').vm;
});
it('should normalize and lowercase text', () => {
const input = 'ÁÉÍÓÚáéíóú';
const expected = 'aeiouaeiou';
expect(vm.normalize(input)).toBe(expected);
});
it('should handle empty string', () => {
const input = '';
const expected = '';
expect(vm.normalize(input)).toBe(expected);
});
it('should handle text without diacritics', () => {
const input = 'hello';
const expected = 'hello';
expect(vm.normalize(input)).toBe(expected);
});
it('should handle mixed text', () => {
const input = 'Héllo Wórld!';
const expected = 'hello world!';
expect(vm.normalize(input)).toBe(expected);
});
});
describe('addChildren', () => {
const module = 'testModule';
beforeEach(() => {
vm = mount().vm;
vi.clearAllMocks();
});
it('should add menu items to parent if matches are found', () => {
const parent = 'testParent';
const route = {
meta: {
menu: 'testMenu',
},
children: [{ name: 'child1' }, { name: 'child2' }],
};
vm.addChildren(module, route, parent);
expect(navigation.addMenuItem).toHaveBeenCalled();
});
it('should handle routes with no meta menu', () => {
const route = {
meta: {},
menus: {},
};
const parent = [];
vm.addChildren(module, route, parent);
expect(navigation.addMenuItem).toHaveBeenCalled();
});
it('should handle empty parent array', () => {
const parent = [];
const route = {
meta: {
menu: 'child11',
},
children: [
{
name: 'child1',
meta: {
menuChildren: [
{
name: 'CustomerCreditContracts',
title: 'creditContracts',
icon: 'vn:solunion',
},
],
},
},
],
};
vm.addChildren(module, route, parent);
expect(navigation.addMenuItem).toHaveBeenCalled();
});
});

View File

@ -106,7 +106,14 @@ function checkIsMain() {
:data-key="dataKey"
:array-data="arrayData"
:columns="columns"
/>
>
<template #moreFilterPanel="{ params, orders, searchFn }">
<slot
name="moreFilterPanel"
v-bind="{ params, orders, searchFn }"
/>
</template>
</VnTableFilter>
</slot>
</template>
</RightAdvancedMenu>

View File

@ -635,6 +635,8 @@ wagon:
name: Name
supplier:
search: Search supplier
searchInfo: Search supplier by id or name
list:
payMethod: Pay method
account: Account

View File

@ -644,6 +644,8 @@ wagon:
volume: Volumen
name: Nombre
supplier:
search: Buscar proveedor
searchInfo: Buscar proveedor por id o nombre
list:
payMethod: Método de pago
account: Cuenta
@ -651,6 +653,7 @@ supplier:
tableVisibleColumns:
nif: NIF/CIF
account: Cuenta
summary:
responsible: Responsable
verified: Verificado

View File

@ -1,12 +1,12 @@
<script setup>
import { Dark, Quasar } from 'quasar';
import { computed } from 'vue';
import { computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { localeEquivalence } from 'src/i18n/index';
import quasarLang from 'src/utils/quasarLang';
import { langs } from 'src/boot/defaults/constants.js';
const { t, locale } = useI18n();
const userLocale = computed({
get() {
return locale.value;
@ -28,7 +28,6 @@ const darkMode = computed({
Dark.set(value);
},
});
const langs = ['en', 'es'];
</script>
<template>

View File

@ -119,7 +119,7 @@ const openSendEmailDialog = async () => {
openConfirmationModal(
t('The consumption report will be sent'),
t('Please, confirm'),
() => sendCampaignMetricsEmail({ address: arrayData.store.data.email })
() => sendCampaignMetricsEmail({ address: arrayData.store.data.email }),
);
};
const sendCampaignMetricsEmail = ({ address }) => {
@ -152,7 +152,7 @@ const updateDateParams = (value, params) => {
v-if="campaignList"
data-key="CustomerConsumption"
url="Clients/consumption"
:order="['itemTypeFk', 'itemName', 'itemSize', 'description']"
:order="['itemTypeFk', 'itemName', 'itemSize', 'description']"
:filter="{ where: { clientFk: route.params.id } }"
:columns="columns"
search-url="consumption"

View File

@ -6,7 +6,7 @@ import VehicleFilter from '../VehicleFilter.js';
<template>
<VnCardBeta
data-key="Vehicle"
base-url="Vehicles"
url="Vehicles"
:filter="VehicleFilter"
:descriptor="VehicleDescriptor"
/>

View File

@ -136,7 +136,7 @@ const columns = computed(() => [
/>
<FetchData
url="Countries"
:filter="{ fields: ['code'] }"
:filter="{ fields: ['name', 'code'] }"
@on-fetch="(data) => (countries = data)"
auto-load
/>
@ -209,7 +209,7 @@ const columns = computed(() => [
v-model="data.countryCodeFk"
:label="$t('globals.country')"
option-value="code"
option-label="code"
option-label="name"
:options="countries"
/>
<VnInput

View File

@ -1,19 +1,13 @@
<script setup>
import VnCard from 'components/common/VnCard.vue';
import SupplierDescriptor from './SupplierDescriptor.vue';
import VnCardBeta from 'src/components/common/VnCardBeta.vue';
import filter from './SupplierFilter.js';
</script>
<template>
<VnCard
<VnCardBeta
data-key="Supplier"
url="Suppliers"
:filter="filter"
:descriptor="SupplierDescriptor"
search-data-key="SupplierList"
:searchbar-props="{
url: 'Suppliers/filter',
searchUrl: 'table',
label: 'Search suppliers',
}"
:filter="filter"
/>
</template>

View File

@ -16,6 +16,7 @@ import axios from 'axios';
import { useStateStore } from 'stores/useStateStore';
import { useState } from 'src/composables/useState';
import { useArrayData } from 'composables/useArrayData';
import RightMenu from 'src/components/common/RightMenu.vue';
const state = useState();
const stateStore = useStateStore();
@ -173,59 +174,59 @@ onMounted(async () => {
</div>
</div>
</Teleport>
<QPage class="column items-center q-pa-md">
<Teleport to="#right-panel" v-if="stateStore.isHeaderMounted()">
<RightMenu>
<template #right-panel>
<SupplierConsumptionFilter data-key="SupplierConsumption" />
</Teleport>
<QTable
:rows="rows"
row-key="id"
hide-header
class="full-width q-mt-md"
:no-data-label="t('No results')"
>
<template #body="{ row }">
<QTr>
<QTd no-hover>
<span class="label">{{ t('supplier.consumption.entry') }}: </span>
<span>{{ row.id }}</span>
</QTd>
<QTd no-hover>
<span class="label">{{ t('globals.date') }}: </span>
<span>{{ toDate(row.shipped) }}</span></QTd
>
<QTd colspan="6" no-hover>
<span class="label">{{ t('globals.reference') }}: </span>
<span>{{ row.invoiceNumber }}</span>
</QTd>
</QTr>
<QTr v-for="(buy, index) in row.buys" :key="index">
<QTd no-hover>
<QBtn flat color="blue" dense no-caps>{{ buy.itemName }}</QBtn>
<ItemDescriptorProxy :id="buy.itemFk" />
</QTd>
</template>
</RightMenu>
<QTable
:rows="rows"
row-key="id"
hide-header
class="full-width q-mt-md"
:no-data-label="t('No results')"
>
<template #body="{ row }">
<QTr>
<QTd no-hover>
<span class="label">{{ t('supplier.consumption.entry') }}: </span>
<span>{{ row.id }}</span>
</QTd>
<QTd no-hover>
<span class="label">{{ t('globals.date') }}: </span>
<span>{{ toDate(row.shipped) }}</span></QTd
>
<QTd colspan="6" no-hover>
<span class="label">{{ t('globals.reference') }}: </span>
<span>{{ row.invoiceNumber }}</span>
</QTd>
</QTr>
<QTr v-for="(buy, index) in row.buys" :key="index">
<QTd no-hover>
<QBtn flat color="blue" dense no-caps>{{ buy.itemName }}</QBtn>
<ItemDescriptorProxy :id="buy.itemFk" />
</QTd>
<QTd no-hover>
<span>{{ buy.subName }}</span>
<FetchedTags :item="buy" />
</QTd>
<QTd no-hover> {{ dashIfEmpty(buy.quantity) }}</QTd>
<QTd no-hover> {{ dashIfEmpty(buy.price) }}</QTd>
<QTd colspan="2" no-hover> {{ dashIfEmpty(buy.total) }}</QTd>
</QTr>
<QTr>
<QTd colspan="5" no-hover>
<span class="label">{{ t('Total entry') }}: </span>
<span>{{ row.total }} </span>
</QTd>
<QTd no-hover>
<span class="label">{{ t('Total stems') }}: </span>
<span>{{ row.quantity }}</span>
</QTd>
</QTr>
</template>
</QTable>
</QPage>
<QTd no-hover>
<span>{{ buy.subName }}</span>
<FetchedTags :item="buy" />
</QTd>
<QTd no-hover> {{ dashIfEmpty(buy.quantity) }}</QTd>
<QTd no-hover> {{ dashIfEmpty(buy.price) }}</QTd>
<QTd colspan="2" no-hover> {{ dashIfEmpty(buy.total) }}</QTd>
</QTr>
<QTr>
<QTd colspan="5" no-hover>
<span class="label">{{ t('Total entry') }}: </span>
<span>{{ row.total }} </span>
</QTd>
<QTd no-hover>
<span class="label">{{ t('Total stems') }}: </span>
<span>{{ row.quantity }}</span>
</QTd>
</QTr>
</template>
</QTable>
</template>
<style scoped lang="scss">

View File

@ -2,14 +2,15 @@
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnTable from 'components/VnTable/VnTable.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import SupplierListFilter from './SupplierListFilter.vue';
import VnSection from 'src/components/common/VnSection.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'src/components/FetchData.vue';
const { t } = useI18n();
const tableRef = ref();
const dataKey = 'SupplierList';
const provincesOptions = ref([]);
const columns = computed(() => [
{
align: 'left',
@ -104,38 +105,62 @@ const columns = computed(() => [
},
]);
</script>
<template>
<VnSearchbar data-key="SuppliersList" :limit="20" :label="t('Search suppliers')" />
<RightMenu>
<template #right-panel>
<SupplierListFilter data-key="SuppliersList" />
</template>
</RightMenu>
<VnTable
ref="tableRef"
data-key="SuppliersList"
url="Suppliers/filter"
redirect="supplier"
:create="{
urlCreate: 'Suppliers/newSupplier',
title: t('Create Supplier'),
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {},
mapper: (data) => {
data.name = data.socialName;
return data;
},
}"
:right-search="false"
order="id ASC"
<FetchData
url="Provinces"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
@on-fetch="(data) => (provincesOptions = data)"
auto-load
/>
<VnSection
:data-key="dataKey"
:columns="columns"
prefix="supplier"
:array-data-props="{
url: 'Suppliers/filter',
order: 'id ASC',
}"
>
<template #more-create-dialog="{ data }">
<VnInput :label="t('globals.name')" v-model="data.socialName" :uppercase="true" />
</template>
</VnTable>
<template #body>
<VnTable
ref="tableRef"
:data-key="dataKey"
:create="{
urlCreate: 'Suppliers/newSupplier',
title: t('Create Supplier'),
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {},
mapper: (data) => {
data.name = data.socialName;
delete data.socialName;
return data;
},
}"
:columns="columns"
redirect="supplier"
:right-search="false"
>
<template #more-create-dialog="{ data }">
<VnInput
:label="t('globals.name')"
v-model="data.socialName"
:uppercase="true"
/>
</template>
</VnTable>
</template>
<template #moreFilterPanel="{ params, searchFn }">
<VnSelect
:label="t('globals.params.provinceFk')"
v-model="params.provinceFk"
@update:model-value="searchFn()"
:options="provincesOptions"
filled
dense
class="q-px-sm q-pr-lg"
/>
</template>
</VnSection>
</template>
<i18n>

View File

@ -1,122 +0,0 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FetchData from 'components/FetchData.vue';
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const { t } = useI18n();
const provincesOptions = ref([]);
const countriesOptions = ref([]);
</script>
<template>
<FetchData
url="Provinces"
:filter="{ fields: ['id', 'name'], order: 'name ASC'}"
@on-fetch="(data) => (provincesOptions = data)"
auto-load
/>
<FetchData
url="countries"
:filter="{ fields: ['id', 'name'], order: 'name ASC'}"
@on-fetch="(data) => (countriesOptions = data)"
auto-load
/>
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
:unremovable-params="['supplierFk']"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<QItem>
<QItemSection>
<VnInput
v-model="params.search"
:label="t('params.search')"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.nickname"
:label="t('params.nickname')"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput v-model="params.nif" :label="t('params.nif')" is-outlined />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
:label="t('params.provinceFk')"
v-model="params.provinceFk"
@update:model-value="searchFn()"
:options="provincesOptions"
option-value="id"
option-label="name"
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
:label="t('params.countryFk')"
v-model="params.countryFk"
@update:model-value="searchFn()"
:options="countriesOptions"
option-value="id"
option-label="name"
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
search: General search
nickname: Alias
nif: Tax number
provinceFk: Province
countryFk: Country
es:
params:
search: Búsqueda general
nickname: Alias
nif: NIF/CIF
provinceFk: Provincia
countryFk: País
</i18n>

View File

@ -376,6 +376,30 @@ onMounted(async () => {
</template>
<template #body-cell-problems="{ row }">
<QTd class="q-gutter-x-xs">
<QIcon
v-if="row.futureAgencyFk !== row.agencyFk && row.agencyFk"
color="primary"
name="vn:agency-term"
size="xs"
class="q-mr-xs"
>
<QTooltip class="column">
<span>
{{
t('advanceTickets.originAgency', {
agency: row.futureAgency,
})
}}
</span>
<span>
{{
t('advanceTickets.destinationAgency', {
agency: row.agency,
})
}}
</span>
</QTooltip>
</QIcon>
<QIcon
v-if="row.isTaxDataChecked === 0"
color="primary"

View File

@ -1,19 +1,12 @@
import { RouterView } from 'vue-router';
export default {
path: '/supplier',
name: 'Supplier',
const supplierCard = {
name: 'SupplierCard',
path: ':id',
component: () => import('src/pages/Supplier/Card/SupplierCard.vue'),
redirect: { name: 'SupplierSummary' },
meta: {
title: 'suppliers',
icon: 'vn:supplier',
moduleName: 'Supplier',
keyBinding: 'p',
},
component: RouterView,
redirect: { name: 'SupplierMain' },
menus: {
main: ['SupplierList'],
card: [
menu: [
'SupplierBasicData',
'SupplierFiscalData',
'SupplierBillingData',
@ -27,21 +20,165 @@ export default {
'SupplierDms',
],
},
children: [
{
name: 'SupplierSummary',
path: 'summary',
meta: {
title: 'summary',
icon: 'launch',
},
component: () => import('src/pages/Supplier/Card/SupplierSummary.vue'),
},
{
path: 'basic-data',
name: 'SupplierBasicData',
meta: {
title: 'basicData',
icon: 'vn:settings',
},
component: () => import('src/pages/Supplier/Card/SupplierBasicData.vue'),
},
{
path: 'fiscal-data',
name: 'SupplierFiscalData',
meta: {
title: 'fiscalData',
icon: 'vn:dfiscales',
},
component: () => import('src/pages/Supplier/Card/SupplierFiscalData.vue'),
},
{
path: 'billing-data',
name: 'SupplierBillingData',
meta: {
title: 'billingData',
icon: 'vn:payment',
},
component: () => import('src/pages/Supplier/Card/SupplierBillingData.vue'),
},
{
path: 'log',
name: 'SupplierLog',
meta: {
title: 'log',
icon: 'vn:History',
},
component: () => import('src/pages/Supplier/Card/SupplierLog.vue'),
},
{
path: 'account',
name: 'SupplierAccounts',
meta: {
title: 'accounts',
icon: 'vn:credit',
},
component: () => import('src/pages/Supplier/Card/SupplierAccounts.vue'),
},
{
path: 'contact',
name: 'SupplierContacts',
meta: {
title: 'contacts',
icon: 'contact_phone',
},
component: () => import('src/pages/Supplier/Card/SupplierContacts.vue'),
},
{
path: 'address',
name: 'SupplierAddresses',
meta: {
title: 'addresses',
icon: 'vn:delivery',
},
component: () => import('src/pages/Supplier/Card/SupplierAddresses.vue'),
},
{
path: 'address/create',
name: 'SupplierAddressesCreate',
component: () =>
import('src/pages/Supplier/Card/SupplierAddressesCreate.vue'),
},
{
path: 'balance',
name: 'SupplierBalance',
meta: {
title: 'balance',
icon: 'balance',
},
component: () => import('src/pages/Supplier/Card/SupplierBalance.vue'),
},
{
path: 'consumption',
name: 'SupplierConsumption',
meta: {
title: 'consumption',
icon: 'show_chart',
},
component: () => import('src/pages/Supplier/Card/SupplierConsumption.vue'),
},
{
path: 'agency-term',
name: 'SupplierAgencyTerm',
meta: {
title: 'agencyTerm',
icon: 'vn:agency-term',
},
component: () => import('src/pages/Supplier/Card/SupplierAgencyTerm.vue'),
},
{
path: 'dms',
name: 'SupplierDms',
meta: {
title: 'dms',
icon: 'smb_share',
},
component: () => import('src/pages/Supplier/Card/SupplierDms.vue'),
},
{
path: 'agency-term/create',
name: 'SupplierAgencyTermCreate',
component: () =>
import('src/pages/Supplier/Card/SupplierAgencyTermCreate.vue'),
},
],
};
export default {
name: 'Supplier',
path: '/supplier',
meta: {
title: 'suppliers',
icon: 'vn:supplier',
moduleName: 'Supplier',
keyBinding: 'p',
menu: ['SupplierList'],
},
component: RouterView,
redirect: { name: 'SupplierMain' },
children: [
{
path: '',
name: 'SupplierMain',
component: () => import('src/components/common/VnModule.vue'),
redirect: { name: 'SupplierList' },
redirect: { name: 'SupplierIndexMain' },
children: [
{
path: 'list',
name: 'SupplierList',
meta: {
title: 'list',
icon: 'view_list',
},
path: '',
name: 'SupplierIndexMain',
redirect: { name: 'SupplierList' },
component: () => import('src/pages/Supplier/SupplierList.vue'),
children: [
{
path: 'list',
name: 'SupplierList',
meta: {
title: 'list',
icon: 'view_list',
},
},
supplierCard,
],
},
{
path: 'create',
@ -54,143 +191,5 @@ export default {
},
],
},
{
name: 'SupplierCard',
path: ':id',
component: () => import('src/pages/Supplier/Card/SupplierCard.vue'),
redirect: { name: 'SupplierSummary' },
children: [
{
name: 'SupplierSummary',
path: 'summary',
meta: {
title: 'summary',
icon: 'launch',
},
component: () =>
import('src/pages/Supplier/Card/SupplierSummary.vue'),
},
{
path: 'basic-data',
name: 'SupplierBasicData',
meta: {
title: 'basicData',
icon: 'vn:settings',
},
component: () =>
import('src/pages/Supplier/Card/SupplierBasicData.vue'),
},
{
path: 'fiscal-data',
name: 'SupplierFiscalData',
meta: {
title: 'fiscalData',
icon: 'vn:dfiscales',
},
component: () =>
import('src/pages/Supplier/Card/SupplierFiscalData.vue'),
},
{
path: 'billing-data',
name: 'SupplierBillingData',
meta: {
title: 'billingData',
icon: 'vn:payment',
},
component: () =>
import('src/pages/Supplier/Card/SupplierBillingData.vue'),
},
{
path: 'log',
name: 'SupplierLog',
meta: {
title: 'log',
icon: 'vn:History',
},
component: () => import('src/pages/Supplier/Card/SupplierLog.vue'),
},
{
path: 'account',
name: 'SupplierAccounts',
meta: {
title: 'accounts',
icon: 'vn:credit',
},
component: () =>
import('src/pages/Supplier/Card/SupplierAccounts.vue'),
},
{
path: 'contact',
name: 'SupplierContacts',
meta: {
title: 'contacts',
icon: 'contact_phone',
},
component: () =>
import('src/pages/Supplier/Card/SupplierContacts.vue'),
},
{
path: 'address',
name: 'SupplierAddresses',
meta: {
title: 'addresses',
icon: 'vn:delivery',
},
component: () =>
import('src/pages/Supplier/Card/SupplierAddresses.vue'),
},
{
path: 'address/create',
name: 'SupplierAddressesCreate',
component: () =>
import('src/pages/Supplier/Card/SupplierAddressesCreate.vue'),
},
{
path: 'balance',
name: 'SupplierBalance',
meta: {
title: 'balance',
icon: 'balance',
},
component: () =>
import('src/pages/Supplier/Card/SupplierBalance.vue'),
},
{
path: 'consumption',
name: 'SupplierConsumption',
meta: {
title: 'consumption',
icon: 'show_chart',
},
component: () =>
import('src/pages/Supplier/Card/SupplierConsumption.vue'),
},
{
path: 'agency-term',
name: 'SupplierAgencyTerm',
meta: {
title: 'agencyTerm',
icon: 'vn:agency-term',
},
component: () =>
import('src/pages/Supplier/Card/SupplierAgencyTerm.vue'),
},
{
path: 'dms',
name: 'SupplierDms',
meta: {
title: 'dms',
icon: 'smb_share',
},
component: () => import('src/pages/Supplier/Card/SupplierDms.vue'),
},
{
path: 'agency-term/create',
name: 'SupplierAgencyTermCreate',
component: () =>
import('src/pages/Supplier/Card/SupplierAgencyTermCreate.vue'),
},
],
},
],
};

View File

@ -0,0 +1,153 @@
import { setActivePinia, createPinia } from 'pinia';
import { describe, beforeEach, afterEach, it, expect, vi, beforeAll } from 'vitest';
import { useNavigationStore } from '../useNavigationStore';
import axios from 'axios';
let store;
vi.mock('src/router/modules', () => [
{ name: 'Item', meta: {} },
{ name: 'Shelving', meta: {} },
{ name: 'Order', meta: {} },
]);
vi.mock('src/filters', () => ({
toLowerCamel: vi.fn((name) => name.toLowerCase()),
}));
const modulesMock = [
{
name: 'Item',
children: null,
title: 'globals.pageTitles.undefined',
icon: undefined,
module: 'item',
isPinned: true,
},
{
name: 'Shelving',
children: null,
title: 'globals.pageTitles.undefined',
icon: undefined,
module: 'shelving',
isPinned: false,
},
{
name: 'Order',
children: null,
title: 'globals.pageTitles.undefined',
icon: undefined,
module: 'order',
isPinned: false,
},
];
const pinnedModulesMock = [
{
name: 'Item',
children: null,
title: 'globals.pageTitles.undefined',
icon: undefined,
module: 'item',
isPinned: true,
},
];
describe('useNavigationStore', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.spyOn(axios, 'get').mockResolvedValue({ data: true });
store = useNavigationStore();
store.getModules = vi.fn().mockReturnValue({
value: modulesMock,
});
store.getPinnedModules = vi.fn().mockReturnValue({
value: pinnedModulesMock,
});
});
afterEach(() => {
vi.clearAllMocks();
});
it('should return modules with correct structure', () => {
const store = useNavigationStore();
const modules = store.getModules();
expect(modules.value).toEqual(modulesMock);
});
it('should return pinned modules', () => {
const store = useNavigationStore();
const pinnedModules = store.getPinnedModules();
expect(pinnedModules.value).toEqual(pinnedModulesMock);
});
it('should toggle pinned modules', () => {
const store = useNavigationStore();
store.togglePinned('item');
store.togglePinned('shelving');
expect(store.pinnedModules).toEqual(['item', 'shelving']);
store.togglePinned('item');
expect(store.pinnedModules).toEqual(['shelving']);
});
it('should fetch pinned modules', async () => {
vi.spyOn(axios, 'get').mockResolvedValue({
data: [{ id: 1, workerFk: 9, moduleFk: 'order', position: 1 }],
});
const store = useNavigationStore();
await store.fetchPinned();
expect(store.pinnedModules).toEqual(['order']);
});
it('should add menu item correctly', () => {
const store = useNavigationStore();
const module = 'customer';
const parent = [];
const route = {
name: 'customer',
title: 'Customer',
icon: 'customer',
meta: {
keyBinding: 'ctrl+shift+c',
name: 'customer',
title: 'Customer',
icon: 'customer',
menu: 'customer',
menuChildren: [{ name: 'customer', title: 'Customer', icon: 'customer' }],
},
};
const result = store.addMenuItem(module, route, parent);
const expectedItem = {
children: [
{
icon: 'customer',
name: 'customer',
title: 'globals.pageTitles.Customer',
},
],
icon: 'customer',
keyBinding: 'ctrl+shift+c',
name: 'customer',
title: 'globals.pageTitles.Customer',
};
expect(result).toEqual(expectedItem);
expect(parent.length).toBe(1);
expect(parent).toEqual([expectedItem]);
});
it('should not add menu item if condition is not met', () => {
const store = useNavigationStore();
const module = 'testModule';
const route = { meta: { hidden: true, menuchildren: {} } };
const parent = [];
const result = store.addMenuItem(module, route, parent);
expect(result).toBeUndefined();
expect(parent.length).toBe(0);
});
});