diff --git a/cypress.config.js b/cypress.config.js index f8e771093..e9aeb547a 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -1,4 +1,7 @@ const { defineConfig } = require('cypress'); +// https://docs.cypress.io/app/tooling/reporters +// https://docs.cypress.io/app/references/configuration +// https://www.npmjs.com/package/cypress-mochawesome-reporter module.exports = defineConfig({ e2e: { @@ -16,6 +19,7 @@ module.exports = defineConfig({ reporterOptions: { charts: true, reportPageTitle: 'Cypress Inline Reporter', + reportFilename: '[status]_[datetime]-report', embeddedScreenshots: true, reportDir: 'test/cypress/reports', inlineAssets: true, diff --git a/package.json b/package.json index 0ee43ce12..04b75a0b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salix-front", - "version": "24.48.0", + "version": "24.50.0", "description": "Salix frontend", "productName": "Salix", "author": "Verdnatura", diff --git a/src/components/CreateManualInvoiceForm.vue b/src/components/CreateManualInvoiceForm.vue deleted file mode 100644 index da006e024..000000000 --- a/src/components/CreateManualInvoiceForm.vue +++ /dev/null @@ -1,155 +0,0 @@ -<script setup> -import { reactive, ref, computed } from 'vue'; -import { useI18n } from 'vue-i18n'; -import { useRouter } from 'vue-router'; - -import FetchData from 'components/FetchData.vue'; -import VnRow from 'components/ui/VnRow.vue'; -import VnSelect from 'src/components/common/VnSelect.vue'; -import VnInput from 'src/components/common/VnInput.vue'; -import FormModelPopup from './FormModelPopup.vue'; -import VnInputDate from './common/VnInputDate.vue'; - -const emit = defineEmits(['onDataSaved']); - -const { t } = useI18n(); -const router = useRouter(); - -const manualInvoiceFormData = reactive({ - maxShipped: Date.vnNew(), -}); - -const formModelPopupRef = ref(); -const invoiceOutSerialsOptions = ref([]); -const taxAreasOptions = ref([]); -const ticketsOptions = ref([]); -const clientsOptions = ref([]); -const isLoading = computed(() => formModelPopupRef.value?.isLoading); - -const onDataSaved = async (formData, requestResponse) => { - emit('onDataSaved', formData, requestResponse); - if (requestResponse && requestResponse.id) - router.push({ name: 'InvoiceOutSummary', params: { id: requestResponse.id } }); -}; -</script> - -<template> - <FetchData - url="InvoiceOutSerials" - :filter="{ where: { code: { neq: 'R' } }, order: ['code'] }" - @on-fetch="(data) => (invoiceOutSerialsOptions = data)" - auto-load - /> - <FetchData - url="TaxAreas" - :filter="{ order: ['code'] }" - @on-fetch="(data) => (taxAreasOptions = data)" - auto-load - /> - <FormModelPopup - ref="formModelPopupRef" - :title="t('Create manual invoice')" - url-create="InvoiceOuts/createManualInvoice" - model="invoiceOut" - :form-initial-data="manualInvoiceFormData" - @on-data-saved="onDataSaved" - > - <template #form-inputs="{ data }"> - <span v-if="isLoading" class="text-primary invoicing-text"> - <QIcon name="warning" class="fill-icon q-mr-sm" size="md" /> - {{ t('Invoicing in progress...') }} - </span> - <VnRow> - <VnSelect - :label="t('Ticket')" - :options="ticketsOptions" - hide-selected - option-label="id" - option-value="id" - v-model="data.ticketFk" - @update:model-value="data.clientFk = null" - url="Tickets" - :where="{ refFk: null }" - :fields="['id', 'nickname']" - :filter-options="{ order: 'shipped DESC' }" - > - <template #option="scope"> - <QItem v-bind="scope.itemProps"> - <QItemSection> - <QItemLabel> #{{ scope.opt?.id }} </QItemLabel> - <QItemLabel caption>{{ scope.opt?.nickname }}</QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelect> - <span class="row items-center" style="max-width: max-content">{{ - t('Or') - }}</span> - <VnSelect - :label="t('Client')" - :options="clientsOptions" - hide-selected - option-label="name" - option-value="id" - v-model="data.clientFk" - @update:model-value="data.ticketFk = null" - url="Clients" - :fields="['id', 'name']" - :filter-options="{ order: 'name ASC' }" - /> - <VnInputDate :label="t('Max date')" v-model="data.maxShipped" /> - </VnRow> - <VnRow> - <VnSelect - :label="t('Serial')" - :options="invoiceOutSerialsOptions" - hide-selected - option-label="description" - option-value="code" - v-model="data.serial" - /> - <VnSelect - :label="t('Area')" - :options="taxAreasOptions" - hide-selected - option-label="code" - option-value="code" - v-model="data.taxArea" - /> - </VnRow> - <VnRow> - <VnInput - :label="t('Reference')" - type="textarea" - v-model="data.reference" - fill-input - autogrow - /> - </VnRow> - </template> - </FormModelPopup> -</template> - -<style lang="scss" scoped> -.invoicing-text { - display: flex; - justify-content: center; - align-items: center; - color: $primary; - font-size: 24px; - margin-bottom: 8px; -} -</style> - -<i18n> -es: - Create manual invoice: Crear factura manual - Ticket: Ticket - Client: Cliente - Max date: Fecha límite - Serial: Serie - Area: Area - Reference: Referencia - Or: O - Invoicing in progress...: Facturación en progreso... -</i18n> diff --git a/src/components/CreateNewCityForm.vue b/src/components/CreateNewCityForm.vue index 85d13beb1..1cbba42fc 100644 --- a/src/components/CreateNewCityForm.vue +++ b/src/components/CreateNewCityForm.vue @@ -17,10 +17,6 @@ const $props = defineProps({ type: Number, default: null, }, - provinces: { - type: Array, - default: () => [], - }, }); const { t } = useI18n(); @@ -48,15 +44,16 @@ const onDataSaved = (...args) => { <template #form-inputs="{ data, validate }"> <VnRow> <VnInput - :label="t('Names')" + :label="t('Name')" v-model="data.name" :rules="validate('city.name')" + required /> <VnSelectProvince :province-selected="$props.provinceSelected" :country-fk="$props.countryFk" v-model="data.provinceFk" - :provinces="$props.provinces" + required /> </VnRow> </template> diff --git a/src/components/CreateNewPostcodeForm.vue b/src/components/CreateNewPostcodeForm.vue index 03cba8ac7..232f86a49 100644 --- a/src/components/CreateNewPostcodeForm.vue +++ b/src/components/CreateNewPostcodeForm.vue @@ -1,5 +1,5 @@ <script setup> -import { reactive, ref, watch } from 'vue'; +import { computed, reactive, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import FetchData from 'components/FetchData.vue'; @@ -22,12 +22,14 @@ const postcodeFormData = reactive({ townFk: null, }); -const townsFetchDataRef = ref(null); -const provincesFetchDataRef = ref(null); -const countriesOptions = ref([]); +const townsFetchDataRef = ref(false); +const countriesRef = ref(false); +const townsRef = ref(false); +const provincesFetchDataRef = ref(false); const provincesOptions = ref([]); -const townsOptions = ref([]); const town = ref({}); +const townFilter = ref({}); +const countryFilter = ref({}); function onDataSaved(formData) { const newPostcode = { @@ -39,7 +41,7 @@ function onDataSaved(formData) { ({ id }) => id === formData.provinceFk ); newPostcode.province = provinceObject?.name; - const countryObject = countriesOptions.value.find( + const countryObject = countriesRef.value.opts.find( ({ id }) => id === formData.countryFk ); newPostcode.country = countryObject?.name; @@ -56,10 +58,19 @@ async function onCityCreated(newTown, formData) { } function setTown(newTown, data) { - if (!newTown) return; town.value = newTown; - data.provinceFk = newTown.provinceFk; - data.countryFk = newTown.province.countryFk; + data.provinceFk = newTown?.provinceFk ?? newTown; + data.countryFk = newTown?.province?.countryFk ?? newTown; +} + +async function setCountry(countryFk, data) { + data.townFk = null; + data.provinceFk = null; + data.countryFk = countryFk; +} + +async function handleProvinces(data) { + provincesOptions.value = data; } async function setProvince(id, data) { @@ -73,61 +84,16 @@ async function onProvinceCreated(data) { await provincesFetchDataRef.value.fetch({ where: { countryFk: postcodeFormData.countryFk }, }); - postcodeFormData.provinceFk.value = data.id; + postcodeFormData.provinceFk = data.id; } -watch( - () => [postcodeFormData.countryFk], - async (newCountryFk, oldValueFk) => { - if (Array.isArray(newCountryFk)) { - newCountryFk = newCountryFk[0]; - } - if (Array.isArray(oldValueFk)) { - oldValueFk = oldValueFk[0]; - } - if (!!oldValueFk && newCountryFk !== oldValueFk) { - postcodeFormData.provinceFk = null; - postcodeFormData.townFk = null; - } - if (oldValueFk !== newCountryFk) { - await provincesFetchDataRef.value.fetch({ - where: { - countryFk: newCountryFk, - }, - }); - await townsFetchDataRef.value.fetch({ - where: { - provinceFk: { - inq: provincesOptions.value.map(({ id }) => id), - }, - }, - }); - } - } -); - -watch( - () => postcodeFormData.provinceFk, - async (newProvinceFk, oldValueFk) => { - if (Array.isArray(newProvinceFk)) { - newProvinceFk = newProvinceFk[0]; - } - if (newProvinceFk !== oldValueFk) { - await townsFetchDataRef.value.fetch({ - where: { provinceFk: newProvinceFk }, - }); - } - } -); -async function handleProvinces(data) { - provincesOptions.value = data; -} -async function handleTowns(data) { - townsOptions.value = data; -} -async function handleCountries(data) { - countriesOptions.value = data; -} +const whereTowns = computed(() => { + return { + provinceFk: { + inq: provincesOptions.value.map(({ id }) => id), + }, + }; +}); </script> <template> @@ -139,14 +105,6 @@ async function handleCountries(data) { auto-load url="Provinces/location" /> - <FetchData - ref="townsFetchDataRef" - :sort-by="['name ASC']" - :limit="30" - @on-fetch="handleTowns" - auto-load - url="Towns/location" - /> <FormModelPopup url-create="postcodes" @@ -164,19 +122,26 @@ async function handleCountries(data) { v-model="data.code" :rules="validate('postcode.code')" clearable + required /> <VnSelectDialog + ref="townsRef" + :sort-by="['name ASC']" + :limit="30" + auto-load + url="Towns/location" + :where="whereTowns" :label="t('City')" @update:model-value="(value) => setTown(value, data)" :tooltip="t('Create city')" v-model="data.townFk" - :options="townsOptions" option-label="name" option-value="id" :rules="validate('postcode.city')" :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]" :emit-value="false" :clearable="true" + required > <template #option="{ itemProps, opt }"> <QItem v-bind="itemProps"> @@ -193,7 +158,6 @@ async function handleCountries(data) { <CreateNewCityForm :country-fk="data.countryFk" :province-selected="data.provinceFk" - :provinces="provincesOptions" @on-data-saved=" (_, requestResponse) => onCityCreated(requestResponse, data) @@ -208,20 +172,25 @@ async function handleCountries(data) { :province-selected="data.provinceFk" @update:model-value="(value) => setProvince(value, data)" v-model="data.provinceFk" - :clearable="true" - :provinces="provincesOptions" + @on-province-fetched="handleProvinces" @on-province-created="onProvinceCreated" + required /> <VnSelect - url="Countries" + ref="countriesRef" + :limit="30" + :filter="countryFilter" :sort-by="['name ASC']" + auto-load + url="Countries" + required :label="t('Country')" - @update:options="handleCountries" hide-selected option-label="name" option-value="id" v-model="data.countryFk" :rules="validate('postcode.countryFk')" + @update:model-value="(value) => setCountry(value, data)" /> </VnRow> </template> diff --git a/src/components/CreateNewProvinceForm.vue b/src/components/CreateNewProvinceForm.vue index d7df38f9f..887ca957c 100644 --- a/src/components/CreateNewProvinceForm.vue +++ b/src/components/CreateNewProvinceForm.vue @@ -1,8 +1,7 @@ <script setup> -import { reactive, ref } from 'vue'; +import { computed, reactive, ref } from 'vue'; import { useI18n } from 'vue-i18n'; -import FetchData from 'components/FetchData.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnInput from 'src/components/common/VnInput.vue'; @@ -21,34 +20,24 @@ const $props = defineProps({ type: Number, default: null, }, - provinces: { - type: Array, - default: () => [], - }, }); -const autonomiesOptions = ref([]); +const autonomiesRef = ref([]); const onDataSaved = (dataSaved, requestResponse) => { - requestResponse.autonomy = autonomiesOptions.value.find( + requestResponse.autonomy = autonomiesRef.value.opts.find( (autonomy) => autonomy.id == requestResponse.autonomyFk ); emit('onDataSaved', dataSaved, requestResponse); }; +const where = computed(() => { + if (!$props.countryFk) { + return {}; + } + return { countryFk: $props.countryFk }; +}); </script> <template> - <FetchData - @on-fetch="(data) => (autonomiesOptions = data)" - auto-load - :filter="{ - where: { - countryFk: $props.countryFk, - }, - }" - url="Autonomies/location" - :sort-by="['name ASC']" - :limit="30" - /> <FormModelPopup :title="t('New province')" :subtitle="t('Please, ensure you put the correct data!')" @@ -63,10 +52,17 @@ const onDataSaved = (dataSaved, requestResponse) => { :label="t('Name')" v-model="data.name" :rules="validate('province.name')" + required /> <VnSelect + required + ref="autonomiesRef" + auto-load + :where="where" + url="Autonomies/location" + :sort-by="['name ASC']" + :limit="30" :label="t('Autonomy')" - :options="autonomiesOptions" hide-selected option-label="name" option-value="id" diff --git a/src/components/CrudModel.vue b/src/components/CrudModel.vue index b1c7606e6..e992334b5 100644 --- a/src/components/CrudModel.vue +++ b/src/components/CrudModel.vue @@ -373,6 +373,7 @@ watch(formUrl, async () => { @click="onSubmit" :disable="!hasChanges" :title="t('globals.save')" + data-cy="crudModelDefaultSaveBtn" /> <slot name="moreAfterActions" /> </QBtnGroup> diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 4504ea6ad..2cb6ad8e6 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -91,6 +91,10 @@ const $props = defineProps({ type: Boolean, default: true, }, + maxWidth: { + type: [String, Boolean], + default: '800px', + }, }); const emit = defineEmits(['onFetch', 'onDataSaved']); const modelValue = computed( @@ -287,6 +291,7 @@ defineExpose({ @submit="save" @reset="reset" class="q-pa-md" + :style="maxWidth ? 'max-width: ' + maxWidth : ''" id="formModel" > <QCard> @@ -376,7 +381,6 @@ defineExpose({ color: black; } #formModel { - max-width: 800px; width: 100%; } diff --git a/src/components/TicketProblems.vue b/src/components/TicketProblems.vue new file mode 100644 index 000000000..2965396b1 --- /dev/null +++ b/src/components/TicketProblems.vue @@ -0,0 +1,40 @@ +<script setup> +defineProps({ row: { type: Object, required: true } }); +</script> +<template> + <span> + <QIcon + v-if="row.isTaxDataChecked === 0" + name="vn:no036" + color="primary" + size="xs" + > + <QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip> + </QIcon> + <QIcon v-if="row.hasTicketRequest" name="vn:buyrequest" color="primary" size="xs"> + <QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip> + </QIcon> + <QIcon v-if="row.itemShortage" name="vn:unavailable" color="primary" size="xs"> + <QTooltip>{{ $t('salesTicketsTable.notVisible') }}</QTooltip> + </QIcon> + <QIcon v-if="row.isFreezed" name="vn:frozen" color="primary" size="xs"> + <QTooltip>{{ $t('salesTicketsTable.clientFrozen') }}</QTooltip> + </QIcon> + <QIcon + v-if="row.risk" + name="vn:risk" + :color="row.hasHighRisk ? 'negative' : 'primary'" + size="xs" + > + <QTooltip> + {{ $t('salesTicketsTable.risk') }}: {{ row.risk - row.credit }} + </QTooltip> + </QIcon> + <QIcon v-if="row.hasComponentLack" name="vn:components" color="primary" size="xs"> + <QTooltip>{{ $t('salesTicketsTable.componentLack') }}</QTooltip> + </QIcon> + <QIcon v-if="row.isTooLittle" name="vn:isTooLittle" color="primary" size="xs"> + <QTooltip>{{ $t('salesTicketsTable.tooLittle') }}</QTooltip> + </QIcon> + </span> +</template> diff --git a/src/components/VnSelectProvince.vue b/src/components/VnSelectProvince.vue index 9fcbef11e..bfc4ab7b7 100644 --- a/src/components/VnSelectProvince.vue +++ b/src/components/VnSelectProvince.vue @@ -1,5 +1,5 @@ <script setup> -import { ref } from 'vue'; +import { ref, watch } from 'vue'; import { useValidator } from 'src/composables/useValidator'; import { useI18n } from 'vue-i18n'; @@ -7,7 +7,7 @@ import VnSelectDialog from 'components/common/VnSelectDialog.vue'; import FetchData from 'components/FetchData.vue'; import CreateNewProvinceForm from './CreateNewProvinceForm.vue'; -const emit = defineEmits(['onProvinceCreated']); +const emit = defineEmits(['onProvinceCreated', 'onProvinceFetched']); const $props = defineProps({ countryFk: { type: Number, @@ -17,20 +17,23 @@ const $props = defineProps({ type: Number, default: null, }, - provinces: { - type: Array, - default: () => [], - }, }); const provinceFk = defineModel({ type: Number, default: null }); const { validate } = useValidator(); const { t } = useI18n(); - +const filter = ref({ + include: { relation: 'country' }, + where: { + countryFk: $props.countryFk, + }, +}); const provincesOptions = ref($props.provinces); -provinceFk.value = $props.provinceSelected; const provincesFetchDataRef = ref(); - +provinceFk.value = $props.provinceSelected; +if (!$props.countryFk) { + filter.value.where = {}; +} async function onProvinceCreated(_, data) { await provincesFetchDataRef.value.fetch({ where: { countryFk: $props.countryFk } }); provinceFk.value = data.id; @@ -39,25 +42,33 @@ async function onProvinceCreated(_, data) { async function handleProvinces(data) { provincesOptions.value = data; } + +watch( + () => $props.countryFk, + async () => { + if ($props.countryFk) { + filter.value.where.countryFk = $props.countryFk; + } else filter.value.where = {}; + await provincesFetchDataRef.value.fetch({}); + emit('onProvinceFetched', provincesOptions.value); + } +); </script> <template> <FetchData ref="provincesFetchDataRef" - :filter="{ - include: { relation: 'country' }, - where: { - countryFk: $props.countryFk, - }, - }" + :filter="filter" @on-fetch="handleProvinces" url="Provinces" + auto-load /> <VnSelectDialog :label="t('Province')" - :options="$props.provinces" + :options="provincesOptions" :tooltip="t('Create province')" hide-selected + :clearable="true" v-model="provinceFk" :rules="validate && validate('postcode.provinceFk')" :acls="[{ model: 'Province', props: '*', accessType: 'WRITE' }]" diff --git a/src/components/VnTable/VnFilter.vue b/src/components/VnTable/VnFilter.vue index 86802ee92..999133130 100644 --- a/src/components/VnTable/VnFilter.vue +++ b/src/components/VnTable/VnFilter.vue @@ -143,6 +143,10 @@ function alignRow() { const showFilter = computed( () => $props.column?.columnFilter !== false && $props.column.name != 'tableActions' ); + +const onTabPressed = async () => { + if (model.value) enterEvent['keyup.enter'](); +}; </script> <template> <div @@ -157,6 +161,7 @@ const showFilter = computed( v-model="model" :components="components" component-prop="columnFilter" + @keydown.tab="onTabPressed" /> </div> </template> diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue index 941477084..ea7cf1dda 100644 --- a/src/components/VnTable/VnTable.vue +++ b/src/components/VnTable/VnTable.vue @@ -162,9 +162,7 @@ onMounted(() => { : $props.defaultMode; stateStore.rightDrawer = quasar.screen.gt.xs; columnsVisibilitySkipped.value = [ - ...splittedColumns.value.columns - .filter((c) => c.visible == false) - .map((c) => c.name), + ...splittedColumns.value.columns.filter((c) => !c.visible).map((c) => c.name), ...['tableActions'], ]; createForm.value = $props.create; @@ -237,7 +235,7 @@ function splitColumns(columns) { if (col.create) splittedColumns.value.create.push(col); if (col.cardVisible) splittedColumns.value.cardVisible.push(col); if ($props.isEditable && col.disable == null) col.disable = false; - if ($props.useModel && col.columnFilter != false) + if ($props.useModel && col.columnFilter !== false) col.columnFilter = { inWhere: true, ...col.columnFilter }; splittedColumns.value.columns.push(col); } @@ -396,7 +394,7 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) { :name="col.orderBy ?? col.name" :data-key="$attrs['data-key']" :search-url="searchUrl" - :vertical="true" + :vertical="false" /> </div> <slot @@ -739,6 +737,7 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) { fab icon="add" shortcut="+" + data-cy="vnTableCreateBtn" /> <QTooltip self="top right"> {{ createForm?.title }} diff --git a/src/components/common/VnAccountNumber.vue b/src/components/common/VnAccountNumber.vue index f7273a72d..c4fa78674 100644 --- a/src/components/common/VnAccountNumber.vue +++ b/src/components/common/VnAccountNumber.vue @@ -1,20 +1,24 @@ <script setup> -import { ref, watch } from 'vue'; +import { nextTick, ref, watch } from 'vue'; import { QInput } from 'quasar'; -const props = defineProps({ +const $props = defineProps({ modelValue: { type: String, default: '', }, + insertable: { + type: Boolean, + default: false, + }, }); const emit = defineEmits(['update:modelValue', 'accountShortToStandard']); -let internalValue = ref(props.modelValue); +let internalValue = ref($props.modelValue); watch( - () => props.modelValue, + () => $props.modelValue, (newVal) => { internalValue.value = newVal; } @@ -28,8 +32,46 @@ watch( } ); +const handleKeydown = (e) => { + if (e.key === 'Backspace') return; + if (e.key === '.') { + accountShortToStandard(); + // TODO: Fix this setTimeout, with nextTick doesn't work + setTimeout(() => { + setCursorPosition(0, e.target); + }, 1); + return; + } + + if ($props.insertable && e.key.match(/[0-9]/)) { + handleInsertMode(e); + } +}; +function setCursorPosition(pos, el = vnInputRef.value) { + el.focus(); + el.setSelectionRange(pos, pos); +} +const vnInputRef = ref(false); +const handleInsertMode = (e) => { + e.preventDefault(); + const input = e.target; + const cursorPos = input.selectionStart; + const { maxlength } = vnInputRef.value; + let currentValue = internalValue.value; + if (!currentValue) currentValue = e.key; + const newValue = e.key; + if (newValue && !isNaN(newValue) && cursorPos < maxlength) { + internalValue.value = + currentValue.substring(0, cursorPos) + + newValue + + currentValue.substring(cursorPos + 1); + } + nextTick(() => { + input.setSelectionRange(cursorPos + 1, cursorPos + 1); + }); +}; function accountShortToStandard() { - internalValue.value = internalValue.value.replace( + internalValue.value = internalValue.value?.replace( '.', '0'.repeat(11 - internalValue.value.length) ); @@ -37,5 +79,5 @@ function accountShortToStandard() { </script> <template> - <q-input v-model="internalValue" /> + <QInput @keydown="handleKeydown" ref="vnInputRef" v-model="internalValue" /> </template> diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue index 4672529c6..57a495ac3 100644 --- a/src/components/common/VnInput.vue +++ b/src/components/common/VnInput.vue @@ -1,5 +1,5 @@ <script setup> -import { computed, ref, useAttrs } from 'vue'; +import { computed, ref, useAttrs, nextTick } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRequired } from 'src/composables/useRequired'; @@ -34,6 +34,14 @@ const $props = defineProps({ type: Boolean, default: true, }, + insertable: { + type: Boolean, + default: false, + }, + maxlength: { + type: Number, + default: null, + }, }); const vnInputRef = ref(null); @@ -69,6 +77,9 @@ const mixinRules = [ requiredFieldRule, ...($attrs.rules ?? []), (val) => { + const { maxlength } = vnInputRef.value; + if (maxlength && +val.length > maxlength) + return t(`maxLength`, { value: maxlength }); const { min, max } = vnInputRef.value.$attrs; if (!min) return null; if (min >= 0) if (Math.floor(val) < min) return t('inputMin', { value: min }); @@ -78,6 +89,33 @@ const mixinRules = [ } }, ]; + +const handleKeydown = (e) => { + if (e.key === 'Backspace') return; + + if ($props.insertable && e.key.match(/[0-9]/)) { + handleInsertMode(e); + } +}; + +const handleInsertMode = (e) => { + e.preventDefault(); + const input = e.target; + const cursorPos = input.selectionStart; + const { maxlength } = vnInputRef.value; + let currentValue = value.value; + if (!currentValue) currentValue = e.key; + const newValue = e.key; + if (newValue && !isNaN(newValue) && cursorPos < maxlength) { + value.value = + currentValue.substring(0, cursorPos) + + newValue + + currentValue.substring(cursorPos + 1); + } + nextTick(() => { + input.setSelectionRange(cursorPos + 1, cursorPos + 1); + }); +}; </script> <template> @@ -89,10 +127,12 @@ const mixinRules = [ :type="$attrs.type" :class="{ required: isRequired }" @keyup.enter="emit('keyup.enter')" + @keydown="handleKeydown" :clearable="false" :rules="mixinRules" :lazy-rules="true" hide-bottom-space + :data-cy="$attrs.dataCy ?? $attrs.label + '_input'" > <template v-if="$slots.prepend" #prepend> <slot name="prepend" /> @@ -129,9 +169,11 @@ const mixinRules = [ <i18n> en: inputMin: Must be more than {value} + maxLength: The value exceeds {value} characters inputMax: Must be less than {value} es: inputMin: Debe ser mayor a {value} + maxLength: El valor excede los {value} carácteres inputMax: Debe ser menor a {value} </i18n> <style lang="scss"> diff --git a/src/components/common/VnInputDate.vue b/src/components/common/VnInputDate.vue index fcc04ddf7..eb9dc0f38 100644 --- a/src/components/common/VnInputDate.vue +++ b/src/components/common/VnInputDate.vue @@ -1,8 +1,7 @@ <script setup> -import { onMounted, watch, computed, ref } from 'vue'; +import { onMounted, watch, computed, ref, useAttrs } from 'vue'; import { date } from 'quasar'; import { useI18n } from 'vue-i18n'; -import { useAttrs } from 'vue'; import VnDate from './VnDate.vue'; import { useRequired } from 'src/composables/useRequired'; diff --git a/src/components/common/VnRadio.vue b/src/components/common/VnRadio.vue index 4eeb9dbe9..69b1501a2 100644 --- a/src/components/common/VnRadio.vue +++ b/src/components/common/VnRadio.vue @@ -2,5 +2,12 @@ const model = defineModel({ type: Boolean, required: true }); </script> <template> - <QRadio v-model="model" v-bind="$attrs" dense :dark="true" class="q-mr-sm" /> + <QRadio + v-model="model" + v-bind="$attrs" + dense + :dark="true" + class="q-mr-sm" + size="xs" + /> </template> diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index 14005e1cc..cd8716194 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -138,8 +138,6 @@ onMounted(() => { if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300); }); -defineExpose({ opts: myOptions }); - const arrayDataKey = $props.dataKey ?? ($props.url?.length > 0 ? $props.url : $attrs.name ?? $attrs.label); @@ -259,6 +257,30 @@ async function onScroll({ to, direction, from, index }) { isLoading.value = false; } } + +defineExpose({ opts: myOptions }); + +function handleKeyDown(event) { + if (event.key === 'Tab') { + event.preventDefault(); + + const inputValue = vnSelectRef.value?.inputValue; + + if (inputValue) { + const matchingOption = myOptions.value.find( + (option) => + option[optionLabel.value].toLowerCase() === inputValue.toLowerCase() + ); + + if (matchingOption) { + emit('update:modelValue', matchingOption[optionValue.value]); + } else { + emit('update:modelValue', inputValue); + } + vnSelectRef.value?.hidePopup(); + } + } +} </script> <template> @@ -269,6 +291,7 @@ async function onScroll({ to, direction, from, index }) { :option-value="optionValue" v-bind="$attrs" @filter="filterHandler" + @keydown="handleKeyDown" :emit-value="nullishToTrue($attrs['emit-value'])" :map-options="nullishToTrue($attrs['map-options'])" :use-input="nullishToTrue($attrs['use-input'])" @@ -283,6 +306,7 @@ async function onScroll({ to, direction, from, index }) { :input-debounce="useURL ? '300' : '0'" :loading="isLoading" @virtual-scroll="onScroll" + :data-cy="$attrs.dataCy ?? $attrs.label + '_select'" > <template #append> <QIcon diff --git a/src/components/common/VnSmsDialog.vue b/src/components/common/VnSmsDialog.vue index 064394445..8851a33b2 100644 --- a/src/components/common/VnSmsDialog.vue +++ b/src/components/common/VnSmsDialog.vue @@ -86,7 +86,7 @@ async function send() { </script> <template> - <QDialog ref="dialogRef"> + <QDialog ref="dialogRef" data-cy="vnSmsDialog"> <QCard class="q-pa-sm"> <QCardSection class="row items-center q-pb-none"> <span class="text-h6 text-grey"> @@ -161,6 +161,7 @@ async function send() { :loading="isLoading" color="primary" unelevated + data-cy="sendSmsBtn" /> </QCardActions> </QCard> diff --git a/src/components/ui/VnFilterPanel.vue b/src/components/ui/VnFilterPanel.vue index b188bde48..8c0dbda94 100644 --- a/src/components/ui/VnFilterPanel.vue +++ b/src/components/ui/VnFilterPanel.vue @@ -37,7 +37,7 @@ const $props = defineProps({ }, hiddenTags: { type: Array, - default: () => ['filter', 'search', 'or', 'and'], + default: () => ['filter', 'or', 'and'], }, customTags: { type: Array, @@ -61,7 +61,6 @@ const emit = defineEmits([ 'update:modelValue', 'refresh', 'clear', - 'search', 'init', 'remove', 'setUserParams', @@ -274,6 +273,7 @@ function sanitizer(params) { :key="chip.label" :removable="!unremovableParams?.includes(chip.label)" @remove="remove(chip.label)" + data-cy="vnFilterPanelChip" > <slot name="tags" :tag="chip" :format-fn="formatValue"> <div class="q-gutter-x-xs"> diff --git a/src/components/ui/VnLinkPhone.vue b/src/components/ui/VnLinkPhone.vue index 4068498cd..027244609 100644 --- a/src/components/ui/VnLinkPhone.vue +++ b/src/components/ui/VnLinkPhone.vue @@ -2,6 +2,7 @@ import { reactive, useAttrs, onBeforeMount, capitalize } from 'vue'; import axios from 'axios'; import { parsePhone } from 'src/filters'; +import useOpenURL from 'src/composables/useOpenURL'; const props = defineProps({ phoneNumber: { type: [String, Number], default: null }, channel: { type: Number, default: null }, @@ -11,25 +12,31 @@ const config = reactive({ sip: { icon: 'phone', href: `sip:${props.phoneNumber}` }, 'say-simple': { icon: 'vn:saysimple', - href: null, channel: props.channel, + url: null, }, }); const type = Object.keys(config).find((key) => key in useAttrs()) || 'sip'; onBeforeMount(async () => { - let { channel } = config[type]; + let { channel, url } = config[type]; if (type === 'say-simple') { - const { url, defaultChannel } = (await axios.get('SaySimpleConfigs/findOne')) - .data; - if (!channel) channel = defaultChannel; - - config[type].href = `${url}?customerIdentity=%2B${parsePhone( - props.phoneNumber - )}&channelId=${channel}`; + const { url: defaultUrl, defaultChannel } = ( + await axios.get('SaySimpleConfigs/findOne') + ).data; + if (!channel) config[type].channel = defaultChannel; + if (!url) config[type].url = defaultUrl; } }); + +function openSaySimple() { + useOpenURL( + `${config[type].url}?customerIdentity=%2B${parsePhone( + props.phoneNumber + )}&channelId=${config[type].channel}` + ); +} </script> <template> <QBtn @@ -40,8 +47,7 @@ onBeforeMount(async () => { size="sm" color="primary" padding="none" - :href="config[type].href" - @click.stop + @click.stop="openSaySimple" > <QTooltip> {{ capitalize(type).replace('-', '') }} diff --git a/src/components/ui/VnRow.vue b/src/components/ui/VnRow.vue index 40dabf610..b57489a98 100644 --- a/src/components/ui/VnRow.vue +++ b/src/components/ui/VnRow.vue @@ -1,5 +1,5 @@ <template> - <div class="vn-row q-gutter-md q-mb-md"> + <div class="vn-row q-gutter-md"> <slot /> </div> </template> @@ -18,6 +18,9 @@ &:not(.wrap) { flex-direction: column; } + &[fixed] { + flex-direction: row; + } } } </style> diff --git a/src/components/ui/VnSearchbar.vue b/src/components/ui/VnSearchbar.vue index da2d370fe..ccf87c6d6 100644 --- a/src/components/ui/VnSearchbar.vue +++ b/src/components/ui/VnSearchbar.vue @@ -130,6 +130,7 @@ async function search() { dense standout autofocus + data-cy="vnSearchBar" > <template #prepend> <QIcon diff --git a/src/composables/downloadFile.js b/src/composables/downloadFile.js index 12639dcd6..4588265a2 100644 --- a/src/composables/downloadFile.js +++ b/src/composables/downloadFile.js @@ -1,11 +1,24 @@ import { useSession } from 'src/composables/useSession'; import { getUrl } from './getUrl'; +import axios from 'axios'; +import { exportFile } from 'quasar'; const { getTokenMultimedia } = useSession(); const token = getTokenMultimedia(); export async function downloadFile(id, model = 'dms', urlPath = '/downloadFile', url) { - let appUrl = await getUrl('', 'lilium'); - appUrl = appUrl.replace('/#/', ''); - window.open(url ?? `${appUrl}/api/${model}/${id}${urlPath}?access_token=${token}`); + const appUrl = (await getUrl('', 'lilium')).replace('/#/', ''); + const response = await axios.get( + url ?? `${appUrl}/api/${model}/${id}${urlPath}?access_token=${token}`, + { responseType: 'blob' } + ); + + const contentDisposition = response.headers['content-disposition']; + const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition); + const filename = + matches != null && matches[1] + ? matches[1].replace(/['"]/g, '') + : 'downloaded-file'; + + exportFile(filename, response.data); } diff --git a/src/css/app.scss b/src/css/app.scss index d4c76ad6b..63a9f5c46 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -241,7 +241,7 @@ input::-webkit-inner-spin-button { th, td { padding: 1px 10px 1px 10px; - max-width: 100px; + max-width: 130px; div span { overflow: hidden; white-space: nowrap; diff --git a/src/filters/parsePhone.js b/src/filters/parsePhone.js index 696f55007..cded224b9 100644 --- a/src/filters/parsePhone.js +++ b/src/filters/parsePhone.js @@ -1,4 +1,7 @@ export default function (phone, prefix = 34) { + if (!phone) { + return; + } if (phone.startsWith('+')) { return `${phone.slice(1)}`; } diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index 31a6931a4..ecfa2c8fe 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -298,6 +298,7 @@ globals: clientsActionsMonitor: Clients and actions serial: Serial medical: Mutual + pit: IRPF RouteExtendedList: Router wasteRecalc: Waste recaclulate operator: Operator @@ -506,6 +507,7 @@ invoiceOut: invoiceWithFutureDate: Exists an invoice with a future date noTicketsToInvoice: There are not tickets to invoice criticalInvoiceError: 'Critical invoicing error, process stopped' + invalidSerialTypeForAll: The serial type must be global when invoicing all clients table: addressId: Address id streetAddress: Street @@ -858,6 +860,7 @@ components: downloadFile: Download file openCard: View openSummary: Summary + viewSummary: Summary cardDescriptor: mainList: Main list summary: Summary diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index ccc21e225..def0b0696 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -303,6 +303,7 @@ globals: clientsActionsMonitor: Clientes y acciones serial: Facturas por serie medical: Mutua + pit: IRPF wasteRecalc: Recalcular mermas operator: Operario parking: Parking @@ -509,6 +510,7 @@ invoiceOut: invoiceWithFutureDate: Existe una factura con una fecha futura noTicketsToInvoice: No existen tickets para facturar criticalInvoiceError: Error crítico en la facturación proceso detenido + invalidSerialTypeForAll: El tipo de serie debe ser global cuando se facturan todos los clientes table: addressId: Id dirección streetAddress: Dirección fiscal diff --git a/src/pages/Claim/Card/ClaimDescriptorMenu.vue b/src/pages/Claim/Card/ClaimDescriptorMenu.vue index d88c3d120..d87091887 100644 --- a/src/pages/Claim/Card/ClaimDescriptorMenu.vue +++ b/src/pages/Claim/Card/ClaimDescriptorMenu.vue @@ -100,7 +100,7 @@ async function remove() { </QMenu> </QItem> <QSeparator /> - <QItem @click="confirmRemove()" v-ripple clickable> + <QItem @click="confirmRemove()" v-ripple clickable data-cy="deleteClaim"> <QItemSection avatar> <QIcon name="delete" /> </QItemSection> diff --git a/src/pages/Entry/EntryLatestBuys.vue b/src/pages/Entry/EntryLatestBuys.vue index 9f15130c0..450efe624 100644 --- a/src/pages/Entry/EntryLatestBuys.vue +++ b/src/pages/Entry/EntryLatestBuys.vue @@ -12,6 +12,7 @@ import VnImg from 'src/components/ui/VnImg.vue'; const stateStore = useStateStore(); const { t } = useI18n(); +const tableRef = ref(); const columns = [ { align: 'center', @@ -234,7 +235,6 @@ const columns = [ format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.landing)), }, ]; -const tableRef = ref(); onMounted(async () => { stateStore.rightDrawer = true; diff --git a/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue b/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue index e85f1f44c..3fd3104bf 100644 --- a/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue +++ b/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue @@ -183,7 +183,7 @@ onMounted(async () => { <i18n> en: invoiceDate: Invoice date - maxShipped: Max date + maxShipped: Max date ticket allClients: All clients oneClient: One client company: Company @@ -195,7 +195,7 @@ en: es: invoiceDate: Fecha de factura - maxShipped: Fecha límite + maxShipped: Fecha límite ticket allClients: Todos los clientes oneClient: Un solo cliente company: Empresa diff --git a/src/pages/InvoiceOut/InvoiceOutList.vue b/src/pages/InvoiceOut/InvoiceOutList.vue index f5a4f7d80..c8fffb0ef 100644 --- a/src/pages/InvoiceOut/InvoiceOutList.vue +++ b/src/pages/InvoiceOut/InvoiceOutList.vue @@ -6,15 +6,19 @@ import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; -import { usePrintService } from 'composables/usePrintService'; -import VnTable from 'components/VnTable/VnTable.vue'; +import { usePrintService } from 'src/composables/usePrintService'; +import VnTable from 'src/components/VnTable/VnTable.vue'; import InvoiceOutSummary from './Card/InvoiceOutSummary.vue'; import { toCurrency, toDate } from 'src/filters/index'; import { useStateStore } from 'stores/useStateStore'; import { QBtn } from 'quasar'; -import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue'; +import axios from 'axios'; import RightMenu from 'src/components/common/RightMenu.vue'; import InvoiceOutFilter from './InvoiceOutFilter.vue'; +import VnRow from 'src/components/ui/VnRow.vue'; +import VnRadio from 'src/components/common/VnRadio.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue'; const { t } = useI18n(); const stateStore = useStateStore(); @@ -26,19 +30,29 @@ const selectedRows = ref([]); const hasSelectedCards = computed(() => selectedRows.value.length > 0); const MODEL = 'InvoiceOuts'; const { openReport } = usePrintService(); +const addressOptions = ref([]); +const selectedOption = ref('ticket'); +async function fetchClientAddress(id) { + const { data } = await axios.get( + `Clients/${id}/addresses?filter[order]=isActive DESC` + ); + addressOptions.value = data; +} + +const exprBuilder = (_, value) => { + return { + or: [{ code: value }, { description: { like: `%${value}%` } }], + }; +}; const columns = computed(() => [ { align: 'center', name: 'id', label: t('invoiceOutList.tableVisibleColumns.id'), - chip: { - condition: () => true, - }, + chip: { condition: () => true }, isId: true, - columnFilter: { - name: 'search', - }, + columnFilter: { name: 'search' }, }, { align: 'left', @@ -58,68 +72,51 @@ const columns = computed(() => [ }, { align: 'left', - name: 'Issued', - label: t('invoiceOutList.tableVisibleColumns.issued'), + name: 'issued', + label: t('invoiceOut.summary.issued'), component: 'date', format: (row) => toDate(row.issued), - columnField: { - component: null, - }, + columnField: { component: null }, }, { align: 'left', name: 'clientFk', - label: t('invoiceOutModule.customer'), + label: t('globals.client'), cardVisible: true, component: 'select', - attrs: { - url: 'Clients', - fields: ['id', 'name'], - }, - columnField: { - component: null, - }, + attrs: { url: 'Clients', fields: ['id', 'name'] }, + columnField: { component: null }, }, { align: 'left', name: 'companyCode', - label: t('invoiceOutModule.company'), + label: t('globals.company'), cardVisible: true, component: 'select', - attrs: { - url: 'Companies', - optionLabel: 'code', - optionValue: 'id', - }, - columnField: { - component: null, - }, + attrs: { url: 'Companies', optionLabel: 'code', optionValue: 'id' }, + columnField: { component: null }, }, { align: 'left', name: 'amount', - label: t('invoiceOutModule.amount'), + label: t('globals.amount'), cardVisible: true, format: (row) => toCurrency(row.amount), }, { align: 'left', name: 'created', - label: t('invoiceOutList.tableVisibleColumns.created'), + label: t('globals.created'), component: 'date', - columnField: { - component: null, - }, + columnField: { component: null }, format: (row) => toDate(row.created), }, { align: 'left', name: 'dued', - label: t('invoiceOutList.tableVisibleColumns.dueDate'), + label: t('invoiceOut.summary.dued'), component: 'date', - columnField: { - component: null, - }, + columnField: { component: null }, format: (row) => toDate(row.dued), }, { @@ -129,11 +126,12 @@ const columns = computed(() => [ { title: t('components.smartCard.viewSummary'), icon: 'preview', + isPrimary: true, action: (row) => viewSummary(row.id, InvoiceOutSummary), }, { - title: t('DownloadPdf'), - icon: 'vn:ticket', + title: t('globals.downloadPdf'), + icon: 'cloud_download', isPrimary: true, action: (row) => openPdf(row.id), }, @@ -172,7 +170,7 @@ watchEffect(selectedRows); <template> <VnSearchbar :info="t('youCanSearchByInvoiceReference')" - :label="t('searchInvoice')" + :label="t('Search invoice')" data-key="invoiceOutList" /> <RightMenu> @@ -188,7 +186,7 @@ watchEffect(selectedRows); @click="downloadPdf()" :disable="!hasSelectedCards" > - <QTooltip>{{ t('globals.downloadPdf') }}</QTooltip> + <QTooltip>{{ t('downloadPdf') }}</QTooltip> </QBtn> </template> </VnSubToolbar> @@ -198,11 +196,9 @@ watchEffect(selectedRows); :url="`${MODEL}/filter`" :create="{ urlCreate: 'InvoiceOuts/createManualInvoice', - title: t('Create manual invoice'), + title: t('createManualInvoice'), onDataSaved: ({ id }) => tableRef.redirect(id), - formInitialData: { - active: true, - }, + formInitialData: { active: true }, }" :right-search="false" v-model:selected="selectedRows" @@ -222,74 +218,199 @@ watchEffect(selectedRows); </span> </template> <template #more-create-dialog="{ data }"> - <div class="flex no-wrap flex-center"> - <VnSelect - url="Tickets" - v-model="data.ticketFk" - :label="t('invoiceOutList.tableVisibleColumns.ticket')" - option-label="id" - option-value="id" - > - <template #option="scope"> - <QItem v-bind="scope.itemProps"> - <QItemSection> - <QItemLabel> #{{ scope.opt?.id }} </QItemLabel> - <QItemLabel caption>{{ scope.opt?.nickname }}</QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelect> - <span class="q-ml-md">O</span> + <div class="row q-col-gutter-xs"> + <div class="col-12"> + <div class="q-col-gutter-xs"> + <VnRow fixed> + <VnRadio + v-model="selectedOption" + val="ticket" + :label="t('globals.ticket')" + class="q-my-none q-mr-md" + /> + + <VnInput + v-show="selectedOption === 'ticket'" + v-model="data.ticketFk" + :label="t('globals.ticket')" + style="flex: 1" + /> + + <div + class="row q-col-gutter-xs q-ml-none" + v-show="selectedOption !== 'ticket'" + > + <div class="col"> + <VnSelect + v-model="data.clientFk" + :label="t('globals.client')" + url="Clients" + :options="customerOptions" + option-label="name" + option-value="id" + @update:model-value="fetchClientAddress" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + #{{ scope.opt?.id }} - + {{ scope.opt?.name }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </div> + <div class="col"> + <VnSelect + v-model="data.addressFk" + :label="t('ticket.summary.consignee')" + :options="addressOptions" + option-label="nickname" + option-value="id" + v-if=" + data.clientFk && + selectedOption === 'consignatario' + " + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel + :class="{ + 'color-vn-label': + !scope.opt?.isActive, + }" + > + {{ + `${ + !scope.opt?.isActive + ? t('inactive') + : '' + } ` + }} + <span>{{ + scope.opt?.nickname + }}</span> + <span + v-if=" + scope.opt?.province || + scope.opt?.city || + scope.opt?.street + " + > + , {{ scope.opt?.street }}, + {{ scope.opt?.city }}, + {{ + scope.opt?.province?.name + }} + - + {{ + scope.opt?.agencyMode + ?.name + }} + </span> + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </div> + </div> + </VnRow> + <VnRow fixed> + <VnRadio + v-model="selectedOption" + val="cliente" + :label="t('globals.client')" + class="q-my-none q-mr-md" + /> + </VnRow> + <VnRow fixed> + <VnRadio + v-model="selectedOption" + val="consignatario" + :label="t('ticket.summary.consignee')" + class="q-my-none q-mr-md" + /> + </VnRow> + </div> + </div> + <div class="full-width"> + <VnRow class="row q-col-gutter-xs"> + <VnSelect + url="InvoiceOutSerials" + v-model="data.serial" + :label="t('invoiceIn.serial')" + :options="invoiceOutSerialsOptions" + option-label="description" + option-value="code" + option-filter + :expr-builder="exprBuilder" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt?.code }} - + {{ scope.opt?.description }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + <VnInputDate + :label="t('invoiceOut.summary.dued')" + v-model="data.maxShipped" + /> + </VnRow> + <VnRow class="row q-col-gutter-xs"> + <VnSelect + url="TaxAreas" + v-model="data.taxArea" + :label="t('invoiceOutList.tableVisibleColumns.taxArea')" + :options="taxAreasOptions" + option-label="code" + option-value="code" + /> + <VnInput + v-model="data.reference" + :label="t('globals.reference')" + /> + </VnRow> + </div> </div> - <VnSelect - url="Clients" - v-model="data.clientFk" - :label="t('invoiceOutModule.customer')" - :options="customerOptions" - option-label="name" - option-value="id" - /> - <VnSelect - url="InvoiceOutSerials" - v-model="data.serial" - :label="t('invoiceOutList.tableVisibleColumns.invoiceOutSerial')" - :options="invoiceOutSerialsOptions" - option-label="description" - option-value="code" - /> - <VnInputDate - :label="t('invoiceOutList.tableVisibleColumns.dueDate')" - v-model="data.maxShipped" - /> - <VnSelect - url="TaxAreas" - v-model="data.taxArea" - :label="t('invoiceOutList.tableVisibleColumns.taxArea')" - :options="taxAreasOptions" - option-label="code" - option-value="code" - /> - <QInput - v-model="data.reference" - :label="t('invoiceOutList.tableVisibleColumns.ref')" - /> </template> </VnTable> </template> +<style lang="scss" scoped> +#formModel .vn-row { + min-height: 45px; + + .q-radio { + align-self: flex-end; + flex: 0.3; + } + + > .q-input, + > .q-select { + flex: 0.75; + } +} +</style> + <i18n> -en: - searchInvoice: Search issued invoice - fileDenied: Browser denied file download... - fileAllowed: Successful download of CSV file - youCanSearchByInvoiceReference: You can search by invoice reference - createInvoice: Make invoice - Create manual invoice: Create manual invoice -es: - searchInvoice: Buscar factura emitida - fileDenied: El navegador denegó la descarga de archivos... - fileAllowed: Descarga exitosa de archivo CSV - youCanSearchByInvoiceReference: Puedes buscar por referencia de la factura - createInvoice: Crear factura - Create manual invoice: Crear factura manual + en: + invoiceId: Invoice ID + youCanSearchByInvoiceReference: You can search by invoice reference + createManualInvoice: Create Manual Invoice + inactive: (Inactive) + + es: + invoiceId: ID de factura + youCanSearchByInvoiceReference: Puedes buscar por referencia de la factura + createManualInvoice: Crear factura manual + inactive: (Inactivo) </i18n> diff --git a/src/pages/InvoiceOut/locale/en.yml b/src/pages/InvoiceOut/locale/en.yml index 5ad92ed09..8cefe8bdc 100644 --- a/src/pages/InvoiceOut/locale/en.yml +++ b/src/pages/InvoiceOut/locale/en.yml @@ -2,6 +2,7 @@ invoiceOutModule: customer: Client amount: Amount company: Company + address: Address invoiceOutList: tableVisibleColumns: id: ID @@ -15,11 +16,11 @@ invoiceOutList: DownloadPdf: Download PDF InvoiceOutSummary: Summary negativeBases: - country: Country - clientId: Client ID - base: Base - ticketId: Ticket - active: Active - hasToInvoice: Has to invoice - verifiedData: Verified data - commercial: Commercial \ No newline at end of file + country: Country + clientId: Client ID + base: Base + ticketId: Ticket + active: Active + hasToInvoice: Has to invoice + verifiedData: Verified data + commercial: Commercial diff --git a/src/pages/InvoiceOut/locale/es.yml b/src/pages/InvoiceOut/locale/es.yml index 192f5b26f..bf5126641 100644 --- a/src/pages/InvoiceOut/locale/es.yml +++ b/src/pages/InvoiceOut/locale/es.yml @@ -4,6 +4,7 @@ invoiceOutModule: customer: Cliente amount: Importe company: Empresa + address: Consignatario invoiceOutList: tableVisibleColumns: id: ID diff --git a/src/pages/Monitor/Ticket/MonitorTickets.vue b/src/pages/Monitor/Ticket/MonitorTickets.vue index 253316a09..e5fea3003 100644 --- a/src/pages/Monitor/Ticket/MonitorTickets.vue +++ b/src/pages/Monitor/Ticket/MonitorTickets.vue @@ -15,6 +15,7 @@ import { toCurrency, dateRange, dashIfEmpty } from 'src/filters'; import RightMenu from 'src/components/common/RightMenu.vue'; import MonitorTicketSearchbar from './MonitorTicketSearchbar.vue'; import MonitorTicketFilter from './MonitorTicketFilter.vue'; +import TicketProblems from 'src/components/TicketProblems.vue'; const DEFAULT_AUTO_REFRESH = 2 * 60 * 1000; // 2min in ms const { t } = useI18n(); @@ -23,9 +24,15 @@ const tableRef = ref(null); const provinceOpts = ref([]); const stateOpts = ref([]); const zoneOpts = ref([]); -const visibleColumns = ref([]); const { viewSummary } = useSummaryDialog(); + const [from, to] = dateRange(Date.vnNew()); +const stateColors = { + notice: 'info', + success: 'positive', + warning: 'warning', + alert: 'negative', +}; function exprBuilder(param, value) { switch (param) { @@ -220,7 +227,7 @@ const columns = computed(() => [ { title: t('salesTicketsTable.goToLines'), icon: 'vn:lines', - color: 'priamry', + color: 'primary', action: (row) => openTab(row.id), isPrimary: true, attrs: { @@ -231,7 +238,7 @@ const columns = computed(() => [ { title: t('salesTicketsTable.preview'), icon: 'preview', - color: 'priamry', + color: 'primary', action: (row) => viewSummary(row.id, TicketSummary), isPrimary: true, attrs: { @@ -249,10 +256,10 @@ const getBadgeAttrs = (date) => { let timeTicket = new Date(date); timeTicket.setHours(0, 0, 0, 0); - let comparation = today - timeTicket; + let timeDiff = today - timeTicket; - if (comparation == 0) return { color: 'warning', 'text-color': 'black' }; - if (comparation < 0) return { color: 'success', 'text-color': 'black' }; + if (timeDiff == 0) return { color: 'warning', 'text-color': 'black' }; + if (timeDiff < 0) return { color: 'success', 'text-color': 'black' }; return { color: 'transparent', 'text-color': 'white' }; }; @@ -267,13 +274,6 @@ const autoRefreshHandler = (value) => { } }; -const stateColors = { - notice: 'info', - success: 'positive', - warning: 'warning', - alert: 'negative', -}; - const totalPriceColor = (ticket) => { const total = parseInt(ticket.totalWithVat); if (total > 0 && total < 50) return 'warning'; @@ -281,10 +281,10 @@ const totalPriceColor = (ticket) => { const formatShippedDate = (date) => { if (!date) return '-'; - const split1 = date.split('T'); - const [year, month, day] = split1[0].split('-'); - const _date = new Date(year, month - 1, day); - return toDateFormat(_date); + const dateSplit = date.split('T'); + const [year, month, day] = dateSplit[0].split('-'); + const newDate = new Date(year, month - 1, day); + return toDateFormat(newDate); }; const openTab = (id) => @@ -332,7 +332,6 @@ const openTab = (id) => :expr-builder="exprBuilder" :offset="50" :columns="columns" - :visible-columns="visibleColumns" :right-search="false" default-mode="table" auto-load @@ -362,61 +361,7 @@ const openTab = (id) => </QCheckbox> </template> <template #column-totalProblems="{ row }"> - <span> - <QIcon - v-if="row.isTaxDataChecked === 0" - name="vn:no036" - color="primary" - size="xs" - > - <QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip> - </QIcon> - <QIcon - v-if="row.hasTicketRequest" - name="vn:buyrequest" - color="primary" - size="xs" - > - <QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip> - </QIcon> - <QIcon - v-if="row.itemShortage" - name="vn:unavailable" - color="primary" - size="xs" - > - <QTooltip>{{ $t('salesTicketsTable.notVisible') }}</QTooltip> - </QIcon> - <QIcon v-if="row.isFreezed" name="vn:frozen" color="primary" size="xs"> - <QTooltip>{{ $t('salesTicketsTable.clientFrozen') }}</QTooltip> - </QIcon> - <QIcon - v-if="row.risk" - name="vn:risk" - :color="row.hasHighRisk ? 'negative' : 'primary'" - size="xs" - > - <QTooltip - >{{ $t('salesTicketsTable.risk') }}: {{ row.risk }}</QTooltip - > - </QIcon> - <QIcon - v-if="row.hasComponentLack" - name="vn:components" - color="primary" - size="xs" - > - <QTooltip>{{ $t('salesTicketsTable.componentLack') }}</QTooltip> - </QIcon> - <QIcon - v-if="row.isTooLittle" - name="vn:isTooLittle" - color="primary" - size="xs" - > - <QTooltip>{{ $t('salesTicketsTable.tooLittle') }}</QTooltip> - </QIcon> - </span> + <TicketProblems :row="row" /> </template> <template #column-id="{ row }"> <span class="link" @click.stop.prevent> @@ -471,7 +416,7 @@ const openTab = (id) => </QIcon> </template> <template #column-zoneFk="{ row }"> - <div @click.stop.prevent :title="row.zoneName"> + <div v-if="row.zoneFk" @click.stop.prevent :title="row.zoneName"> <span class="link">{{ row.zoneName }}</span> <ZoneDescriptorProxy :id="row.zoneFk" /> </div> diff --git a/src/pages/Order/Card/CatalogFilterValueDialog.vue b/src/pages/Order/Card/CatalogFilterValueDialog.vue index 53bb87f8d..b91e7d229 100644 --- a/src/pages/Order/Card/CatalogFilterValueDialog.vue +++ b/src/pages/Order/Card/CatalogFilterValueDialog.vue @@ -49,7 +49,7 @@ const getSelectedTagValues = async (tag) => { <template> <QForm @submit="applyTags()" class="all-pointer-events"> - <QCard class="q-pa-sm column q-pa-lg"> + <QCard class="q-pa-sm column q-pa-lg" data-cy="catalogFilterValueDialog"> <VnSelect :label="t('params.tag')" v-model="selectedTag" @@ -63,6 +63,7 @@ const getSelectedTagValues = async (tag) => { :emit-value="false" use-input @update:model-value="getSelectedTagValues" + data-cy="catalogFilterValueDialogTagSelect" /> <div v-for="(value, index) in tagValues" @@ -93,6 +94,7 @@ const getSelectedTagValues = async (tag) => { :disable="!value" is-outlined class="col" + data-cy="catalogFilterValueDialogValueInput" /> <QBtn icon="delete" diff --git a/src/pages/Order/Card/OrderCatalog.vue b/src/pages/Order/Card/OrderCatalog.vue index 90bce38fd..a71065521 100644 --- a/src/pages/Order/Card/OrderCatalog.vue +++ b/src/pages/Order/Card/OrderCatalog.vue @@ -98,7 +98,7 @@ watch( /> </QScrollArea> </QDrawer> - <QPage class="column items-center q-pa-md"> + <QPage class="column items-center q-pa-md" data-cy="orderCatalogPage"> <div class="full-width"> <VnPaginate :data-key="dataKey" @@ -118,6 +118,7 @@ watch( :item="row" is-catalog class="fill-icon" + data-cy="orderCatalogItem" /> </div> </template> diff --git a/src/pages/Order/Card/OrderCatalogFilter.vue b/src/pages/Order/Card/OrderCatalogFilter.vue index 6202a6f90..1dd569fb5 100644 --- a/src/pages/Order/Card/OrderCatalogFilter.vue +++ b/src/pages/Order/Card/OrderCatalogFilter.vue @@ -178,6 +178,7 @@ function addOrder(value, field, params) { ? resetCategory(params, searchFn) : removeTagGroupParam(params, searchFn, valIndex) " + data-cy="catalogFilterCustomTag" > <strong v-if="customTag.label === 'categoryFk' && categoryList"> {{ @@ -211,6 +212,7 @@ function addOrder(value, field, params) { :name="category.icon" class="category-icon" @click="selectCategory(params, category, searchFn)" + data-cy="catalogFilterCategory" > <QTooltip> {{ t(category.name) }} @@ -234,6 +236,7 @@ function addOrder(value, field, params) { sort-by="name ASC" :disable="!params.categoryFk" @update:model-value="searchFn()" + data-cy="catalogFilterType" > <template #option="{ itemProps, opt }"> <QItem v-bind="itemProps"> @@ -285,6 +288,7 @@ function addOrder(value, field, params) { :is-clearable="false" v-model="searchByTag" @keyup.enter="(val) => onSearchByTag(val, params)" + data-cy="catalogFilterValueInput" > <template #prepend> <QIcon name="search" /> @@ -297,6 +301,7 @@ function addOrder(value, field, params) { color="primary" size="md" dense + data-cy="catalogFilterValueDialogBtn" /> <QPopupProxy> <CatalogFilterValueDialog diff --git a/src/pages/Order/Card/OrderCreateDialog.vue b/src/pages/Order/Card/OrderCreateDialog.vue index 3f6cc914b..c78b04d7f 100644 --- a/src/pages/Order/Card/OrderCreateDialog.vue +++ b/src/pages/Order/Card/OrderCreateDialog.vue @@ -1,6 +1,6 @@ <script setup> import { useRouter } from 'vue-router'; -import { onMounted, ref } from 'vue'; +import { reactive, onMounted, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import axios from 'axios'; import { useState } from 'composables/useState'; @@ -9,7 +9,6 @@ import VnRow from 'components/ui/VnRow.vue'; import VnSelect from 'components/common/VnSelect.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; import { useDialogPluginComponent } from 'quasar'; -import { reactive } from 'vue'; const { t } = useI18n(); const state = useState(); @@ -48,10 +47,6 @@ const fetchAgencyList = async (landed, addressFk) => { agencyList.value = data; }; -// const fetchOrderDetails = (order) => { -// fetchAddressList(order?.addressFk); -// fetchAgencyList(order?.landed, order?.addressFk); -// }; const $props = defineProps({ clientFk: { type: Number, @@ -63,39 +58,6 @@ const initialFormState = reactive({ addressId: null, clientFk: $props.clientFk, }); -// const orderMapper = (order) => { -// return { -// addressId: order.addressFk, -// agencyModeId: order.agencyModeFk, -// landed: new Date(order.landed).toISOString(), -// }; -// }; -// const orderFilter = { -// include: [ -// { relation: 'agencyMode', scope: { fields: ['name'] } }, -// { -// relation: 'address', -// scope: { fields: ['nickname'] }, -// }, -// { relation: 'rows', scope: { fields: ['id'] } }, -// { -// relation: 'client', -// scope: { -// fields: [ -// 'salesPersonFk', -// 'name', -// 'isActive', -// 'isFreezed', -// 'isTaxDataChecked', -// ], -// include: { -// relation: 'salesPersonUser', -// scope: { fields: ['id', 'name'] }, -// }, -// }, -// }, -// ], -// }; const onClientChange = async (clientId = $props.clientFk) => { const { data } = await axios.get(`Clients/${clientId}`); diff --git a/src/pages/Supplier/Card/SupplierFiscalData.vue b/src/pages/Supplier/Card/SupplierFiscalData.vue index 547842960..1a79be8bc 100644 --- a/src/pages/Supplier/Card/SupplierFiscalData.vue +++ b/src/pages/Supplier/Card/SupplierFiscalData.vue @@ -9,6 +9,7 @@ import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnLocation from 'src/components/common/VnLocation.vue'; +import VnAccountNumber from 'src/components/common/VnAccountNumber.vue'; const route = useRoute(); const { t } = useI18n(); @@ -100,10 +101,13 @@ function handleLocation(data, location) { /> </VnRow> <VnRow> - <VnInput + <VnAccountNumber v-model="data.account" :label="t('supplier.fiscalData.account')" clearable + data-cy="supplierFiscalDataAccount" + insertable + :maxlength="10" /> <VnSelect :label="t('supplier.fiscalData.sageTaxTypeFk')" diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue b/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue index 1fc54f486..0c53552fe 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue @@ -33,6 +33,7 @@ const canEditZone = useAcl().hasAny([ const agencyFetchRef = ref(); const warehousesOptions = ref([]); const companiesOptions = ref([]); +const currenciesOptions = ref([]); const agenciesOptions = ref([]); const zonesOptions = ref([]); const addresses = ref([]); diff --git a/src/pages/Ticket/Card/TicketDescriptor.vue b/src/pages/Ticket/Card/TicketDescriptor.vue index f8ffb43ed..4e77b633f 100644 --- a/src/pages/Ticket/Card/TicketDescriptor.vue +++ b/src/pages/Ticket/Card/TicketDescriptor.vue @@ -130,6 +130,7 @@ function ticketFilter(ticket) { <QBadge text-color="black" :color="entity.ticketState.state.classColor" + data-cy="ticketDescriptorStateBadge" > {{ entity.ticketState.state.name }} </QBadge> @@ -174,7 +175,7 @@ function ticketFilter(ticket) { <QTooltip>{{ t('Client Frozen') }}</QTooltip> </QIcon> <QIcon - v-if="entity.problem.includes('hasRisk')" + v-if="entity?.problem?.includes('hasRisk')" name="vn:risk" size="xs" color="primary" diff --git a/src/pages/Ticket/Card/TicketEditMana.vue b/src/pages/Ticket/Card/TicketEditMana.vue index 3d5b04a41..693875712 100644 --- a/src/pages/Ticket/Card/TicketEditMana.vue +++ b/src/pages/Ticket/Card/TicketEditMana.vue @@ -75,6 +75,7 @@ const cancel = () => { dense style="width: 50%" @click="save()" + data-cy="saveManaBtn" > {{ t('globals.save') }} </QBtn> diff --git a/src/pages/Ticket/Card/TicketNotes.vue b/src/pages/Ticket/Card/TicketNotes.vue index 6861cf000..f558b71cc 100644 --- a/src/pages/Ticket/Card/TicketNotes.vue +++ b/src/pages/Ticket/Card/TicketNotes.vue @@ -80,12 +80,14 @@ async function handleSave() { option-value="id" v-model="row.observationTypeFk" :disable="!!row.id" + data-cy="ticketNotesObservationType" /> <VnInput :label="t('basicData.description')" v-model="row.description" class="col" @keyup.enter="handleSave" + data-cy="ticketNotesDescription" /> <QIcon name="delete" @@ -93,6 +95,7 @@ async function handleSave() { class="cursor-pointer" color="primary" @click="handleDelete(row)" + data-cy="ticketNotesRemoveNoteBtn" > <QTooltip> {{ t('ticketNotes.removeNote') }} @@ -107,6 +110,7 @@ async function handleSave() { class="fill-icon-on-hover q-ml-md" color="primary" @click="ticketNotesCrudRef.insert()" + data-cy="ticketNotesAddNoteBtn" > <QTooltip> {{ t('ticketNotes.addNote') }} diff --git a/src/pages/Ticket/Card/TicketSale.vue b/src/pages/Ticket/Card/TicketSale.vue index 95ef3118e..b534170c9 100644 --- a/src/pages/Ticket/Card/TicketSale.vue +++ b/src/pages/Ticket/Card/TicketSale.vue @@ -555,6 +555,7 @@ watch( color="primary" :disable="!isTicketEditable || ticketState === 'OK'" @click="changeTicketState('OK')" + data-cy="ticketSaleOkStateBtn" > <QTooltip>{{ t(`Change ticket state to 'Ok'`) }}</QTooltip> </QBtn> @@ -563,6 +564,7 @@ watch( color="primary" :label="t('ticketList.state')" :disable="!isTicketEditable" + data-cy="ticketSaleStateDropdown" > <VnSelect :options="editableStatesOptions" @@ -572,6 +574,7 @@ watch( hide-dropdown-icon focus-on-mount @update:model-value="changeTicketState" + data-cy="ticketSaleStateSelect" /> </QBtnDropdown> <TicketSaleMoreActions @@ -604,6 +607,7 @@ watch( icon="vn:splitline" :disable="!isTicketEditable || !hasSelectedRows" @click="setTransferParams()" + data-cy="ticketSaleTransferBtn" > <QTooltip>{{ t('Transfer lines') }}</QTooltip> <TicketTransfer @@ -683,7 +687,13 @@ watch( {{ t('ticketSale.visible') }}: {{ row.visible || 0 }} </QTooltip> </QIcon> - <QIcon v-if="row.reserved" color="primary" name="vn:reserva" size="xs"> + <QIcon + v-if="row.reserved" + color="primary" + name="vn:reserva" + size="xs" + data-cy="ticketSaleReservedIcon" + > <QTooltip> {{ t('ticketSale.reserved') }} </QTooltip> @@ -832,7 +842,14 @@ watch( </VnTable> <QPageSticky :offset="[20, 20]" style="z-index: 2"> - <QBtn @click="newOrderFromTicket()" color="primary" fab icon="add" shortcut="+" /> + <QBtn + @click="newOrderFromTicket()" + color="primary" + fab + icon="add" + shortcut="+" + data-cy="ticketSaleAddToBasketBtn" + /> <QTooltip class="text-no-wrap"> {{ t('Add item to basket') }} </QTooltip> diff --git a/src/pages/Ticket/Card/TicketSaleMoreActions.vue b/src/pages/Ticket/Card/TicketSaleMoreActions.vue index 87e1d2a48..bd2099756 100644 --- a/src/pages/Ticket/Card/TicketSaleMoreActions.vue +++ b/src/pages/Ticket/Card/TicketSaleMoreActions.vue @@ -175,6 +175,7 @@ const createRefund = async (withWarehouse) => { color="primary" :label="t('ticketSale.more')" :disable="disable" + data-cy="ticketSaleMoreActionsDropdown" > <template #label> <QTooltip>{{ t('Select lines to see the options') }}</QTooltip> @@ -186,6 +187,7 @@ const createRefund = async (withWarehouse) => { v-close-popup v-ripple @click="showSmsDialog('productNotAvailable')" + data-cy="sendShortageSMSItem" > <QItemSection> <QItemLabel>{{ t('Send shortage SMS') }}</QItemLabel> @@ -197,12 +199,18 @@ const createRefund = async (withWarehouse) => { v-close-popup v-ripple @click="calculateSalePrice()" + data-cy="recalculatePriceItem" > <QItemSection> <QItemLabel>{{ t('Recalculate price') }}</QItemLabel> </QItemSection> </QItem> - <QItem clickable v-ripple @click="emit('getMana')"> + <QItem + clickable + v-ripple + @click="emit('getMana')" + data-cy="updateDiscountItem" + > <QItemSection> <QItemLabel>{{ t('Update discount') }}</QItemLabel> </QItemSection> @@ -211,6 +219,7 @@ const createRefund = async (withWarehouse) => { v-model.number="newDiscount" :label="t('ticketSale.discount')" type="number" + data-cy="ticketSaleDiscountInput" /> </TicketEditManaProxy> </QItem> @@ -220,6 +229,7 @@ const createRefund = async (withWarehouse) => { v-close-popup v-ripple @click="createClaim()" + data-cy="createClaimItem" > <QItemSection> <QItemLabel>{{ t('Add claim') }}</QItemLabel> @@ -231,6 +241,7 @@ const createRefund = async (withWarehouse) => { v-close-popup v-ripple @click="setReserved(true)" + data-cy="markAsReservedItem" > <QItemSection> <QItemLabel>{{ t('Mark as reserved') }}</QItemLabel> @@ -242,12 +253,13 @@ const createRefund = async (withWarehouse) => { v-close-popup v-ripple @click="setReserved(false)" + data-cy="unmarkAsReservedItem" > <QItemSection> <QItemLabel>{{ t('Unmark as reserved') }}</QItemLabel> </QItemSection> </QItem> - <QItem clickable v-ripple> + <QItem clickable v-ripple data-cy="ticketSaleRefundItem"> <QItemSection> <QItemLabel>{{ t('Refund') }}</QItemLabel> </QItemSection> @@ -256,12 +268,22 @@ const createRefund = async (withWarehouse) => { </QItemSection> <QMenu anchor="top end" self="top start" auto-close bordered> <QList> - <QItem v-ripple clickable @click="createRefund(true)"> + <QItem + v-ripple + clickable + @click="createRefund(true)" + data-cy="ticketSaleRefundWithWarehouse" + > <QItemSection> {{ t('with warehouse') }} </QItemSection> </QItem> - <QItem v-ripple clickable @click="createRefund(false)"> + <QItem + v-ripple + clickable + @click="createRefund(false)" + data-cy="ticketSaleRefundWithoutWarehouse" + > <QItemSection> {{ t('without warehouse') }} </QItemSection> diff --git a/src/pages/Ticket/Card/TicketSummary.vue b/src/pages/Ticket/Card/TicketSummary.vue index 5bda60cba..2f5f69e1c 100644 --- a/src/pages/Ticket/Card/TicketSummary.vue +++ b/src/pages/Ticket/Card/TicketSummary.vue @@ -96,6 +96,7 @@ function toTicketUrl(section) { ref="summaryRef" :url="`Tickets/${entityId}/summary`" data-key="TicketSummary" + data-cy="ticketSummary" > <template #header-left> <VnToSummary diff --git a/src/pages/Ticket/Card/TicketTransfer.vue b/src/pages/Ticket/Card/TicketTransfer.vue index d7aff3ef7..005d74a0e 100644 --- a/src/pages/Ticket/Card/TicketTransfer.vue +++ b/src/pages/Ticket/Card/TicketTransfer.vue @@ -91,7 +91,7 @@ onMounted(() => (_transfer.value = $props.transfer)); </script> <template> - <QPopupProxy ref="QPopupProxyRef"> + <QPopupProxy ref="QPopupProxyRef" data-cy="ticketTransferPopup"> <QCard class="q-px-md" style="display: flex; width: 80vw"> <QTable :rows="transfer.sales" diff --git a/src/pages/Ticket/Card/TicketTransferForm.vue b/src/pages/Ticket/Card/TicketTransferForm.vue index d07a9d2e7..ece3a3b97 100644 --- a/src/pages/Ticket/Card/TicketTransferForm.vue +++ b/src/pages/Ticket/Card/TicketTransferForm.vue @@ -57,6 +57,7 @@ defineExpose({ transferSales }); v-model.number="_transfer.ticketId" :label="t('Transfer to ticket')" :clearable="false" + data-cy="ticketTransferDestinationTicketInput" > <template #append> <QBtn @@ -64,6 +65,7 @@ defineExpose({ transferSales }); color="primary" @click="transferSales(_transfer.ticketId)" style="width: 30px" + data-cy="ticketTransferTransferBtn" /> </template> </VnInput> @@ -72,6 +74,7 @@ defineExpose({ transferSales }); color="primary" class="full-width q-my-lg" @click="transferSales()" + data-cy="ticketTransferNewTicketBtn" /> </QForm> </template> diff --git a/src/pages/Ticket/TicketAdvance.vue b/src/pages/Ticket/TicketAdvance.vue index 8de602b37..71e3926ac 100644 --- a/src/pages/Ticket/TicketAdvance.vue +++ b/src/pages/Ticket/TicketAdvance.vue @@ -215,7 +215,7 @@ const requestComponentUpdate = async (ticket, isWithoutNegatives) => { if (!newLanded) { notify(t('advanceTickets.noDeliveryZone'), 'negative'); - return; + throw new Error(t('advanceTickets.noDeliveryZone')); } ticket.landed = newLanded.landed; @@ -299,10 +299,10 @@ const splitTickets = async () => { const { query, params } = await requestComponentUpdate(ticket, true); await axios.post(query, params); progressAdd(ticket.futureId); - } catch (error) { + } catch (e) { splitErrors.value.push({ id: ticket.futureId, - reason: error.response?.data?.error?.message, + reason: e.message || e.response?.data?.error?.message, }); progressAdd(ticket.futureId); } diff --git a/src/pages/Ticket/TicketList.vue b/src/pages/Ticket/TicketList.vue index 6f6c556ca..2fe4fcddc 100644 --- a/src/pages/Ticket/TicketList.vue +++ b/src/pages/Ticket/TicketList.vue @@ -22,6 +22,7 @@ import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorP import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue'; import { toTimeFormat } from 'src/filters/date'; import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue'; +import TicketProblems from 'src/components/TicketProblems.vue'; const route = useRoute(); const router = useRouter(); @@ -455,6 +456,7 @@ function setReference(data) { data-key="TicketList" :label="t('Search ticket')" :info="t('You can search by ticket id or alias')" + data-cy="ticketListSearchBar" /> <RightMenu> <template #right-panel> @@ -482,68 +484,10 @@ function setReference(data) { 'row-key': 'id', selection: 'multiple', }" + data-cy="ticketListTable" > <template #column-statusIcons="{ row }"> - <div class="q-gutter-x-xs"> - <QIcon - v-if="row.isTaxDataChecked === 0" - color="primary" - name="vn:no036" - size="xs" - > - <QTooltip> - {{ t('No verified data') }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.hasTicketRequest" - color="primary" - name="vn:buyrequest" - size="xs" - > - <QTooltip> - {{ t('Purchase request') }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.itemShortage" - color="primary" - name="vn:unavailable" - size="xs" - > - <QTooltip> - {{ t('Not visible') }} - </QTooltip> - </QIcon> - <QIcon v-if="row.isFreezed" color="primary" name="vn:frozen" size="xs"> - <QTooltip> - {{ t('Client frozen') }} - </QTooltip> - </QIcon> - <QIcon v-if="row.risk" color="primary" name="vn:risk" size="xs"> - <QTooltip> {{ t('Risk') }}: {{ row.risk }} </QTooltip> - </QIcon> - <QIcon - v-if="row.hasComponentLack" - color="primary" - name="vn:components" - size="xs" - > - <QTooltip> - {{ t('Component lack') }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.hasRounding" - color="primary" - name="sync_problem" - size="xs" - > - <QTooltip> - {{ t('Rounding') }} - </QTooltip> - </QIcon> - </div> + <TicketProblems :row="row" /> </template> <template #column-salesPersonFk="{ row }"> <span class="link" @click.stop> diff --git a/src/pages/Travel/Card/TravelSummary.vue b/src/pages/Travel/Card/TravelSummary.vue index f4331ccb2..be1a12406 100644 --- a/src/pages/Travel/Card/TravelSummary.vue +++ b/src/pages/Travel/Card/TravelSummary.vue @@ -300,10 +300,6 @@ const getLink = (param) => `#/travel/${entityId.value}/${param}`; <VnLv :label="t('globals.reference')" :value="travel.ref" /> <VnLv label="m³" :value="travel.m3" /> <VnLv :label="t('globals.totalEntries')" :value="travel.totalEntries" /> - <VnLv - :label="t('travel.basicData.daysInForward')" - :value="travel?.daysInForward" - /> </QCard> <QCard class="full-width"> <VnTitle :text="t('travel.summary.entries')" /> diff --git a/src/pages/Worker/Card/WorkerPit.vue b/src/pages/Worker/Card/WorkerPit.vue new file mode 100644 index 000000000..c58196c7b --- /dev/null +++ b/src/pages/Worker/Card/WorkerPit.vue @@ -0,0 +1,263 @@ +<script setup> +import { ref } from 'vue'; +import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; +import VnInputDate from 'src/components/common/VnInputDate.vue'; +import axios from 'axios'; +import FetchData from 'components/FetchData.vue'; +import FormModel from 'src/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'; +import CrudModel from 'components/CrudModel.vue'; +import VnTitle from 'src/components/common/VnTitle.vue'; +import { useQuasar } from 'quasar'; +import VnConfirm from 'components/ui/VnConfirm.vue'; +import useNotify from 'src/composables/useNotify.js'; +const { notify } = useNotify(); +const route = useRoute(); +const { t } = useI18n(); +const disabilityGradesOptions = ref(); +const workerPitCrudRef = ref({}); +const insertTag = () => { + workerPitCrudRef.value.insert(); +}; +const quasar = useQuasar(); +const deleteRelative = async (id) => { + await new Promise((resolve) => { + quasar + .dialog({ + component: VnConfirm, + componentProps: { + title: t('Remove Relative'), + message: t('Do you want to remove this relative?'), + }, + }) + .onOk(() => { + resolve(true); + }) + .onCancel(() => { + resolve(false); + }); + }); + await axios.delete(`WorkerRelatives/${id}`); + workerPitCrudRef.value.reload(); + notify('Relative removed', 'positive'); +}; +</script> + +<template> + <FetchData + url="DisabilityGrades" + @on-fetch="(data) => (disabilityGradesOptions = data)" + auto-load + /> + + <FormModel + url="WorkerIrpfs" + :filter="{ where: { workerFk: route.params.id } }" + auto-load + data-key="workerIrpfs" + :max-width="false" + > + <template #form="{ data }"> + <QCard class="q-px-lg q-py-lg"> + <VnTitle :text="t('IRPF')" /> + <VnRow> + <VnInput + :label="t('familySituation')" + clearable + v-model="data.familySituation" + /> + <VnInput :label="t('spouseNif')" clearable v-model="data.spouseNif" /> + </VnRow> + <VnRow> + <VnSelect + :label="t('disabilityGrades')" + :options="disabilityGradesOptions" + option-label="description" + option-value="id" + v-model="data.disabilityGradeFk" + id="disabilityGrades" + data-cy="disabilityGrades" + hide-selected + /> + + <VnInputDate + :label="t('geographicMobilityDate')" + v-model="data.geographicMobilityDate" + /> + </VnRow> + <VnRow> + <VnInput + clearable + v-model="data.childPension" + :label="t(`childPension`)" + /> + <VnInput + clearable + v-model="data.spousePension" + :label="t(`spousePension`)" + /> + </VnRow> + <VnRow> + <QCheckbox v-model="data.isDependend" :label="t(`isDependend`)" /> + <QCheckbox + v-model="data.hasHousingPaymentBefore" + :label="t(`hasHousingPaymentBefore`)" + /> + </VnRow> + <VnRow> + <QCheckbox + v-model="data.hasHousingPaymentAfter" + :label="t(`hasHousingPaymentAfter`)" + /> + <QCheckbox + v-model="data.hasExtendedWorking" + :label="t(`hasExtendedWorking`)" + /> + </VnRow> + </QCard> + + <CrudModel + ref="workerPitCrudRef" + data-key="workerPit" + url="WorkerRelatives" + auto-load + :filter="{ + where: { workerFk: route.params.id }, + }" + :data-required="{ workerFk: route.params.id }" + :has-sub-toolbar="false" + > + <template #body="{ rows }"> + <QCard class="q-px-lg q-py-lg" flat> + <div class="row no-wrap justify-between q-pb-md"> + <VnTitle :text="t('Relatives')" /> + <QBtnGroup push style="column-gap: 10px"> + <QBtn + color="primary" + icon="restart_alt" + flat + @click="workerPitCrudRef.reset" + :disable="!workerPitCrudRef.hasChanges" + :title="t('globals.reset')" + /> + <QBtn + ref="saveButtonRef" + color="primary" + icon="save" + @click="workerPitCrudRef.onSubmit" + :disable="!workerPitCrudRef.hasChanges" + :title="t('globals.save')" + data-cy="workerPitRelativeSaveBtn" + /> + </QBtnGroup> + </div> + <div + v-for="(row, index) in rows" + :key="index" + class="row no-wrap q-mb-lg q-gutter-lg" + padding="none" + > + <VnSelect + :options="[ + { id: 0, name: 'Ascendiente' }, + { id: 1, name: 'Descendiente' }, + ]" + :label="t('isDescendant')" + v-model="row.isDescendant" + class="q-gutter-xs q-mb-xs" + /> + <VnSelect + :label="t('disabilityGrades')" + :options="disabilityGradesOptions" + option-label="description" + option-value="id" + v-model="row.disabilityGradeFk" + class="q-gutter-xs q-mb-xs" + /> + <VnInput + type="number" + v-model="row.birthed" + :label="t(`birthed`)" + /> + + <VnInput + type="number" + v-model="row.adoptionYear" + :label="t(`adoptionYear`)" + /> + <QCheckbox + v-model="row.isDependend" + :label="t(`isDependend`)" + /> + <QCheckbox + v-model="row.isJointCustody" + :label="t(`isJointCustody`)" + size="xs" + /> + <QBtn + @click="deleteRelative(rows[0].id)" + class="cursor-pointer" + color="primary" + flat + icon="delete" + style="flex: 0" + /> + </div> + <VnRow class="justify-left items-center"> + <QBtn + @click="insertTag(rows)" + class="cursor-pointer" + color="primary" + flat + icon="add" + shortcut="+" + style="flex: 0" + data-cy="addRelative" + /> + </VnRow> + </QCard> + </template> + </CrudModel> + </template> + </FormModel> +</template> + +<i18n> +es: + familySituation: Situación familiar + disabilityGrades: Discapacidad + geographicMobilityDate: Movilidad geografica + childPension: Pensión hijos + spousePension: Pensión cónyuge + isDependend: Ayuda / Movilidad reducida + spouseNif: NIF cónyuge + hasHousingPaymentBefore: Pagos vivienda anterior 2011 + hasHousingPaymentAfter: Pagos vivienda posterior 2011 + hasExtendedWorking: Prolongación actividad laboral + isDescendant: Descen/Ascen + disabilityGradeFk: Discapacidad + birthed: Año nacimiento + adoptionYear: Año adopción + isJointCustody: Computo por entero + Relatives: Relacionados +en: + familySituation: Family Situation + disabilityGrades: Disability Grades + geographicMobilityDate: Geographic Mobility Date + childPension: Child Pension + spousePension: Spouse Pension + isDependend: Dependent Suport / Reduced Mobility + spouseNif: Spouse NIF (Tax ID) + hasHousingPaymentBefore: Housing Payments Before 2011 + hasHousingPaymentAfter: Housing Payments After 2011 + hasExtendedWorking: Extended Work Activity + isDescendant: Descendant/Ascendant + disabilityGradeFk: Disability Grade + birthed: Birth Year + adoptionYear: Adoption Year + isJointCustody: Joint custody + Relatives: Relatives +</i18n> diff --git a/src/router/modules/Supplier.js b/src/router/modules/Supplier.js index 143d7c824..c08fb5961 100644 --- a/src/router/modules/Supplier.js +++ b/src/router/modules/Supplier.js @@ -113,7 +113,7 @@ export default { name: 'SupplierAccounts', meta: { title: 'accounts', - icon: 'vn:account', + icon: 'vn:credit', }, component: () => import('src/pages/Supplier/Card/SupplierAccounts.vue'), diff --git a/src/router/modules/worker.js b/src/router/modules/worker.js index d1feff23d..925019734 100644 --- a/src/router/modules/worker.js +++ b/src/router/modules/worker.js @@ -24,6 +24,7 @@ export default { 'WorkerDms', 'WorkerTimeControl', 'WorkerLocker', + 'WorkerPit', 'WorkerBalance', 'WorkerFormation', 'WorkerMedical', @@ -216,6 +217,15 @@ export default { }, component: () => import('src/pages/Worker/Card/WorkerMedical.vue'), }, + { + name: 'WorkerPit', + path: 'pit', + meta: { + title: 'pit', + icon: 'lock', + }, + component: () => import('src/pages/Worker/Card/WorkerPit.vue'), + }, { name: 'WorkerOperator', path: 'operator', diff --git a/src/stores/invoiceOutGlobal.js b/src/stores/invoiceOutGlobal.js index 35f834f3d..332494aa8 100644 --- a/src/stores/invoiceOutGlobal.js +++ b/src/stores/invoiceOutGlobal.js @@ -162,6 +162,15 @@ export const useInvoiceOutGlobalStore = defineStore({ ); throw new Error('Invalid Serial Type'); } + + if (clientsToInvoice === 'all' && params.serialType !== 'global') { + notify( + 'invoiceOut.globalInvoices.errors.invalidSerialTypeForAll', + 'negative' + ); + throw new Error('For "all" clients, the serialType must be "global"'); + } + if (!params.companyFk) { notify('invoiceOut.globalInvoices.errors.chooseValidCompany', 'negative'); throw new Error('Invalid company'); diff --git a/test/cypress/integration/Order/orderCatalog.spec.js b/test/cypress/integration/Order/orderCatalog.spec.js new file mode 100644 index 000000000..45eda6f1f --- /dev/null +++ b/test/cypress/integration/Order/orderCatalog.spec.js @@ -0,0 +1,112 @@ +/// <reference types="cypress" /> +describe('OrderCatalog', () => { + beforeEach(() => { + cy.login('developer'); + cy.viewport(1920, 720); + cy.visit('/#/order/8/catalog'); + }); + + const checkCustomFilterTag = (filterName = 'Plant') => { + cy.dataCy('catalogFilterCustomTag').should('exist'); + cy.dataCy('catalogFilterCustomTag').contains(filterName); + }; + + const checkFilterTag = (filterName = 'Plant') => { + cy.dataCy('vnFilterPanelChip').should('exist'); + cy.dataCy('vnFilterPanelChip').contains(filterName); + }; + + const selectCategory = (categoryIndex = 1, categoryName = 'Plant') => { + cy.get( + `div.q-page-container div:nth-of-type(${categoryIndex}) > [data-cy='catalogFilterCategory']` + ).should('exist'); + cy.get( + `div.q-page-container div:nth-of-type(${categoryIndex}) > [data-cy='catalogFilterCategory']` + ).click(); + checkCustomFilterTag(categoryName); + }; + + const searchByCustomTagInput = (option) => { + cy.dataCy('catalogFilterValueInput').find('input').last().focus(); + cy.dataCy('catalogFilterValueInput').find('input').last().type(option); + cy.dataCy('catalogFilterValueInput').find('input').last().type('{enter}'); + checkCustomFilterTag(option); + }; + + const selectTypeFilter = (option) => { + cy.selectOption( + 'div.q-page-container div.list > div:nth-of-type(2) div:nth-of-type(3)', + option + ); + checkFilterTag(option); + }; + + it('Shows empty state', () => { + cy.dataCy('orderCatalogPage').should('exist'); + cy.dataCy('orderCatalogPage').contains('No data to display'); + }); + + it('filter by category', () => { + selectCategory(); + cy.dataCy('orderCatalogItem').should('exist'); + }); + + it('filters by type', () => { + selectCategory(); + selectTypeFilter('Anthurium'); + }); + + it('filters by custom value select', () => { + selectCategory(); + searchByCustomTagInput('Silver'); + }); + + it('filters by custom value dialog', () => { + Cypress.on('uncaught:exception', (err) => { + if (err.message.includes('canceled')) { + return false; + } + }); + selectCategory(); + cy.dataCy('catalogFilterValueDialogBtn').should('exist'); + cy.dataCy('catalogFilterValueDialogBtn').last().click(); + cy.dataCy('catalogFilterValueDialogTagSelect').should('exist'); + cy.selectOption("[data-cy='catalogFilterValueDialogTagSelect']", 'Tallos'); + cy.dataCy('catalogFilterValueDialogValueInput').find('input').focus(); + cy.dataCy('catalogFilterValueDialogValueInput').find('input').type('2'); + cy.dataCy('catalogFilterValueDialogValueInput').find('input').type('{enter}'); + checkCustomFilterTag('2'); + }); + + it('removes a secondary tag', () => { + selectCategory(); + selectTypeFilter('Anthurium'); + cy.dataCy('vnFilterPanelChip').should('exist'); + cy.get( + "div.q-page-container [data-cy='vnFilterPanelChip'] > i.q-chip__icon--remove" + ) + .contains('cancel') + .should('exist'); + cy.get( + "div.q-page-container [data-cy='vnFilterPanelChip'] > i.q-chip__icon--remove" + ) + .contains('cancel') + .click(); + cy.dataCy('vnFilterPanelChip').should('not.exist'); + }); + + it('Removes category tag', () => { + selectCategory(); + cy.get( + "div.q-page-container [data-cy='catalogFilterCustomTag'] > i.q-chip__icon--remove" + ) + .contains('cancel') + .should('exist'); + cy.get( + "div.q-page-container [data-cy='catalogFilterCustomTag'] > i.q-chip__icon--remove" + ) + .contains('cancel') + .click(); + cy.dataCy('catalogFilterCustomTag').should('not.exist'); + }); +}); diff --git a/test/cypress/integration/item/01_summary.spec.js b/test/cypress/integration/item/01_summary.spec.js new file mode 100644 index 000000000..c44f4d047 --- /dev/null +++ b/test/cypress/integration/item/01_summary.spec.js @@ -0,0 +1,25 @@ +describe.skip('Item summary path', () => { + beforeEach(() => { + const itemId = 1; + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/item/${itemId}`); + }); + it('should search for an item', async () => {}); + it(`should check the item summary preview shows fields from basic data`, async () => {}); + it(`should check the item summary preview shows fields from tags`, async () => {}); + it(`should check the item summary preview shows fields from botanical`, async () => {}); + it(`should check the item summary preview shows fields from barcode`, async () => {}); + it(`should close the summary popup`, async () => {}); + it('should search for other item', async () => {}); + it(`should now check the item summary preview shows fields from basic data`, async () => {}); + it(`should now check the item summary preview shows fields from tags`, async () => {}); + it(`should now check the item summary preview shows fields from botanical`, async () => {}); + it(`should now close the summary popup`, async () => {}); + it(`should navigate to one of the items detailed section`, async () => {}); + it(`should check the descritor edit button is not visible for employee`, async () => {}); + it(`should check the item summary shows fields from basic data section`, async () => {}); + it(`should check the item summary shows fields from tags section`, async () => {}); + it(`should check the item summary shows fields from botanical section`, async () => {}); + it(`should check the item summary shows fields from barcodes section`, async () => {}); +}); diff --git a/test/cypress/integration/item/02_basic_data.spec.js b/test/cypress/integration/item/02_basic_data.spec.js new file mode 100644 index 000000000..ada9ef57c --- /dev/null +++ b/test/cypress/integration/item/02_basic_data.spec.js @@ -0,0 +1,10 @@ +describe.skip('Item Edit basic data path', () => { + beforeEach(() => { + const itemId = 1; + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/item/${itemId}`); + }); + it(`should edit the item basic data and confirm the item data was edited`, async () => {}); + it(`should create a new intrastat and save it`, async () => {}); +}); diff --git a/test/cypress/integration/item/03_tax.spec.js b/test/cypress/integration/item/03_tax.spec.js new file mode 100644 index 000000000..593dbfb36 --- /dev/null +++ b/test/cypress/integration/item/03_tax.spec.js @@ -0,0 +1,12 @@ +describe.skip('Item edit tax path', () => { + beforeEach(() => { + const itemId = 1; + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/item/${itemId}`); + }); + it(`should add the item tax to all countries`, async () => {}); + it(`should confirm the first item tax class was edited`, async () => {}); + it(`should confirm the second item tax class was edited`, async () => {}); + it(`should edit the first class without saving the form`, async () => {}); +}); diff --git a/test/cypress/integration/item/04_tags.spec.js b/test/cypress/integration/item/04_tags.spec.js new file mode 100644 index 000000000..720d19a89 --- /dev/null +++ b/test/cypress/integration/item/04_tags.spec.js @@ -0,0 +1,12 @@ +describe.skip('Item create tags path', () => { + beforeEach(() => { + const itemId = 1; + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/item/${itemId}`); + }); + it('should create a new tag and delete a former one', async () => {}); + it('should confirm the fourth row data is the expected one', async () => {}); + it('should confirm the fifth row data is the expected one', async () => {}); + it('should confirm the sixth row data is the expected one', async () => {}); +}); diff --git a/test/cypress/integration/item/05_botanical.spec.js b/test/cypress/integration/item/05_botanical.spec.js new file mode 100644 index 000000000..72e7f3fee --- /dev/null +++ b/test/cypress/integration/item/05_botanical.spec.js @@ -0,0 +1,14 @@ +describe.skip('Item Create botanical path', () => { + beforeEach(() => { + const itemId = 1; + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/item/${itemId}`); + }); + it(`should create a new botanical for the item`, async () => {}); + it(`should confirm the Genus for the item was created`, async () => {}); + it(`should confirm the Species for the item was created`, async () => {}); + it(`should edit botanical for the item`, async () => {}); + it(`should confirm the Genus for the item was edited`, async () => {}); + it(`should confirm the Species for the item was edited`, async () => {}); +}); diff --git a/test/cypress/integration/item/06_barcode.spec.js b/test/cypress/integration/item/06_barcode.spec.js new file mode 100644 index 000000000..cea3c13f2 --- /dev/null +++ b/test/cypress/integration/item/06_barcode.spec.js @@ -0,0 +1,10 @@ +describe.skip('Item Create barcodes path', () => { + beforeEach(() => { + const itemId = 1; + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/item/${itemId}`); + }); + it(`should click create a new code and delete a former one`, async () => {}); + it(`should confirm the barcode 5 is created and it is now the third barcode as the first was deleted`, async () => {}); +}); diff --git a/test/cypress/integration/item/07_create.spec.js b/test/cypress/integration/item/07_create.spec.js new file mode 100644 index 000000000..7555d6927 --- /dev/null +++ b/test/cypress/integration/item/07_create.spec.js @@ -0,0 +1,13 @@ +describe.skip('Item Create', () => { + beforeEach(() => { + const itemId = 1; + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/item/${itemId}`); + }); + it('should access to the create item view by clicking the create floating button', async () => {}); + it('should return to the item index by clickig the cancel button', async () => {}); + it('should now access to the create item view by clicking the create floating button', async () => {}); + it('should throw an error when insert an invalid priority', async () => {}); + it('should create the Infinity Gauntlet item', async () => {}); +}); diff --git a/test/cypress/integration/item/08_regularize.spec.js b/test/cypress/integration/item/08_regularize.spec.js new file mode 100644 index 000000000..74d12846f --- /dev/null +++ b/test/cypress/integration/item/08_regularize.spec.js @@ -0,0 +1,24 @@ +describe.skip('Item regularize path', () => { + beforeEach(() => { + const itemId = 1; + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/item/${itemId}`); + }); + it('should edit the user local warehouse', async () => {}); + it('should check the local settings were saved', async () => {}); + it('should search for a specific item', async () => {}); + it('should open the regularize dialog and check the warehouse matches the local user settings', async () => {}); + it('should regularize the item', async () => {}); + it('should click on the Tickets button of the top bar menu', async () => {}); + it('should clear the user local settings now', async () => {}); + it('should search for the ticket with alias missing', async () => {}); + it(`should check the ticket sale quantity is showing a negative value`, async () => {}); + it(`should check the ticket sale discount is 100%`, async () => {}); + it('should now click on the Items button of the top bar menu', async () => {}); + it('should search for the item once again', async () => {}); + it('should regularize the item once more', async () => {}); + it('should again click on the Tickets button of the top bar menu', async () => {}); + it('should search for the ticket missing once again', async () => {}); + it(`should check the ticket contains now two sales`, async () => {}); +}); diff --git a/test/cypress/integration/item/09_index.spec.js b/test/cypress/integration/item/09_index.spec.js new file mode 100644 index 000000000..5648a8b5f --- /dev/null +++ b/test/cypress/integration/item/09_index.spec.js @@ -0,0 +1,14 @@ +describe.skip('Item index path', () => { + beforeEach(() => { + const itemId = 1; + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/item/${itemId}`); + }); + it('should click on the fields to show button to open the list of columns to show', async () => {}); + it('should unmark all checkboxes except the first and the last ones', async () => {}); + it('should navigate forth and back to see the images column is still visible', async () => {}); + it('should check the ids column is not visible', async () => {}); + it('should mark all unchecked boxes to leave the index as it was', async () => {}); + it('should now navigate forth and back to see the ids column is now visible', async () => {}); +}); diff --git a/test/cypress/integration/item/10_item_log.spec.js b/test/cypress/integration/item/10_item_log.spec.js new file mode 100644 index 000000000..3cc64dbbb --- /dev/null +++ b/test/cypress/integration/item/10_item_log.spec.js @@ -0,0 +1,12 @@ +describe.skip('Item log path', () => { + beforeEach(() => { + const itemId = 1; + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/item/${itemId}`); + }); + it(`should search for the Knowledge artifact to confirm it isn't created yet`, async () => {}); + it('should access to the create item view by clicking the create floating button', async () => {}); + it('should create the Knowledge artifact item', async () => {}); + it('should return to the items index by clicking the return to items button', async () => {}); +}); diff --git a/test/cypress/integration/item/11_descriptor.spec.js b/test/cypress/integration/item/11_descriptor.spec.js new file mode 100644 index 000000000..6ea89c365 --- /dev/null +++ b/test/cypress/integration/item/11_descriptor.spec.js @@ -0,0 +1,11 @@ +describe.skip('Item descriptor path', () => { + beforeEach(() => { + const itemId = 1; + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/item/${itemId}`); + }); + it('should set the item to inactive', async () => {}); + it('should reload the section and check the inactive icon is visible', async () => {}); + it('should set the item back to active', async () => {}); +}); diff --git a/test/cypress/integration/item/12_request.spec.js b/test/cypress/integration/item/12_request.spec.js new file mode 100644 index 000000000..f4e26f421 --- /dev/null +++ b/test/cypress/integration/item/12_request.spec.js @@ -0,0 +1,12 @@ +describe.skip('Item request path', () => { + beforeEach(() => { + const itemId = 1; + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/item/${itemId}`); + }); + it('should reach the item request section', async () => {}); + it('should fill the id and quantity then check the concept was updated', async () => {}); + it('should check the status of the request should now be accepted', async () => {}); + it('should now click on the second declain request icon then type the reason', async () => {}); +}); diff --git a/test/cypress/integration/item/13_fixedPrice.spec.js b/test/cypress/integration/item/13_fixedPrice.spec.js new file mode 100644 index 000000000..44fdfde46 --- /dev/null +++ b/test/cypress/integration/item/13_fixedPrice.spec.js @@ -0,0 +1,12 @@ +describe.skip('Item fixed prices path', () => { + beforeEach(() => { + const itemId = 1; + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/item/${itemId}`); + }); + it('should filter using all the fields', async () => {}); + it('should click on the add new fixed price button', async () => {}); + it('should fill the fixed price data', async () => {}); + it('should reload the section and check the created price has the expected ID', async () => {}); +}); diff --git a/test/cypress/integration/ticket/ticketList.spec.js b/test/cypress/integration/ticket/ticketList.spec.js new file mode 100644 index 000000000..bbdbcea92 --- /dev/null +++ b/test/cypress/integration/ticket/ticketList.spec.js @@ -0,0 +1,54 @@ +/// <reference types="cypress" /> +describe('TicketList', () => { + const firstRow = 'tbody > :nth-child(1)'; + + beforeEach(() => { + cy.login('developer'); + cy.viewport(1920, 1080); + cy.visit('/#/ticket/list'); + }); + + const searchResults = (search) => { + cy.dataCy('vnSearchBar').find('input').focus(); + if (search) cy.dataCy('vnSearchBar').find('input').type(search); + cy.dataCy('vnSearchBar').find('input').type('{enter}'); + cy.dataCy('ticketListTable').should('exist'); + cy.get(firstRow).should('exist'); + }; + + it('should search results', () => { + cy.dataCy('ticketListTable').should('not.exist'); + cy.get('.q-field__control').should('exist'); + searchResults(); + }); + + it('should open ticket sales', () => { + searchResults(); + cy.window().then((win) => { + cy.stub(win, 'open').as('windowOpen'); + }); + cy.get(firstRow).find('.q-btn:first').click(); + cy.get('@windowOpen').should('be.calledWithMatch', /\/ticket\/\d+\/sale/); + }); + + it('should open ticket summary', () => { + searchResults(); + cy.get(firstRow).find('.q-btn:last').click(); + cy.dataCy('ticketSummary').should('exist'); + }); + + it('Client list create new client', () => { + cy.dataCy('vnTableCreateBtn').should('exist'); + cy.dataCy('vnTableCreateBtn').click(); + const data = { + Customer: { val: 1, type: 'select' }, + Warehouse: { val: 'Warehouse One', type: 'select' }, + Address: { val: 'employee', type: 'select' }, + Landed: { val: '01-01-2024', type: 'date' }, + }; + cy.fillInForm(data); + cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.checkNotification('Data created'); + cy.url().should('match', /\/ticket\/\d+\/summary/); + }); +}); diff --git a/test/cypress/integration/ticket/ticketNotes.spec.js b/test/cypress/integration/ticket/ticketNotes.spec.js new file mode 100644 index 000000000..ef196c783 --- /dev/null +++ b/test/cypress/integration/ticket/ticketNotes.spec.js @@ -0,0 +1,25 @@ +/// <reference types="cypress" /> +describe('TicketRequest', () => { + beforeEach(() => { + cy.login('developer'); + cy.viewport(1920, 1080); + cy.visit('/#/ticket/31/observation'); + }); + + it('Creates and deletes a note', () => { + cy.dataCy('ticketNotesAddNoteBtn').should('exist'); + cy.dataCy('ticketNotesAddNoteBtn').click(); + cy.dataCy('ticketNotesObservationType').should('exist'); + cy.selectOption('[data-cy="ticketNotesObservationType"]:last', 'Weight'); + cy.dataCy('ticketNotesDescription').should('exist'); + cy.get('[data-cy="ticketNotesDescription"]:last').type( + 'This is a note description' + ); + cy.dataCy('crudModelDefaultSaveBtn').click(); + cy.checkNotification('Data saved'); + cy.dataCy('ticketNotesRemoveNoteBtn').should('exist'); + cy.dataCy('ticketNotesRemoveNoteBtn').click(); + cy.dataCy('VnConfirm_confirm').click(); + cy.checkNotification('Data saved'); + }); +}); diff --git a/test/cypress/integration/ticket/ticketRequest.spec.js b/test/cypress/integration/ticket/ticketRequest.spec.js new file mode 100644 index 000000000..b9dc509ef --- /dev/null +++ b/test/cypress/integration/ticket/ticketRequest.spec.js @@ -0,0 +1,22 @@ +/// <reference types="cypress" /> +describe('TicketRequest', () => { + beforeEach(() => { + cy.login('developer'); + cy.viewport(1920, 1080); + cy.visit('/#/ticket/31/request'); + }); + + it('Creates a new request', () => { + cy.dataCy('vnTableCreateBtn').should('exist'); + cy.dataCy('vnTableCreateBtn').click(); + const data = { + Description: { val: 'Purchase description' }, + Atender: { val: 'buyerNick', type: 'select' }, + Quantity: { val: 2 }, + Price: { val: 123 }, + }; + cy.fillInForm(data); + cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.checkNotification('Data created'); + }); +}); diff --git a/test/cypress/integration/ticket/ticketSale.spec.js b/test/cypress/integration/ticket/ticketSale.spec.js new file mode 100644 index 000000000..60f31dbf6 --- /dev/null +++ b/test/cypress/integration/ticket/ticketSale.spec.js @@ -0,0 +1,131 @@ +/// <reference types="cypress" /> + +const c = require('croppie'); + +describe('TicketSale', () => { + beforeEach(() => { + cy.login('developer'); + cy.viewport(1920, 1080); + cy.visit('/#/ticket/31/sale'); + }); + + const firstRow = 'tbody > :nth-child(1)'; + + const selectFirstRow = () => { + cy.waitForElement(firstRow); + cy.get(firstRow).find('.q-checkbox__inner').click(); + }; + + it('it should add item to basket', () => { + cy.window().then((win) => { + cy.stub(win, 'open').as('windowOpen'); + }); + cy.dataCy('ticketSaleAddToBasketBtn').should('exist'); + cy.dataCy('ticketSaleAddToBasketBtn').click(); + cy.get('@windowOpen').should('be.calledWithMatch', /\/order\/\d+\/catalog/); + }); + + it('should send SMS', () => { + selectFirstRow(); + cy.dataCy('ticketSaleMoreActionsDropdown').click(); + cy.waitForElement('[data-cy="sendShortageSMSItem"]'); + cy.dataCy('sendShortageSMSItem').should('exist'); + cy.dataCy('sendShortageSMSItem').click(); + cy.dataCy('vnSmsDialog').should('exist'); + cy.dataCy('sendSmsBtn').click(); + cy.checkNotification('SMS sent'); + }); + + it('should recalculate price when "Recalculate price" is clicked', () => { + cy.intercept('POST', '**/recalculatePrice').as('recalculatePrice'); + selectFirstRow(); + cy.dataCy('ticketSaleMoreActionsDropdown').click(); + cy.waitForElement('[data-cy="recalculatePriceItem"]'); + cy.dataCy('recalculatePriceItem').should('exist'); + cy.dataCy('recalculatePriceItem').click(); + cy.wait('@recalculatePrice').its('response.statusCode').should('eq', 200); + cy.checkNotification('Data saved'); + }); + + it('should update discount when "Update discount" is clicked', () => { + selectFirstRow(); + cy.dataCy('ticketSaleMoreActionsDropdown').click(); + cy.waitForElement('[data-cy="updateDiscountItem"]'); + cy.dataCy('updateDiscountItem').should('exist'); + cy.dataCy('updateDiscountItem').click(); + cy.waitForElement('[data-cy="ticketSaleDiscountInput"]'); + cy.dataCy('ticketSaleDiscountInput').find('input').focus(); + cy.dataCy('ticketSaleDiscountInput').find('input').type('10'); + cy.dataCy('saveManaBtn').click(); + cy.waitForElement('.q-notification__message'); + cy.checkNotification('Data saved'); + }); + + it('adds claim', () => { + selectFirstRow(); + cy.dataCy('ticketSaleMoreActionsDropdown').click(); + cy.dataCy('createClaimItem').click(); + cy.dataCy('VnConfirm_confirm').click(); + cy.url().should('match', /\/claim\/\d+\/basic-data/); + // Delete created claim to avoid cluttering the database + cy.dataCy('descriptor-more-opts').click(); + cy.dataCy('deleteClaim').click(); + cy.dataCy('VnConfirm_confirm').click(); + cy.checkNotification('Data deleted'); + }); + + it('marks row as reserved', () => { + selectFirstRow(); + cy.dataCy('ticketSaleMoreActionsDropdown').click(); + cy.waitForElement('[data-cy="markAsReservedItem"]'); + cy.dataCy('markAsReservedItem').click(); + cy.dataCy('ticketSaleReservedIcon').should('exist'); + }); + + it('unmarks row as reserved', () => { + selectFirstRow(); + cy.dataCy('ticketSaleMoreActionsDropdown').click(); + cy.waitForElement('[data-cy="unmarkAsReservedItem"]'); + cy.dataCy('unmarkAsReservedItem').click(); + cy.dataCy('ticketSaleReservedIcon').should('not.exist'); + }); + + it('refunds row with warehouse', () => { + selectFirstRow(); + cy.dataCy('ticketSaleMoreActionsDropdown').click(); + cy.dataCy('ticketSaleRefundItem').click(); + cy.dataCy('ticketSaleRefundWithWarehouse').click(); + cy.checkNotification('The following refund ticket have been created'); + }); + + it('refunds row without warehouse', () => { + selectFirstRow(); + cy.dataCy('ticketSaleMoreActionsDropdown').click(); + cy.dataCy('ticketSaleRefundItem').click(); + cy.dataCy('ticketSaleRefundWithoutWarehouse').click(); + cy.checkNotification('The following refund ticket have been created'); + }); + + it('transfers ticket', () => { + cy.visit('/#/ticket/32/sale'); + selectFirstRow(); + cy.dataCy('ticketSaleTransferBtn').click(); + cy.dataCy('ticketTransferPopup').should('exist'); + cy.dataCy('ticketTransferNewTicketBtn').click(); + // existen 3 elementos "tbody" necesito checkear que el segundo elemento tbody tenga una row sola + cy.get('tbody').eq(1).find('tr').should('have.length', 1); + selectFirstRow(); + cy.dataCy('ticketSaleTransferBtn').click(); + cy.dataCy('ticketTransferPopup').should('exist'); + cy.dataCy('ticketTransferDestinationTicketInput').find('input').focus(); + cy.dataCy('ticketTransferDestinationTicketInput').find('input').type('32'); + cy.dataCy('ticketTransferTransferBtn').click(); + // checkear que la url contenga /ticket/1000002/sale + cy.url().should('match', /\/ticket\/32\/sale/); + }); + + it('should redirect to ticket logs', () => { + cy.get(firstRow).find('.q-btn:last').click(); + cy.url().should('match', /\/ticket\/31\/log/); + }); +}); diff --git a/test/cypress/integration/vnComponent/VnAccountNumber.spec.js b/test/cypress/integration/vnComponent/VnAccountNumber.spec.js new file mode 100644 index 000000000..000c2151d --- /dev/null +++ b/test/cypress/integration/vnComponent/VnAccountNumber.spec.js @@ -0,0 +1,39 @@ +describe('VnInput Component', () => { + beforeEach(() => { + cy.login('developer'); + cy.viewport(1920, 1080); + cy.visit('/#/supplier/1/fiscal-data'); + cy.domContentLoad(); + }); + + it('should replace character at cursor position in insert mode', () => { + // Simula escribir en el input + cy.dataCy('supplierFiscalDataAccount').clear(); + cy.dataCy('supplierFiscalDataAccount').type('4100000001'); + // Coloca el cursor en la posición 0 + cy.dataCy('supplierFiscalDataAccount').type('{movetostart}'); + // Escribe un número y verifica que se reemplace correctamente + cy.dataCy('supplierFiscalDataAccount').type('999'); + cy.dataCy('supplierFiscalDataAccount') + .should('have.value', '9990000001'); + }); + + it('should replace character at cursor position in insert mode', () => { + // Simula escribir en el input + cy.dataCy('supplierFiscalDataAccount').clear(); + cy.dataCy('supplierFiscalDataAccount').type('4100000001'); + // Coloca el cursor en la posición 0 + cy.dataCy('supplierFiscalDataAccount').type('{movetostart}'); + // Escribe un número y verifica que se reemplace correctamente en la posicion incial + cy.dataCy('supplierFiscalDataAccount').type('999'); + cy.dataCy('supplierFiscalDataAccount') + .should('have.value', '9990000001'); + }); + + it('should respect maxlength prop', () => { + cy.dataCy('supplierFiscalDataAccount').clear(); + cy.dataCy('supplierFiscalDataAccount').type('123456789012345'); + cy.dataCy('supplierFiscalDataAccount') + .should('have.value', '1234567890'); // asumiendo que maxlength es 10 + }); +}); diff --git a/test/cypress/integration/vnComponent/vnBreadcrumbs.spec.js b/test/cypress/integration/vnComponent/VnBreadcrumbs.spec.js similarity index 100% rename from test/cypress/integration/vnComponent/vnBreadcrumbs.spec.js rename to test/cypress/integration/vnComponent/VnBreadcrumbs.spec.js diff --git a/test/cypress/integration/vnComponent/vnLocation.spec.js b/test/cypress/integration/vnComponent/VnLocation.spec.js similarity index 71% rename from test/cypress/integration/vnComponent/vnLocation.spec.js rename to test/cypress/integration/vnComponent/VnLocation.spec.js index 06c23f4ee..b98d42fdd 100644 --- a/test/cypress/integration/vnComponent/vnLocation.spec.js +++ b/test/cypress/integration/vnComponent/VnLocation.spec.js @@ -1,3 +1,5 @@ +const { randomNumber, randomString } = require('../../support'); + describe('VnLocation', () => { const locationOptions = '[role="listbox"] > div.q-virtual-scroll__content > .q-item'; const dialogInputs = '.q-dialog label input'; @@ -99,7 +101,7 @@ describe('VnLocation', () => { }); it('Create postCode', () => { - const postCode = '1234475'; + const postCode = Math.floor(100000 + Math.random() * 900000); const province = 'Valencia'; cy.get(createLocationButton).click(); cy.get('.q-card > h1').should('have.text', 'New postcode'); @@ -115,9 +117,10 @@ describe('VnLocation', () => { checkVnLocation(postCode, province); }); - it('Create city', () => { - const postCode = '9011'; - const province = 'Saskatchew'; + + it('Create city without country', () => { + const postCode = randomNumber(); + const province = randomString({ length: 4 }); cy.get(createLocationButton).click(); cy.get(dialogInputs).eq(0).type(postCode); cy.get( @@ -131,6 +134,58 @@ describe('VnLocation', () => { checkVnLocation(postCode, province); }); + it('Create province without country', () => { + const provinceName = 'Saskatchew'.concat(Math.random(1 * 100)); + cy.get(createLocationButton).click(); + cy.get( + `${createForm.prefix} > :nth-child(5) > .q-select > ${createForm.sufix} > :nth-child(2) ` + ) + .eq(0) + .click(); + cy.selectOption('#q-portal--dialog--3 .q-select', 'one'); + cy.countSelectOptions('#q-portal--dialog--3 .q-select', 4); + cy.get('#q-portal--dialog--3 .q-input').type(provinceName); + + cy.get('#q-portal--dialog--3 .q-btn--standard').click(); + }); + + it('Create city with country', () => { + const cityName = 'Saskatchew'.concat(Math.random(1 * 100)); + cy.get(createLocationButton).click(); + cy.selectOption( + `${createForm.prefix} > :nth-child(5) > :nth-child(3) `, + 'Italia' + ); + cy.get( + `${createForm.prefix} > :nth-child(4) > .q-select > ${createForm.sufix} > :nth-child(2) > .q-icon` + ).click(); + cy.selectOption('#q-portal--dialog--4 .q-select', 'Province four'); + cy.countSelectOptions('#q-portal--dialog--4 .q-select', 1); + + cy.get('#q-portal--dialog--4 .q-input').type(cityName); + cy.get('#q-portal--dialog--4 .q-btn--standard').click(); + }); + + it('Create province with country', () => { + const provinceName = 'Saskatchew'.concat(Math.random(1 * 100)); + cy.get(createLocationButton).click(); + cy.selectOption( + `${createForm.prefix} > :nth-child(5) > :nth-child(3) `, + 'España' + ); + cy.get( + `${createForm.prefix} > :nth-child(5) > .q-select > ${createForm.sufix} > :nth-child(2) ` + ) + .eq(0) + .click(); + + cy.selectOption('#q-portal--dialog--4 .q-select', 'one'); + cy.countSelectOptions('#q-portal--dialog--4 .q-select', 2); + + cy.get('#q-portal--dialog--4 .q-input').type(provinceName); + cy.get('#q-portal--dialog--4 .q-btn--standard').click(); + }); + function checkVnLocation(postCode, province) { cy.get(`${createForm.prefix}`).should('not.exist'); cy.get('.q-form > .q-card > .vn-row:nth-child(6)') diff --git a/test/cypress/integration/vnComponent/vnLog.spec.js b/test/cypress/integration/vnComponent/VnLog.spec.js similarity index 100% rename from test/cypress/integration/vnComponent/vnLog.spec.js rename to test/cypress/integration/vnComponent/VnLog.spec.js diff --git a/test/cypress/integration/vnComponent/vnSearchBar.spec.js b/test/cypress/integration/vnComponent/VnSearchBar.spec.js similarity index 100% rename from test/cypress/integration/vnComponent/vnSearchBar.spec.js rename to test/cypress/integration/vnComponent/VnSearchBar.spec.js diff --git a/test/cypress/integration/worker/workerPit.spec.js b/test/cypress/integration/worker/workerPit.spec.js new file mode 100644 index 000000000..cc3a87637 --- /dev/null +++ b/test/cypress/integration/worker/workerPit.spec.js @@ -0,0 +1,40 @@ +describe('WorkerPit', () => { + const familySituationInput = '[data-cy="Family Situation_input"]'; + const familySituation = '1'; + const childPensionInput = '[data-cy="Child Pension_input"]'; + const childPension = '120'; + const spouseNifInput = '[data-cy="Spouse Pension_input"]'; + const spouseNif = '65117125P'; + const spousePensionInput = '[data-cy="Spouse Pension_input"]'; + const spousePension = '120'; + const addRelative = '[data-cy="addRelative"]'; + const isDescendantSelect = '[data-cy="Descendant/Ascendant_select"]'; + const birthedInput = '[data-cy="Birth Year_input"]'; + const birthed = '2002'; + const adoptionYearInput = '[data-cy="Adoption Year_input"]'; + const adoptionYear = '2004'; + const saveRelative = '[data-cy="workerPitRelativeSaveBtn"]'; + const savePIT = '#st-actions > .q-btn-group > .q-btn--standard'; + + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('developer'); + cy.visit(`/#/worker/1107/pit`); + }); + + it('complete PIT', () => { + cy.get(familySituationInput).type(familySituation); + cy.get(childPensionInput).type(childPension); + cy.get(spouseNifInput).type(spouseNif); + cy.get(spousePensionInput).type(spousePension); + cy.get(savePIT).click(); + }); + + it('complete relative', () => { + cy.get(addRelative).click(); + cy.get(isDescendantSelect).type('{downArrow}{downArrow}{enter}'); + cy.get(birthedInput).type(birthed); + cy.get(adoptionYearInput).type(adoptionYear); + cy.get(saveRelative).click(); + }); +}); diff --git a/test/cypress/integration/zone/01_basic-data.spec.js b/test/cypress/integration/zone/01_basic-data.spec.js new file mode 100644 index 000000000..111f2495c --- /dev/null +++ b/test/cypress/integration/zone/01_basic-data.spec.js @@ -0,0 +1,20 @@ +describe.skip('Zone basic data path', () => { + beforeEach(() => { + const zoneId = 1; + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/zone/${zoneId}`); + }); + it('should reach the basic data section', async () => {}); + it('should edit de form and then save', async () => {}); + it('should now reload the section', async () => {}); + it('should confirm the name was updated', async () => {}); + it('should confirm the agency was updated', async () => {}); + it('should confirm the max volume was updated', async () => {}); + it('should confirm the traveling days were updated', async () => {}); + it('should confirm the closing hour was updated', async () => {}); + it('should confirm the price was updated', async () => {}); + it('should confirm the bonus was updated', async () => {}); + it('should confirm the inflation was updated', async () => {}); + it('should confirm the volumetric checkbox was checked', async () => {}); +}); diff --git a/test/cypress/integration/zone/02_descriptor.spec.js b/test/cypress/integration/zone/02_descriptor.spec.js new file mode 100644 index 000000000..02f2d30ce --- /dev/null +++ b/test/cypress/integration/zone/02_descriptor.spec.js @@ -0,0 +1,10 @@ +describe('Zone descriptor path', () => { + beforeEach(() => { + const zoneId = 1; + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/zone/${zoneId}`); + }); + it('should eliminate the zone using the descriptor option', async () => {}); + it('should search for the deleted zone to find no results', async () => {}); +}); diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index cb1d3de44..21121d9df 100755 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -86,11 +86,17 @@ Cypress.Commands.add('getValue', (selector) => { }); // Fill Inputs -Cypress.Commands.add('selectOption', (selector, option) => { +Cypress.Commands.add('selectOption', (selector, option, timeout) => { cy.waitForElement(selector); cy.get(selector).click(); + cy.wait(timeout || 1000); cy.get('.q-menu .q-item').contains(option).click(); }); +Cypress.Commands.add('countSelectOptions', (selector, option) => { + cy.waitForElement(selector); + cy.get(selector).click(); + cy.get('.q-menu .q-item').should('have.length', option); +}); Cypress.Commands.add('fillInForm', (obj, form = '.q-form > .q-card') => { cy.waitForElement('.q-form > .q-card'); diff --git a/test/cypress/support/index.js b/test/cypress/support/index.js index 4385698ec..c57c1a303 100644 --- a/test/cypress/support/index.js +++ b/test/cypress/support/index.js @@ -15,3 +15,19 @@ import './commands'; +function randomString(options = { length: 10 }) { + let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + return randomizeValue(possible, options); +} + +function randomNumber(options = { length: 10 }) { + let possible = '0123456789'; + return randomizeValue(possible, options); +} + +function randomizeValue(characterSet, options) { + return Array.from({ length: options.length }, () => + characterSet.charAt(Math.floor(Math.random() * characterSet.length)) + ).join(''); +} +export { randomString, randomNumber, randomizeValue }; diff --git a/test/vitest/__tests__/components/common/VnLinkPhone.spec.js b/test/vitest/__tests__/components/common/VnLinkPhone.spec.js index e460ab2fc..e31bff4a8 100644 --- a/test/vitest/__tests__/components/common/VnLinkPhone.spec.js +++ b/test/vitest/__tests__/components/common/VnLinkPhone.spec.js @@ -2,6 +2,14 @@ import { describe, it, expect } from 'vitest'; import parsePhone from 'src/filters/parsePhone'; describe('parsePhone filter', () => { + it('no phone', () => { + const resultado = parsePhone(null, '34'); + expect(resultado).toBe(undefined); + }); + it('no phone and no prefix', () => { + const resultado = parsePhone(null, null); + expect(resultado).toBe(undefined); + }); it("adds prefix +34 if it doesn't have one", () => { const resultado = parsePhone('123456789', '34'); expect(resultado).toBe('34123456789'); diff --git a/test/vitest/__tests__/composables/downloadFile.spec.js b/test/vitest/__tests__/composables/downloadFile.spec.js index f611479bf..f53b56b3e 100644 --- a/test/vitest/__tests__/composables/downloadFile.spec.js +++ b/test/vitest/__tests__/composables/downloadFile.spec.js @@ -1,25 +1,36 @@ -import { vi, describe, expect, it } from 'vitest'; +import { vi, describe, expect, it, beforeAll, afterAll } from 'vitest'; import { axios } from 'app/test/vitest/helper'; import { downloadFile } from 'src/composables/downloadFile'; import { useSession } from 'src/composables/useSession'; - const session = useSession(); const token = session.getToken(); describe('downloadFile', () => { + const baseUrl = 'http://localhost:9000'; + let defaulCreateObjectURL; + + beforeAll(() => { + defaulCreateObjectURL = window.URL.createObjectURL; + window.URL.createObjectURL = vi.fn(() => 'blob:http://localhost:9000/blob-id'); + }); + + afterAll(() => (window.URL.createObjectURL = defaulCreateObjectURL)); + it('should open a new window to download the file', async () => { - const url = 'http://localhost:9000'; - - vi.spyOn(axios, 'get').mockResolvedValueOnce({ data: url }); - - const mockWindowOpen = vi.spyOn(window, 'open'); + const res = { + data: new Blob(['file content'], { type: 'application/octet-stream' }), + headers: { 'content-disposition': 'attachment; filename="test-file.txt"' }, + }; + vi.spyOn(axios, 'get').mockImplementation((url) => { + if (url == 'Urls/getUrl') return Promise.resolve({ data: baseUrl }); + else if (url.includes('downloadFile')) return Promise.resolve(res); + }); await downloadFile(1); - expect(mockWindowOpen).toHaveBeenCalledWith( - `${url}/api/dms/1/downloadFile?access_token=${token}` + expect(axios.get).toHaveBeenCalledWith( + `${baseUrl}/api/dms/1/downloadFile?access_token=${token}`, + { responseType: 'blob' } ); - - mockWindowOpen.mockRestore(); }); });