0
0
Fork 0

Merge branch 'dev' into 6895-removeShelving

This commit is contained in:
Carlos Satorres 2024-05-02 10:09:41 +00:00
commit 8bb5f77b57
35 changed files with 1339 additions and 148 deletions

View File

@ -169,6 +169,7 @@ watch(modelValue, (newValue) => {
ref="vnSelectRef" ref="vnSelectRef"
:class="{ required: $attrs.required }" :class="{ required: $attrs.required }"
:rules="$attrs.required ? [requiredFieldRule] : null" :rules="$attrs.required ? [requiredFieldRule] : null"
virtual-scroll-slice-size="options.length"
> >
<template v-if="isClearable" #append> <template v-if="isClearable" #append>
<QIcon <QIcon

View File

@ -79,6 +79,7 @@ watch(
const isLoading = ref(false); const isLoading = ref(false);
async function search() { async function search() {
store.filter.where = {};
isLoading.value = true; isLoading.value = true;
const params = { ...userParams.value }; const params = { ...userParams.value };
store.userParamsChanged = true; store.userParamsChanged = true;

View File

@ -95,6 +95,8 @@ const addFilter = async (filter, params) => {
}; };
async function fetch() { async function fetch() {
store.filter.skip = 0;
store.skip = 0;
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
if (!store.hasMoreData) { if (!store.hasMoreData) {
isLoading.value = false; isLoading.value = false;

View File

@ -81,8 +81,10 @@ async function search() {
const staticParams = Object.entries(store.userParams).filter( const staticParams = Object.entries(store.userParams).filter(
([key, value]) => value && (props.staticParams || []).includes(key) ([key, value]) => value && (props.staticParams || []).includes(key)
); );
// const filter =props?.where? { where: JSON.parse(props.where) }: {}
await arrayData.applyFilter({ await arrayData.applyFilter({
params: { params: {
// filter ,
...Object.fromEntries(staticParams), ...Object.fromEntries(staticParams),
search: searchText.value, search: searchText.value,
}, },
@ -106,6 +108,7 @@ async function search() {
let targetUrl; let targetUrl;
if (path.endsWith('/list')) targetUrl = path.replace('/list', `/${targetId}/summary`); if (path.endsWith('/list')) targetUrl = path.replace('/list', `/${targetId}/summary`);
if (path.endsWith('-list')) targetUrl = path.replace('-list', `/${targetId}/summary`);
else if (path.includes(':id')) targetUrl = path.replace(':id', targetId); else if (path.includes(':id')) targetUrl = path.replace(':id', targetId);
await router.push({ path: targetUrl }); await router.push({ path: targetUrl });

View File

@ -1,6 +1,5 @@
<script setup> <script setup>
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const $props = defineProps({ const $props = defineProps({

View File

@ -3,11 +3,14 @@ import { useRole } from './useRole';
import { useUserConfig } from './useUserConfig'; import { useUserConfig } from './useUserConfig';
import axios from 'axios'; import axios from 'axios';
import useNotify from './useNotify'; import useNotify from './useNotify';
import { useTokenConfig } from './useTokenConfig';
const TOKEN_MULTIMEDIA = 'tokenMultimedia'; const TOKEN_MULTIMEDIA = 'tokenMultimedia';
const TOKEN = 'token'; const TOKEN = 'token';
export function useSession() { export function useSession() {
const { notify } = useNotify(); const { notify } = useNotify();
let isCheckingToken = false;
let intervalId = null;
function getToken() { function getToken() {
const localToken = localStorage.getItem(TOKEN); const localToken = localStorage.getItem(TOKEN);
@ -22,10 +25,24 @@ export function useSession() {
return localTokenMultimedia || sessionTokenMultimedia || ''; return localTokenMultimedia || sessionTokenMultimedia || '';
} }
function setToken(data) { function setSession(data) {
const storage = data.keepLogin ? localStorage : sessionStorage; let keepLogin = data.keepLogin;
const storage = keepLogin ? localStorage : sessionStorage;
storage.setItem(TOKEN, data.token); storage.setItem(TOKEN, data.token);
storage.setItem(TOKEN_MULTIMEDIA, data.tokenMultimedia); storage.setItem(TOKEN_MULTIMEDIA, data.tokenMultimedia);
storage.setItem('created', data.created);
storage.setItem('ttl', data.ttl);
sessionStorage.setItem('keepLogin', keepLogin);
}
function keepLogin() {
return sessionStorage.getItem('keepLogin');
}
function setToken({ token, tokenMultimedia }) {
const storage = keepLogin() ? localStorage : sessionStorage;
storage.setItem(TOKEN, token);
storage.setItem(TOKEN_MULTIMEDIA, tokenMultimedia);
} }
async function destroyToken(url, storage, key) { async function destroyToken(url, storage, key) {
if (storage.getItem(key)) { if (storage.getItem(key)) {
@ -45,11 +62,15 @@ export function useSession() {
tokenMultimedia: 'Accounts/logout', tokenMultimedia: 'Accounts/logout',
token: 'VnUsers/logout', token: 'VnUsers/logout',
}; };
const storage = keepLogin() ? localStorage : sessionStorage;
for (const [key, url] of Object.entries(tokens)) { for (const [key, url] of Object.entries(tokens)) {
await destroyToken(url, localStorage, key); await destroyToken(url, storage, key);
await destroyToken(url, sessionStorage, key);
} }
localStorage.clear();
sessionStorage.clear();
const { setUser } = useState(); const { setUser } = useState();
setUser({ setUser({
@ -59,22 +80,75 @@ export function useSession() {
lang: '', lang: '',
darkMode: null, darkMode: null,
}); });
stopRenewer();
} }
async function login(token, tokenMultimedia, keepLogin) { async function login(data) {
setToken({ token, tokenMultimedia, keepLogin }); setSession(data);
await useRole().fetch(); await useRole().fetch();
await useUserConfig().fetch(); await useUserConfig().fetch();
await useTokenConfig().fetch();
startInterval();
} }
function isLoggedIn() { function isLoggedIn() {
const localToken = localStorage.getItem(TOKEN); const localToken = localStorage.getItem(TOKEN);
const sessionToken = sessionStorage.getItem(TOKEN); const sessionToken = sessionStorage.getItem(TOKEN);
startInterval();
return !!(localToken || sessionToken); return !!(localToken || sessionToken);
} }
function startInterval() {
stopRenewer();
const renewPeriod = +sessionStorage.getItem('renewPeriod');
if (!renewPeriod) return;
intervalId = setInterval(() => checkValidity(), renewPeriod * 1000);
}
function stopRenewer() {
clearInterval(intervalId);
}
async function renewToken() {
const _token = getToken();
const token = await axios.post('VnUsers/renewToken', {
headers: { Authorization: _token },
});
const _tokenMultimedia = getTokenMultimedia();
const tokenMultimedia = await axios.post('VnUsers/renewToken', {
headers: { Authorization: _tokenMultimedia },
});
setToken({ token: token.data.id, tokenMultimedia: tokenMultimedia.data.id });
}
async function checkValidity() {
const { getTokenConfig } = useState();
const tokenConfig = getTokenConfig() ?? sessionStorage.getItem('tokenConfig');
const storage = keepLogin() ? localStorage : sessionStorage;
const created = +storage.getItem('created');
const ttl = +storage.getItem('ttl');
if (isCheckingToken || !created) return;
isCheckingToken = true;
const renewPeriodInSeconds = Math.min(ttl, tokenConfig.value.renewPeriod) * 1000;
const maxDate = created + renewPeriodInSeconds;
const now = new Date().getTime();
if (isNaN(renewPeriodInSeconds) || now <= maxDate) {
return (isCheckingToken = false);
}
await renewToken();
isCheckingToken = false;
}
return { return {
getToken, getToken,
getTokenMultimedia, getTokenMultimedia,
@ -82,5 +156,8 @@ export function useSession() {
destroy, destroy,
login, login,
isLoggedIn, isLoggedIn,
checkValidity,
setSession,
renewToken,
}; };
} }

View File

@ -13,6 +13,7 @@ const user = ref({
}); });
const roles = ref([]); const roles = ref([]);
const tokenConfig = ref({});
const drawer = ref(true); const drawer = ref(true);
const headerMounted = ref(false); const headerMounted = ref(false);
@ -52,6 +53,15 @@ export function useState() {
function setRoles(data) { function setRoles(data) {
roles.value = data; roles.value = data;
} }
function getTokenConfig() {
return computed(() => {
return tokenConfig.value;
});
}
function setTokenConfig(data) {
tokenConfig.value = data;
}
function set(name, data) { function set(name, data) {
state.value[name] = ref(data); state.value[name] = ref(data);
@ -70,6 +80,8 @@ export function useState() {
setUser, setUser,
getRoles, getRoles,
setRoles, setRoles,
getTokenConfig,
setTokenConfig,
set, set,
get, get,
unset, unset,

View File

@ -0,0 +1,28 @@
import axios from 'axios';
import { useState } from './useState';
import useNotify from './useNotify';
export function useTokenConfig() {
const state = useState();
const { notify } = useNotify();
async function fetch() {
try {
const { data } = await axios.get('AccessTokenConfigs/findOne', {
filter: { fields: ['renewInterval', 'renewPeriod'] },
});
if (!data) return;
state.setTokenConfig(data);
sessionStorage.setItem('renewPeriod', data.renewPeriod);
return data;
} catch (error) {
notify('errors.tokenConfig', 'negative');
console.error('Error fetching token config:', error);
}
}
return {
fetch,
state,
};
}

View File

@ -106,6 +106,7 @@ errors:
statusBadGateway: It seems that the server has fall down statusBadGateway: It seems that the server has fall down
statusGatewayTimeout: Could not contact the server statusGatewayTimeout: Could not contact the server
userConfig: Error fetching user config userConfig: Error fetching user config
tokenConfig: Error fetching token config
writeRequest: The requested operation could not be completed writeRequest: The requested operation could not be completed
login: login:
title: Login title: Login
@ -1135,6 +1136,8 @@ item:
tax: Tax tax: Tax
barcode: Barcode barcode: Barcode
botanical: Botanical botanical: Botanical
itemTypeCreate: New item type
family: Item Type
descriptor: descriptor:
item: Item item: Item
buyer: Buyer buyer: Buyer
@ -1219,6 +1222,11 @@ item:
minSalesQuantity: 'Cantidad mínima de venta' minSalesQuantity: 'Cantidad mínima de venta'
genus: 'Genus' genus: 'Genus'
specie: 'Specie' specie: 'Specie'
item/itemType:
pageTitles:
itemType: Item type
basicData: Basic data
summary: Summary
components: components:
topbar: {} topbar: {}
itemsFilterPanel: itemsFilterPanel:

View File

@ -106,6 +106,7 @@ errors:
statusBadGateway: Parece ser que el servidor ha caído statusBadGateway: Parece ser que el servidor ha caído
statusGatewayTimeout: No se ha podido contactar con el servidor statusGatewayTimeout: No se ha podido contactar con el servidor
userConfig: Error al obtener configuración de usuario userConfig: Error al obtener configuración de usuario
tokenConfig: Error al obtener configuración de token
writeRequest: No se pudo completar la operación solicitada writeRequest: No se pudo completar la operación solicitada
login: login:
title: Inicio de sesión title: Inicio de sesión
@ -1134,6 +1135,8 @@ item:
botanical: 'Botánico' botanical: 'Botánico'
barcode: 'Código de barras' barcode: 'Código de barras'
log: Historial log: Historial
itemTypeCreate: Nueva familia
family: Familia
descriptor: descriptor:
item: Artículo item: Artículo
buyer: Comprador buyer: Comprador
@ -1218,6 +1221,11 @@ item:
achieved: 'Conseguido' achieved: 'Conseguido'
concept: 'Concepto' concept: 'Concepto'
state: 'Estado' state: 'Estado'
item/itemType:
pageTitles:
itemType: Familia
basicData: Datos básicos
summary: Resumen
components: components:
topbar: {} topbar: {}
itemsFilterPanel: itemsFilterPanel:

View File

@ -37,6 +37,7 @@ const marker_labels = [
{ value: DEFAULT_MAX_RESPONSABILITY, label: t('claim.summary.person') }, { value: DEFAULT_MAX_RESPONSABILITY, label: t('claim.summary.person') },
]; ];
const multiplicatorValue = ref(); const multiplicatorValue = ref();
const loading = ref(false);
const columns = computed(() => [ const columns = computed(() => [
{ {
@ -118,7 +119,7 @@ async function updateDestinations(claimDestinationFk) {
async function updateDestination(claimDestinationFk, row, options = {}) { async function updateDestination(claimDestinationFk, row, options = {}) {
if (claimDestinationFk) { if (claimDestinationFk) {
await axios.post('Claims/updateClaimDestination', { await post('Claims/updateClaimDestination', {
claimDestinationFk, claimDestinationFk,
rows: Array.isArray(row) ? row : [row], rows: Array.isArray(row) ? row : [row],
}); });
@ -127,7 +128,7 @@ async function updateDestination(claimDestinationFk, row, options = {}) {
} }
async function regularizeClaim() { async function regularizeClaim() {
await axios.post(`Claims/${claimId}/regularizeClaim`); await post(`Claims/${claimId}/regularizeClaim`);
await claimRef.value.fetch(); await claimRef.value.fetch();
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
quasar.notify({ quasar.notify({
@ -146,7 +147,7 @@ async function onUpdateGreugeAccept() {
const freightPickUpPrice = const freightPickUpPrice =
(await axios.get(`GreugeConfigs/findOne`)).data.freightPickUpPrice * (await axios.get(`GreugeConfigs/findOne`)).data.freightPickUpPrice *
multiplicatorValue.value; multiplicatorValue.value;
await axios.post(`Greuges`, { await post(`Greuges`, {
clientFk: claim.value.clientFk, clientFk: claim.value.clientFk,
description: `${t('ClaimGreugeDescription')} ${claimId}`.toUpperCase(), description: `${t('ClaimGreugeDescription')} ${claimId}`.toUpperCase(),
amount: freightPickUpPrice, amount: freightPickUpPrice,
@ -165,14 +166,22 @@ async function save(data) {
} }
async function importToNewRefundTicket() { async function importToNewRefundTicket() {
const query = `ClaimBeginnings/${claimId}/importToNewRefundTicket`; await post(`ClaimBeginnings/${claimId}/importToNewRefundTicket`);
await axios.post(query); await claimActionsForm.value.reload();
claimActionsForm.value.reload();
quasar.notify({ quasar.notify({
message: t('globals.dataSaved'), message: t('globals.dataSaved'),
type: 'positive', type: 'positive',
}); });
} }
async function post(query, params) {
loading.value = true;
try {
await axios.post(query, params);
} finally {
loading.value = false;
}
}
</script> </script>
<template> <template>
<FetchData <FetchData
@ -279,6 +288,7 @@ async function importToNewRefundTicket() {
:default-save="false" :default-save="false"
:default-reset="false" :default-reset="false"
@on-fetch="setData" @on-fetch="setData"
:limit="0"
auto-load auto-load
> >
<template #body> <template #body>
@ -390,6 +400,7 @@ async function importToNewRefundTicket() {
icon="check" icon="check"
@click="regularizeClaim" @click="regularizeClaim"
:disable="claim.claimStateFk == resolvedStateId" :disable="claim.claimStateFk == resolvedStateId"
:loading="loading"
/> />
<QBtn <QBtn
@ -401,6 +412,7 @@ async function importToNewRefundTicket() {
:title="t('Change destination')" :title="t('Change destination')"
icon="swap_horiz" icon="swap_horiz"
@click="dialogDestination = !dialogDestination" @click="dialogDestination = !dialogDestination"
:loading="loading"
/> />
<QBtn <QBtn
color="primary" color="primary"
@ -411,6 +423,7 @@ async function importToNewRefundTicket() {
icon="Upload" icon="Upload"
@click="importToNewRefundTicket" @click="importToNewRefundTicket"
:disable="claim.claimStateFk == resolvedStateId" :disable="claim.claimStateFk == resolvedStateId"
:loading="loading"
/> />
</template> </template>
</CrudModel> </CrudModel>

View File

@ -2,7 +2,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
@ -37,8 +37,6 @@ const claimFilter = {
], ],
}; };
const workers = ref([]);
const workersCopy = ref([]);
const claimStates = ref([]); const claimStates = ref([]);
const claimStatesCopy = ref([]); const claimStatesCopy = ref([]);
const optionsList = ref([]); const optionsList = ref([]);
@ -47,6 +45,7 @@ function setWorkers(data) {
workers.value = data; workers.value = data;
workersCopy.value = data; workersCopy.value = data;
} }
const workersOptions = ref([]);
function setClaimStates(data) { function setClaimStates(data) {
claimStates.value = data; claimStates.value = data;
@ -68,25 +67,6 @@ async function getEnumValues() {
getEnumValues(); getEnumValues();
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 = { const statesFilter = {
options: claimStates, options: claimStates,
filterFn: (options, value) => { filterFn: (options, value) => {
@ -106,7 +86,7 @@ const statesFilter = {
<FetchData <FetchData
url="Workers/activeWithInheritedRole" url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }" :filter="{ where: { role: 'salesPerson' } }"
@on-fetch="setWorkers" @on-fetch="(data) => (workersOptions = data)"
auto-load auto-load
/> />
<FetchData url="ClaimStates" @on-fetch="setClaimStates" auto-load /> <FetchData url="ClaimStates" @on-fetch="setClaimStates" auto-load />
@ -135,18 +115,15 @@ const statesFilter = {
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<QSelect <VnSelect
:label="t('claim.basicData.assignedTo')"
v-model="data.workerFk" v-model="data.workerFk"
:options="workers" :options="workersOptions"
option-value="id" option-value="id"
option-label="name" option-label="name"
emit-value emit-value
:label="t('claim.basicData.assignedTo')" auto-load
map-options
use-input
@filter="(value, update) => filter(value, update, workerFilter)"
:rules="validate('claim.claimStateFk')" :rules="validate('claim.claimStateFk')"
:input-debounce="0"
> >
<template #before> <template #before>
<QAvatar color="orange"> <QAvatar color="orange">
@ -157,7 +134,7 @@ const statesFilter = {
/> />
</QAvatar> </QAvatar>
</template> </template>
</QSelect> </VnSelect>
</div> </div>
<div class="col"> <div class="col">
<QSelect <QSelect

View File

@ -1 +1,277 @@
<template>Item diary (CREAR CUANDO SE DESARROLLE EL MODULO DE ITEMS)</template> <script setup>
import { onMounted, computed, onUnmounted, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import FetchData from 'components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import { useStateStore } from 'stores/useStateStore';
import { toDateFormat } from 'src/filters/date.js';
import { dashIfEmpty } from 'src/filters';
import { date } from 'quasar';
import { useState } from 'src/composables/useState';
const { t } = useI18n();
const route = useRoute();
const stateStore = useStateStore();
const state = useState();
const user = state.getUser();
const today = ref(Date.vnNew());
const warehousesOptions = ref([]);
const itemBalancesRef = ref(null);
const itemsBalanceFilter = reactive({
where: { itemFk: route.params.id, warehouseFk: null, date: null },
});
const itemBalances = ref([]);
const warehouseFk = ref(null);
const _showWhatsBeforeInventory = ref(false);
const columns = computed(() => [
{
name: 'claim',
align: 'left',
field: 'itemFk',
},
{
label: t('itemDiary.date'),
name: 'date',
field: 'shipped',
align: 'left',
},
{
label: t('itemDiary.id'),
name: 'id',
align: 'left',
},
{
label: t('itemDiary.state'),
field: 'stateName',
name: 'state',
align: 'left',
format: (val) => dashIfEmpty(val),
},
{
label: t('itemDiary.reference'),
field: 'reference',
name: 'reference',
align: 'left',
format: (val) => dashIfEmpty(val),
},
{
label: t('itemDiary.client'),
name: 'client',
align: 'left',
format: (val) => dashIfEmpty(val),
},
{
label: t('itemDiary.in'),
field: 'invalue',
name: 'in',
align: 'left',
format: (val) => dashIfEmpty(val),
},
{
label: t('itemDiary.out'),
field: 'out',
name: 'out',
align: 'left',
format: (val) => dashIfEmpty(val),
},
{
label: t('itemDiary.balance'),
name: 'balance',
align: 'left',
},
]);
const showWhatsBeforeInventory = computed({
get: () => _showWhatsBeforeInventory.value,
set: (val) => {
_showWhatsBeforeInventory.value = val;
if (!val) itemsBalanceFilter.where.date = null;
else itemsBalanceFilter.where.date = new Date();
},
});
const fetchItemBalances = async () => await itemBalancesRef.value.fetch();
const getBadgeAttrs = (_date) => {
const isSameDate = date.isSameDate(today.value, _date);
const attrs = {
'text-color': isSameDate ? 'black' : 'white',
color: isSameDate ? 'warning' : 'transparent',
};
return attrs;
};
const getIdDescriptor = (row) => {
let descriptor = EntryDescriptorProxy;
if (row.isTicket) descriptor = TicketDescriptorProxy;
return descriptor;
};
onMounted(async () => {
today.value.setHours(0, 0, 0, 0);
if (route.query.warehouseFk) warehouseFk.value = route.query.warehouseFk;
else if (user.value) warehouseFk.value = user.value.warehouseFk;
itemsBalanceFilter.where.warehouseFk = warehouseFk.value;
await fetchItemBalances();
});
onUnmounted(() => (stateStore.rightDrawer = false));
</script>
<template>
<FetchData
ref="itemBalancesRef"
url="Items/getBalance"
:filter="itemsBalanceFilter"
@on-fetch="(data) => (itemBalances = data)"
/>
<FetchData
url="Warehouses"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
auto-load
@on-fetch="(data) => (warehousesOptions = data)"
/>
<QToolbar class="justify-end">
<div id="st-data" class="row">
<VnSelect
:label="t('itemDiary.warehouse')"
:options="warehousesOptions"
hide-selected
option-label="name"
option-value="id"
dense
v-model="itemsBalanceFilter.where.warehouseFk"
@update:model-value="fetchItemBalances"
class="q-mr-lg"
/>
<QCheckbox
:label="t('itemDiary.showBefore')"
v-model="showWhatsBeforeInventory"
@update:model-value="fetchItemBalances"
class="q-mr-lg"
/>
<VnInputDate
v-if="showWhatsBeforeInventory"
:label="t('itemDiary.since')"
dense
v-model="itemsBalanceFilter.where.date"
@update:model-value="fetchItemBalances"
/>
</div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<QPage class="column items-center q-pa-md">
<QTable
:rows="itemBalances"
:columns="columns"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
>
<template #body-cell-claim="{ row }">
<QTd @click.stop>
<QBtn
v-show="row.claimFk"
flat
color="primary"
:to="{ name: 'ClaimSummary', params: { id: row.claimFk } }"
icon="vn:claims"
dense
>
<QTooltip>
{{ t('itemDiary.claim') }}: {{ row.claimFk }}
</QTooltip>
</QBtn>
</QTd>
</template>
<template #body-cell-date="{ row }">
<QTd @click.stop>
<QBadge
v-bind="getBadgeAttrs(row.shipped)"
class="q-ma-none"
dense
style="font-size: 14px"
>
{{ toDateFormat(row.shipped) }}
</QBadge>
</QTd>
</template>
<template #body-cell-id="{ row }">
<QTd @click.stop>
<component
:is="getIdDescriptor(row)"
:id="row.origin"
class="q-ma-none"
dense
style="font-size: 14px"
>
{{ row.origin }}
</component>
<span class="link">{{ row.origin }}</span>
</QTd>
</template>
<template #body-cell-client="{ row }">
<QTd @click.stop>
<QBadge
:color="row.highlighted ? 'warning' : 'transparent'"
:text-color="row.highlighted ? 'black' : 'white'"
dense
style="font-size: 14px"
>
<span v-if="row.isTicket" class="link">
{{ dashIfEmpty(row.name) }}
<CustomerDescriptorProxy :id="row.clientFk" />
</span>
<span v-else>{{ dashIfEmpty(row.name) }}</span>
</QBadge>
</QTd>
</template>
<template #body-cell-in="{ row }">
<QTd @click.stop>
<span :class="{ 'is-in': row.invalue }">
{{ dashIfEmpty(row.invalue) }}
</span>
</QTd>
</template>
<template #body-cell-balance="{ row }">
<QTd @click.stop>
<QBadge
class="balance-negative"
:color="
row.lineFk == row.lastPreparedLineFk
? 'grey-13'
: 'transparent'
"
:text-color="
row.lineFk == row.lastPreparedLineFk
? 'black'
: row.balance < 0
? 'negative'
: ''
"
dense
style="font-size: 14px"
>
<span>{{ dashIfEmpty(row.balance) }}</span>
</QBadge>
</QTd>
</template>
</QTable>
</QPage>
</template>
<style lang="scss" scoped>
.is-in {
color: $positive;
}
</style>

View File

@ -0,0 +1,91 @@
<script setup>
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
const { t } = useI18n();
const router = useRouter();
const newItemTypeForm = reactive({});
const workersOptions = ref([]);
const categoriesOptions = ref([]);
const temperaturesOptions = ref([]);
const redirectToItemTypeBasicData = (_, { id }) => {
router.push({ name: 'ItemTypeBasicData', params: { id } });
};
</script>
<template>
<FetchData
url="Workers"
@on-fetch="(data) => (workersOptions = data)"
:filter="{ order: 'firstName ASC', fields: ['id', 'firstName'] }"
auto-load
/>
<FetchData
url="ItemCategories"
@on-fetch="(data) => (categoriesOptions = data)"
:filter="{ order: 'name ASC', fields: ['id', 'name'] }"
auto-load
/>
<FetchData
url="Temperatures"
@on-fetch="(data) => (temperaturesOptions = data)"
:filter="{ order: 'name ASC', fields: ['code', 'name'] }"
auto-load
/>
<QPage>
<VnSubToolbar />
<FormModel
url-create="ItemTypes"
model="itemTypeCreate"
:form-initial-data="newItemTypeForm"
observe-form-changes
@on-data-saved="redirectToItemTypeBasicData"
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<VnInput v-model="data.code" :label="t('itemType.shared.code')" />
<VnInput v-model="data.name" :label="t('itemType.shared.name')" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelect
v-model="data.workerFk"
:label="t('itemType.shared.worker')"
:options="workersOptions"
option-value="id"
option-label="firstName"
hide-selected
/>
<VnSelect
v-model="data.categoryFk"
:label="t('itemType.shared.category')"
:options="categoriesOptions"
option-value="id"
option-label="name"
hide-selected
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelect
v-model="data.temperatureFk"
:label="t('itemType.shared.temperature')"
:options="temperaturesOptions"
option-value="code"
option-label="name"
hide-selected
/>
</VnRow>
</template>
</FormModel>
</QPage>
</template>

View File

@ -0,0 +1,142 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
import ItemTypeSummary from 'src/pages/ItemType/Card/ItemTypeSummary.vue';
import ItemTypeFilter from 'src/pages/ItemType/ItemTypeFilter.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
const stateStore = useStateStore();
const router = useRouter();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const redirectToItemTypeSummary = (id) => {
router.push({ name: 'ItemTypeSummary', params: { id } });
};
const redirectToCreateView = () => {
router.push({ name: 'ItemTypeCreate' });
};
const exprBuilder = (param, value) => {
switch (param) {
case 'name':
return {
name: { like: `%${value}%` },
};
case 'code':
return {
code: { like: `%${value}%` },
};
case 'search':
if (value) {
if (!isNaN(value)) {
return { id: value };
} else {
return {
or: [
{
name: {
like: `%${value}%`,
},
},
{
code: {
like: `%${value}%`,
},
},
],
};
}
}
}
};
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="ItemTypeList"
url="ItemTypes"
:label="t('Search item type')"
:info="t('Search itemType by id, name or code')"
:expr-builder="exprBuilder"
/>
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<ItemTypeFilter data-key="ItemTypeList" />
</QScrollArea>
</QDrawer>
<QPage class="column items-center q-pa-md">
<div class="vn-card-list">
<VnPaginate
data-key="ItemTypeList"
url="ItemTypes"
:order="['name']"
auto-load
:expr-builder="exprBuilder"
>
<template #body="{ rows }">
<CardList
v-for="row of rows"
:key="row.id"
:title="row.code"
@click="redirectToItemTypeSummary(row.id)"
:id="row.id"
>
<template #list-items>
<VnLv :label="t('Name')" :value="row.name" />
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, ItemTypeSummary)"
color="primary"
type="submit"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
</QPage>
<QPageSticky :offset="[20, 20]">
<QBtn fab icon="add" color="primary" @click="redirectToCreateView()" />
<QTooltip>
{{ t('New item type') }}
</QTooltip>
</QPageSticky>
</template>
<i18n>
es:
New item type: Nueva familia
Name: Nombre
Search item type: Buscar familia
Search itemType by id, name or code: Buscar familia por id, nombre o código
</i18n>

View File

@ -0,0 +1,13 @@
itemDiary:
date: Date
id: Id
state: State
reference: Reference
client: Client
in: In
out: Out
balance: Balance
claim: Claim
showBefore: Show what's before the inventory
since: Since
warehouse: Warehouse

View File

@ -0,0 +1,13 @@
itemDiary:
date: Fecha
id: Id
state: Estado
reference: Referencia
client: Cliente
in: Entrada
out: Salida
balance: Balance
claim: Reclamación
showBefore: Mostrar lo anterior al inventario
since: Desde
warehouse: Almacén

View File

@ -0,0 +1,79 @@
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
const route = useRoute();
const { t } = useI18n();
const workersOptions = ref([]);
const categoriesOptions = ref([]);
const temperaturesOptions = ref([]);
</script>
<template>
<FetchData
url="Workers"
@on-fetch="(data) => (workersOptions = data)"
:filter="{ order: 'firstName ASC', fields: ['id', 'firstName'] }"
auto-load
/>
<FetchData
url="ItemCategories"
@on-fetch="(data) => (categoriesOptions = data)"
:filter="{ order: 'name ASC', fields: ['id', 'name'] }"
auto-load
/>
<FetchData
url="Temperatures"
@on-fetch="(data) => (temperaturesOptions = data)"
:filter="{ order: 'name ASC', fields: ['code', 'name'] }"
auto-load
/>
<FormModel
:url="`ItemTypes/${route.params.id}`"
:url-update="`ItemTypes/${route.params.id}`"
model="itemTypeBasicData"
auto-load
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<VnInput v-model="data.code" :label="t('shared.code')" />
<VnInput v-model="data.name" :label="t('shared.name')" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelect
v-model="data.workerFk"
:label="t('shared.worker')"
:options="workersOptions"
option-value="id"
option-label="firstName"
hide-selected
/>
<VnSelect
v-model="data.categoryFk"
:label="t('shared.category')"
:options="categoriesOptions"
option-value="id"
option-label="name"
hide-selected
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelect
v-model="data.temperatureFk"
:label="t('shared.temperature')"
:options="temperaturesOptions"
option-value="code"
option-label="name"
hide-selected
/>
</VnRow>
</template>
</FormModel>
</template>

View File

@ -0,0 +1,12 @@
<script setup>
import VnCard from 'components/common/VnCard.vue';
import ItemTypeDescriptor from 'src/pages/ItemType/Card/ItemTypeDescriptor.vue';
</script>
<template>
<VnCard
data-key="ItemTypeSummary"
base-url="ItemTypes"
:descriptor="ItemTypeDescriptor"
/>
</template>

View File

@ -0,0 +1,85 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import useCardDescription from 'src/composables/useCardDescription';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
summary: {
type: Object,
default: null,
},
});
const route = useRoute();
const { t } = useI18n();
const itemTypeFilter = {
include: [
{ relation: 'worker' },
{ relation: 'category' },
{ relation: 'itemPackingType' },
{ relation: 'temperature' },
],
};
const entityId = computed(() => {
return $props.id || route.params.id;
});
const data = ref(useCardDescription());
const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id));
</script>
<template>
<CardDescriptor
module="ItemType"
:url="`ItemTypes/${entityId}`"
:filter="itemTypeFilter"
:title="data.title"
:subtitle="data.subtitle"
@on-fetch="setData"
data-key="entry"
>
<template #header-extra-action>
<QBtn
round
flat
size="sm"
icon="vn:item"
color="white"
:to="{ name: 'ItemTypeList' }"
>
<QTooltip>
{{ t('Go to module index') }}
</QTooltip>
</QBtn>
</template>
<template #body="{ entity }">
<VnLv :label="t('shared.code')" :value="entity.code" />
<VnLv :label="t('shared.name')" :value="entity.name" />
<VnLv :label="t('shared.worker')">
<template #value>
<span class="link">{{ entity.worker?.firstName }}</span>
<WorkerDescriptorProxy :id="entity.worker?.id" />
</template>
</VnLv>
<VnLv :label="t('shared.category')" :value="entity.category?.name" />
</template>
</CardDescriptor>
</template>
<i18n>
es:
Go to module index: Ir al índice del módulo
</i18n>

View File

@ -0,0 +1,109 @@
<script setup>
import { ref, computed, onUpdated } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue';
onUpdated(() => summaryRef.value.fetch());
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const itemTypeFilter = {
include: [
{ relation: 'worker' },
{ relation: 'category' },
{ relation: 'itemPackingType' },
{ relation: 'temperature' },
],
};
const entityId = computed(() => $props.id || route.params.id);
const summaryRef = ref();
const itemType = ref();
async function setItemTypeData(data) {
if (data) itemType.value = data;
}
</script>
<template>
<CardSummary
ref="summaryRef"
:url="`ItemTypes/${entityId}`"
data-key="ItemTypeSummary"
:filter="itemTypeFilter"
@on-fetch="(data) => setItemTypeData(data)"
class="full-width"
>
<template #header-left>
<router-link
v-if="route.name !== 'ItemTypeSummary'"
:to="{ name: 'ItemTypeSummary', params: { id: entityId } }"
class="header link"
>
<QIcon name="open_in_new" color="white" size="sm" />
</router-link>
</template>
<template #header>
<span>
{{ itemType.id }} - {{ itemType.name }} -
{{ itemType.worker?.firstName }}
{{ itemType.worker?.lastName }}
</span>
</template>
<template #body>
<QCard class="vn-one">
<router-link
:to="{ name: 'ItemTypeBasicData', params: { id: entityId } }"
class="header header-link"
>
{{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" />
</router-link>
<VnLv :label="t('summary.id')" :value="itemType.id" />
<VnLv :label="t('shared.code')" :value="itemType.code" />
<VnLv :label="t('shared.name')" :value="itemType.name" />
<VnLv :label="t('shared.worker')">
<template #value>
<span class="link">{{ itemType.worker?.firstName }}</span>
<WorkerDescriptorProxy :id="itemType.worker?.id" />
</template>
</VnLv>
<VnLv :label="t('shared.category')" :value="itemType.category?.name" />
<VnLv
:label="t('shared.temperature')"
:value="itemType.temperature?.name"
/>
<VnLv :label="t('summary.life')" :value="itemType.life" />
<VnLv :label="t('summary.promo')" :value="itemType.promo" />
<VnLv
:label="t('summary.itemPackingType')"
:value="itemType.itemPackingType?.description"
/>
<VnLv
class="large-label"
:label="t('summary.isUnconventionalSize')"
:value="itemType.isUnconventionalSize"
/>
</QCard>
</template>
</CardSummary>
</template>
<style lang="scss">
.large-label > div.label {
width: 15em !important;
}
</style>

View File

@ -0,0 +1,55 @@
<script setup>
import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const emit = defineEmits(['search']);
</script>
<template>
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
@search="emit('search')"
>
<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 }">
<QItem>
<QItemSection>
<VnInput :label="t('Name')" v-model="params.name" is-outlined />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput v-model="params.code" :label="t('Code')" is-outlined />
</QItemSection>
</QItem>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
name: Name
code: Code
es:
params:
name: Nombre
code: Código
Name: Nombre
Code: Código
</i18n>

View File

@ -0,0 +1,12 @@
shared:
code: Code
name: Name
worker: Worker
category: Category
temperature: Temperature
summary:
id: id
life: Life
promo: Promo
itemPackingType: Item packing type
isUnconventionalSize: Is unconventional size

View File

@ -0,0 +1,17 @@
shared:
code: Código
name: Nombre
worker: Trabajador
category: Reino
temperature: Temperatura
summary:
id: id
code: Código
name: Nombre
worker: Trabajador
category: Reino
temperature: Temperatura
life: Vida
promo: Promoción
itemPackingType: Tipo de embalaje
isUnconventionalSize: Es de tamaño poco convencional

View File

@ -38,7 +38,13 @@ async function onSubmit() {
if (!multimediaToken) return; if (!multimediaToken) return;
await session.login(data.token, multimediaToken.id, keepLogin.value); const login = {
...data,
created: Date.now(),
tokenMultimedia: multimediaToken.id,
keepLogin: keepLogin.value,
};
await session.login(login);
quasar.notify({ quasar.notify({
message: t('login.loginSuccess'), message: t('login.loginSuccess'),

View File

@ -20,14 +20,14 @@ const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const entityId = computed(() => $props.id || route.params.id); const entityId = computed(() => $props.id || route.params.id);
const isDialog = Boolean($props.id); const isDialog = false;
const hideRightDrawer = () => { // const hideRightDrawer = () => {
if (!isDialog) { // if (!isDialog) {
stateStore.rightDrawer = false; // stateStore.rightDrawer = false;
} // }
}; // };
onMounted(hideRightDrawer); // onMounted(hideRightDrawer);
onUnmounted(hideRightDrawer); // onUnmounted(hideRightDrawer);
const filter = { const filter = {
include: [ include: [
{ {
@ -46,23 +46,6 @@ const filter = {
</script> </script>
<template> <template>
<template v-if="!isDialog && stateStore.isHeaderMounted()">
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<div class="q-pa-md"> <div class="q-pa-md">
<CardSummary ref="summary" :url="`Shelvings/${entityId}`" :filter="filter"> <CardSummary ref="summary" :url="`Shelvings/${entityId}`" :filter="filter">
<template #header="{ entity }"> <template #header="{ entity }">
@ -102,18 +85,4 @@ const filter = {
</template> </template>
</CardSummary> </CardSummary>
</div> </div>
<QDrawer
v-if="!isDialog"
v-model="stateStore.rightDrawer"
side="right"
:width="256"
show-if-above
>
<QScrollArea class="fit text-grey-8">
<ShelvingFilter
data-key="ShelvingList"
@search="router.push({ name: 'ShelvingList' })"
/>
</QScrollArea>
</QDrawer>
</template> </template>

View File

@ -12,6 +12,7 @@ import { useSession } from 'src/composables/useSession';
import { useRole } from 'src/composables/useRole'; import { useRole } from 'src/composables/useRole';
import { useUserConfig } from 'src/composables/useUserConfig'; import { useUserConfig } from 'src/composables/useUserConfig';
import { toLowerCamel } from 'src/filters'; import { toLowerCamel } from 'src/filters';
import { useTokenConfig } from 'src/composables/useTokenConfig';
const state = useState(); const state = useState();
const session = useSession(); const session = useSession();
@ -55,6 +56,7 @@ export default route(function (/* { store, ssrContext } */) {
if (stateRoles.length === 0) { if (stateRoles.length === 0) {
await useRole().fetch(); await useRole().fetch();
await useUserConfig().fetch(); await useUserConfig().fetch();
await useTokenConfig().fetch();
} }
const matches = to.matched; const matches = to.matched;
const hasRequiredRoles = matches.every((route) => { const hasRequiredRoles = matches.every((route) => {

View File

@ -6,6 +6,7 @@ export default {
meta: { meta: {
title: 'department', title: 'department',
icon: 'vn:greuge', icon: 'vn:greuge',
moduleName: 'Department',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'DepartmentCard' }, redirect: { name: 'DepartmentCard' },

View File

@ -16,6 +16,7 @@ import Entry from './entry';
import roadmap from './roadmap'; import roadmap from './roadmap';
import Parking from './parking'; import Parking from './parking';
import Agency from './agency'; import Agency from './agency';
import ItemType from './itemType';
export default [ export default [
Item, Item,
@ -36,4 +37,5 @@ export default [
roadmap, roadmap,
Parking, Parking,
Agency, Agency,
ItemType,
]; ];

View File

@ -11,7 +11,13 @@ export default {
component: RouterView, component: RouterView,
redirect: { name: 'ItemMain' }, redirect: { name: 'ItemMain' },
menus: { menus: {
main: ['ItemList', 'WasteBreakdown', 'ItemFixedPrice', 'ItemRequest'], main: [
'ItemList',
'WasteBreakdown',
'ItemFixedPrice',
'ItemRequest',
'ItemTypeList',
],
card: [ card: [
'ItemBasicData', 'ItemBasicData',
'ItemLog', 'ItemLog',
@ -68,6 +74,23 @@ export default {
'https://grafana.verdnatura.es/d/TTNXQAxVk'; 'https://grafana.verdnatura.es/d/TTNXQAxVk';
}, },
}, },
{
path: 'item-type-list',
name: 'ItemTypeList',
meta: {
title: 'family',
icon: 'contact_support',
},
component: () => import('src/pages/Item/ItemTypeList.vue'),
},
{
path: 'item-type-list/create',
name: 'ItemTypeCreate',
meta: {
title: 'itemTypeCreate',
},
component: () => import('src/pages/Item/ItemTypeCreate.vue'),
},
{ {
path: 'request', path: 'request',
name: 'ItemRequest', name: 'ItemRequest',
@ -134,8 +157,8 @@ export default {
path: 'diary', path: 'diary',
name: 'ItemDiary', name: 'ItemDiary',
meta: { meta: {
title: 'basicData', title: 'diary',
icon: 'vn:settings', icon: 'vn:transaction',
}, },
component: () => import('src/pages/Item/Card/ItemDiary.vue'), component: () => import('src/pages/Item/Card/ItemDiary.vue'),
}, },
@ -144,7 +167,7 @@ export default {
name: 'ItemLog', name: 'ItemLog',
meta: { meta: {
title: 'log', title: 'log',
icon: 'history', icon: 'vn:History',
}, },
component: () => import('src/pages/Item/Card/ItemLog.vue'), component: () => import('src/pages/Item/Card/ItemLog.vue'),
}, },

View File

@ -0,0 +1,46 @@
import { RouterView } from 'vue-router';
export default {
path: '/item/item-type',
name: 'ItemType',
meta: {
title: 'itemType',
icon: 'contact_support',
moduleName: 'ItemType',
},
component: RouterView,
redirect: { name: 'ItemTypeList' },
menus: {
main: [],
card: ['ItemTypeBasicData'],
},
children: [
{
name: 'ItemTypeCard',
path: ':id',
component: () => import('src/pages/ItemType/Card/ItemTypeCard.vue'),
redirect: { name: 'ItemTypeSummary' },
children: [
{
name: 'ItemTypeSummary',
path: 'summary',
meta: {
title: 'summary',
},
component: () =>
import('src/pages/ItemType/Card/ItemTypeSummary.vue'),
},
{
name: 'ItemTypeBasicData',
path: 'basic-data',
meta: {
title: 'basicData',
icon: 'vn:settings',
},
component: () =>
import('src/pages/ItemType/Card/ItemTypeBasicData.vue'),
},
],
},
],
};

View File

@ -6,6 +6,7 @@ export default {
meta: { meta: {
title: 'roadmap', title: 'roadmap',
icon: 'vn:troncales', icon: 'vn:troncales',
moduleName: 'Roadmap',
}, },
component: RouterView, component: RouterView,
redirect: { name: 'RouteMain' }, redirect: { name: 'RouteMain' },

View File

@ -10,6 +10,7 @@ import supplier from './modules/Supplier';
import route from './modules/route'; import route from './modules/route';
import travel from './modules/travel'; import travel from './modules/travel';
import department from './modules/department'; import department from './modules/department';
import ItemType from './modules/itemType';
import shelving from 'src/router/modules/shelving'; import shelving from 'src/router/modules/shelving';
import order from 'src/router/modules/order'; import order from 'src/router/modules/order';
import entry from 'src/router/modules/entry'; import entry from 'src/router/modules/entry';
@ -73,6 +74,7 @@ const routes = [
entry, entry,
parking, parking,
agency, agency,
ItemType,
{ {
path: '/:catchAll(.*)*', path: '/:catchAll(.*)*',
name: 'NotFound', name: 'NotFound',

View File

@ -1,5 +1,5 @@
import { vi, describe, expect, it } from 'vitest'; import { vi, describe, expect, it, beforeAll, beforeEach } from 'vitest';
import { axios } from 'app/test/vitest/helper'; import { axios, flushPromises } from 'app/test/vitest/helper';
import { useSession } from 'composables/useSession'; import { useSession } from 'composables/useSession';
import { useState } from 'composables/useState'; import { useState } from 'composables/useState';
@ -63,73 +63,148 @@ describe('session', () => {
}); });
}); });
describe('login', () => { describe(
const expectedUser = { 'login',
id: 999, () => {
name: `T'Challa`, const expectedUser = {
nickname: 'Black Panther', id: 999,
lang: 'en', name: `T'Challa`,
userConfig: { nickname: 'Black Panther',
darkMode: false, lang: 'en',
}, userConfig: {
}; darkMode: false,
const rolesData = [
{
role: {
name: 'salesPerson',
}, },
}, };
{ const rolesData = [
role: { {
name: 'admin', role: {
name: 'salesPerson',
},
}, },
}, {
]; role: {
name: 'admin',
},
},
];
it('should fetch the user roles and then set token in the sessionStorage', async () => { it('should fetch the user roles and then set token in the sessionStorage', async () => {
const expectedRoles = ['salesPerson', 'admin']; const expectedRoles = ['salesPerson', 'admin'];
vi.spyOn(axios, 'get').mockResolvedValue({ vi.spyOn(axios, 'get').mockResolvedValue({
data: { roles: rolesData, user: expectedUser }, data: { roles: rolesData, user: expectedUser },
});
const expectedToken = 'mySessionToken';
const expectedTokenMultimedia = 'mySessionTokenMultimedia';
const keepLogin = false;
await session.login({
token: expectedToken,
tokenMultimedia: expectedTokenMultimedia,
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);
await session.destroy(); // this clears token and user for any other test
}); });
const expectedToken = 'mySessionToken'; it('should fetch the user roles and then set token in the localStorage', async () => {
const expectedTokenMultimedia = 'mySessionTokenMultimedia'; const expectedRoles = ['salesPerson', 'admin'];
const keepLogin = false; vi.spyOn(axios, 'get').mockResolvedValue({
data: { roles: rolesData, user: expectedUser },
});
await session.login(expectedToken,expectedTokenMultimedia, keepLogin); const expectedToken = 'myLocalToken';
const expectedTokenMultimedia = 'myLocalTokenMultimedia';
const keepLogin = true;
const roles = state.getRoles(); await session.login({
const localToken = localStorage.getItem('token'); token: expectedToken,
const sessionToken = sessionStorage.getItem('token'); tokenMultimedia: expectedTokenMultimedia,
keepLogin,
});
expect(roles.value).toEqual(expectedRoles); const roles = state.getRoles();
expect(localToken).toBeNull(); const localToken = localStorage.getItem('token');
expect(sessionToken).toEqual(expectedToken); const sessionToken = sessionStorage.getItem('token');
await session.destroy(); // this clears token and user for any other test expect(roles.value).toEqual(expectedRoles);
expect(localToken).toEqual(expectedToken);
expect(sessionToken).toBeNull();
await session.destroy(); // this clears token and user for any other test
});
},
{}
);
describe('RenewToken', () => {
const expectedToken = 'myToken';
const expectedTokenMultimedia = 'myTokenMultimedia';
const currentDate = new Date();
beforeAll(() => {
const tokenConfig = {
id: 1,
renewPeriod: 21600,
courtesyTime: 60,
renewInterval: 300,
};
state.setTokenConfig(tokenConfig);
sessionStorage.setItem('renewPeriod', 1);
}); });
it('NOT Should renewToken', async () => {
const data = {
token: expectedToken,
tokenMultimedia: expectedTokenMultimedia,
keepLogin: false,
ttl: 1,
created: Date.now(),
};
session.setSession(data);
expect(sessionStorage.getItem('keepLogin')).toBeFalsy();
expect(sessionStorage.getItem('created')).toBeDefined();
expect(sessionStorage.getItem('ttl')).toEqual(1);
await session.checkValidity();
expect(sessionStorage.getItem('token')).toEqual(expectedToken);
expect(sessionStorage.getItem('tokenMultimedia')).toEqual(
expectedTokenMultimedia
);
});
it('Should renewToken', async () => {
currentDate.setMinutes(currentDate.getMinutes() - 100);
const data = {
token: expectedToken,
tokenMultimedia: expectedTokenMultimedia,
keepLogin: false,
ttl: 1,
created: currentDate,
};
session.setSession(data);
it('should fetch the user roles and then set token in the localStorage', async () => { vi.spyOn(axios, 'post')
const expectedRoles = ['salesPerson', 'admin']; .mockResolvedValueOnce({
vi.spyOn(axios, 'get').mockResolvedValue({ data: { id: '' },
data: { roles: rolesData, user: expectedUser }, })
}); .mockResolvedValueOnce({
data: {
const expectedToken = 'myLocalToken'; id: '',
const expectedTokenMultimedia = 'myLocalTokenMultimedia'; },
const keepLogin = true; });
expect(sessionStorage.getItem('keepLogin')).toBeFalsy();
await session.login(expectedToken, expectedTokenMultimedia, keepLogin); expect(sessionStorage.getItem('created')).toBeDefined();
expect(sessionStorage.getItem('ttl')).toEqual(1);
const roles = state.getRoles(); await session.checkValidity();
const localToken = localStorage.getItem('token'); expect(sessionStorage.getItem('token')).not.toEqual(expectedToken);
const sessionToken = sessionStorage.getItem('token'); expect(sessionStorage.getItem('tokenMultimedia')).not.toEqual(
expectedTokenMultimedia
expect(roles.value).toEqual(expectedRoles); );
expect(localToken).toEqual(expectedToken);
expect(sessionToken).toBeNull();
await session.destroy(); // this clears token and user for any other test
}); });
}); });
}); });

View File

@ -0,0 +1,31 @@
import { vi, describe, expect, it } from 'vitest';
import { axios, flushPromises } from 'app/test/vitest/helper';
import { useTokenConfig } from 'composables/useTokenConfig';
const tokenConfig = useTokenConfig();
describe('useTokenConfig', () => {
describe('fetch', () => {
it('should call setTokenConfig of the state with the expected data', async () => {
const data = {
id: 1,
renewPeriod: 21600,
courtesyTime: 60,
renewInterval: 300,
};
vi.spyOn(axios, 'get').mockResolvedValueOnce({
data,
});
vi.spyOn(tokenConfig.state, 'setTokenConfig');
tokenConfig.fetch();
await flushPromises();
expect(tokenConfig.state.setTokenConfig).toHaveBeenCalledWith(data);
const renewPeriod = sessionStorage.getItem('renewPeriod');
expect(renewPeriod).toEqual(data.renewPeriod);
});
});
});