diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index 480d36e4f..05c74f00a 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -169,6 +169,7 @@ watch(modelValue, (newValue) => { ref="vnSelectRef" :class="{ required: $attrs.required }" :rules="$attrs.required ? [requiredFieldRule] : null" + virtual-scroll-slice-size="options.length" > <template v-if="isClearable" #append> <QIcon diff --git a/src/components/ui/VnFilterPanel.vue b/src/components/ui/VnFilterPanel.vue index 96d097191..eb0bbbe66 100644 --- a/src/components/ui/VnFilterPanel.vue +++ b/src/components/ui/VnFilterPanel.vue @@ -79,6 +79,7 @@ watch( const isLoading = ref(false); async function search() { + store.filter.where = {}; isLoading.value = true; const params = { ...userParams.value }; store.userParamsChanged = true; diff --git a/src/components/ui/VnSearchbar.vue b/src/components/ui/VnSearchbar.vue index fc8475ace..344267ef7 100644 --- a/src/components/ui/VnSearchbar.vue +++ b/src/components/ui/VnSearchbar.vue @@ -81,8 +81,10 @@ async function search() { const staticParams = Object.entries(store.userParams).filter( ([key, value]) => value && (props.staticParams || []).includes(key) ); + // const filter =props?.where? { where: JSON.parse(props.where) }: {} await arrayData.applyFilter({ params: { + // filter , ...Object.fromEntries(staticParams), search: searchText.value, }, @@ -106,6 +108,7 @@ async function search() { let targetUrl; 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); await router.push({ path: targetUrl }); diff --git a/src/components/ui/VnUserLink.vue b/src/components/ui/VnUserLink.vue index 47287c12b..33836550a 100644 --- a/src/components/ui/VnUserLink.vue +++ b/src/components/ui/VnUserLink.vue @@ -1,6 +1,5 @@ <script setup> import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; -import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; import { useI18n } from 'vue-i18n'; const $props = defineProps({ diff --git a/src/composables/useSession.js b/src/composables/useSession.js index 8583e10d4..56bce0279 100644 --- a/src/composables/useSession.js +++ b/src/composables/useSession.js @@ -3,11 +3,14 @@ import { useRole } from './useRole'; import { useUserConfig } from './useUserConfig'; import axios from 'axios'; import useNotify from './useNotify'; +import { useTokenConfig } from './useTokenConfig'; const TOKEN_MULTIMEDIA = 'tokenMultimedia'; const TOKEN = 'token'; export function useSession() { const { notify } = useNotify(); + let isCheckingToken = false; + let intervalId = null; function getToken() { const localToken = localStorage.getItem(TOKEN); @@ -22,10 +25,24 @@ export function useSession() { return localTokenMultimedia || sessionTokenMultimedia || ''; } - function setToken(data) { - const storage = data.keepLogin ? localStorage : sessionStorage; + function setSession(data) { + let keepLogin = data.keepLogin; + const storage = keepLogin ? localStorage : sessionStorage; storage.setItem(TOKEN, data.token); 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) { if (storage.getItem(key)) { @@ -45,11 +62,15 @@ export function useSession() { tokenMultimedia: 'Accounts/logout', token: 'VnUsers/logout', }; + const storage = keepLogin() ? localStorage : sessionStorage; + for (const [key, url] of Object.entries(tokens)) { - await destroyToken(url, localStorage, key); - await destroyToken(url, sessionStorage, key); + await destroyToken(url, storage, key); } + localStorage.clear(); + sessionStorage.clear(); + const { setUser } = useState(); setUser({ @@ -59,22 +80,75 @@ export function useSession() { lang: '', darkMode: null, }); + + stopRenewer(); } - async function login(token, tokenMultimedia, keepLogin) { - setToken({ token, tokenMultimedia, keepLogin }); + async function login(data) { + setSession(data); await useRole().fetch(); await useUserConfig().fetch(); + await useTokenConfig().fetch(); + + startInterval(); } function isLoggedIn() { const localToken = localStorage.getItem(TOKEN); const sessionToken = sessionStorage.getItem(TOKEN); - + startInterval(); 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 { getToken, getTokenMultimedia, @@ -82,5 +156,8 @@ export function useSession() { destroy, login, isLoggedIn, + checkValidity, + setSession, + renewToken, }; } diff --git a/src/composables/useState.js b/src/composables/useState.js index e0b742a73..e671d41bd 100644 --- a/src/composables/useState.js +++ b/src/composables/useState.js @@ -13,6 +13,7 @@ const user = ref({ }); const roles = ref([]); +const tokenConfig = ref({}); const drawer = ref(true); const headerMounted = ref(false); @@ -52,6 +53,15 @@ export function useState() { function setRoles(data) { roles.value = data; } + function getTokenConfig() { + return computed(() => { + return tokenConfig.value; + }); + } + + function setTokenConfig(data) { + tokenConfig.value = data; + } function set(name, data) { state.value[name] = ref(data); @@ -70,6 +80,8 @@ export function useState() { setUser, getRoles, setRoles, + getTokenConfig, + setTokenConfig, set, get, unset, diff --git a/src/composables/useTokenConfig.js b/src/composables/useTokenConfig.js new file mode 100644 index 000000000..5cf1b34ee --- /dev/null +++ b/src/composables/useTokenConfig.js @@ -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, + }; +} diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index abe59fe18..96adf0e23 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -106,6 +106,7 @@ errors: statusBadGateway: It seems that the server has fall down statusGatewayTimeout: Could not contact the server userConfig: Error fetching user config + tokenConfig: Error fetching token config writeRequest: The requested operation could not be completed login: title: Login @@ -1135,6 +1136,8 @@ item: tax: Tax barcode: Barcode botanical: Botanical + itemTypeCreate: New item type + family: Item Type descriptor: item: Item buyer: Buyer @@ -1219,6 +1222,11 @@ item: minSalesQuantity: 'Cantidad mínima de venta' genus: 'Genus' specie: 'Specie' +item/itemType: + pageTitles: + itemType: Item type + basicData: Basic data + summary: Summary components: topbar: {} itemsFilterPanel: diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index 06aa057e3..cb2be6dc9 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -106,6 +106,7 @@ errors: statusBadGateway: Parece ser que el servidor ha caído statusGatewayTimeout: No se ha podido contactar con el servidor 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 login: title: Inicio de sesión @@ -1134,6 +1135,8 @@ item: botanical: 'Botánico' barcode: 'Código de barras' log: Historial + itemTypeCreate: Nueva familia + family: Familia descriptor: item: Artículo buyer: Comprador @@ -1218,6 +1221,11 @@ item: achieved: 'Conseguido' concept: 'Concepto' state: 'Estado' +item/itemType: + pageTitles: + itemType: Familia + basicData: Datos básicos + summary: Resumen components: topbar: {} itemsFilterPanel: diff --git a/src/pages/Claim/Card/ClaimBasicData.vue b/src/pages/Claim/Card/ClaimBasicData.vue index 47d1ac1c6..be2efa31a 100644 --- a/src/pages/Claim/Card/ClaimBasicData.vue +++ b/src/pages/Claim/Card/ClaimBasicData.vue @@ -2,7 +2,7 @@ import { ref } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; - +import VnSelect from 'src/components/common/VnSelect.vue'; import FetchData from 'components/FetchData.vue'; import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; @@ -37,8 +37,6 @@ const claimFilter = { ], }; -const workers = ref([]); -const workersCopy = ref([]); const claimStates = ref([]); const claimStatesCopy = ref([]); const optionsList = ref([]); @@ -47,6 +45,7 @@ function setWorkers(data) { workers.value = data; workersCopy.value = data; } +const workersOptions = ref([]); function setClaimStates(data) { claimStates.value = data; @@ -68,25 +67,6 @@ async function 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 = { options: claimStates, filterFn: (options, value) => { @@ -106,7 +86,7 @@ const statesFilter = { <FetchData url="Workers/activeWithInheritedRole" :filter="{ where: { role: 'salesPerson' } }" - @on-fetch="setWorkers" + @on-fetch="(data) => (workersOptions = data)" auto-load /> <FetchData url="ClaimStates" @on-fetch="setClaimStates" auto-load /> @@ -135,18 +115,15 @@ const statesFilter = { </VnRow> <VnRow class="row q-gutter-md q-mb-md"> <div class="col"> - <QSelect + <VnSelect + :label="t('claim.basicData.assignedTo')" v-model="data.workerFk" - :options="workers" + :options="workersOptions" option-value="id" option-label="name" emit-value - :label="t('claim.basicData.assignedTo')" - map-options - use-input - @filter="(value, update) => filter(value, update, workerFilter)" + auto-load :rules="validate('claim.claimStateFk')" - :input-debounce="0" > <template #before> <QAvatar color="orange"> @@ -157,7 +134,7 @@ const statesFilter = { /> </QAvatar> </template> - </QSelect> + </VnSelect> </div> <div class="col"> <QSelect diff --git a/src/pages/Item/Card/ItemDiary.vue b/src/pages/Item/Card/ItemDiary.vue index 21249349f..c2687e0fd 100644 --- a/src/pages/Item/Card/ItemDiary.vue +++ b/src/pages/Item/Card/ItemDiary.vue @@ -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> diff --git a/src/pages/Item/ItemTypeCreate.vue b/src/pages/Item/ItemTypeCreate.vue new file mode 100644 index 000000000..af3dc34ea --- /dev/null +++ b/src/pages/Item/ItemTypeCreate.vue @@ -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> diff --git a/src/pages/Item/ItemTypeList.vue b/src/pages/Item/ItemTypeList.vue new file mode 100644 index 000000000..9d826655f --- /dev/null +++ b/src/pages/Item/ItemTypeList.vue @@ -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> diff --git a/src/pages/Item/locale/en.yml b/src/pages/Item/locale/en.yml new file mode 100644 index 000000000..ec3b134e8 --- /dev/null +++ b/src/pages/Item/locale/en.yml @@ -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 diff --git a/src/pages/Item/locale/es.yml b/src/pages/Item/locale/es.yml new file mode 100644 index 000000000..4f76313fa --- /dev/null +++ b/src/pages/Item/locale/es.yml @@ -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 diff --git a/src/pages/ItemType/Card/ItemTypeBasicData.vue b/src/pages/ItemType/Card/ItemTypeBasicData.vue new file mode 100644 index 000000000..0bdfab951 --- /dev/null +++ b/src/pages/ItemType/Card/ItemTypeBasicData.vue @@ -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> diff --git a/src/pages/ItemType/Card/ItemTypeCard.vue b/src/pages/ItemType/Card/ItemTypeCard.vue new file mode 100644 index 000000000..cb8adf7f6 --- /dev/null +++ b/src/pages/ItemType/Card/ItemTypeCard.vue @@ -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> diff --git a/src/pages/ItemType/Card/ItemTypeDescriptor.vue b/src/pages/ItemType/Card/ItemTypeDescriptor.vue new file mode 100644 index 000000000..e565a791b --- /dev/null +++ b/src/pages/ItemType/Card/ItemTypeDescriptor.vue @@ -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> diff --git a/src/pages/ItemType/Card/ItemTypeSummary.vue b/src/pages/ItemType/Card/ItemTypeSummary.vue new file mode 100644 index 000000000..62d1c74ab --- /dev/null +++ b/src/pages/ItemType/Card/ItemTypeSummary.vue @@ -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> diff --git a/src/pages/ItemType/ItemTypeFilter.vue b/src/pages/ItemType/ItemTypeFilter.vue new file mode 100644 index 000000000..2a86795c2 --- /dev/null +++ b/src/pages/ItemType/ItemTypeFilter.vue @@ -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> diff --git a/src/pages/ItemType/locale/en.yml b/src/pages/ItemType/locale/en.yml new file mode 100644 index 000000000..7889418ea --- /dev/null +++ b/src/pages/ItemType/locale/en.yml @@ -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 diff --git a/src/pages/ItemType/locale/es.yml b/src/pages/ItemType/locale/es.yml new file mode 100644 index 000000000..9a94dceb6 --- /dev/null +++ b/src/pages/ItemType/locale/es.yml @@ -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 diff --git a/src/pages/Login/LoginMain.vue b/src/pages/Login/LoginMain.vue index fcde51edf..5a3490f50 100644 --- a/src/pages/Login/LoginMain.vue +++ b/src/pages/Login/LoginMain.vue @@ -38,7 +38,13 @@ async function onSubmit() { 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({ message: t('login.loginSuccess'), diff --git a/src/pages/Shelving/Card/ShelvingSummary.vue b/src/pages/Shelving/Card/ShelvingSummary.vue index 49470719e..649799b00 100644 --- a/src/pages/Shelving/Card/ShelvingSummary.vue +++ b/src/pages/Shelving/Card/ShelvingSummary.vue @@ -20,14 +20,14 @@ const router = useRouter(); const { t } = useI18n(); const entityId = computed(() => $props.id || route.params.id); -const isDialog = Boolean($props.id); -const hideRightDrawer = () => { - if (!isDialog) { - stateStore.rightDrawer = false; - } -}; -onMounted(hideRightDrawer); -onUnmounted(hideRightDrawer); +const isDialog = false; +// const hideRightDrawer = () => { +// if (!isDialog) { +// stateStore.rightDrawer = false; +// } +// }; +// onMounted(hideRightDrawer); +// onUnmounted(hideRightDrawer); const filter = { include: [ { @@ -46,23 +46,6 @@ const filter = { </script> <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"> <CardSummary ref="summary" :url="`Shelvings/${entityId}`" :filter="filter"> <template #header="{ entity }"> @@ -102,18 +85,4 @@ const filter = { </template> </CardSummary> </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> diff --git a/src/router/index.js b/src/router/index.js index 3e442f0e6..7a0aedcae 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -12,6 +12,7 @@ import { useSession } from 'src/composables/useSession'; import { useRole } from 'src/composables/useRole'; import { useUserConfig } from 'src/composables/useUserConfig'; import { toLowerCamel } from 'src/filters'; +import { useTokenConfig } from 'src/composables/useTokenConfig'; const state = useState(); const session = useSession(); @@ -55,6 +56,7 @@ export default route(function (/* { store, ssrContext } */) { if (stateRoles.length === 0) { await useRole().fetch(); await useUserConfig().fetch(); + await useTokenConfig().fetch(); } const matches = to.matched; const hasRequiredRoles = matches.every((route) => { diff --git a/src/router/modules/department.js b/src/router/modules/department.js index aaffc3460..dfd5e64ba 100644 --- a/src/router/modules/department.js +++ b/src/router/modules/department.js @@ -6,6 +6,7 @@ export default { meta: { title: 'department', icon: 'vn:greuge', + moduleName: 'Department', }, component: RouterView, redirect: { name: 'DepartmentCard' }, diff --git a/src/router/modules/index.js b/src/router/modules/index.js index 6f4b0b35e..941358d26 100644 --- a/src/router/modules/index.js +++ b/src/router/modules/index.js @@ -16,6 +16,7 @@ import Entry from './entry'; import roadmap from './roadmap'; import Parking from './parking'; import Agency from './agency'; +import ItemType from './itemType'; export default [ Item, @@ -36,4 +37,5 @@ export default [ roadmap, Parking, Agency, + ItemType, ]; diff --git a/src/router/modules/item.js b/src/router/modules/item.js index bc1e72a94..d79ac3071 100644 --- a/src/router/modules/item.js +++ b/src/router/modules/item.js @@ -11,7 +11,13 @@ export default { component: RouterView, redirect: { name: 'ItemMain' }, menus: { - main: ['ItemList', 'WasteBreakdown', 'ItemFixedPrice', 'ItemRequest'], + main: [ + 'ItemList', + 'WasteBreakdown', + 'ItemFixedPrice', + 'ItemRequest', + 'ItemTypeList', + ], card: [ 'ItemBasicData', 'ItemLog', @@ -68,6 +74,23 @@ export default { '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', name: 'ItemRequest', @@ -134,8 +157,8 @@ export default { path: 'diary', name: 'ItemDiary', meta: { - title: 'basicData', - icon: 'vn:settings', + title: 'diary', + icon: 'vn:transaction', }, component: () => import('src/pages/Item/Card/ItemDiary.vue'), }, @@ -144,7 +167,7 @@ export default { name: 'ItemLog', meta: { title: 'log', - icon: 'history', + icon: 'vn:History', }, component: () => import('src/pages/Item/Card/ItemLog.vue'), }, diff --git a/src/router/modules/itemType.js b/src/router/modules/itemType.js new file mode 100644 index 000000000..8064c41ff --- /dev/null +++ b/src/router/modules/itemType.js @@ -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'), + }, + ], + }, + ], +}; diff --git a/src/router/modules/roadmap.js b/src/router/modules/roadmap.js index 02edf94be..6b2aa6a13 100644 --- a/src/router/modules/roadmap.js +++ b/src/router/modules/roadmap.js @@ -6,6 +6,7 @@ export default { meta: { title: 'roadmap', icon: 'vn:troncales', + moduleName: 'Roadmap', }, component: RouterView, redirect: { name: 'RouteMain' }, diff --git a/src/router/routes.js b/src/router/routes.js index ca52441e7..92145d44e 100644 --- a/src/router/routes.js +++ b/src/router/routes.js @@ -10,6 +10,7 @@ import supplier from './modules/Supplier'; import route from './modules/route'; import travel from './modules/travel'; import department from './modules/department'; +import ItemType from './modules/itemType'; import shelving from 'src/router/modules/shelving'; import order from 'src/router/modules/order'; import entry from 'src/router/modules/entry'; @@ -73,6 +74,7 @@ const routes = [ entry, parking, agency, + ItemType, { path: '/:catchAll(.*)*', name: 'NotFound', diff --git a/test/vitest/__tests__/composables/useSession.spec.js b/test/vitest/__tests__/composables/useSession.spec.js index f9f3dcb80..2292859a9 100644 --- a/test/vitest/__tests__/composables/useSession.spec.js +++ b/test/vitest/__tests__/composables/useSession.spec.js @@ -1,5 +1,5 @@ -import { vi, describe, expect, it } from 'vitest'; -import { axios } from 'app/test/vitest/helper'; +import { vi, describe, expect, it, beforeAll, beforeEach } from 'vitest'; +import { axios, flushPromises } from 'app/test/vitest/helper'; import { useSession } from 'composables/useSession'; import { useState } from 'composables/useState'; @@ -63,73 +63,148 @@ describe('session', () => { }); }); - describe('login', () => { - const expectedUser = { - id: 999, - name: `T'Challa`, - nickname: 'Black Panther', - lang: 'en', - userConfig: { - darkMode: false, - }, - }; - const rolesData = [ - { - role: { - name: 'salesPerson', + describe( + 'login', + () => { + const expectedUser = { + id: 999, + name: `T'Challa`, + nickname: 'Black Panther', + lang: 'en', + userConfig: { + darkMode: false, }, - }, - { - role: { - name: 'admin', + }; + 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']; - vi.spyOn(axios, 'get').mockResolvedValue({ - data: { roles: rolesData, user: expectedUser }, + it('should fetch the user roles and then set token in the sessionStorage', async () => { + const expectedRoles = ['salesPerson', 'admin']; + vi.spyOn(axios, 'get').mockResolvedValue({ + 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'; - const expectedTokenMultimedia = 'mySessionTokenMultimedia'; - const keepLogin = false; + it('should fetch the user roles and then set token in the localStorage', async () => { + const expectedRoles = ['salesPerson', 'admin']; + 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(); - const localToken = localStorage.getItem('token'); - const sessionToken = sessionStorage.getItem('token'); + await session.login({ + token: expectedToken, + tokenMultimedia: expectedTokenMultimedia, + keepLogin, + }); - expect(roles.value).toEqual(expectedRoles); - expect(localToken).toBeNull(); - expect(sessionToken).toEqual(expectedToken); + const roles = state.getRoles(); + const localToken = localStorage.getItem('token'); + 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 () => { - const expectedRoles = ['salesPerson', 'admin']; - vi.spyOn(axios, 'get').mockResolvedValue({ - data: { roles: rolesData, user: expectedUser }, - }); - - const expectedToken = 'myLocalToken'; - const expectedTokenMultimedia = 'myLocalTokenMultimedia'; - const keepLogin = true; - - await session.login(expectedToken, expectedTokenMultimedia, 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(); - - await session.destroy(); // this clears token and user for any other test + vi.spyOn(axios, 'post') + .mockResolvedValueOnce({ + data: { id: '' }, + }) + .mockResolvedValueOnce({ + data: { + id: '', + }, + }); + expect(sessionStorage.getItem('keepLogin')).toBeFalsy(); + expect(sessionStorage.getItem('created')).toBeDefined(); + expect(sessionStorage.getItem('ttl')).toEqual(1); + await session.checkValidity(); + expect(sessionStorage.getItem('token')).not.toEqual(expectedToken); + expect(sessionStorage.getItem('tokenMultimedia')).not.toEqual( + expectedTokenMultimedia + ); }); }); }); diff --git a/test/vitest/__tests__/composables/useTokenConfig.spec.js b/test/vitest/__tests__/composables/useTokenConfig.spec.js new file mode 100644 index 000000000..a25a4abb1 --- /dev/null +++ b/test/vitest/__tests__/composables/useTokenConfig.spec.js @@ -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); + }); + }); +});