diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a973777a..31b8831f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,7 +113,10 @@ - refs #7355 fix lists redirects summary by:carlossa - refs #7355 fix roles by:carlossa - refs #7355 fix search exprBuilder by:carlossa -- refs #7355 fix vnTable by:carlos + <<<<<<< HEAD +- # refs #7355 fix vnTable by:carlos +- refs #7355 fix vnTable by:carlossa + > > > > > > > 1b8a72175cc1dbae0590217b03d855bf2ff6d07d # Version 24.32 - 2024-08-06 diff --git a/package.json b/package.json index 72bada823..f25e570a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salix-front", - "version": "24.34.0", + "version": "24.36.0", "description": "Salix frontend", "productName": "Salix", "author": "Verdnatura", diff --git a/src/boot/mainShortcutMixin.js b/src/boot/mainShortcutMixin.js new file mode 100644 index 000000000..3b5c604b7 --- /dev/null +++ b/src/boot/mainShortcutMixin.js @@ -0,0 +1,38 @@ +import routes from 'src/router/modules'; +import { useRouter } from 'vue-router'; + +let isNotified = false; + +export default { + created: function () { + const router = useRouter(); + const keyBindingMap = routes + .filter((route) => route.meta.keyBinding) + .reduce((map, route) => { + map[route.meta.keyBinding.toLowerCase()] = route.path; + return map; + }, {}); + + const handleKeyDown = (event) => { + const { ctrlKey, altKey, key } = event; + + if (ctrlKey && altKey && keyBindingMap[key] && !isNotified) { + event.preventDefault(); + router.push(keyBindingMap[key]); + isNotified = true; + } + }; + + const handleKeyUp = (event) => { + const { ctrlKey, altKey } = event; + + // Resetea la bandera cuando se sueltan las teclas ctrl o alt + if (!ctrlKey || !altKey) { + isNotified = false; + } + }; + + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + }, +}; diff --git a/src/boot/quasar.js b/src/boot/quasar.js index a8d9b7ad9..caf573ac7 100644 --- a/src/boot/quasar.js +++ b/src/boot/quasar.js @@ -1,6 +1,8 @@ import { boot } from 'quasar/wrappers'; import qFormMixin from './qformMixin'; +import mainShortcutMixin from './mainShortcutMixin'; export default boot(({ app }) => { app.mixin(qFormMixin); + app.mixin(mainShortcutMixin); }); diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 22ef1622c..05f947cf3 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -22,7 +22,7 @@ const { t } = useI18n(); const { validate } = useValidator(); const { notify } = useNotify(); const route = useRoute(); - +const myForm = ref(null); const $props = defineProps({ url: { type: String, @@ -109,11 +109,14 @@ const defaultButtons = computed(() => ({ color: 'primary', icon: 'save', label: 'globals.save', + click: () => myForm.value.submit(), + type: 'submit', }, reset: { color: 'primary', icon: 'restart_alt', label: 'globals.reset', + click: () => reset(), }, ...$props.defaultButtons, })); @@ -276,7 +279,14 @@ defineExpose({ </script> <template> <div class="column items-center full-width"> - <QForm @submit="save" @reset="reset" class="q-pa-md" id="formModel"> + <QForm + ref="myForm" + v-if="formData" + @submit="save" + @reset="reset" + class="q-pa-md" + id="formModel" + > <QCard> <slot v-if="formData" @@ -304,7 +314,7 @@ defineExpose({ :color="defaultButtons.reset.color" :icon="defaultButtons.reset.icon" flat - @click="reset" + @click="defaultButtons.reset.click" :disable="!hasChanges" :title="t(defaultButtons.reset.label)" /> @@ -344,7 +354,7 @@ defineExpose({ :label="tMobile('globals.save')" color="primary" icon="save" - @click="save" + @click="defaultButtons.save.click" :disable="!hasChanges" :title="t(defaultButtons.save.label)" /> diff --git a/src/components/RefundInvoiceForm.vue b/src/components/RefundInvoiceForm.vue new file mode 100644 index 000000000..1b545103a --- /dev/null +++ b/src/components/RefundInvoiceForm.vue @@ -0,0 +1,173 @@ +<script setup> +import { ref, reactive } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRouter } from 'vue-router'; +import { useDialogPluginComponent } from 'quasar'; +import VnRow from 'components/ui/VnRow.vue'; +import FetchData from 'components/FetchData.vue'; +import VnSelect from 'components/common/VnSelect.vue'; +import FormPopup from './FormPopup.vue'; +import axios from 'axios'; +import useNotify from 'src/composables/useNotify.js'; + +const $props = defineProps({ + invoiceOutData: { + type: Object, + default: () => {}, + }, +}); + +const { dialogRef } = useDialogPluginComponent(); +const { t } = useI18n(); +const router = useRouter(); +const { notify } = useNotify(); + +const rectificativeTypeOptions = ref([]); +const siiTypeInvoiceOutsOptions = ref([]); +const inheritWarehouse = ref(true); +const invoiceParams = reactive({ + id: $props.invoiceOutData?.id, +}); +const invoiceCorrectionTypesOptions = ref([]); + +const refund = async () => { + const params = { + id: invoiceParams.id, + cplusRectificationTypeFk: invoiceParams.cplusRectificationTypeFk, + siiTypeInvoiceOutFk: invoiceParams.siiTypeInvoiceOutFk, + invoiceCorrectionTypeFk: invoiceParams.invoiceCorrectionTypeFk, + }; + + try { + const { data } = await axios.post('InvoiceOuts/refundAndInvoice', params); + notify(t('Refunded invoice'), 'positive'); + const [id] = data?.refundId || []; + if (id) router.push({ name: 'InvoiceOutSummary', params: { id } }); + } catch (err) { + console.error('Error refunding invoice', err); + } +}; +</script> + +<template> + <FetchData + url="CplusRectificationTypes" + :filter="{ order: 'description' }" + @on-fetch=" + (data) => ( + (rectificativeTypeOptions = data), + (invoiceParams.cplusRectificationTypeFk = data.filter( + (type) => type.description == 'I – Por diferencias' + )[0].id) + ) + " + auto-load + /> + <FetchData + url="SiiTypeInvoiceOuts" + :filter="{ where: { code: { like: 'R%' } } }" + @on-fetch=" + (data) => ( + (siiTypeInvoiceOutsOptions = data), + (invoiceParams.siiTypeInvoiceOutFk = data.filter( + (type) => type.code == 'R4' + )[0].id) + ) + " + auto-load + /> + <FetchData + url="InvoiceCorrectionTypes" + @on-fetch="(data) => (invoiceCorrectionTypesOptions = data)" + auto-load + /> + + <QDialog ref="dialogRef"> + <FormPopup + @on-submit="refund()" + :custom-submit-button-label="t('Accept')" + :default-cancel-button="false" + > + <template #form-inputs> + <VnRow> + <VnSelect + :label="t('Rectificative type')" + :options="rectificativeTypeOptions" + hide-selected + option-label="description" + option-value="id" + v-model="invoiceParams.cplusRectificationTypeFk" + :required="true" + /> + </VnRow> + <VnRow> + <VnSelect + :label="t('Class')" + :options="siiTypeInvoiceOutsOptions" + hide-selected + option-label="description" + option-value="id" + v-model="invoiceParams.siiTypeInvoiceOutFk" + :required="true" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt?.code }} - + {{ scope.opt?.description }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </VnRow> + + <VnRow> + <VnSelect + :label="t('Type')" + :options="invoiceCorrectionTypesOptions" + hide-selected + option-label="description" + option-value="id" + v-model="invoiceParams.invoiceCorrectionTypeFk" + :required="true" + /> </VnRow + ><VnRow> + <div> + <QCheckbox + :label="t('Inherit warehouse')" + v-model="inheritWarehouse" + /> + <QIcon name="info" class="cursor-info q-ml-sm" size="sm"> + <QTooltip>{{ t('Inherit warehouse tooltip') }}</QTooltip> + </QIcon> + </div> + </VnRow> + </template> + </FormPopup> + </QDialog> +</template> + +<i18n> +en: + Refund invoice: Refund invoice + Rectificative type: Rectificative type + Class: Class + Type: Type + Refunded invoice: Refunded invoice + Inherit warehouse: Inherit the warehouse + Inherit warehouse tooltip: Select this option to inherit the warehouse when refunding the invoice + Accept: Accept + Error refunding invoice: Error refunding invoice +es: + Refund invoice: Abonar factura + Rectificative type: Tipo rectificativa + Class: Clase + Type: Tipo + Refunded invoice: Factura abonada + Inherit warehouse: Heredar el almacén + Inherit warehouse tooltip: Seleccione esta opción para heredar el almacén al abonar la factura. + Accept: Aceptar + Error refunding invoice: Error abonando factura +</i18n> diff --git a/src/components/TransferInvoiceForm.vue b/src/components/TransferInvoiceForm.vue index 17c11d87e..f7050cdba 100644 --- a/src/components/TransferInvoiceForm.vue +++ b/src/components/TransferInvoiceForm.vue @@ -2,13 +2,12 @@ import { ref, reactive } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; -import { useQuasar } from 'quasar'; +import { useQuasar, useDialogPluginComponent } from 'quasar'; import VnConfirm from 'components/ui/VnConfirm.vue'; import VnRow from 'components/ui/VnRow.vue'; import FetchData from 'components/FetchData.vue'; import VnSelect from 'components/common/VnSelect.vue'; import FormPopup from './FormPopup.vue'; -import { useDialogPluginComponent } from 'quasar'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; @@ -18,19 +17,19 @@ const $props = defineProps({ default: () => {}, }, }); + const { dialogRef } = useDialogPluginComponent(); const quasar = useQuasar(); const { t } = useI18n(); const router = useRouter(); const { notify } = useNotify(); -const checked = ref(true); -const transferInvoiceParams = reactive({ - id: $props.invoiceOutData?.id, - refFk: $props.invoiceOutData?.ref, -}); const rectificativeTypeOptions = ref([]); const siiTypeInvoiceOutsOptions = ref([]); +const checked = ref(true); +const transferInvoiceParams = reactive({ + id: $props.invoiceOutData?.id, +}); const invoiceCorrectionTypesOptions = ref([]); const selectedClient = (client) => { @@ -44,10 +43,9 @@ const makeInvoice = async () => { const params = { id: transferInvoiceParams.id, cplusRectificationTypeFk: transferInvoiceParams.cplusRectificationTypeFk, + siiTypeInvoiceOutFk: transferInvoiceParams.siiTypeInvoiceOutFk, invoiceCorrectionTypeFk: transferInvoiceParams.invoiceCorrectionTypeFk, newClientFk: transferInvoiceParams.newClientFk, - refFk: transferInvoiceParams.refFk, - siiTypeInvoiceOutFk: transferInvoiceParams.siiTypeInvoiceOutFk, makeInvoice: checked.value, }; @@ -74,7 +72,7 @@ const makeInvoice = async () => { } } - const { data } = await axios.post('InvoiceOuts/transferInvoice', params); + const { data } = await axios.post('InvoiceOuts/transfer', params); notify(t('Transferred invoice'), 'positive'); const id = data?.[0]; if (id) router.push({ name: 'InvoiceOutSummary', params: { id } }); diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue index 6c77d44df..c998228bc 100644 --- a/src/components/VnTable/VnTable.vue +++ b/src/components/VnTable/VnTable.vue @@ -107,7 +107,7 @@ const orders = ref(parseOrder(routeQuery.filter?.order)); const CrudModelRef = ref({}); const showForm = ref(false); const splittedColumns = ref({ columns: [] }); -const columnsVisibilitySkiped = ref(); +const columnsVisibilitySkipped = ref(); const createForm = ref(); const tableModes = [ @@ -135,7 +135,7 @@ onMounted(() => { ? CARD_MODE : $props.defaultMode; stateStore.rightDrawer = true; - columnsVisibilitySkiped.value = [ + columnsVisibilitySkipped.value = [ ...splittedColumns.value.columns .filter((c) => c.visible == false) .map((c) => c.name), @@ -178,10 +178,20 @@ function setUserParams(watchedParams, watchedOrder) { watchedParams = { ...watchedParams, ...where }; delete watchedParams.filter; delete params.value?.filter; - params.value = { ...params.value, ...watchedParams }; + params.value = { ...params.value, ...sanitizer(watchedParams) }; orders.value = parseOrder(order); } +function sanitizer(params) { + for (const [key, value] of Object.entries(params)) { + if (typeof value == 'object') { + const param = Object.values(value)[0]; + if (typeof param == 'string') params[key] = param.replaceAll('%', ''); + } + } + return params; +} + function splitColumns(columns) { splittedColumns.value = { columns: [], @@ -332,296 +342,280 @@ defineExpose({ </QScrollArea> </QDrawer> <!-- class in div to fix warn--> - <div class="q-px-md"> - <CrudModel - v-bind="$attrs" - :limit="20" - ref="CrudModelRef" - @on-fetch="(...args) => emit('onFetch', ...args)" - :search-url="searchUrl" - :disable-infinite-scroll="isTableMode" - @save-changes="reload" - :has-sub-toolbar="$attrs['hasSubToolbar'] ?? isEditable" - :auto-load="hasParams || $attrs['auto-load']" - > - <template - v-for="(_, slotName) in $slots" - #[slotName]="slotData" - :key="slotName" + + <CrudModel + v-bind="$attrs" + :class="$attrs['class'] ?? 'q-px-md'" + :limit="20" + ref="CrudModelRef" + @on-fetch="(...args) => emit('onFetch', ...args)" + :search-url="searchUrl" + :disable-infinite-scroll="isTableMode" + @save-changes="reload" + :has-sub-toolbar="$attrs['hasSubToolbar'] ?? isEditable" + :auto-load="hasParams || $attrs['auto-load']" + > + <template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName"> + <slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" /> + </template> + <template #body="{ rows }"> + <QTable + v-bind="table" + class="vnTable" + :columns="splittedColumns.columns" + :rows="rows" + v-model:selected="selected" + :grid="!isTableMode" + table-header-class="bg-header" + card-container-class="grid-three" + flat + :style="isTableMode && `max-height: ${tableHeight}`" + virtual-scroll + @virtual-scroll=" + (event) => + event.index > rows.length - 2 && + CrudModelRef.vnPaginateRef.paginate() + " + @row-click="(_, row) => rowClickFunction && rowClickFunction(row)" + @update:selected="emit('update:selected', $event)" > - <slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" /> - </template> - <template #body="{ rows }"> - <QTable - v-bind="table" - class="vnTable" - :columns="splittedColumns.columns" - :rows="rows" - v-model:selected="selected" - :grid="!isTableMode" - table-header-class="bg-header" - card-container-class="grid-three" - flat - :style="isTableMode && `max-height: ${tableHeight}`" - virtual-scroll - @virtual-scroll=" - (event) => - event.index > rows.length - 2 && - CrudModelRef.vnPaginateRef.paginate() - " - @row-click="(_, row) => rowClickFunction && rowClickFunction(row)" - @update:selected="emit('update:selected', $event)" - > - <template #top-left v-if="!$props.withoutHeader"> - <slot name="top-left"></slot> - </template> - <template #top-right v-if="!$props.withoutHeader"> - <VnVisibleColumn - v-if="isTableMode" - v-model="splittedColumns.columns" - :table-code="tableCode ?? route.name" - :skip="columnsVisibilitySkiped" - /> - <QBtnToggle - v-model="mode" - toggle-color="primary" - class="bg-vn-section-color" - dense - :options="tableModes" - /> - <QBtn - v-if="$props.rightSearch" - icon="filter_alt" - class="bg-vn-section-color q-ml-md" - dense - @click="stateStore.toggleRightDrawer()" - /> - </template> - <template #header-cell="{ col }"> - <QTh v-if="col.visible ?? true"> - <div - class="column self-start q-ml-xs ellipsis" - :class="`text-${col?.align ?? 'left'}`" - :style="$props.columnSearch ? 'height: 75px' : ''" - > - <div - class="row items-center no-wrap" - style="height: 30px" - > - <VnTableOrder - v-model="orders[col.orderBy ?? col.name]" - :name="col.orderBy ?? col.name" - :label="col?.label" - :data-key="$attrs['data-key']" - :search-url="searchUrl" - /> - </div> - <VnTableFilter - v-if="$props.columnSearch" - :column="col" - :show-title="true" + <template #top-left v-if="!$props.withoutHeader"> + <slot name="top-left"></slot> + </template> + <template #top-right v-if="!$props.withoutHeader"> + <VnVisibleColumn + v-if="isTableMode" + v-model="splittedColumns.columns" + :table-code="tableCode ?? route.name" + :skip="columnsVisibilitySkipped" + /> + <QBtnToggle + v-model="mode" + toggle-color="primary" + class="bg-vn-section-color" + dense + :options="tableModes.filter((mode) => !mode.disable)" + /> + <QBtn + v-if="$props.rightSearch" + icon="filter_alt" + class="bg-vn-section-color q-ml-md" + dense + @click="stateStore.toggleRightDrawer()" + /> + </template> + <template #header-cell="{ col }"> + <QTh v-if="col.visible ?? true"> + <div + class="column self-start q-ml-xs ellipsis" + :class="`text-${col?.align ?? 'left'}`" + :style="$props.columnSearch ? 'height: 75px' : ''" + > + <div class="row items-center no-wrap" style="height: 30px"> + <QTooltip v-if="col.toolTip">{{ col.toolTip }}</QTooltip> + <VnTableOrder + v-model="orders[col.orderBy ?? col.name]" + :name="col.orderBy ?? col.name" + :label="col?.label" :data-key="$attrs['data-key']" - v-model="params[columnName(col)]" :search-url="searchUrl" - class="full-width" /> </div> - </QTh> - </template> - <template #header-cell-tableActions> - <QTh auto-width class="sticky" /> - </template> - <template #body-cell-tableStatus="{ col, row }"> - <QTd auto-width :class="getColAlign(col)"> - <VnTableChip - :columns="splittedColumns.columnChips" + <VnTableFilter + v-if="$props.columnSearch" + :column="col" + :show-title="true" + :data-key="$attrs['data-key']" + v-model="params[columnName(col)]" + :search-url="searchUrl" + class="full-width" + /> + </div> + </QTh> + </template> + <template #header-cell-tableActions> + <QTh auto-width class="sticky" /> + </template> + <template #body-cell-tableStatus="{ col, row }"> + <QTd auto-width :class="getColAlign(col)"> + <VnTableChip :columns="splittedColumns.columnChips" :row="row"> + <template #afterChip> + <slot name="afterChip" :row="row"></slot> + </template> + </VnTableChip> + </QTd> + </template> + <template #body-cell="{ col, row, rowIndex }"> + <!-- Columns --> + <QTd + auto-width + class="no-margin q-px-xs" + :class="[getColAlign(col), col.columnClass]" + v-if="col.visible ?? true" + @click.ctrl=" + ($event) => + rowCtrlClickFunction && rowCtrlClickFunction($event, row) + " + > + <slot + :name="`column-${col.name}`" + :col="col" + :row="row" + :row-index="rowIndex" + > + <VnTableColumn + :column="col" :row="row" - > - <template #afterChip> - <slot name="afterChip" :row="row"></slot> - </template> - </VnTableChip> - </QTd> - </template> - <template #body-cell="{ col, row, rowIndex }"> - <!-- Columns --> - <QTd - auto-width - class="no-margin q-px-xs" - :class="[getColAlign(col), col.columnClass]" - v-if="col.visible ?? true" - @click.ctrl=" - ($event) => - rowCtrlClickFunction && - rowCtrlClickFunction($event, row) + :is-editable="col.isEditable ?? isEditable" + v-model="row[col.name]" + component-prop="columnField" + /> + </slot> + </QTd> + </template> + <template #body-cell-tableActions="{ col, row }"> + <QTd + auto-width + :class="getColAlign(col)" + class="sticky no-padding" + @click="stopEventPropagation($event)" + > + <QBtn + v-for="(btn, index) of col.actions" + :key="index" + :title="btn.title" + :icon="btn.icon" + class="q-px-sm" + flat + :class=" + btn.isPrimary ? 'text-primary-light' : 'color-vn-text ' + " + :style="`visibility: ${ + (btn.show && btn.show(row)) ?? true ? 'visible' : 'hidden' + }`" + @click="btn.action(row)" + /> + </QTd> + </template> + <template #item="{ row, colsMap }"> + <component + :is="$props.redirect ? 'router-link' : 'span'" + :to="`/${$props.redirect}/` + row.id" + > + <QCard + bordered + flat + class="row no-wrap justify-between cursor-pointer" + @click=" + (_, row) => { + $props.rowClick && $props.rowClick(row); + } " > - <slot - :name="`column-${col.name}`" - :col="col" - :row="row" - :row-index="rowIndex" - > - <VnTableColumn - :column="col" - :row="row" - :is-editable="col.isEditable ?? isEditable" - v-model="row[col.name]" - component-prop="columnField" - /> - </slot> - </QTd> - </template> - <template #body-cell-tableActions="{ col, row }"> - <QTd - auto-width - :class="getColAlign(col)" - class="sticky no-padding" - @click="stopEventPropagation($event)" - > - <QBtn - v-for="(btn, index) of col.actions" - :key="index" - :title="btn.title" - :icon="btn.icon" - class="q-px-sm" - flat - :class=" - btn.isPrimary - ? 'text-primary-light' - : 'color-vn-text ' - " - :style="`visibility: ${ - (btn.show && btn.show(row)) ?? true - ? 'visible' - : 'hidden' - }`" - @click="btn.action(row)" - /> - </QTd> - </template> - <template #item="{ row, colsMap }"> - <component - :is="$props.redirect ? 'router-link' : 'span'" - :to="`/${$props.redirect}/` + row.id" - > - <QCard - bordered - flat - class="row no-wrap justify-between cursor-pointer" - @click=" - (_, row) => { - $props.rowClick && $props.rowClick(row); - } - " + <QCardSection + vertical + class="no-margin no-padding" + :class="colsMap.tableActions ? 'w-80' : 'fit'" > + <!-- Chips --> <QCardSection - vertical - class="no-margin no-padding" - :class="colsMap.tableActions ? 'w-80' : 'fit'" + v-if="splittedColumns.chips.length" + class="no-margin q-px-xs q-py-none" > - <!-- Chips --> - <QCardSection - v-if="splittedColumns.chips.length" - class="no-margin q-px-xs q-py-none" + <VnTableChip + :columns="splittedColumns.chips" + :row="row" > - <VnTableChip - :columns="splittedColumns.chips" - :row="row" - > - <template #afterChip> - <slot name="afterChip" :row="row"></slot> - </template> - </VnTableChip> - </QCardSection> - <!-- Title --> - <QCardSection - v-if="splittedColumns.title" - class="q-pl-sm q-py-none text-primary-light text-bold text-h6 cardEllipsis" + <template #afterChip> + <slot name="afterChip" :row="row"></slot> + </template> + </VnTableChip> + </QCardSection> + <!-- Title --> + <QCardSection + v-if="splittedColumns.title" + class="q-pl-sm q-py-none text-primary-light text-bold text-h6 cardEllipsis" + > + <span + :title="row[splittedColumns.title.name]" + @click="stopEventPropagation($event)" + class="cursor-text" > - <span - :title="row[splittedColumns.title.name]" - @click="stopEventPropagation($event)" - class="cursor-text" - > - {{ row[splittedColumns.title.name] }} - </span> - </QCardSection> - <!-- Fields --> - <QCardSection - class="q-pl-sm q-pr-lg q-py-xs" - :class="$props.cardClass" + {{ row[splittedColumns.title.name] }} + </span> + </QCardSection> + <!-- Fields --> + <QCardSection + class="q-pl-sm q-pr-lg q-py-xs" + :class="$props.cardClass" + > + <div + v-for="( + col, index + ) of splittedColumns.cardVisible" + :key="col.name" + class="fields" > - <div - v-for="( - col, index - ) of splittedColumns.cardVisible" - :key="col.name" - class="fields" + <VnLv + :label=" + !col.component && col.label + ? `${col.label}:` + : '' + " > - <VnLv - :label=" - !col.component && col.label - ? `${col.label}:` - : '' - " - > - <template #value> - <span - @click=" - stopEventPropagation($event) - " + <template #value> + <span + @click="stopEventPropagation($event)" + > + <slot + :name="`column-${col.name}`" + :col="col" + :row="row" + :row-index="index" > - <slot - :name="`column-${col.name}`" - :col="col" + <VnTableColumn + :column="col" :row="row" - :row-index="index" - > - <VnTableColumn - :column="col" - :row="row" - :is-editable="false" - v-model="row[col.name]" - component-prop="columnField" - :show-label="true" - /> - </slot> - </span> - </template> - </VnLv> - </div> - </QCardSection> + :is-editable="false" + v-model="row[col.name]" + component-prop="columnField" + :show-label="true" + /> + </slot> + </span> + </template> + </VnLv> + </div> </QCardSection> - <!-- Actions --> - <QCardSection - v-if="colsMap.tableActions" - class="column flex-center w-10 no-margin q-pa-xs q-gutter-y-xs" - @click="stopEventPropagation($event)" - > - <QBtn - v-for="(btn, index) of splittedColumns.actions - .actions" - :key="index" - :title="btn.title" - :icon="btn.icon" - class="q-pa-xs" - flat - :class=" - btn.isPrimary - ? 'text-primary-light' - : 'color-vn-text ' - " - @click="btn.action(row)" - /> - </QCardSection> - </QCard> - </component> - </template> - </QTable> - </template> - </CrudModel> - </div> + </QCardSection> + <!-- Actions --> + <QCardSection + v-if="colsMap.tableActions" + class="column flex-center w-10 no-margin q-pa-xs q-gutter-y-xs" + @click="stopEventPropagation($event)" + > + <QBtn + v-for="(btn, index) of splittedColumns.actions + .actions" + :key="index" + :title="btn.title" + :icon="btn.icon" + class="q-pa-xs" + flat + :class=" + btn.isPrimary + ? 'text-primary-light' + : 'color-vn-text ' + " + @click="btn.action(row)" + /> + </QCardSection> + </QCard> + </component> + </template> + </QTable> + </template> + </CrudModel> <QPageSticky v-if="create" :offset="[20, 20]" style="z-index: 2"> <QBtn @click="showForm = !showForm" color="primary" fab icon="add" /> <QTooltip> diff --git a/src/components/common/VnCard.vue b/src/components/common/VnCard.vue index 03a7ce7da..a7fe651ad 100644 --- a/src/components/common/VnCard.vue +++ b/src/components/common/VnCard.vue @@ -8,7 +8,6 @@ import VnSubToolbar from '../ui/VnSubToolbar.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue'; import LeftMenu from 'components/LeftMenu.vue'; import RightMenu from 'components/common/RightMenu.vue'; - const props = defineProps({ dataKey: { type: String, required: true }, baseUrl: { type: String, default: undefined }, @@ -74,7 +73,7 @@ if (props.baseUrl) { <QPage> <VnSubToolbar /> <div :class="[useCardSize(), $attrs.class]"> - <RouterView /> + <RouterView :key="route.fullPath" /> </div> </QPage> </QPageContainer> diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue index 33b97e29d..75d4b8a28 100644 --- a/src/components/common/VnInput.vue +++ b/src/components/common/VnInput.vue @@ -1,6 +1,7 @@ <script setup> import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; +import { useValidator } from 'src/composables/useValidator'; const emit = defineEmits([ 'update:modelValue', @@ -27,9 +28,11 @@ const $props = defineProps({ default: true, }, }); +const { validations } = useValidator(); const { t } = useI18n(); -const requiredFieldRule = (val) => !!val || t('globals.fieldRequired'); +const requiredFieldRule = (val) => validations().required($attrs.required, val); + const vnInputRef = ref(null); const value = computed({ get() { @@ -57,21 +60,22 @@ const focus = () => { defineExpose({ focus, }); +import { useAttrs } from 'vue'; +const $attrs = useAttrs(); -const inputRules = [ +const mixinRules = [ + requiredFieldRule, + ...($attrs.rules ?? []), (val) => { const { min } = vnInputRef.value.$attrs; + if (!min) return null; if (min >= 0) if (Math.floor(val) < min) return t('inputMin', { value: min }); }, ]; </script> <template> - <div - @mouseover="hover = true" - @mouseleave="hover = false" - :rules="$attrs.required ? [requiredFieldRule] : null" - > + <div @mouseover="hover = true" @mouseleave="hover = false"> <QInput ref="vnInputRef" v-model="value" @@ -80,7 +84,7 @@ const inputRules = [ :class="{ required: $attrs.required }" @keyup.enter="emit('keyup.enter')" :clearable="false" - :rules="inputRules" + :rules="mixinRules" :lazy-rules="true" hide-bottom-space > diff --git a/src/components/common/VnInputDate.vue b/src/components/common/VnInputDate.vue index 6e57a8a53..f94130da4 100644 --- a/src/components/common/VnInputDate.vue +++ b/src/components/common/VnInputDate.vue @@ -3,7 +3,7 @@ import { onMounted, watch, computed, ref } from 'vue'; import { date } from 'quasar'; import { useI18n } from 'vue-i18n'; -const model = defineModel({ type: String }); +const model = defineModel({ type: [String, Date] }); const $props = defineProps({ isOutlined: { type: Boolean, diff --git a/src/components/common/VnInputTime.vue b/src/components/common/VnInputTime.vue index 0b4e72cb8..b3478bb23 100644 --- a/src/components/common/VnInputTime.vue +++ b/src/components/common/VnInputTime.vue @@ -14,7 +14,7 @@ const props = defineProps({ default: false, }, }); -const initialDate = ref(model.value); +const initialDate = ref(model.value ?? Date.vnNew()); const { t } = useI18n(); const requiredFieldRule = (val) => !!val || t('globals.fieldRequired'); diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index b6dc226cc..1e3a32f48 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -77,6 +77,10 @@ const $props = defineProps({ type: Object, default: null, }, + noOne: { + type: Boolean, + default: false, + }, }); const { t } = useI18n(); @@ -89,6 +93,11 @@ const myOptionsOriginal = ref([]); const vnSelectRef = ref(); const dataRef = ref(); const lastVal = ref(); +const noOneText = t('globals.noOne'); +const noOneOpt = ref({ + [optionValue.value]: false, + [optionLabel.value]: noOneText, +}); const value = computed({ get() { @@ -104,9 +113,11 @@ watch(options, (newValue) => { setOptions(newValue); }); -watch(modelValue, (newValue) => { +watch(modelValue, async (newValue) => { if (!myOptions.value.some((option) => option[optionValue.value] == newValue)) - fetchFilter(newValue); + await fetchFilter(newValue); + + if ($props.noOne) myOptions.value.unshift(noOneOpt.value); }); onMounted(() => { @@ -169,6 +180,7 @@ async function fetchFilter(val) { const fetchOptions = { where, include, limit }; if (fields) fetchOptions.fields = fields; if (sortBy) fetchOptions.order = sortBy; + return dataRef.value.fetch(fetchOptions); } @@ -189,6 +201,9 @@ async function filterHandler(val, update) { } else newOptions = filter(val, myOptionsOriginal.value); update( () => { + if ($props.noOne && noOneText.toLowerCase().includes(val.toLowerCase())) + newOptions.unshift(noOneOpt.value); + myOptions.value = newOptions; }, (ref) => { diff --git a/src/components/ui/FetchedTags.vue b/src/components/ui/FetchedTags.vue index beaa85bfe..a0edf85f8 100644 --- a/src/components/ui/FetchedTags.vue +++ b/src/components/ui/FetchedTags.vue @@ -2,10 +2,6 @@ import { computed } from 'vue'; const $props = defineProps({ - maxLength: { - type: Number, - required: true, - }, item: { type: Object, required: true, diff --git a/src/components/ui/VnConfirm.vue b/src/components/ui/VnConfirm.vue index 0480650db..fd4860107 100644 --- a/src/components/ui/VnConfirm.vue +++ b/src/components/ui/VnConfirm.vue @@ -15,7 +15,7 @@ const props = defineProps({ default: null, }, message: { - type: String, + type: [String, Boolean], default: null, }, data: { @@ -35,7 +35,10 @@ defineEmits(['confirm', ...useDialogPluginComponent.emits]); const { dialogRef, onDialogOK } = useDialogPluginComponent(); const title = props.title || t('Confirm'); -const message = props.message || t('Are you sure you want to continue?'); +const message = + props.message || + (props.message !== false ? t('Are you sure you want to continue?') : false); + const isLoading = ref(false); async function confirm() { @@ -61,12 +64,12 @@ async function confirm() { size="xl" v-if="icon" /> - <span class="text-h6 text-grey">{{ title }}</span> + <span class="text-h6">{{ title }}</span> <QSpace /> <QBtn icon="close" :disable="isLoading" flat round dense v-close-popup /> </QCardSection> <QCardSection class="row items-center"> - <span v-html="message"></span> + <span v-if="message !== false" v-html="message" /> <slot name="customHTML"></slot> </QCardSection> <QCardActions align="right"> diff --git a/src/components/ui/VnFilterPanel.vue b/src/components/ui/VnFilterPanel.vue index ae6ee95df..998211c39 100644 --- a/src/components/ui/VnFilterPanel.vue +++ b/src/components/ui/VnFilterPanel.vue @@ -24,7 +24,7 @@ const $props = defineProps({ type: Boolean, default: true, }, - unRemovableParams: { + unremovableParams: { type: Array, required: false, default: () => [], @@ -92,16 +92,18 @@ function setUserParams(watchedParams) { const order = watchedParams.filter?.order; delete watchedParams.filter; - userParams.value = { ...userParams.value, ...sanitizer(watchedParams) }; + userParams.value = sanitizer(watchedParams); emit('setUserParams', userParams.value, order); } watch( - () => [route.query[$props.searchUrl], arrayData.store.userParams], - ([newSearchUrl, newUserParams], [oldSearchUrl, oldUserParams]) => { - if (newSearchUrl || oldSearchUrl) setUserParams(newSearchUrl); - if (newUserParams || oldUserParams) setUserParams(newUserParams); - } + () => route.query[$props.searchUrl], + (val, oldValue) => (val || oldValue) && setUserParams(val) +); + +watch( + () => arrayData.store.userParams, + (val, oldValue) => (val || oldValue) && setUserParams(val) ); watch( @@ -148,7 +150,7 @@ async function clearFilters() { arrayData.reset(['skip', 'filter.skip', 'page']); // Filtrar los params no removibles const removableFilters = Object.keys(userParams.value).filter((param) => - $props.unRemovableParams.includes(param) + $props.unremovableParams.includes(param) ); const newParams = {}; // Conservar solo los params que no son removibles @@ -180,10 +182,10 @@ const tagsList = computed(() => { }); const tags = computed(() => { - return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.key)); + return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.label)); }); const customTags = computed(() => - tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.key)) + tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label)) ); async function remove(key) { @@ -269,7 +271,7 @@ function sanitizer(params) { <VnFilterPanelChip v-for="chip of tags" :key="chip.label" - :removable="!unRemovableParams.includes(chip.label)" + :removable="!unremovableParams?.includes(chip.label)" @remove="remove(chip.label)" > <slot name="tags" :tag="chip" :format-fn="formatValue"> diff --git a/src/components/ui/VnPaginate.vue b/src/components/ui/VnPaginate.vue index 0df719c66..22edfe869 100644 --- a/src/components/ui/VnPaginate.vue +++ b/src/components/ui/VnPaginate.vue @@ -10,6 +10,10 @@ const props = defineProps({ type: String, required: true, }, + class: { + type: String, + default: '', + }, autoLoad: { type: Boolean, default: false, @@ -215,7 +219,7 @@ defineExpose({ fetch, addFilter, paginate }); v-if="store.data" @load="onLoad" :offset="offset" - class="full-width" + :class="['full-width', props.class]" :disable="disableInfiniteScroll || !store.hasMoreData" v-bind="$attrs" > diff --git a/src/components/ui/VnSearchbar.vue b/src/components/ui/VnSearchbar.vue index a4375f36c..a78403b5c 100644 --- a/src/components/ui/VnSearchbar.vue +++ b/src/components/ui/VnSearchbar.vue @@ -63,17 +63,13 @@ const props = defineProps({ type: String, default: '', }, - makeFetch: { - type: Boolean, - default: true, - }, - searchUrl: { - type: String, - default: 'params', + whereFilter: { + type: Function, + default: undefined, }, }); -const searchText = ref(''); +const searchText = ref(); let arrayDataProps = { ...props }; if (props.redirect) arrayDataProps = { @@ -107,13 +103,20 @@ async function search() { const staticParams = Object.entries(store.userParams); arrayData.reset(['skip', 'page']); - if (props.makeFetch) - await arrayData.applyFilter({ - params: { - ...Object.fromEntries(staticParams), - search: searchText.value, - }, - }); + const filter = { + params: { + ...Object.fromEntries(staticParams), + search: searchText.value, + }, + }; + + if (props.whereFilter) { + filter.filter = { + where: props.whereFilter(searchText.value), + }; + delete filter.params.search; + } + await arrayData.applyFilter(filter); } </script> <template> diff --git a/src/composables/getDateQBadgeColor.js b/src/composables/getDateQBadgeColor.js index be9ef41b5..a91213a0a 100644 --- a/src/composables/getDateQBadgeColor.js +++ b/src/composables/getDateQBadgeColor.js @@ -7,5 +7,5 @@ export function getDateQBadgeColor(date) { let comparation = today - timeTicket; if (comparation == 0) return 'warning'; - if (comparation < 0) return 'negative'; + if (comparation < 0) return 'success'; } diff --git a/src/composables/useValidator.js b/src/composables/useValidator.js index 5ad96ea1b..7a7032608 100644 --- a/src/composables/useValidator.js +++ b/src/composables/useValidator.js @@ -28,7 +28,7 @@ export function useValidator() { } const { t } = useI18n(); - const validations = function (validation) { + const validations = function (validation = {}) { return { format: (value) => { const { allowNull, with: format, allowBlank } = validation; @@ -40,12 +40,15 @@ export function useValidator() { if (!isValid) return message; }, presence: (value) => { - let message = `Value can't be empty`; + let message = t(`globals.valueCantBeEmpty`); if (validation.message) message = t(validation.message) || validation.message; return !validator.isEmpty(value ? String(value) : '') || message; }, + required: (required, value) => { + return required ? !!value || t('globals.fieldRequired') : null; + }, length: (value) => { const options = { min: validation.min || validation.is, @@ -71,12 +74,17 @@ export function useValidator() { return validator.isInt(value) || 'Value should be integer'; return validator.isNumeric(value) || 'Value should be a number'; }, + min: (value, min) => { + if (min >= 0) + if (Math.floor(value) < min) return t('inputMin', { value: min }); + }, custom: (value) => validation.bindedFunction(value) || 'Invalid value', }; }; return { validate, + validations, models, }; } diff --git a/src/filters/date.js b/src/filters/date.js index f9fd1e0b2..058c90060 100644 --- a/src/filters/date.js +++ b/src/filters/date.js @@ -20,21 +20,21 @@ export function isValidDate(date) { * Converts a given date to a specific format. * * @param {number|string|Date} date - The date to be formatted. + * @param {Object} opts - Optional parameters to customize the output format. * @returns {string} The formatted date as a string in 'dd/mm/yyyy' format. If the provided date is not valid, an empty string is returned. * * @example * // returns "02/12/2022" * toDateFormat(new Date(2022, 11, 2)); */ -export function toDateFormat(date, locale = 'es-ES') { - if (!isValidDate(date)) { - return ''; - } - return new Date(date).toLocaleDateString(locale, { - year: 'numeric', - month: '2-digit', - day: '2-digit', - }); +export function toDateFormat(date, locale = 'es-ES', opts = {}) { + if (!isValidDate(date)) return ''; + + const format = Object.assign( + { year: 'numeric', month: '2-digit', day: '2-digit' }, + opts + ); + return new Date(date).toLocaleDateString(locale, format); } /** diff --git a/src/filters/dateRange.js b/src/filters/dateRange.js index 7aa2869e5..4c0cfe654 100644 --- a/src/filters/dateRange.js +++ b/src/filters/dateRange.js @@ -1,7 +1,7 @@ export default function dateRange(value) { const minHour = new Date(value); minHour.setHours(0, 0, 0, 0); - const maxHour = new Date(); + const maxHour = new Date(value); maxHour.setHours(23, 59, 59, 59); return [minHour, maxHour]; diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index 2055846b2..548981de1 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -67,6 +67,7 @@ globals: allRows: 'All { numberRows } row(s)' markAll: Mark all requiredField: Required field + valueCantBeEmpty: Value cannot be empty class: clase type: Type reason: reason @@ -94,6 +95,7 @@ globals: from: From to: To notes: Notes + refresh: Refresh pageTitles: logIn: Login summary: Summary @@ -255,7 +257,10 @@ globals: twoFactor: Two factor recoverPassword: Recover password resetPassword: Reset password + ticketsMonitor: Tickets monitor + clientsActionsMonitor: Clients and actions serial: Serial + medical: Mutual created: Created worker: Worker now: Now @@ -269,6 +274,7 @@ globals: subtitle: Are you sure exit without saving? createInvoiceIn: Create invoice in myAccount: My account + noOne: No one errors: statusUnauthorized: Access denied statusInternalServerError: An internal server error has ocurred @@ -455,6 +461,7 @@ entry: travelFk: Travel isExcludedFromAvailable: Inventory isRaid: Raid + invoiceAmount: Import summary: commission: Commission currency: Currency @@ -872,6 +879,7 @@ worker: timeControl: Time control locker: Locker balance: Balance + medical: Medical list: name: Name email: Email @@ -951,6 +959,15 @@ worker: amount: Importe remark: Bonficado hasDiploma: Diploma + medical: + tableVisibleColumns: + date: Date + time: Hour + center: Formation Center + invoice: Invoice + amount: Amount + isFit: Fit + remark: Observations imageNotFound: Image not found balance: tableVisibleColumns: @@ -1123,9 +1140,12 @@ travel: agency: Agency shipped: Shipped landed: Landed + shipHour: Shipment Hour + landHour: Landing Hour warehouseIn: Warehouse in warehouseOut: Warehouse out totalEntries: Total entries + totalEntriesTooltip: Total entries summary: confirmed: Confirmed entryId: Entry Id diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index 5a8edf226..ad018b5cd 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -76,6 +76,9 @@ globals: warehouse: Almacén company: Empresa fieldRequired: Campo requerido + valueCantBeEmpty: El valor no puede estar vacío + Value can't be blank: El valor no puede estar en blanco + Value can't be null: El valor no puede ser nulo allowedFilesText: 'Tipos de archivo permitidos: { allowedContentTypes }' smsSent: SMS enviado confirmDeletion: Confirmar eliminación @@ -94,6 +97,7 @@ globals: from: Desde to: Hasta notes: Notas + refresh: Actualizar pageTitles: logIn: Inicio de sesión summary: Resumen @@ -236,7 +240,7 @@ globals: purchaseRequest: Petición de compra weeklyTickets: Tickets programados formation: Formación - locations: Ubicaciones + locations: Localizaciones warehouses: Almacenes roles: Roles connections: Conexiones @@ -257,7 +261,10 @@ globals: twoFactor: Doble factor recoverPassword: Recuperar contraseña resetPassword: Restablecer contraseña + ticketsMonitor: Monitor de tickets + clientsActionsMonitor: Clientes y acciones serial: Facturas por serie + medical: Mutua created: Fecha creación worker: Trabajador now: Ahora @@ -271,6 +278,7 @@ globals: subtitle: ¿Seguro que quiere salir sin guardar? createInvoiceIn: Crear factura recibida myAccount: Mi cuenta + noOne: Nadie errors: statusUnauthorized: Acceso denegado statusInternalServerError: Ha ocurrido un error interno del servidor @@ -454,6 +462,7 @@ entry: travelFk: Envio isExcludedFromAvailable: Inventario isRaid: Redada + invoiceAmount: Importe summary: commission: Comisión currency: Moneda @@ -873,6 +882,8 @@ worker: timeControl: Control de horario locker: Taquilla balance: Balance + formation: Formación + medical: Mutua list: name: Nombre email: Email @@ -943,6 +954,15 @@ worker: amount: Importe remark: Bonficado hasDiploma: Diploma + medical: + tableVisibleColumns: + date: Fecha + time: Hora + center: Centro de Formación + invoice: Factura + amount: Importe + isFit: Apto + remark: Observaciones imageNotFound: No se ha encontrado la imagen balance: tableVisibleColumns: @@ -1100,11 +1120,14 @@ travel: id: Id ref: Referencia agency: Agencia - shipped: Enviado - landed: Llegada - warehouseIn: Almacén de salida - warehouseOut: Almacén de entrada - totalEntries: Total de entradas + shipped: F.envío + shipHour: Hora de envío + landHour: Hora de llegada + landed: F.entrega + warehouseIn: Alm.salida + warehouseOut: Alm.entrada + totalEntries: ∑ + totalEntriesTooltip: Entradas totales summary: confirmed: Confirmado entryId: Id entrada @@ -1269,6 +1292,7 @@ components: clone: Clonar openCard: Ficha openSummary: Detalles + viewSummary: Vista previa cardDescriptor: mainList: Listado principal summary: Resumen diff --git a/src/pages/Account/AccountAcls.vue b/src/pages/Account/AccountAcls.vue index 110358e7f..dd93a0cb5 100644 --- a/src/pages/Account/AccountAcls.vue +++ b/src/pages/Account/AccountAcls.vue @@ -141,6 +141,7 @@ const deleteAcl = async ({ id }) => { formInitialData: {}, }" order="id DESC" + :disable-option="{ card: true }" :columns="columns" default-mode="table" :right-search="true" diff --git a/src/pages/Account/AccountAliasList.vue b/src/pages/Account/AccountAliasList.vue index b6f7b219c..c67283297 100644 --- a/src/pages/Account/AccountAliasList.vue +++ b/src/pages/Account/AccountAliasList.vue @@ -21,24 +21,21 @@ const columns = computed(() => [ { align: 'left', name: 'id', - label: t('id'), + label: t('Id'), isId: true, - field: 'id', cardVisible: true, }, { align: 'left', name: 'alias', - label: t('alias'), - field: 'alias', + label: t('Alias'), cardVisible: true, create: true, }, { align: 'left', name: 'description', - label: t('description'), - field: 'description', + label: t('Description'), cardVisible: true, create: true, }, @@ -69,9 +66,17 @@ const columns = computed(() => [ }" order="id DESC" :columns="columns" + :disable-option="{ card: true }" default-mode="table" redirect="account/alias" :is-editable="true" :use-model="true" /> </template> + +<i18n> + es: + Id: Id + Alias: Alias + Description: Descripción +</i18n> diff --git a/src/pages/Account/AccountList.vue b/src/pages/Account/AccountList.vue index cdd88551b..d698596b9 100644 --- a/src/pages/Account/AccountList.vue +++ b/src/pages/Account/AccountList.vue @@ -14,15 +14,23 @@ const columns = computed(() => [ { align: 'left', name: 'id', - label: t('id'), + label: t('Id'), isId: true, field: 'id', cardVisible: true, + columnFilter: { + component: 'select', + name: 'search', + attrs: { + url: 'VnUsers/preview', + fields: ['id', 'name'], + }, + }, }, { align: 'left', name: 'username', - label: t('nickname'), + label: t('Nickname'), isTitle: true, component: 'input', columnField: { @@ -37,7 +45,7 @@ const columns = computed(() => [ { align: 'left', name: 'name', - label: t('name'), + label: t('Name'), component: 'input', columnField: { component: null, @@ -65,6 +73,7 @@ const columns = computed(() => [ title: t('View Summary'), icon: 'preview', action: (row) => viewSummary(row.id, AccountSummary), + isPrimary: true, }, ], }, @@ -108,3 +117,10 @@ const exprBuilder = (param, value) => { :use-model="true" /> </template> + +<i18n> + es: + Id: Id + Nickname: Nickname + Name: Nombre +</i18n> diff --git a/src/pages/Account/Card/AccountCard.vue b/src/pages/Account/Card/AccountCard.vue index a9857b283..67fa15898 100644 --- a/src/pages/Account/Card/AccountCard.vue +++ b/src/pages/Account/Card/AccountCard.vue @@ -15,7 +15,6 @@ const { t } = useI18n(); url: 'VnUsers/preview', label: t('account.search'), info: t('account.searchInfo'), - searchUrl: 'table', }" /> </template> diff --git a/src/pages/Account/Card/AccountPrivileges.vue b/src/pages/Account/Card/AccountPrivileges.vue index 1300f5018..0574e08d2 100644 --- a/src/pages/Account/Card/AccountPrivileges.vue +++ b/src/pages/Account/Card/AccountPrivileges.vue @@ -1,5 +1,5 @@ <script setup> -import { ref } from 'vue'; +import { ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; @@ -12,17 +12,14 @@ const route = useRoute(); const rolesOptions = ref([]); const formModelRef = ref(); +watch( + () => route.params.id, + () => formModelRef.value.reset() +); </script> <template> <FetchData url="VnRoles" auto-load @on-fetch="(data) => (rolesOptions = data)" /> - <FormModel - ref="formModelRef" - model="AccountPrivileges" - :url="`VnUsers/${route.params.id}/privileges`" - :url-create="`VnUsers/${route.params.id}/privileges`" - auto-load - @on-data-saved="formModelRef.fetch()" - > + <FormModel ref="formModelRef" model="AccountPrivileges" url="VnUsers" auto-load> <template #form="{ data }"> <div class="q-gutter-y-sm"> <QCheckbox diff --git a/src/pages/Account/Role/AccountRoles.vue b/src/pages/Account/Role/AccountRoles.vue index 8f3372a6d..ea175d913 100644 --- a/src/pages/Account/Role/AccountRoles.vue +++ b/src/pages/Account/Role/AccountRoles.vue @@ -21,24 +21,30 @@ const columns = computed(() => [ { align: 'left', name: 'id', - label: t('id'), + label: t('Id'), isId: true, columnFilter: { inWhere: true, + component: 'select', + name: 'search', + attrs: { + url: 'VnRoles', + fields: ['id', 'name'], + }, }, cardVisible: true, }, { align: 'left', name: 'name', - label: t('name'), + label: t('Name'), cardVisible: true, create: true, }, { align: 'left', name: 'description', - label: t('description'), + label: t('Description'), cardVisible: true, create: true, }, @@ -51,6 +57,7 @@ const columns = computed(() => [ title: t('View Summary'), icon: 'preview', action: (row) => viewSummary(row.id, RoleSummary), + isPrimary: true, }, ], }, @@ -93,8 +100,16 @@ const exprBuilder = (param, value) => { }, }" order="id ASC" + :disable-option="{ card: true }" :columns="columns" default-mode="table" redirect="account/role" /> </template> + +<i18n> + es: + Id: Id + Description: Descripción + Name: Nombre +</i18n> diff --git a/src/pages/Claim/Card/ClaimSummary.vue b/src/pages/Claim/Card/ClaimSummary.vue index 9538e753f..244eb5936 100644 --- a/src/pages/Claim/Card/ClaimSummary.vue +++ b/src/pages/Claim/Card/ClaimSummary.vue @@ -1,11 +1,10 @@ <script setup> import axios from 'axios'; -import { onMounted, ref, computed } from 'vue'; +import { ref, computed } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { toDate, toCurrency } from 'src/filters'; import dashIfEmpty from 'src/filters/dashIfEmpty'; -import { getUrl } from 'src/composables/getUrl'; import { useSession } from 'src/composables/useSession'; import VnLv from 'src/components/ui/VnLv.vue'; diff --git a/src/pages/Claim/ClaimList.vue b/src/pages/Claim/ClaimList.vue index b03dfb226..eae9721ab 100644 --- a/src/pages/Claim/ClaimList.vue +++ b/src/pages/Claim/ClaimList.vue @@ -79,7 +79,7 @@ const columns = computed(() => [ align: 'left', label: t('claim.state'), format: ({ stateCode }) => - claimFilterRef.value?.states.find(({code}) => code === stateCode) + claimFilterRef.value?.states.find(({ code }) => code === stateCode) ?.description, name: 'stateCode', chip: { @@ -100,7 +100,7 @@ const columns = computed(() => [ name: 'tableActions', actions: [ { - title: t('Client ticket list'), + title: t('components.smartCard.viewSummary'), icon: 'preview', action: (row) => viewSummary(row.id, ClaimSummary), }, diff --git a/src/pages/Customer/Card/CustomerSummary.vue b/src/pages/Customer/Card/CustomerSummary.vue index 15bf19b48..da50ba239 100644 --- a/src/pages/Customer/Card/CustomerSummary.vue +++ b/src/pages/Customer/Card/CustomerSummary.vue @@ -217,7 +217,11 @@ const creditWarning = computed(() => { /> </QCard> <QCard class="vn-one" v-if="entity.account"> - <VnTitle :text="t('customer.summary.businessData')" /> + <VnTitle + :url="`https://grafana.verdnatura.es/d/adjlxzv5yjt34d/analisis-de-clientes-7c-crm?orgId=1&var-clientFk=${entityId}`" + :text="t('customer.summary.businessData')" + icon="vn:grafana" + /> <VnLv :label="t('customer.summary.totalGreuge')" :value="toCurrency(entity.totalGreuge)" diff --git a/src/pages/Customer/CustomerList.vue b/src/pages/Customer/CustomerList.vue index 82ad559ad..0dc7f09be 100644 --- a/src/pages/Customer/CustomerList.vue +++ b/src/pages/Customer/CustomerList.vue @@ -357,7 +357,7 @@ const columns = computed(() => [ isPrimary: true, }, { - title: t('Client ticket list'), + title: t('components.smartCard.viewSummary'), icon: 'preview', action: (row) => viewSummary(row.id, CustomerSummary), }, diff --git a/src/pages/Entry/Card/EntryBuys.vue b/src/pages/Entry/Card/EntryBuys.vue index 6e66f4ce7..e9a9ab815 100644 --- a/src/pages/Entry/Card/EntryBuys.vue +++ b/src/pages/Entry/Card/EntryBuys.vue @@ -423,7 +423,7 @@ const lockIconType = (groupingMode, mode) => { <span v-if="props.row.item.subName" class="subName"> {{ props.row.item.subName }} </span> - <FetchedTags :item="props.row.item" :max-length="5" /> + <FetchedTags :item="props.row.item" /> </QTd> </QTr> </template> diff --git a/src/pages/Entry/Card/EntrySummary.vue b/src/pages/Entry/Card/EntrySummary.vue index b32dc70a9..379be1d2f 100644 --- a/src/pages/Entry/Card/EntrySummary.vue +++ b/src/pages/Entry/Card/EntrySummary.vue @@ -319,7 +319,7 @@ const fetchEntryBuys = async () => { <span v-if="row.item.subName" class="subName"> {{ row.item.subName }} </span> - <FetchedTags :item="row.item" :max-length="5" /> + <FetchedTags :item="row.item" /> </QTd> </QTr> <!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys --> diff --git a/src/pages/Entry/EntryList.vue b/src/pages/Entry/EntryList.vue index bd5ace677..6f7ff1935 100644 --- a/src/pages/Entry/EntryList.vue +++ b/src/pages/Entry/EntryList.vue @@ -7,11 +7,16 @@ import { useStateStore } from 'stores/useStateStore'; import VnTable from 'components/VnTable/VnTable.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import { toDate } from 'src/filters'; +import { useSummaryDialog } from 'src/composables/useSummaryDialog'; +import EntrySummary from './Card/EntrySummary.vue'; +import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; +import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue'; const stateStore = useStateStore(); const { t } = useI18n(); const tableRef = ref(); +const { viewSummary } = useSummaryDialog(); const entryFilter = { include: [ { @@ -142,6 +147,12 @@ const columns = computed(() => [ create: true, format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef), }, + { + align: 'left', + label: t('entry.list.tableVisibleColumns.invoiceAmount'), + name: 'invoiceAmount', + cardVisible: true, + }, { align: 'left', label: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'), @@ -168,6 +179,18 @@ const columns = computed(() => [ inWhere: true, }, }, + { + align: 'right', + name: 'tableActions', + actions: [ + { + title: t('components.smartCard.viewSummary'), + icon: 'preview', + action: (row) => viewSummary(row.id, EntrySummary), + isPrimary: true, + }, + ], + }, ]); onMounted(async () => { stateStore.rightDrawer = true; @@ -201,7 +224,20 @@ onMounted(async () => { redirect="entry" auto-load :right-search="false" - /> + > + <template #column-supplierFk="{ row }"> + <span class="link" @click.stop> + {{ row.supplierName }} + <SupplierDescriptorProxy :id="row.supplierFk" /> + </span> + </template> + <template #column-travelFk="{ row }"> + <span class="link" @click.stop> + {{ row.travelRef }} + <TravelDescriptorProxy :id="row.travelFk" /> + </span> + </template> + </VnTable> </template> <i18n> diff --git a/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue b/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue index 7dbd0fe9e..62beb88ad 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue @@ -18,6 +18,7 @@ const { notify } = useNotify(); const { t } = useI18n(); const arrayData = useArrayData(); const invoiceIn = computed(() => arrayData.store.data); +const currency = computed(() => invoiceIn.value?.currency?.code); const rowsSelected = ref([]); const banks = ref([]); @@ -139,9 +140,9 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount, <QTd> <VnInputNumber :class="{ - 'no-pointer-events': !isNotEuro(invoiceIn.currency.code), + 'no-pointer-events': !isNotEuro(currency), }" - :disable="!isNotEuro(invoiceIn.currency.code)" + :disable="!isNotEuro(currency)" v-model="row.foreignValue" clearable clear-icon="close" @@ -154,9 +155,7 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount, <QTd /> <QTd /> <QTd> - {{ - toCurrency(getTotalAmount(rows), invoiceIn.currency.code) - }} + {{ toCurrency(getTotalAmount(rows), currency) }} </QTd> <QTd /> </QTr> @@ -208,11 +207,9 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount, :label="t('Foreign value')" class="full-width" :class="{ - 'no-pointer-events': !isNotEuro( - invoiceIn.currency.code - ), + 'no-pointer-events': !isNotEuro(currency), }" - :disable="!isNotEuro(invoiceIn.currency.code)" + :disable="!isNotEuro(currency)" v-model="props.row.foreignValue" clearable clear-icon="close" diff --git a/src/pages/InvoiceIn/Card/InvoiceInVat.vue b/src/pages/InvoiceIn/Card/InvoiceInVat.vue index 4dac5058e..34b0b64bd 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInVat.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInVat.vue @@ -242,11 +242,12 @@ const formatOpt = (row, { model, options }, prop) => { </template> <template #body-cell-taxablebase="{ row }"> <QTd> + {{ currency }} <VnInputNumber :class="{ - 'no-pointer-events': isNotEuro(invoiceIn.currency.code), + 'no-pointer-events': isNotEuro(currency), }" - :disable="isNotEuro(invoiceIn.currency.code)" + :disable="isNotEuro(currency)" label="" clear-icon="close" v-model="row.taxableBase" @@ -312,9 +313,9 @@ const formatOpt = (row, { model, options }, prop) => { <QTd> <VnInputNumber :class="{ - 'no-pointer-events': !isNotEuro(invoiceIn.currency.code), + 'no-pointer-events': !isNotEuro(currency), }" - :disable="!isNotEuro(invoiceIn.currency.code)" + :disable="!isNotEuro(currency)" v-model="row.foreignValue" /> </QTd> @@ -361,12 +362,10 @@ const formatOpt = (row, { model, options }, prop) => { <VnInputNumber :label="t('Taxable base')" :class="{ - 'no-pointer-events': isNotEuro( - invoiceIn.currency.code - ), + 'no-pointer-events': isNotEuro(currency), }" class="full-width" - :disable="isNotEuro(invoiceIn.currency.code)" + :disable="isNotEuro(currency)" clear-icon="close" v-model="props.row.taxableBase" clearable @@ -427,11 +426,9 @@ const formatOpt = (row, { model, options }, prop) => { :label="t('Foreign value')" class="full-width" :class="{ - 'no-pointer-events': !isNotEuro( - invoiceIn.currency.code - ), + 'no-pointer-events': !isNotEuro(currency), }" - :disable="!isNotEuro(invoiceIn.currency.code)" + :disable="!isNotEuro(currency)" v-model="props.row.foreignValue" /> </QItem> diff --git a/src/pages/InvoiceOut/Card/InvoiceOutDescriptorMenu.vue b/src/pages/InvoiceOut/Card/InvoiceOutDescriptorMenu.vue index 4c02ccf84..213e25b4a 100644 --- a/src/pages/InvoiceOut/Card/InvoiceOutDescriptorMenu.vue +++ b/src/pages/InvoiceOut/Card/InvoiceOutDescriptorMenu.vue @@ -5,6 +5,7 @@ import { useRouter } from 'vue-router'; import { useQuasar } from 'quasar'; import TransferInvoiceForm from 'src/components/TransferInvoiceForm.vue'; +import RefundInvoiceForm from 'src/components/RefundInvoiceForm.vue'; import SendEmailDialog from 'components/common/SendEmailDialog.vue'; import useNotify from 'src/composables/useNotify'; @@ -141,6 +142,15 @@ const showTransferInvoiceForm = async () => { }, }); }; + +const showRefundInvoiceForm = () => { + quasar.dialog({ + component: RefundInvoiceForm, + componentProps: { + invoiceOutData: $props.invoiceOutData, + }, + }); +}; </script> <template> @@ -229,10 +239,13 @@ const showTransferInvoiceForm = async () => { <QMenu anchor="top end" self="top start"> <QList> <QItem v-ripple clickable @click="refundInvoice(true)"> - <QItemSection>{{ t('With warehouse') }}</QItemSection> + <QItemSection>{{ t('With warehouse, no invoice') }}</QItemSection> </QItem> <QItem v-ripple clickable @click="refundInvoice(false)"> - <QItemSection>{{ t('Without warehouse') }}</QItemSection> + <QItemSection>{{ t('Without warehouse, no invoice') }}</QItemSection> + </QItem> + <QItem v-ripple clickable @click="showRefundInvoiceForm()"> + <QItemSection>{{ t('Invoiced') }}</QItemSection> </QItem> </QList> </QMenu> @@ -255,8 +268,9 @@ es: As CSV: como CSV Send PDF: Enviar PDF Send CSV: Enviar CSV - With warehouse: Con almacén - Without warehouse: Sin almacén + With warehouse, no invoice: Con almacén, sin factura + Without warehouse, no invoice: Sin almacén, sin factura + Invoiced: Facturado InvoiceOut deleted: Factura eliminada Confirm deletion: Confirmar eliminación Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura? diff --git a/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue b/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue index d4373f311..5bcb21001 100644 --- a/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue +++ b/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue @@ -101,7 +101,17 @@ onMounted(async () => { dense outlined rounded - /> + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + #{{ scope.opt?.id }} {{ scope.opt?.name }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> <VnSelect :label="t('invoiceOutSerialType')" v-model="formData.serialType" diff --git a/src/pages/InvoiceOut/InvoiceOutList.vue b/src/pages/InvoiceOut/InvoiceOutList.vue index f6dc0c674..63d93e074 100644 --- a/src/pages/InvoiceOut/InvoiceOutList.vue +++ b/src/pages/InvoiceOut/InvoiceOutList.vue @@ -19,7 +19,6 @@ const stateStore = useStateStore(); const { viewSummary } = useSummaryDialog(); const tableRef = ref(); const invoiceOutSerialsOptions = ref([]); -const ticketsOptions = ref([]); const customerOptions = ref([]); const selectedRows = ref([]); const hasSelectedCards = computed(() => selectedRows.value.length > 0); @@ -122,7 +121,7 @@ const columns = computed(() => [ name: 'tableActions', actions: [ { - title: t('InvoiceOutSummary'), + title: t('components.smartCard.viewSummary'), icon: 'preview', action: (row) => viewSummary(row.id, InvoiceOutSummary), }, @@ -226,10 +225,18 @@ watchEffect(selectedRows); url="Tickets" v-model="data.ticketFk" :label="t('invoiceOutList.tableVisibleColumns.ticket')" - :options="ticketsOptions" - option-label="nickname" + 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> <VnSelect diff --git a/src/pages/Item/Card/ItemBotanical.vue b/src/pages/Item/Card/ItemBotanical.vue index e9f1f11ee..0687b8db3 100644 --- a/src/pages/Item/Card/ItemBotanical.vue +++ b/src/pages/Item/Card/ItemBotanical.vue @@ -14,8 +14,6 @@ const route = useRoute(); const { t } = useI18n(); const itemBotanicalsRef = ref(null); -const itemGenusOptions = ref([]); -const itemSpeciesOptions = ref([]); const itemBotanicals = ref([]); let itemBotanicalsForm = reactive({ itemFk: null }); diff --git a/src/pages/Item/ItemFixedPrice.vue b/src/pages/Item/ItemFixedPrice.vue index 2ecd1f21b..6b469d13a 100644 --- a/src/pages/Item/ItemFixedPrice.vue +++ b/src/pages/Item/ItemFixedPrice.vue @@ -436,7 +436,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); {{ row.name }} </span> <ItemDescriptorProxy :id="row.itemFk" /> - <FetchedTags :item="row" :max-length="6" /> + <FetchedTags :item="row" /> </QTd> </template> <template #body-cell-groupingPrice="props"> diff --git a/src/pages/Item/ItemList.vue b/src/pages/Item/ItemList.vue index f1e3629cd..2e829c6a1 100644 --- a/src/pages/Item/ItemList.vue +++ b/src/pages/Item/ItemList.vue @@ -517,7 +517,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); <template #body-cell-description="{ row }"> <QTd class="col"> <span>{{ row.name }} {{ row.subName }}</span> - <FetchedTags :item="row" :max-length="6" /> + <FetchedTags :item="row" /> </QTd> </template> <template #body-cell-isActive="{ row }"> diff --git a/src/pages/Item/ItemRequest.vue b/src/pages/Item/ItemRequest.vue index c1ee15a7e..8a41bbe04 100644 --- a/src/pages/Item/ItemRequest.vue +++ b/src/pages/Item/ItemRequest.vue @@ -1,7 +1,6 @@ <script setup> import { ref, computed, onMounted, onBeforeMount, watch } from 'vue'; import { useI18n } from 'vue-i18n'; -import FetchData from 'components/FetchData.vue'; import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; diff --git a/src/pages/Monitor/MonitorClients.vue b/src/pages/Monitor/MonitorClients.vue new file mode 100644 index 000000000..ff51e4464 --- /dev/null +++ b/src/pages/Monitor/MonitorClients.vue @@ -0,0 +1,145 @@ +<script setup> +import { ref, computed } from 'vue'; +import { useI18n } from 'vue-i18n'; +import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; +import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; +import { toDateFormat } from 'src/filters/date.js'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import VnInputDate from 'src/components/common/VnInputDate.vue'; +import VnRow from 'src/components/ui/VnRow.vue'; +import { dateRange } from 'src/filters'; +const { t } = useI18n(); + +const dates = dateRange(Date.vnNew()); +const from = ref(dates[0]); +const to = ref(dates[1]); + +const filter = computed(() => { + const obj = {}; + const formatFrom = setHours(from.value, 'from'); + const formatTo = setHours(to.value, 'to'); + let stamp; + + if (!formatFrom && formatTo) stamp = { lte: formatTo }; + else if (formatFrom && !formatTo) stamp = { gte: formatFrom }; + else if (formatFrom && formatTo) stamp = { between: [formatFrom, formatTo] }; + + return Object.assign(obj, { where: { 'v.stamp': stamp } }); +}); + +function exprBuilder(param, value) { + switch (param) { + case 'clientFk': + return { [`c.id`]: value }; + case 'salesPersonFk': + return { [`c.${param}`]: value }; + } +} + +function setHours(date, type) { + if (!date) return null; + + const d = new Date(date); + if (type == 'from') d.setHours(0, 0, 0, 0); + else d.setHours(23, 59, 59, 59); + return d; +} + +const columns = computed(() => [ + { + label: t('salesClientsTable.date'), + name: 'dated', + field: 'dated', + align: 'left', + columnFilter: false, + format: (row) => toDateFormat(row.dated, 'es-ES', { year: '2-digit' }), + }, + { + label: t('salesClientsTable.hour'), + name: 'hour', + field: 'hour', + align: 'left', + columnFilter: false, + }, + { + label: t('salesClientsTable.salesPerson'), + name: 'salesPersonFk', + field: 'salesPerson', + align: 'left', + columnField: { + component: null, + }, + optionFilter: 'firstName', + columnFilter: { + component: 'select', + attrs: { + url: 'Workers/activeWithInheritedRole', + fields: ['id', 'name'], + sortBy: 'nickname ASC', + where: { role: 'salesPerson' }, + useLike: false, + }, + }, + columnClass: 'no-padding', + }, + { + label: t('salesClientsTable.client'), + field: 'clientName', + name: 'clientFk', + align: 'left', + columnField: { + component: null, + }, + orderBy: 'c.name', + columnFilter: { + component: 'select', + attrs: { + url: 'Clients', + fields: ['id', 'name'], + sortBy: 'name ASC', + }, + }, + columnClass: 'no-padding', + }, +]); +</script> + +<template> + <VnTable + ref="table" + data-key="SalesMonitorClients" + url="SalesMonitors/clientsFilter" + search-url="SalesMonitorClients" + :order="['dated DESC', 'hour DESC']" + :expr-builder="exprBuilder" + :filter="filter" + :offset="50" + auto-load + :columns="columns" + :right-search="false" + default-mode="table" + :disable-option="{ card: true }" + dense + class="q-px-none" + > + <template #top-left> + <VnRow> + <VnInputDate v-model="from" :label="$t('globals.from')" dense /> + <VnInputDate v-model="to" :label="$t('globals.to')" dense /> + </VnRow> + </template> + <template #column-salesPersonFk="{ row }"> + <span class="link" :title="row.salesPerson" v-text="row.salesPerson" /> + <WorkerDescriptorProxy :id="row.salesPersonFk" dense /> + </template> + <template #column-clientFk="{ row }"> + <span class="link" :title="row.clientName" v-text="row.clientName" /> + <CustomerDescriptorProxy :id="row.clientFk" /> + </template> + </VnTable> +</template> +<style lang="scss" scoped> +.full-width .vn-row > * { + flex: 0.4; +} +</style> diff --git a/src/pages/Monitor/MonitorClientsActions.vue b/src/pages/Monitor/MonitorClientsActions.vue new file mode 100644 index 000000000..821773bbf --- /dev/null +++ b/src/pages/Monitor/MonitorClientsActions.vue @@ -0,0 +1,26 @@ +<script setup> +import SalesClientTable from './MonitorClients.vue'; +import SalesOrdersTable from './MonitorOrders.vue'; +import VnRow from 'src/components/ui/VnRow.vue'; +</script> +<template> + <VnRow + class="q-pa-md" + :style="{ 'flex-direction': $q.screen.lt.lg ? 'column' : 'row', gap: '0px' }" + > + <div style="flex: 0.3"> + <span + class="q-ml-md text-body1" + v-text="$t('salesMonitor.clientsOnWebsite')" + /> + <SalesClientTable /> + </div> + <div style="flex: 0.7"> + <span + class="q-ml-md text-body1" + v-text="$t('salesMonitor.recentOrderActions')" + /> + <SalesOrdersTable /> + </div> + </VnRow> +</template> diff --git a/src/pages/Monitor/MonitorList.vue b/src/pages/Monitor/MonitorList.vue deleted file mode 100644 index 4906247e8..000000000 --- a/src/pages/Monitor/MonitorList.vue +++ /dev/null @@ -1,90 +0,0 @@ -<script setup> -import { onMounted, onUnmounted, ref } from 'vue'; -import { useI18n } from 'vue-i18n'; - -import { useStateStore } from 'stores/useStateStore'; -import SalesClientTable from './SalesClientsTable.vue'; -import SalesOrdersTable from './SalesOrdersTable.vue'; -import SalesTicketsTable from './SalesTicketsTable.vue'; -import VnSearchbar from 'components/ui/VnSearchbar.vue'; - -const { t } = useI18n(); -const stateStore = useStateStore(); - -const expanded = ref(true); - -onMounted(async () => { - stateStore.leftDrawer = false; -}); - -onUnmounted(() => (stateStore.leftDrawer = true)); -</script> - -<template> - <template v-if="stateStore.isHeaderMounted()"> - <Teleport to="#searchbar"> - <VnSearchbar - data-key="SalesMonitorTickets" - url="SalesMonitors/salesFilter" - :redirect="false" - :label="t('searchBar.label')" - :info="t('searchBar.info')" - /> - </Teleport> - </template> - <QPage class="column items-center q-pa-md"> - <QCard class="full-width q-mb-lg"> - <QExpansionItem v-model="expanded" dense :duration="150"> - <template v-if="!expanded" #header> - <div class="row full-width"> - <span class="flex col text-body1"> - {{ t('salesMonitor.clientsOnWebsite') }} - </span> - <span class="flex col q-ml-xl text-body1"> - {{ t('salesMonitor.recentOrderActions') }} - </span> - </div> - </template> - <template #default> - <div class="expansion-tables-container"> - <QCardSection class="col"> - <span class="flex col q-mb-sm text-body1"> - {{ t('salesMonitor.clientsOnWebsite') }} - </span> - <SalesClientTable /> - </QCardSection> - <QCardSection class="col"> - <span class="flex col q-mb-sm text-body1"> - {{ t('salesMonitor.recentOrderActions') }} - </span> - <SalesOrdersTable /> - </QCardSection> - </div> - </template> - </QExpansionItem> - </QCard> - <QCard class="full-width"> - <QItem class="justify-between"> - <QItemLabel class="col slider-container"> - <span class="text-body1" - >{{ t('salesMonitor.ticketsMonitor') }} - </span> - <QCardSection class="col" style="padding-inline: 0" - ><SalesTicketsTable /> - </QCardSection> - </QItemLabel> - </QItem> - </QCard> - </QPage> -</template> - -<style lang="scss" scoped> -.expansion-tables-container { - display: flex; - border-top: 1px solid $color-spacer; - - @media (max-width: $breakpoint-md-max) { - flex-direction: column; - } -} -</style> diff --git a/src/pages/Monitor/MonitorOrders.vue b/src/pages/Monitor/MonitorOrders.vue new file mode 100644 index 000000000..eb455a239 --- /dev/null +++ b/src/pages/Monitor/MonitorOrders.vue @@ -0,0 +1,203 @@ +<script setup> +import { ref, computed } from 'vue'; +import { useI18n } from 'vue-i18n'; +import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; +import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; +import VnTable from 'components/VnTable/VnTable.vue'; + +import { toDateFormat, toDateTimeFormat } from 'src/filters/date.js'; +import { toCurrency } from 'src/filters'; +import { useVnConfirm } from 'composables/useVnConfirm'; +import axios from 'axios'; + +const { t } = useI18n(); +const { openConfirmationModal } = useVnConfirm(); + +const table = ref(); +const selectedRows = ref([]); + +function exprBuilder(param, value) { + switch (param) { + case 'clientFk': + return { [`c.id`]: value }; + case 'salesPersonFk': + return { [`c.salesPersonFk`]: value }; + } +} + +const columns = computed(() => [ + { + label: t('salesOrdersTable.dateSend'), + name: 'dateSend', + field: 'dateSend', + align: 'left', + orderBy: 'date_send', + columnFilter: false, + }, + { + label: t('salesOrdersTable.dateMake'), + name: 'dateMake', + field: 'dateMake', + align: 'left', + orderBy: 'date_make', + columnFilter: false, + format: (row) => toDateTimeFormat(row.date_make), + }, + { + label: t('salesOrdersTable.client'), + name: 'clientFk', + align: 'left', + columnFilter: { + component: 'select', + attrs: { + url: 'Clients', + fields: ['id', 'name'], + sortBy: 'name ASC', + }, + }, + }, + { + label: t('salesOrdersTable.agency'), + name: 'agencyName', + align: 'left', + columnFilter: false, + }, + { + label: t('salesOrdersTable.salesPerson'), + name: 'salesPersonFk', + align: 'left', + optionFilter: 'firstName', + columnFilter: { + component: 'select', + attrs: { + url: 'Workers/activeWithInheritedRole', + fields: ['id', 'name'], + sortBy: 'nickname ASC', + where: { role: 'salesPerson' }, + useLike: false, + }, + }, + }, + { + label: t('salesOrdersTable.import'), + name: 'import', + field: 'import', + align: 'left', + columnFilter: false, + format: (row) => toCurrency(row.import), + }, +]); + +const getBadgeColor = (date) => { + const today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + + const orderLanded = new Date(date); + orderLanded.setHours(0, 0, 0, 0); + + const difference = today - orderLanded; + + if (difference == 0) return 'warning'; + if (difference < 0) return 'success'; + if (difference > 0) return 'alert'; +}; + +const removeOrders = async () => { + try { + const selectedIds = selectedRows.value.map((row) => row.id); + const params = { deletes: selectedIds }; + await axios.post('SalesMonitors/deleteOrders', params); + selectedRows.value = []; + await table.value.reload(); + } catch (err) { + console.error('Error deleting orders', err); + } +}; + +const openTab = (id) => + window.open(`#/order/${id}/summary`, '_blank', 'noopener, noreferrer'); +</script> +<template> + <VnTable + ref="table" + class="q-px-none" + data-key="SalesMonitorOrders" + url="SalesMonitors/ordersFilter" + search-url="SalesMonitorOrders" + order="date_send DESC" + :right-search="false" + :expr-builder="exprBuilder" + auto-load + :columns="columns" + :table="{ + 'row-key': 'id', + selection: 'multiple', + 'hide-bottom': true, + }" + default-mode="table" + :row-click="({ id }) => openTab(id)" + v-model:selected="selectedRows" + :disable-option="{ card: true }" + > + <template #top-left> + <QBtn + icon="refresh" + size="md" + color="primary" + dense + flat + @click="$refs.table.reload()" + > + <QTooltip>{{ $t('globals.refresh') }}</QTooltip> + </QBtn> + <QBtn + v-if="selectedRows.length" + icon="delete" + size="md" + dense + flat + color="primary" + @click=" + openConfirmationModal( + $t('salesOrdersTable.deleteConfirmTitle'), + $t('salesOrdersTable.deleteConfirmMessage'), + removeOrders + ) + " + > + <QTooltip>{{ t('salesOrdersTable.delete') }}</QTooltip> + </QBtn> + </template> + <template #column-dateSend="{ row }"> + <QTd> + <QBadge + :color="getBadgeColor(row.date_send)" + text-color="black" + class="q-pa-sm" + style="font-size: 14px" + > + {{ toDateFormat(row.date_send) }} + </QBadge> + </QTd> + </template> + + <template #column-clientFk="{ row }"> + <QTd @click.stop> + <span class="link" v-text="row.clientName" :title="row.clientName" /> + <CustomerDescriptorProxy :id="row.clientFk" /> + </QTd> + </template> + + <template #column-salesPersonFk="{ row }"> + <QTd @click.stop> + <span class="link" v-text="row.salesPerson" /> + <WorkerDescriptorProxy :id="row.salesPersonFk" dense /> + </QTd> + </template> + </VnTable> +</template> +<style lang="scss" scoped> +.q-td { + max-width: 140px; +} +</style> diff --git a/src/pages/Monitor/SalesClientsTable.vue b/src/pages/Monitor/SalesClientsTable.vue deleted file mode 100644 index 2fb9e8e2f..000000000 --- a/src/pages/Monitor/SalesClientsTable.vue +++ /dev/null @@ -1,147 +0,0 @@ -<script setup> -import { ref, computed, reactive, watch } from 'vue'; -import { useI18n } from 'vue-i18n'; - -import FetchData from 'components/FetchData.vue'; -import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; -import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; - -import { toDateFormat } from 'src/filters/date.js'; -import VnTable from 'src/components/VnTable/VnTable.vue'; - -const { t } = useI18n(); - -const paginateRef = ref(null); -const workersActiveOptions = ref([]); -const clientsOptions = ref([]); - -const from = ref(Date.vnNew()); -const to = ref(Date.vnNew()); - -const dateRange = computed(() => { - const minHour = new Date(from.value); - minHour.setHours(0, 0, 0, 0); - const maxHour = new Date(to.value); - maxHour.setHours(23, 59, 59, 59); - return [minHour, maxHour]; -}); - -const filter = reactive({ - where: { - 'v.stamp': { - between: dateRange.value, - }, - }, -}); - -const refetch = async () => await paginateRef.value.fetch(); - -watch(dateRange, (val) => { - filter.where['v.stamp'].between = val; - refetch(); -}); - -function exprBuilder(param, value) { - switch (param) { - case 'clientFk': - return { [`c.id`]: value }; - case 'salesPersonFk': - return { [`c.${param}`]: value }; - } -} - -const params = reactive({}); - -const columns = computed(() => [ - { - label: t('salesClientsTable.date'), - name: 'dated', - field: 'dated', - align: 'left', - columnFilter: null, - sortable: true, - format: (row) => toDateFormat(row.dated), - }, - { - label: t('salesClientsTable.hour'), - name: 'hour', - field: 'hour', - align: 'left', - sortable: true, - }, - { - label: t('salesClientsTable.salesPerson'), - name: 'salesPerson', - field: 'salesPerson', - align: 'left', - sortable: true, - columnField: { - component: null, - }, - }, - { - label: t('salesClientsTable.client'), - field: 'clientName', - name: 'client', - align: 'left', - sortable: true, - columnField: { - component: null, - }, - }, -]); -</script> - -<template> - <FetchData - url="Workers/activeWithInheritedRole" - :filter="{ - fields: ['id', 'nickname'], - order: 'nickname ASC', - where: { role: 'salesPerson' }, - }" - auto-load - @on-fetch="(data) => (workersActiveOptions = data)" - /> - <FetchData - url="Clients" - :filter="{ - fields: ['id', 'name'], - order: 'name ASC', - }" - auto-load - @on-fetch="(data) => (clientsOptions = data)" - /> - <QCard style="max-height: 380px; overflow-y: scroll"> - <VnTable - ref="paginateRef" - data-key="SalesMonitorClients" - url="SalesMonitors/clientsFilter" - :order="['dated DESC', 'hour DESC']" - :limit="6" - :expr-builder="exprBuilder" - :user-params="params" - :filter="filter" - :offset="50" - auto-load - :columns="columns" - :right-search="false" - default-mode="table" - dense - :without-header="true" - > - <template #column-salesPerson="{ row }"> - <QTd> - <span class="link">{{ row.salesPerson }}</span> - <WorkerDescriptorProxy :id="row.salesPersonFk" dense /> - </QTd> - </template> - <template #column-client="{ row }"> - <QTd> - <span class="link">{{ row.clientName }}</span> - <CustomerDescriptorProxy :id="row.clientFk" /> - </QTd> - </template> - </VnTable> - </QCard> -</template> diff --git a/src/pages/Monitor/SalesOrdersTable.vue b/src/pages/Monitor/SalesOrdersTable.vue deleted file mode 100644 index f0c389aa6..000000000 --- a/src/pages/Monitor/SalesOrdersTable.vue +++ /dev/null @@ -1,204 +0,0 @@ -<script setup> -import { ref, computed } from 'vue'; -import { useI18n } from 'vue-i18n'; - -import FetchData from 'components/FetchData.vue'; -import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; -import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; -import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; -import VnTable from 'components/VnTable/VnTable.vue'; - -import { toDateFormat, toDateTimeFormat } from 'src/filters/date.js'; -import { toCurrency } from 'src/filters'; -import { useVnConfirm } from 'composables/useVnConfirm'; -import axios from 'axios'; - -const { t } = useI18n(); -const { openConfirmationModal } = useVnConfirm(); - -const paginateRef = ref(null); -const workersActiveOptions = ref([]); -const clientsOptions = ref([]); -const selectedRows = ref([]); - -const dateRange = (value) => { - const minHour = new Date(value); - minHour.setHours(0, 0, 0, 0); - const maxHour = new Date(value); - maxHour.setHours(23, 59, 59, 59); - - return [minHour, maxHour]; -}; - -function exprBuilder(param, value) { - switch (param) { - case 'date_send': - return { - [`o.date_send`]: { - between: dateRange(value), - }, - }; - case 'clientFk': - return { [`c.id`]: value }; - case 'salesPersonFk': - return { [`c.${param}`]: value }; - } -} - -const columns = computed(() => [ - { - label: t('salesOrdersTable.date'), - name: 'date', - field: 'dated', - align: 'left', - sortable: true, - cardVisible: true, - }, - { - label: t('salesOrdersTable.client'), - name: 'client', - align: 'left', - sortable: true, - cardVisible: true, - }, - { - label: t('salesOrdersTable.salesPerson'), - name: 'salesPerson', - align: 'left', - sortable: true, - cardVisible: true, - }, -]); - -const getBadgeColor = (date) => { - const today = Date.vnNew(); - today.setHours(0, 0, 0, 0); - - const orderLanded = new Date(date); - orderLanded.setHours(0, 0, 0, 0); - - const difference = today - orderLanded; - - if (difference == 0) return 'warning'; - if (difference < 0) return 'success'; - if (difference > 0) return 'alert'; -}; - -const removeOrders = async () => { - try { - const selectedIds = selectedRows.value.map((row) => row.id); - const params = { deletes: selectedIds }; - await axios.post('SalesMonitors/deleteOrders', params); - selectedRows.value = []; - await paginateRef.value.fetch(); - } catch (err) { - console.error('Error deleting orders', err); - } -}; - -const redirectToOrderSummary = (orderId) => { - const url = `#/order/${orderId}/summary`; - window.open(url, '_blank'); -}; -</script> - -<template> - <FetchData - url="Workers/activeWithInheritedRole" - :filter="{ - fields: ['id', 'nickname'], - order: 'nickname ASC', - where: { role: 'salesPerson' }, - }" - auto-load - @on-fetch="(data) => (workersActiveOptions = data)" - /> - <FetchData - url="Clients" - :filter="{ - fields: ['id', 'name'], - order: 'name ASC', - }" - auto-load - @on-fetch="(data) => (clientsOptions = data)" - /> - - <VnSubToolbar /> - <QCard style="max-height: 380px; overflow-y: scroll"> - <VnTable - ref="paginateRef" - data-key="SalesMonitorOrders" - url="SalesMonitors/ordersFilter" - order="date_make DESC" - :limit="6" - :right-search="false" - :expr-builder="exprBuilder" - auto-load - :columns="columns" - :table="{ - 'row-key': 'id', - selection: 'multiple', - 'hide-bottom': true, - }" - default-mode="table" - :without-header="false" - @row-click="(_, row) => redirectToOrderSummary(row.id)" - v-model:selected="selectedRows" - > - <template #top-left> - <QBtn - v-if="selectedRows.length > 0" - icon="delete" - size="md" - color="primary" - @click=" - openConfirmationModal( - t('salesOrdersTable.deleteConfirmTitle'), - t('salesOrdersTable.deleteConfirmMessage'), - removeOrders - ) - " - > - <QTooltip>{{ t('salesOrdersTable.delete') }}</QTooltip> - </QBtn> - </template> - <template #column-date="{ row }"> - <QTd> - <QBadge - :color="getBadgeColor(row.date_send)" - text-color="black" - class="q-pa-sm q-mb-md" - style="font-size: 14px" - > - {{ toDateFormat(row.date_send) }} - </QBadge> - <div>{{ toDateTimeFormat(row.date_make) }}</div> - </QTd> - </template> - <template #column-client="{ row }"> - <QTd> - <div class="q-mb-md"> - <span class="link">{{ row.clientName }}</span> - <CustomerDescriptorProxy :id="row.clientFk" /> - </div> - <span> {{ row.agencyName }}</span> - </QTd> - </template> - - <template #column-salesPerson="{ row }"> - <QTd> - <div class="q-mb-md"> - <span class="link">{{ row.salesPerson }}</span> - <WorkerDescriptorProxy :id="row.salesPersonFk" dense /> - </div> - <span>{{ toCurrency(row.import) }}</span> - </QTd> - </template> - </VnTable> - </QCard> -</template> -<style lang="scss" scoped> -.q-td { - color: gray; -} -</style> diff --git a/src/pages/Monitor/Ticket/MonitorTicketFilter.vue b/src/pages/Monitor/Ticket/MonitorTicketFilter.vue new file mode 100644 index 000000000..167a10465 --- /dev/null +++ b/src/pages/Monitor/Ticket/MonitorTicketFilter.vue @@ -0,0 +1,286 @@ +<script setup> +import { ref } from 'vue'; +import { useI18n } from 'vue-i18n'; + +import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; +import VnFilterPanelChip from 'src/components/ui/VnFilterPanelChip.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import VnInputNumber from 'src/components/common/VnInputNumber.vue'; +import FetchData from 'src/components/FetchData.vue'; +import { dateRange } from 'src/filters'; + +defineProps({ dataKey: { type: String, required: true } }); +const { t } = useI18n(); +const warehouses = ref(); +const groupedStates = ref(); + +const handleScopeDays = (params, days, callback) => { + const [from, to] = dateRange(Date.vnNew()); + if (!days) { + Object.assign(params, { from, to, scopeDays: 1 }); + } else { + params.from = from; + to.setDate(to.getDate() + days); + params.to = to; + } + if (callback) callback(); +}; +</script> +<template> + <FetchData url="Warehouses" auto-load @on-fetch="(data) => (warehouses = data)" /> + <FetchData + url="AlertLevels" + auto-load + @on-fetch=" + (data) => + (groupedStates = data.map((x) => Object.assign(x, { code: t(x.code) }))) + " + /> + <VnFilterPanel + :data-key="dataKey" + :search-button="true" + :hidden-tags="['from', 'to']" + :custom-tags="['scopeDays']" + :unremovable-params="['from', 'to', 'scopeDays']" + > + <template #tags="{ tag, formatFn }"> + <div class="q-gutter-x-xs"> + <strong v-text="`${t(`params.${tag.label}`)}:`" /> + <span v-text="formatFn(tag.value)" /> + </div> + </template> + <template #customTags="{ params, searchFn, formatFn }"> + <VnFilterPanelChip + v-if="params.scopeDays" + removable + @remove="handleScopeDays(params, null, searchFn)" + > + <strong v-text="`${t(`params.scopeDays`)}:`" /> + <span v-text="formatFn(params.scopeDays)" /> + </VnFilterPanelChip> + </template> + <template #body="{ params }"> + <QItem> + <QItemSection> + <VnInput + :label="t('params.clientFk')" + v-model="params.clientFk" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + :label="t('params.orderFk')" + v-model="params.orderFk" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInputNumber + :label="t('params.scopeDays')" + v-model="params.scopeDays" + is-outlined + @update:model-value="(val) => handleScopeDays(params, val)" + @remove="(val) => handleScopeDays(params, val)" + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + :label="t('params.nickname')" + v-model="params.nickname" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + outlined + dense + rounded + :label="t('params.salesPersonFk')" + v-model="params.salesPersonFk" + url="Workers/search" + :params="{ departmentCodes: ['VT'] }" + is-outlined + option-value="code" + option-label="name" + :no-one="true" + > + <template #option="{ opt, itemProps }"> + <QItem v-bind="itemProps"> + <QItemSection> + <QItemLabel>{{ opt.name }}</QItemLabel> + <QItemLabel + v-if="opt.code" + class="text-grey text-caption" + > + {{ `${opt.nickname}, ${opt.code}` }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + :label="t('params.refFk')" + v-model="params.refFk" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + outlined + dense + rounded + :label="t('params.agencyModeFk')" + v-model="params.agencyModeFk" + url="AgencyModes/isActive" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + outlined + dense + rounded + :label="t('params.stateFk')" + v-model="params.stateFk" + url="States" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + outlined + dense + rounded + :label="t('params.groupedStates')" + v-model="params.alertLevel" + :options="groupedStates" + option-label="code" + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + outlined + dense + rounded + :label="t('params.warehouseFk')" + v-model="params.warehouseFk" + :options="warehouses" + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + outlined + dense + rounded + :label="t('params.provinceFk')" + v-model="params.provinceFk" + url="Provinces" + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <QCheckbox + :label="t('params.myTeam')" + v-model="params.myTeam" + toggle-indeterminate + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <QCheckbox + :label="t('params.problems')" + v-model="params.problems" + toggle-indeterminate + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <QCheckbox + :label="t('params.pending')" + v-model="params.pending" + toggle-indeterminate + /> + </QItemSection> + </QItem> + </template> + </VnFilterPanel> +</template> +<i18n> +en: + params: + clientFk: Client id + orderFk: Order id + scopeDays: Days onward + nickname: Nickname + salesPersonFk: Sales person + refFk: Invoice + agencyModeFk: Agency + stateFk: State + groupedStates: Grouped State + warehouseFk: Warehouse + provinceFk: Province + myTeam: My team + problems: With problems + pending: Pending + from: From + to: To + alertLevel: Grouped State + FREE: Free + DELIVERED: Delivered + ON_PREPARATION: On preparation + ON_PREVIOUS: On previous + PACKED: Packed + No one: No one + +es: + params: + clientFk: Id cliente + orderFk: Id cesta + scopeDays: Días en adelante + nickname: Nombre mostrado + salesPersonFk: Comercial + refFk: Factura + agencyModeFk: Agencia + stateFk: Estado + groupedStates: Estado agrupado + warehouseFk: Almacén + provinceFk: Provincia + myTeam: Mi equipo + problems: Con problemas + pending: Pendiente + from: Desde + To: Hasta + alertLevel: Estado agrupado + FREE: Libre + DELIVERED: Servido + ON_PREPARATION: En preparación + ON_PREVIOUS: En previa + PACKED: Encajado +</i18n> diff --git a/src/pages/Monitor/Ticket/MonitorTicketSearchbar.vue b/src/pages/Monitor/Ticket/MonitorTicketSearchbar.vue new file mode 100644 index 000000000..4950ab381 --- /dev/null +++ b/src/pages/Monitor/Ticket/MonitorTicketSearchbar.vue @@ -0,0 +1,12 @@ +<script setup> +import VnSearchbar from 'components/ui/VnSearchbar.vue'; +</script> +<template> + <VnSearchbar + data-key="SalesMonitorTickets" + url="SalesMonitors/salesFilter" + :redirect="false" + :label="$t('searchBar.label')" + :info="$t('searchBar.info')" + /> +</template> diff --git a/src/pages/Monitor/SalesTicketsTable.vue b/src/pages/Monitor/Ticket/MonitorTickets.vue similarity index 69% rename from src/pages/Monitor/SalesTicketsTable.vue rename to src/pages/Monitor/Ticket/MonitorTickets.vue index b301c1145..258b5022f 100644 --- a/src/pages/Monitor/SalesTicketsTable.vue +++ b/src/pages/Monitor/Ticket/MonitorTickets.vue @@ -1,37 +1,38 @@ <script setup> -import { ref, computed, reactive } from 'vue'; +import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import FetchData from 'components/FetchData.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; -import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue'; import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue'; import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue'; import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue'; import TicketSummary from 'src/pages/Ticket/Card/TicketSummary.vue'; import VnTable from 'components/VnTable/VnTable.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; -import { toDateFormat, toTimeFormat } from 'src/filters/date.js'; -import { toCurrency, dateRange } from 'src/filters'; +import { toDateFormat } from 'src/filters/date.js'; +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'; -const DEFAULT_AUTO_REFRESH = 1000; +const DEFAULT_AUTO_REFRESH = 2 * 60 * 1000; // 2min in ms const { t } = useI18n(); const autoRefresh = ref(false); -const paginateRef = ref(null); -const workersActiveOptions = ref([]); -const provincesOptions = ref([]); -const statesOptions = ref([]); -const zonesOptions = ref([]); +const tableRef = ref(null); +const provinceOpts = ref([]); +const stateOpts = ref([]); +const zoneOpts = ref([]); const visibleColumns = ref([]); -const allColumnNames = ref([]); const { viewSummary } = useSummaryDialog(); +const [from, to] = dateRange(Date.vnNew()); function exprBuilder(param, value) { switch (param) { case 'stateFk': return { 'ts.stateFk': value }; case 'salesPersonFk': - return { 'c.salesPersonFk': value }; + return { 'c.salesPersonFk': !value ? null : value }; case 'provinceFk': return { 'a.provinceFk': value }; case 'theoreticalHour': @@ -48,15 +49,12 @@ function exprBuilder(param, value) { } } -const filter = { order: ['totalProblems DESC'] }; -let params = reactive({}); - const columns = computed(() => [ { label: t('salesTicketsTable.problems'), - name: 'problems', + name: 'totalProblems', align: 'left', - sortable: true, + columnFilter: false, attrs: { dense: true, @@ -64,13 +62,12 @@ const columns = computed(() => [ }, { label: t('salesTicketsTable.identifier'), - name: 'identifier', + name: 'id', field: 'id', align: 'left', - sortable: true, columnFilter: { - component: 'input', + component: 'number', name: 'id', attrs: { dense: true, @@ -79,41 +76,41 @@ const columns = computed(() => [ }, { label: t('salesTicketsTable.client'), - name: 'client', + name: 'clientFk', align: 'left', field: 'nickname', - sortable: true, columnFilter: { - component: 'input', - name: 'nickname', + component: 'select', attrs: { - dense: true, + url: 'Clients', + fields: ['id', 'name', 'nickname'], + sortBy: 'name ASC', }, }, }, { label: t('salesTicketsTable.salesPerson'), - name: 'salesPerson', + name: 'salesPersonFk', field: 'userName', align: 'left', - sortable: true, + optionFilter: 'firstName', columnFilter: { component: 'select', - name: 'salesPersonFk', attrs: { - options: workersActiveOptions.value, - 'option-value': 'id', - 'option-label': 'name', - dense: true, + url: 'Workers/activeWithInheritedRole', + fields: ['id', 'name'], + sortBy: 'nickname ASC', + where: { role: 'salesPerson' }, + useLike: false, }, }, }, { label: t('salesTicketsTable.date'), - name: 'date', + name: 'shippedDate', style: { 'max-width': '100px' }, align: 'left', - sortable: true, + columnFilter: { component: 'date', name: 'shippedDate', @@ -124,61 +121,39 @@ const columns = computed(() => [ }, { label: t('salesTicketsTable.theoretical'), - name: 'theoretical', + name: 'theoreticalhour', field: 'zoneLanding', align: 'left', - sortable: true, - format: (val) => toTimeFormat(val), - columnFilter: { - component: 'input', - name: 'theoreticalHour', - attrs: { - dense: true, - }, - }, + format: (row) => row.theoreticalhour, + columnFilter: false, }, { label: t('salesTicketsTable.practical'), - name: 'practical', + name: 'practicalHour', field: 'practicalHour', align: 'left', - sortable: true, - columnFilter: { - component: 'input', - name: 'practicalHour', - attrs: { - dense: true, - }, - }, + format: (row) => row.practicalHour, + columnFilter: false, }, { label: t('salesTicketsTable.preparation'), - name: 'preparation', + name: 'preparationHour', field: 'shipped', align: 'left', - sortable: true, - format: (val) => toTimeFormat(val), - columnFilter: { - component: 'input', - name: 'shippedDate', - attrs: { - dense: true, - }, - }, + format: (row) => row.preparationHour, + columnFilter: false, }, - { label: t('salesTicketsTable.province'), - name: 'province', + name: 'provinceFk', field: 'province', align: 'left', - style: { 'max-width': '100px' }, - sortable: true, + format: (row) => row.province, columnFilter: { component: 'select', name: 'provinceFk', attrs: { - options: provincesOptions.value, + options: provinceOpts.value, 'option-value': 'id', 'option-label': 'name', dense: true, @@ -190,12 +165,11 @@ const columns = computed(() => [ name: 'state', align: 'left', style: { 'max-width': '100px' }, - sortable: true, columnFilter: { component: 'select', name: 'stateFk', attrs: { - options: statesOptions.value, + options: stateOpts.value, 'option-value': 'id', 'option-label': 'name', dense: true, @@ -207,10 +181,7 @@ const columns = computed(() => [ name: 'isFragile', field: 'isFragile', align: 'left', - sortable: true, - columnFilter: { - inWhere: true, - }, + columnFilter: false, attrs: { 'checked-icon': 'local_bar', 'unchecked-icon': 'local_bar', @@ -220,14 +191,14 @@ const columns = computed(() => [ }, { label: t('salesTicketsTable.zone'), - name: 'zone', + name: 'zoneFk', align: 'left', - sortable: true, + columnFilter: { component: 'select', name: 'zoneFk', attrs: { - options: zonesOptions.value, + options: zoneOpts.value, 'option-value': 'id', 'option-label': 'name', dense: true, @@ -236,13 +207,13 @@ const columns = computed(() => [ }, { label: t('salesTicketsTable.total'), - name: 'total', + name: 'totalWithVat', field: 'totalWithVat', align: 'left', style: { 'max-width': '75px' }, - sortable: true, + columnFilter: { - component: 'input', + component: 'number', name: 'totalWithVat', attrs: { dense: true, @@ -258,7 +229,7 @@ const columns = computed(() => [ title: t('salesTicketsTable.goToLines'), icon: 'vn:lines', color: 'priamry', - action: (row) => redirectToSales(row.id), + action: (row) => openTab(row.id), isPrimary: true, attrs: { flat: true, @@ -297,18 +268,13 @@ let refreshTimer = null; const autoRefreshHandler = (value) => { if (value) - refreshTimer = setInterval(() => paginateRef.value.fetch(), DEFAULT_AUTO_REFRESH); + refreshTimer = setInterval(() => tableRef.value.reload(), DEFAULT_AUTO_REFRESH); else { clearInterval(refreshTimer); refreshTimer = null; } }; -const redirectToTicketSummary = (id) => { - const url = `#/ticket/${id}/summary`; - window.open(url, '_blank'); -}; - const stateColors = { notice: 'info', success: 'positive', @@ -329,23 +295,10 @@ const formatShippedDate = (date) => { return toDateFormat(_date); }; -const redirectToSales = (id) => { - const url = `#/ticket/${id}/sale`; - window.open(url, '_blank'); -}; +const openTab = (id) => + window.open(`#/ticket/${id}/sale`, '_blank', 'noopener, noreferrer'); </script> - <template> - <FetchData - url="Workers/activeWithInheritedRole" - :filter="{ - fields: ['id', 'nickname'], - order: 'nickname ASC', - where: { role: 'salesPerson' }, - }" - auto-load - @on-fetch="(data) => (workersActiveOptions = data)" - /> <FetchData url="Provinces" :filter="{ @@ -353,7 +306,7 @@ const redirectToSales = (id) => { order: 'name ASC', }" auto-load - @on-fetch="(data) => (provincesOptions = data)" + @on-fetch="(data) => (provinceOpts = data)" /> <FetchData url="States" @@ -362,7 +315,7 @@ const redirectToSales = (id) => { order: 'name ASC', }" auto-load - @on-fetch="(data) => (statesOptions = data)" + @on-fetch="(data) => (stateOpts = data)" /> <FetchData url="Zones" @@ -371,46 +324,60 @@ const redirectToSales = (id) => { order: 'name ASC', }" auto-load - @on-fetch="(data) => (zonesOptions = data)" + @on-fetch="(data) => (zoneOpts = data)" /> + <MonitorTicketSearchbar /> + <RightMenu> + <template #right-panel> + <MonitorTicketFilter data-key="saleMonitorTickets" /> + </template> + </RightMenu> <VnTable - ref="paginateRef" - data-key="SalesMonitorTickets" + ref="tableRef" + data-key="saleMonitorTickets" url="SalesMonitors/salesFilter" - :filter="filter" - :limit="20" + search-url="saleMonitorTickets" :expr-builder="exprBuilder" - :user-params="params" :offset="50" :columns="columns" :visible-columns="visibleColumns" :right-search="false" default-mode="table" auto-load - @row-click="(_, row) => redirectToTicketSummary(row.id)" + :row-click="({ id }) => openTab(id)" + :disable-option="{ card: true }" + :user-params="{ from, to, scopeDays: 1 }" > <template #top-left> - <TableVisibleColumns - :all-columns="allColumnNames" - table-code="ticketsMonitor" - labels-traductions-path="salesTicketsTable" - @on-config-saved="visibleColumns = [...$event, 'rowActions']" - /> + <QBtn + icon="refresh" + size="md" + color="primary" + class="q-mr-sm" + dense + flat + @click="$refs.tableRef.reload()" + > + <QTooltip>{{ $t('globals.refresh') }}</QTooltip> + </QBtn> <QCheckbox v-model="autoRefresh" - :label="t('salesTicketsTable.autoRefresh')" + :label="$t('salesTicketsTable.autoRefresh')" @update:model-value="autoRefreshHandler" - /> + dense + > + <QTooltip>{{ $t('refreshInfo') }}</QTooltip> + </QCheckbox> </template> - <template #column-problems="{ row }"> - <QTd class="no-padding" style="max-width: 50px"> + <template #column-totalProblems="{ row }"> + <QTd class="no-padding" style="max-width: 60px"> <QIcon v-if="row.isTaxDataChecked === 0" name="vn:no036" color="primary" size="xs" > - <QTooltip>{{ t('salesTicketsTable.noVerifiedData') }}</QTooltip> + <QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip> </QIcon> <QIcon v-if="row.hasTicketRequest" @@ -418,7 +385,7 @@ const redirectToSales = (id) => { color="primary" size="xs" > - <QTooltip>{{ t('salesTicketsTable.purchaseRequest') }}</QTooltip> + <QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip> </QIcon> <QIcon v-if="row.itemShortage" @@ -426,10 +393,10 @@ const redirectToSales = (id) => { color="primary" size="xs" > - <QTooltip>{{ t('salesTicketsTable.notVisible') }}</QTooltip> + <QTooltip>{{ $t('salesTicketsTable.notVisible') }}</QTooltip> </QIcon> <QIcon v-if="row.isFreezed" name="vn:frozen" color="primary" size="xs"> - <QTooltip>{{ t('salesTicketsTable.clientFrozen') }}</QTooltip> + <QTooltip>{{ $t('salesTicketsTable.clientFrozen') }}</QTooltip> </QIcon> <QIcon v-if="row.risk" @@ -437,7 +404,9 @@ const redirectToSales = (id) => { :color="row.hasHighRisk ? 'negative' : 'primary'" size="xs" > - <QTooltip>{{ t('salesTicketsTable.risk') }}: {{ row.risk }}</QTooltip> + <QTooltip + >{{ $t('salesTicketsTable.risk') }}: {{ row.risk }}</QTooltip + > </QIcon> <QIcon v-if="row.hasComponentLack" @@ -445,7 +414,7 @@ const redirectToSales = (id) => { color="primary" size="xs" > - <QTooltip>{{ t('salesTicketsTable.componentLack') }}</QTooltip> + <QTooltip>{{ $t('salesTicketsTable.componentLack') }}</QTooltip> </QIcon> <QIcon v-if="row.isTooLittle" @@ -453,11 +422,11 @@ const redirectToSales = (id) => { color="primary" size="xs" > - <QTooltip>{{ t('salesTicketsTable.tooLittle') }}</QTooltip> + <QTooltip>{{ $t('salesTicketsTable.tooLittle') }}</QTooltip> </QIcon> </QTd> </template> - <template #column-identifier="{ row }"> + <template #column-id="{ row }"> <QTd class="no-padding"> <span class="link" @click.stop.prevent> {{ row.id }} @@ -465,19 +434,19 @@ const redirectToSales = (id) => { </span> </QTd> </template> - <template #column-client="{ row }"> - <QTd class="no-padding" @click.stop.prevent> + <template #column-clientFk="{ row }"> + <QTd class="no-padding" @click.stop :title="row.nickname"> <span class="link">{{ row.nickname }}</span> <CustomerDescriptorProxy :id="row.clientFk" /> </QTd> </template> - <template #column-salesPerson="{ row }"> - <QTd class="no-padding" @click.stop.prevent> - <span class="link">{{ row.userName }}</span> + <template #column-salesPersonFk="{ row }"> + <QTd class="no-padding" @click.stop :title="row.userName"> + <span class="link" v-text="dashIfEmpty(row.userName)" /> <WorkerDescriptorProxy :id="row.salesPersonFk" /> </QTd> </template> - <template #column-date="{ row }"> + <template #column-shippedDate="{ row }"> <QTd class="no-padding"> <QBadge v-bind="getBadgeAttrs(row.shippedDate)" @@ -488,6 +457,11 @@ const redirectToSales = (id) => { </QBadge> </QTd> </template> + <template #column-provinceFk="{ row }"> + <QTd class="no-padding"> + <span :title="row.province" v-text="row.province" /> + </QTd> + </template> <template #column-state="{ row }"> <QTd class="no-padding" @click.stop.prevent> <div v-if="row.refFk"> @@ -508,17 +482,17 @@ const redirectToSales = (id) => { <template #column-isFragile="{ row }"> <QTd class="no-padding"> <QIcon v-if="row.isFragile" name="local_bar" color="primary" size="sm"> - <QTooltip>{{ t('salesTicketsTable.isFragile') }}</QTooltip> + <QTooltip>{{ $t('salesTicketsTable.isFragile') }}</QTooltip> </QIcon> </QTd> </template> - <template #column-zone="{ row }"> - <QTd class="no-padding" @click.stop.prevent> + <template #column-zoneFk="{ row }"> + <QTd class="no-padding" @click.stop.prevent :title="row.zoneName"> <span class="link">{{ row.zoneName }}</span> <ZoneDescriptorProxy :id="row.zoneFk" /> </QTd> </template> - <template #column-total="{ row }"> + <template #column-totalWithVat="{ row }"> <QTd class="no-padding"> <QBadge :color="totalPriceColor(row) || 'transparent'" diff --git a/src/pages/Monitor/locale/en.yml b/src/pages/Monitor/locale/en.yml index f58db7854..4cdd245aa 100644 --- a/src/pages/Monitor/locale/en.yml +++ b/src/pages/Monitor/locale/en.yml @@ -11,11 +11,14 @@ salesClientsTable: client: Client salesOrdersTable: delete: Delete - date: Date + dateSend: Send date + dateMake: Make date client: Client salesPerson: Salesperson deleteConfirmTitle: Delete selected elements deleteConfirmMessage: All the selected elements will be deleted. Are you sure you want to continue? + agency: Agency + import: Import salesTicketsTable: autoRefresh: Auto-refresh problems: Problems @@ -43,3 +46,4 @@ salesTicketsTable: searchBar: label: Search tickets info: Search tickets by id or alias +refreshInfo: Toggle auto-refresh every 2 minutes diff --git a/src/pages/Monitor/locale/es.yml b/src/pages/Monitor/locale/es.yml index 918b51813..8087bb444 100644 --- a/src/pages/Monitor/locale/es.yml +++ b/src/pages/Monitor/locale/es.yml @@ -11,11 +11,14 @@ salesClientsTable: client: Cliente salesOrdersTable: delete: Eliminar - date: Fecha + dateSend: Fecha de envío + dateMake: Fecha de realización client: Cliente salesPerson: Comercial deleteConfirmTitle: Eliminar los elementos seleccionados deleteConfirmMessage: Todos los elementos seleccionados serán eliminados. ¿Seguro que quieres continuar? + agency: Agencia + import: Importe salesTicketsTable: autoRefresh: Auto-refresco problems: Problemas @@ -43,3 +46,4 @@ salesTicketsTable: searchBar: label: Buscar tickets info: Buscar tickets por identificador o alias +refreshInfo: Conmuta el refresco automático cada 2 minutos diff --git a/src/pages/Order/Card/OrderCatalogFilter.vue b/src/pages/Order/Card/OrderCatalogFilter.vue index 850abb755..938cc4fe2 100644 --- a/src/pages/Order/Card/OrderCatalogFilter.vue +++ b/src/pages/Order/Card/OrderCatalogFilter.vue @@ -380,21 +380,6 @@ function addOrder(value, field, params) { @click="tagValues.push({})" /> </QItem> - <!-- <QItem> - <QItemSection class="q-py-sm"> - <QBtn - :label="t('Search')" - class="full-width" - color="primary" - dense - icon="search" - rounded - type="button" - unelevated - @click.stop="applyTagFilter(params, searchFn)" - /> - </QItemSection> - </QItem> --> <QSeparator /> </template> </VnFilterPanel> diff --git a/src/pages/Order/Card/OrderCatalogItemDialog.vue b/src/pages/Order/Card/OrderCatalogItemDialog.vue index 3f97443ca..46a50c021 100644 --- a/src/pages/Order/Card/OrderCatalogItemDialog.vue +++ b/src/pages/Order/Card/OrderCatalogItemDialog.vue @@ -77,10 +77,6 @@ const addToOrder = async () => { </template> <style lang="scss" scoped> -// .container { -// max-width: 768px; -// width: 100%; -// } .td { width: 200px; } diff --git a/src/pages/Order/Card/OrderLines.vue b/src/pages/Order/Card/OrderLines.vue index dab4a959c..17a157797 100644 --- a/src/pages/Order/Card/OrderLines.vue +++ b/src/pages/Order/Card/OrderLines.vue @@ -14,6 +14,7 @@ import FetchData from 'src/components/FetchData.vue'; import VnImg from 'src/components/ui/VnImg.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import FetchedTags from 'src/components/ui/FetchedTags.vue'; +import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; const router = useRouter(); const stateStore = useStateStore(); @@ -280,7 +281,12 @@ watch( <VnImg :id="parseInt(row?.item?.image)" class="rounded" /> </div> </template> - + <template #column-id="{ row }"> + <span class="link" @click.stop> + {{ row?.item?.id }} + <ItemDescriptorProxy :id="row?.item?.id" /> + </span> + </template> <template #column-itemFk="{ row }"> <div class="row column full-width justify-between items-start"> {{ row?.item?.name }} @@ -288,7 +294,7 @@ watch( {{ row?.item?.subName.toUpperCase() }} </div> </div> - <FetchedTags :item="row?.item" :max-length="6" /> + <FetchedTags :item="row?.item" /> </template> <template #column-amount="{ row }"> {{ toCurrency(row.quantity * row.price) }} diff --git a/src/pages/Order/Card/OrderSearchbar.vue b/src/pages/Order/Card/OrderSearchbar.vue index a768768a5..fa30a097f 100644 --- a/src/pages/Order/Card/OrderSearchbar.vue +++ b/src/pages/Order/Card/OrderSearchbar.vue @@ -10,7 +10,7 @@ const { t } = useI18n(); data-key="OrderList" url="Orders/filter" :label="t('Search order')" - :info="t('You can search orders by reference')" + :info="t('Search orders by ticket id')" /> </template> @@ -18,5 +18,5 @@ const { t } = useI18n(); <i18n> es: Search order: Buscar orden - You can search orders by reference: Puedes buscar por referencia de la orden + Search orders by ticket id: Buscar pedido por id ticket </i18n> diff --git a/src/pages/Order/Card/OrderSummary.vue b/src/pages/Order/Card/OrderSummary.vue index 09c47d4a7..60358f744 100644 --- a/src/pages/Order/Card/OrderSummary.vue +++ b/src/pages/Order/Card/OrderSummary.vue @@ -192,7 +192,7 @@ const detailsColumns = ref([ </span> </div> </div> - <FetchedTags :item="props.row.item" :max-length="5" /> + <FetchedTags :item="props.row.item" /> </QTd> <QTd key="quantity" :props="props"> {{ props.row.quantity }} diff --git a/src/pages/Order/Card/OrderVolume.vue b/src/pages/Order/Card/OrderVolume.vue index b16022c2f..27ee24197 100644 --- a/src/pages/Order/Card/OrderVolume.vue +++ b/src/pages/Order/Card/OrderVolume.vue @@ -2,8 +2,9 @@ import axios from 'axios'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import { ref } from 'vue'; +import { ref, onMounted } from 'vue'; import { dashIfEmpty } from 'src/filters'; +import { useStateStore } from 'stores/useStateStore'; import FetchData from 'components/FetchData.vue'; import FetchedTags from 'components/ui/FetchedTags.vue'; @@ -58,6 +59,9 @@ const loadVolumes = async (rows) => { }); volumes.value = rows; }; + +const stateStore = useStateStore(); +onMounted(async () => (stateStore.rightDrawer = false)); </script> <template> @@ -84,6 +88,7 @@ const loadVolumes = async (rows) => { @on-fetch="(data) => loadVolumes(data)" :right-search="false" :column-search="false" + :disable-option="{ card: true }" > <template #column-itemFk="{ row }"> <span class="link"> @@ -92,7 +97,13 @@ const loadVolumes = async (rows) => { </span> </template> <template #column-description="{ row }"> - <FetchedTags :item="row.item" :max-length="5" /> + <div class="row column full-width justify-between items-start"> + {{ row?.item?.name }} + <div v-if="row?.item?.subName" class="subName"> + {{ row?.item?.subName.toUpperCase() }} + </div> + </div> + <FetchedTags :item="row?.item" /> </template> <template #column-volume="{ rowIndex }"> {{ volumes?.[rowIndex]?.volume }} @@ -121,6 +132,11 @@ const loadVolumes = async (rows) => { } } } + +.subName { + color: var(--vn-label-color); + text-transform: uppercase; +} </style> <i18n> en: diff --git a/src/pages/Order/OrderList.vue b/src/pages/Order/OrderList.vue index 9870be9b3..d96a33ef5 100644 --- a/src/pages/Order/OrderList.vue +++ b/src/pages/Order/OrderList.vue @@ -11,6 +11,9 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import OrderSearchbar from './Card/OrderSearchbar.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import OrderFilter from './Card/OrderFilter.vue'; +import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue'; +import WorkerDescriptorProxy from '../Worker/Card/WorkerDescriptorProxy.vue'; +import { toDateTimeFormat } from 'src/filters/date'; const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); @@ -75,7 +78,7 @@ const columns = computed(() => [ label: t('module.created'), component: 'date', cardVisible: true, - format: (row) => toDate(row?.landed), + format: (row) => toDateTimeFormat(row?.landed), columnField: { component: null, }, @@ -115,6 +118,7 @@ const columns = computed(() => [ }, }, cardVisible: true, + columnClass: 'expand', }, { align: 'left', @@ -132,6 +136,7 @@ const columns = computed(() => [ title: t('InvoiceOutSummary'), icon: 'preview', action: (row) => viewSummary(row.id, OrderSummary), + isPrimary: true, }, ], }, @@ -154,6 +159,16 @@ async function fetchAgencies({ landed, addressId }) { }); agencyList.value = data; } + +const getDateColor = (date) => { + const today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + const timeTicket = new Date(date); + timeTicket.setHours(0, 0, 0, 0); + const comparation = today - timeTicket; + if (comparation == 0) return 'bg-warning'; + if (comparation < 0) return 'bg-success'; +}; </script> <template> <OrderSearchbar /> @@ -183,6 +198,25 @@ async function fetchAgencies({ landed, addressId }) { :columns="columns" redirect="order" > + <template #column-clientFk="{ row }"> + <span class="link" @click.stop> + {{ row?.clientName }} + <CustomerDescriptorProxy :id="row?.clientFk" /> + </span> + </template> + <template #column-salesPersonFk="{ row }"> + <span class="link" @click.stop> + {{ row?.name }} + <WorkerDescriptorProxy :id="row?.salesPersonFk" /> + </span> + </template> + <template #column-landed="{ row }"> + <span v-if="getDateColor(row.landed)"> + <QChip :class="getDateColor(row.landed)" dense square> + {{ toDate(row?.landed) }} + </QChip> + </span> + </template> <template #more-create-dialog="{ data }"> <VnSelect url="Clients" diff --git a/src/pages/Route/Card/RouteSummary.vue b/src/pages/Route/Card/RouteSummary.vue index d7a02833e..3f9b1a2a9 100644 --- a/src/pages/Route/Card/RouteSummary.vue +++ b/src/pages/Route/Card/RouteSummary.vue @@ -217,7 +217,7 @@ const ticketColumns = ref([ <template #body-cell-city="{ value, row }"> <QTd auto-width> <span - class="text-primary cursor-pointer" + class="link cursor-pointer" @click="openBuscaman(entity?.route?.vehicleFk, [row])" > {{ value }} @@ -226,7 +226,7 @@ const ticketColumns = ref([ </template> <template #body-cell-client="{ value, row }"> <QTd auto-width> - <span class="text-primary cursor-pointer"> + <span class="link cursor-pointer"> {{ value }} <CustomerDescriptorProxy :id="row?.clientFk" /> </span> @@ -234,7 +234,7 @@ const ticketColumns = ref([ </template> <template #body-cell-ticket="{ value, row }"> <QTd auto-width class="text-center"> - <span class="text-primary cursor-pointer"> + <span class="link cursor-pointer"> {{ value }} <TicketDescriptorProxy :id="row?.id" /> </span> diff --git a/src/pages/Route/RouteList.vue b/src/pages/Route/RouteList.vue index e24ed33ed..7e2358236 100644 --- a/src/pages/Route/RouteList.vue +++ b/src/pages/Route/RouteList.vue @@ -87,6 +87,7 @@ const columns = computed(() => [ label: 'agencyName', }, }, + columnClass: 'expand', }, { align: 'left', @@ -142,17 +143,9 @@ const columns = computed(() => [ { align: 'center', name: 'm3', - label: 'volume', + label: t('Volume'), cardVisible: true, - }, - { - align: 'left', - name: 'description', - label: t('Description'), - isTitle: true, - create: true, - component: 'input', - field: 'description', + columnClass: 'shrink', }, { align: 'left', @@ -168,12 +161,38 @@ const columns = computed(() => [ component: 'time', columnFilter: false, }, + { + align: 'center', + name: 'kmStart', + label: t('KmStart'), + columnClass: 'shrink', + create: true, + visible: false, + }, + { + align: 'center', + name: 'kmEnd', + label: t('KmEnd'), + columnClass: 'shrink', + create: true, + visible: false, + }, + { + align: 'left', + name: 'description', + label: t('Description'), + isTitle: true, + create: true, + component: 'input', + field: 'description', + }, { align: 'left', name: 'isOk', label: t('Served'), component: 'checkbox', columnFilter: false, + columnClass: 'shrink', }, { align: 'right', @@ -185,7 +204,7 @@ const columns = computed(() => [ action: (row) => openTicketsDialog(row?.id), }, { - title: t('Preview'), + title: t('components.smartCard.viewSummary'), icon: 'preview', action: (row) => viewSummary(row?.id, RouteSummary), }, @@ -368,10 +387,13 @@ es: Worker: Trabajador Agency: Agencia Vehicle: Vehículo + Volume: Volumen Date: Fecha Description: Descripción Hour started: Hora inicio Hour finished: Hora fin + KmStart: Km inicio + KmEnd: Km fin Served: Servida newRoute: Nueva Ruta Clone Selected Routes: Clonar rutas seleccionadas diff --git a/src/pages/Shelving/Card/ShelvingForm.vue b/src/pages/Shelving/Card/ShelvingForm.vue index aee6f7f3a..dc0234c22 100644 --- a/src/pages/Shelving/Card/ShelvingForm.vue +++ b/src/pages/Shelving/Card/ShelvingForm.vue @@ -1,5 +1,6 @@ <script setup> import { useI18n } from 'vue-i18n'; +import { computed } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import VnRow from 'components/ui/VnRow.vue'; import FormModel from 'components/FormModel.vue'; @@ -10,8 +11,8 @@ import VnSelect from 'src/components/common/VnSelect.vue'; const { t } = useI18n(); const route = useRoute(); const router = useRouter(); -const shelvingId = route.params?.id || null; -const isNew = Boolean(!shelvingId); +const entityId = computed(() => route.params.id ?? null); +const isNew = Boolean(!entityId.value); const defaultInitialData = { parkingFk: null, priority: null, @@ -42,15 +43,15 @@ const onSave = (shelving, newShelving) => { }; </script> <template> - <VnSubToolbar /> + <VnSubToolbar v-if="isNew" /> <FormModel - :url="isNew ? null : `Shelvings/${shelvingId}`" + :url="isNew ? null : `Shelvings/${entityId}`" :url-create="isNew ? 'Shelvings' : null" :observe-form-changes="!isNew" :filter="shelvingFilter" model="shelving" :auto-load="!isNew" - :form-initial-data="defaultInitialData" + :form-initial-data="isNew ? defaultInitialData : null" @on-data-saved="onSave" > <template #form="{ data, validate }"> diff --git a/src/pages/Supplier/Card/SupplierConsumption.vue b/src/pages/Supplier/Card/SupplierConsumption.vue index 100a38b2a..8fa6a1e5c 100644 --- a/src/pages/Supplier/Card/SupplierConsumption.vue +++ b/src/pages/Supplier/Card/SupplierConsumption.vue @@ -208,7 +208,7 @@ onMounted(async () => { <QTd no-hover> <span>{{ buy.subName }}</span> - <FetchedTags :item="buy" :max-length="5" /> + <FetchedTags :item="buy" /> </QTd> <QTd no-hover> {{ dashIfEmpty(buy.quantity) }}</QTd> <QTd no-hover> {{ dashIfEmpty(buy.price) }}</QTd> diff --git a/src/pages/Ticket/Card/BasicData/BasicDataTable.vue b/src/pages/Ticket/Card/BasicData/BasicDataTable.vue index 48b8c882f..7f2f100ad 100644 --- a/src/pages/Ticket/Card/BasicData/BasicDataTable.vue +++ b/src/pages/Ticket/Card/BasicData/BasicDataTable.vue @@ -245,7 +245,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); <div class="column"> <span>{{ row.item.name }}</span> <span class="color-vn-label">{{ row.item.subName }}</span> - <FetchedTags :item="row.item" :max-length="6" /> + <FetchedTags :item="row.item" /> </div> </QTd> </template> diff --git a/src/pages/Ticket/Card/TicketComponents.vue b/src/pages/Ticket/Card/TicketComponents.vue index 3954b5a62..6131c92db 100644 --- a/src/pages/Ticket/Card/TicketComponents.vue +++ b/src/pages/Ticket/Card/TicketComponents.vue @@ -310,7 +310,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); <div class="column"> <span>{{ row.item.name }}</span> <span class="color-vn-label">{{ row.item.subName }}</span> - <FetchedTags :item="row.item" :max-length="6" /> + <FetchedTags :item="row.item" /> </div> </QTd> </template> diff --git a/src/pages/Ticket/Card/TicketDescriptorMenu.vue b/src/pages/Ticket/Card/TicketDescriptorMenu.vue index d5a578ca7..9a50288a0 100644 --- a/src/pages/Ticket/Card/TicketDescriptorMenu.vue +++ b/src/pages/Ticket/Card/TicketDescriptorMenu.vue @@ -31,7 +31,6 @@ const actions = { try { const { data } = await axios.post(`Tickets/cloneAll`, { - shipped: ticket.value.shipped, ticketsIds: [ticket.value.id], withWarehouse: true, negative: false, diff --git a/src/pages/Ticket/Card/TicketSale.vue b/src/pages/Ticket/Card/TicketSale.vue index bd7297b56..2ea12bb05 100644 --- a/src/pages/Ticket/Card/TicketSale.vue +++ b/src/pages/Ticket/Card/TicketSale.vue @@ -656,7 +656,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); <div class="column"> <span>{{ row.concept }}</span> <span class="color-vn-label">{{ row.item?.subName }}</span> - <FetchedTags v-if="row.item" :item="row.item" :max-length="6" /> + <FetchedTags v-if="row.item" :item="row.item" /> <QPopupProxy v-if="row.id && isTicketEditable"> <VnInput v-model="row.concept" @change="updateConcept(row)" /> </QPopupProxy> diff --git a/src/pages/Ticket/Card/TicketSaleMoreActions.vue b/src/pages/Ticket/Card/TicketSaleMoreActions.vue index f04a13c4e..94db67be2 100644 --- a/src/pages/Ticket/Card/TicketSaleMoreActions.vue +++ b/src/pages/Ticket/Card/TicketSaleMoreActions.vue @@ -131,7 +131,11 @@ const createClaim = () => { onCreateClaimAccepted ); else - openConfirmationModal(t('Do you want to create a claim?'), onCreateClaimAccepted); + openConfirmationModal( + t('Do you want to create a claim?'), + false, + onCreateClaimAccepted + ); }; const onCreateClaimAccepted = async () => { diff --git a/src/pages/Ticket/Card/TicketSaleTracking.vue b/src/pages/Ticket/Card/TicketSaleTracking.vue index e699d2bfd..6978d92c8 100644 --- a/src/pages/Ticket/Card/TicketSaleTracking.vue +++ b/src/pages/Ticket/Card/TicketSaleTracking.vue @@ -412,7 +412,7 @@ const qCheckBoxController = (sale, action) => { <span v-if="row.subName" class="color-vn-label"> {{ row.subName }} </span> - <FetchedTags :item="row" :max-length="6" tag="value" /> + <FetchedTags :item="row" tag="value" /> </div> </QTd> </template> diff --git a/src/pages/Ticket/Card/TicketSummary.vue b/src/pages/Ticket/Card/TicketSummary.vue index 2d02ad5ed..3851bf5d6 100644 --- a/src/pages/Ticket/Card/TicketSummary.vue +++ b/src/pages/Ticket/Card/TicketSummary.vue @@ -403,7 +403,6 @@ async function changeState(value) { <FetchedTags class="fetched-tags" :item="props.row.item" - :max-length="5" ></FetchedTags> </QTd> <QTd>{{ props.row.price }} €</QTd> diff --git a/src/pages/Ticket/Card/TicketVolume.vue b/src/pages/Ticket/Card/TicketVolume.vue index 93da31e53..68d2a1f73 100644 --- a/src/pages/Ticket/Card/TicketVolume.vue +++ b/src/pages/Ticket/Card/TicketVolume.vue @@ -145,7 +145,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); <div class="column"> <span>{{ row.item.name }}</span> <span class="color-vn-label">{{ row.item.subName }}</span> - <FetchedTags :item="row.item" :max-length="6" /> + <FetchedTags :item="row.item" /> </div> </QTd> </template> diff --git a/src/pages/Travel/TravelList.vue b/src/pages/Travel/TravelList.vue index 8989e485c..c7ad908f9 100644 --- a/src/pages/Travel/TravelList.vue +++ b/src/pages/Travel/TravelList.vue @@ -10,6 +10,7 @@ import { computed } from 'vue'; import TravelSummary from './Card/TravelSummary.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue'; import { toDate } from 'src/filters'; +import { getDateQBadgeColor } from 'src/composables/getDateQBadgeColor.js'; const { viewSummary } = useSummaryDialog(); const router = useRouter(); const { t } = useI18n(); @@ -46,14 +47,12 @@ const columns = computed(() => [ name: 'id', label: t('travel.travelList.tableVisibleColumns.id'), isId: true, - field: 'id', cardVisible: true, }, { align: 'left', name: 'ref', label: t('travel.travelList.tableVisibleColumns.ref'), - field: 'ref', component: 'input', columnField: { component: null, @@ -65,7 +64,6 @@ const columns = computed(() => [ align: 'left', name: 'agencyModeFk', label: t('travel.travelList.tableVisibleColumns.agency'), - field: 'agencyModeFk', component: 'select', attrs: { url: 'agencyModes', @@ -78,37 +76,10 @@ const columns = computed(() => [ cardVisible: true, create: true, }, - { - align: 'left', - name: 'shipped', - label: t('travel.travelList.tableVisibleColumns.shipped'), - field: 'shipped', - component: 'date', - columnField: { - component: null, - }, - cardVisible: true, - create: true, - format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.shipped)), - }, - { - align: 'left', - name: 'landed', - label: t('travel.travelList.tableVisibleColumns.landed'), - field: 'landed', - component: 'date', - columnField: { - component: null, - }, - cardVisible: true, - create: true, - format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.landed)), - }, { align: 'left', name: 'warehouseInFk', label: t('travel.travelList.tableVisibleColumns.warehouseIn'), - field: 'warehouseInFk', component: 'select', attrs: { url: 'warehouses', @@ -123,11 +94,28 @@ const columns = computed(() => [ cardVisible: true, create: true, }, + { + align: 'left', + name: 'shipped', + label: t('travel.travelList.tableVisibleColumns.shipped'), + component: 'date', + columnField: { + component: null, + }, + cardVisible: true, + create: true, + format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.shipped)), + }, + { + align: 'left', + name: 'shipmentHour', + label: t('travel.travelList.tableVisibleColumns.shipHour'), + cardVisible: true, + }, { align: 'left', name: 'warehouseOutFk', label: t('travel.travelList.tableVisibleColumns.warehouseOut'), - field: 'warehouseOutFk', component: 'select', attrs: { url: 'warehouses', @@ -140,12 +128,30 @@ const columns = computed(() => [ cardVisible: true, create: true, }, + { + align: 'left', + name: 'landed', + label: t('travel.travelList.tableVisibleColumns.landed'), + component: 'date', + columnField: { + component: null, + }, + cardVisible: true, + create: true, + format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.landed)), + }, + { + align: 'left', + name: 'landingHour', + label: t('travel.travelList.tableVisibleColumns.landHour'), + cardVisible: true, + }, { align: 'left', name: 'totalEntries', label: t('travel.travelList.tableVisibleColumns.totalEntries'), - field: 'totalEntries', component: 'input', + toolTip: t('travel.travelList.tableVisibleColumns.totalEntriesTooltip'), columnField: { component: null, }, @@ -165,13 +171,15 @@ const columns = computed(() => [ }, { title: t('Add entry'), - icon: 'contact_support', + icon: 'vn:ticket', action: redirectCreateEntryView, + isPrimary: true, }, { - title: t('View Summary'), + title: t('components.smartCard.viewSummary'), icon: 'preview', action: (row) => viewSummary(row.id, TravelSummary), + isPrimary: true, }, ], }, @@ -202,12 +210,43 @@ const columns = computed(() => [ redirect="travel" :is-editable="false" :use-model="true" - /> + > + <template #column-shipped="{ row }"> + <QBadge + text-color="black" + v-if="getDateQBadgeColor(row.shipped)" + :color="getDateQBadgeColor(row.shipped)" + > + {{ toDate(row.shipped) }} + </QBadge> + <span v-else>{{ toDate(row.shipped) }}</span> + <QIcon + name="flight_takeoff" + size="sm" + :class="{ 'is-active': row.isDelivered }" + /> + </template> + <template #column-landed="{ row }"> + <QBadge + text-color="black" + v-if="getDateQBadgeColor(row.landed)" + :color="getDateQBadgeColor(row.landed)" + > + {{ toDate(row.landed) }} + </QBadge> + <span v-else>{{ toDate(row.landed) }}</span> + <QIcon + name="flight_land" + size="sm" + :class="{ 'is-active': row.isReceived }" + /> + </template> + </VnTable> </template> <i18n> en: - addEntry: Add entry + Add entry: Add entry searchByIdOrReference: Search by ID or reference es: @@ -215,4 +254,12 @@ es: searchByIdOrReference: Buscar por ID o por referencia You can search by travel id or name: Buscar por envio por id o nombre Search travel: Buscar envio + Clone: Clonar + Add entry: Añadir Entrada </i18n> + +<style lang="scss" scoped> +.is-active { + color: #c8e484; +} +</style> diff --git a/src/pages/Worker/Card/WorkerMedical.vue b/src/pages/Worker/Card/WorkerMedical.vue new file mode 100644 index 000000000..6bca4ae85 --- /dev/null +++ b/src/pages/Worker/Card/WorkerMedical.vue @@ -0,0 +1,91 @@ +<script setup> +import { ref, computed } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRoute } from 'vue-router'; +import VnTable from 'components/VnTable/VnTable.vue'; +const tableRef = ref(); +const { t } = useI18n(); +const route = useRoute(); +const entityId = computed(() => route.params.id); + +const columns = [ + { + align: 'left', + name: 'date', + label: t('worker.medical.tableVisibleColumns.date'), + create: true, + component: 'date', + }, + { + align: 'left', + name: 'time', + label: t('worker.medical.tableVisibleColumns.time'), + create: true, + component: 'time', + attrs: { + timeOnly: true, + }, + }, + { + align: 'left', + name: 'centerFk', + label: t('worker.medical.tableVisibleColumns.center'), + create: true, + component: 'select', + attrs: { + url: 'medicalCenters', + fields: ['id', 'name'], + }, + }, + { + align: 'left', + name: 'invoice', + label: t('worker.medical.tableVisibleColumns.invoice'), + create: true, + component: 'input', + }, + { + align: 'left', + name: 'amount', + label: t('worker.medical.tableVisibleColumns.amount'), + create: true, + component: 'input', + }, + { + align: 'left', + name: 'isFit', + label: t('worker.medical.tableVisibleColumns.isFit'), + create: true, + component: 'checkbox', + }, + { + align: 'left', + name: 'remark', + label: t('worker.medical.tableVisibleColumns.remark'), + create: true, + component: 'input', + }, +]; +</script> +<template> + <VnTable + ref="tableRef" + data-key="WorkerMedical" + :url="`Workers/${entityId}/medicalReview`" + save-url="MedicalReviews/crud" + :create="{ + urlCreate: 'medicalReviews', + title: t('Create medicalReview'), + onDataSaved: () => tableRef.reload(), + formInitialData: { + workerFk: entityId, + }, + }" + order="date DESC" + :columns="columns" + auto-load + :right-search="false" + :is-editable="true" + :use-model="true" + /> +</template> diff --git a/src/pages/Worker/Card/WorkerSummary.vue b/src/pages/Worker/Card/WorkerSummary.vue index 33d949dbb..ab201f964 100644 --- a/src/pages/Worker/Card/WorkerSummary.vue +++ b/src/pages/Worker/Card/WorkerSummary.vue @@ -10,7 +10,6 @@ import CardSummary from 'components/ui/CardSummary.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnTitle from 'src/components/common/VnTitle.vue'; import RoleDescriptorProxy from 'src/pages/Account/Role/Card/RoleDescriptorProxy.vue'; -import VnRow from 'src/components/ui/VnRow.vue'; import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; const route = useRoute(); diff --git a/src/pages/Worker/WorkerList.vue b/src/pages/Worker/WorkerList.vue index 0ea094e23..91d96a162 100644 --- a/src/pages/Worker/WorkerList.vue +++ b/src/pages/Worker/WorkerList.vue @@ -77,7 +77,7 @@ const columns = computed(() => [ name: 'tableActions', actions: [ { - title: t('InvoiceOutSummary'), + title: t('components.smartCard.viewSummary'), icon: 'preview', action: (row) => viewSummary(row.id, WorkerSummary), }, diff --git a/src/pages/Zone/Card/ZoneBasicData.vue b/src/pages/Zone/Card/ZoneBasicData.vue index a4874e5fc..512d07636 100644 --- a/src/pages/Zone/Card/ZoneBasicData.vue +++ b/src/pages/Zone/Card/ZoneBasicData.vue @@ -83,6 +83,7 @@ const agencyOptions = ref([]); :label="t('Price')" type="number" min="0" + required="true" clearable /> <VnInput @@ -95,7 +96,12 @@ const agencyOptions = ref([]); </VnRow> <VnRow> - <VnInput v-model="data.inflation" :label="t('Inflation')" clearable /> + <VnInput + v-model="data.inflation" + :label="t('Inflation')" + type="number" + clearable + /> <QCheckbox v-model="data.isVolumetric" :label="t('Volumetric')" diff --git a/src/pages/Zone/Card/ZoneCard.vue b/src/pages/Zone/Card/ZoneCard.vue index b81ee9039..d61c61abf 100644 --- a/src/pages/Zone/Card/ZoneCard.vue +++ b/src/pages/Zone/Card/ZoneCard.vue @@ -2,36 +2,37 @@ import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import { computed } from 'vue'; + import VnCard from 'components/common/VnCard.vue'; import ZoneDescriptor from './ZoneDescriptor.vue'; -import ZoneSearchbar from './ZoneSearchbar.vue'; +import ZoneFilterPanel from '../ZoneFilterPanel.vue'; const { t } = useI18n(); const route = useRoute(); - const routeName = computed(() => route.name); -const searchBarDataKeys = { - ZoneWarehouses: 'ZoneWarehouses', - ZoneSummary: 'ZoneSummary', - ZoneLocations: 'ZoneLocations', - ZoneEvents: 'ZoneEvents', -}; + +function notIsLocations(ifIsFalse, ifIsTrue) { + if (routeName.value != 'ZoneLocations') return ifIsFalse; + return ifIsTrue; +} </script> <template> <VnCard - data-key="Zone" + data-key="zone" + base-url="Zones" :descriptor="ZoneDescriptor" - :search-data-key="searchBarDataKeys[routeName]" :filter-panel="ZoneFilterPanel" + :search-data-key="notIsLocations('ZoneList', 'ZoneLocations')" :searchbar-props="{ url: 'Zones', - label: t('list.searchZone'), + label: notIsLocations(t('list.searchZone'), t('list.searchLocation')), info: t('list.searchInfo'), + whereFilter: notIsLocations((value) => { + return /^\d+$/.test(value) + ? { id: value } + : { name: { like: `%${value}%` } }; + }), }" - > - <template #searchbar> - <ZoneSearchbar /> - </template> - </VnCard> + /> </template> diff --git a/src/pages/Zone/Card/ZoneDescriptorMenuItems.vue b/src/pages/Zone/Card/ZoneDescriptorMenuItems.vue index 8f1168ce9..22d5bcd5e 100644 --- a/src/pages/Zone/Card/ZoneDescriptorMenuItems.vue +++ b/src/pages/Zone/Card/ZoneDescriptorMenuItems.vue @@ -8,13 +8,6 @@ import VnConfirm from 'components/ui/VnConfirm.vue'; import axios from 'axios'; -const $props = defineProps({ - zone: { - type: Object, - default: () => {}, - }, -}); - const { t } = useI18n(); const { push, currentRoute } = useRouter(); const zoneId = currentRoute.value.params.id; @@ -22,32 +15,21 @@ const zoneId = currentRoute.value.params.id; const actions = { clone: async () => { const opts = { message: t('Zone cloned'), type: 'positive' }; - let clonedZoneId; try { - const { data } = await axios.post(`Zones/${zoneId}/clone`, { - shipped: $props.zone.value.shipped, - }); - clonedZoneId = data; + const { data } = await axios.post(`Zones/${zoneId}/clone`, {}); + notify(opts); + push(`/zone/${data.id}/basic-data`); } catch (e) { opts.message = t('It was not able to clone the zone'); opts.type = 'negative'; - } finally { - notify(opts); - - if (clonedZoneId) push({ name: 'ZoneSummary', params: { id: clonedZoneId } }); } }, remove: async () => { try { - await axios.post(`Zones/${zoneId}/setDeleted`); + await axios.post(`Zones/${zoneId}/deleteZone`); notify({ message: t('Zone deleted'), type: 'positive' }); - notify({ - message: t('You can undo this action within the first hour'), - icon: 'info', - }); - push({ name: 'ZoneList' }); } catch (e) { notify({ message: e.message, type: 'negative' }); @@ -64,30 +46,31 @@ function openConfirmDialog(callback) { } </script> <template> - <QItem @click="openConfirmDialog('clone')" v-ripple clickable> - <QItemSection avatar> - <QIcon name="content_copy" /> - </QItemSection> - <QItemSection>{{ t('To clone zone') }}</QItemSection> - </QItem> <QItem @click="openConfirmDialog('remove')" v-ripple clickable> <QItemSection avatar> <QIcon name="delete" /> </QItemSection> <QItemSection>{{ t('deleteZone') }}</QItemSection> </QItem> + <QItem @click="openConfirmDialog('clone')" v-ripple clickable> + <QItemSection avatar> + <QIcon name="content_copy" /> + </QItemSection> + <QItemSection>{{ t('cloneZone') }}</QItemSection> + </QItem> </template> <i18n> en: - deleteZone: Delete zone + deleteZone: Delete + cloneZone: Clone confirmDeletion: Confirm deletion confirmDeletionMessage: Are you sure you want to delete this zone? es: - To clone zone: Clonar zone - deleteZone: Eliminar zona + cloneZone: Clonar + deleteZone: Eliminar confirmDeletion: Confirmar eliminación confirmDeletionMessage: Seguro que quieres eliminar este zona? - + Zone deleted: Zona eliminada </i18n> diff --git a/src/pages/Zone/Card/ZoneEventExclusionForm.vue b/src/pages/Zone/Card/ZoneEventExclusionForm.vue index 721f4bbc3..0ba2e640a 100644 --- a/src/pages/Zone/Card/ZoneEventExclusionForm.vue +++ b/src/pages/Zone/Card/ZoneEventExclusionForm.vue @@ -58,20 +58,12 @@ const arrayData = useArrayData('ZoneEvents'); const exclusionGeoCreate = async () => { try { - if (isNew.value) { - const params = { - zoneFk: parseInt(route.params.id), - date: dated.value, - geoIds: tickedNodes.value, - }; - await axios.post('Zones/exclusionGeo', params); - } else { - const params = { - zoneExclusionFk: props.event?.zoneExclusionFk, - geoIds: tickedNodes.value, - }; - await axios.post('Zones/updateExclusionGeo', params); - } + const params = { + zoneFk: parseInt(route.params.id), + date: dated.value, + geoIds: tickedNodes.value, + }; + await axios.post('Zones/exclusionGeo', params); await refetchEvents(); } catch (err) { console.error('Error creating exclusion geo: ', err); @@ -85,7 +77,7 @@ const exclusionCreate = async () => { { dated: dated.value }, ]); else - await axios.put(`Zones/${route.params.id}/exclusions/${props.event?.id}`, { + await axios.post(`Zones/${route.params.id}/exclusions`, { dated: dated.value, }); @@ -103,8 +95,7 @@ const onSubmit = async () => { const deleteEvent = async () => { try { if (!props.event) return; - const exclusionId = props.event?.zoneExclusionFk || props.event?.id; - await axios.delete(`Zones/${route.params.id}/exclusions/${exclusionId}`); + await axios.delete(`Zones/${route.params.id}/exclusions`); await refetchEvents(); } catch (err) { console.error('Error deleting event: ', err); @@ -141,7 +132,11 @@ onMounted(() => { > <template #form-inputs> <VnRow class="row q-gutter-md q-mb-lg"> - <VnInputDate :label="t('eventsInclusionForm.day')" v-model="dated" /> + <VnInputDate + :label="t('eventsInclusionForm.day')" + v-model="dated" + :model-value="props.date" + /> </VnRow> <div class="column q-gutter-y-sm q-mb-md"> <QRadio diff --git a/src/pages/Zone/Card/ZoneEvents.vue b/src/pages/Zone/Card/ZoneEvents.vue index e4fe5ff22..21991481a 100644 --- a/src/pages/Zone/Card/ZoneEvents.vue +++ b/src/pages/Zone/Card/ZoneEvents.vue @@ -13,8 +13,8 @@ import { reactive } from 'vue'; const { t } = useI18n(); const stateStore = useStateStore(); -const firstDay = ref(null); -const lastDay = ref(null); +const firstDay = ref(); +const lastDay = ref(); const events = ref([]); const formModeName = ref('include'); @@ -44,34 +44,15 @@ onUnmounted(() => (stateStore.rightDrawer = false)); </script> <template> - <template v-if="stateStore.isHeaderMounted()"> - <Teleport to="#actions-append"> - <div class="row q-gutter-x-sm"> - <QBtn - flat - @click="stateStore.toggleRightDrawer()" - round - dense - icon="menu" - > - <QTooltip bottom anchor="bottom right"> - {{ t('globals.collapseMenu') }} - </QTooltip> - </QBtn> - </div> - </Teleport> - </template> - <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> - <QScrollArea class="fit text-grey-8"> - <ZoneEventsPanel - :first-day="firstDay" - :last-day="lastDay" - :events="events" - v-model:formModeName="formModeName" - @open-zone-form="openForm" - /> - </QScrollArea> - </QDrawer> + <Teleport to="#right-panel" v-if="useStateStore().isHeaderMounted()"> + <ZoneEventsPanel + :first-day="firstDay" + :last-day="lastDay" + :events="events" + v-model:formModeName="formModeName" + @open-zone-form="openForm" + /> + </Teleport> <QPage class="q-pa-md flex justify-center"> <ZoneCalendarGrid v-model:events="events" diff --git a/src/pages/Zone/Card/ZoneLocationsTree.vue b/src/pages/Zone/Card/ZoneLocationsTree.vue index 70384a1bb..cb1508ed6 100644 --- a/src/pages/Zone/Card/ZoneLocationsTree.vue +++ b/src/pages/Zone/Card/ZoneLocationsTree.vue @@ -1,10 +1,7 @@ <script setup> import { onMounted, ref, computed, watch, onUnmounted } from 'vue'; -import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; -import VnInput from 'src/components/common/VnInput.vue'; - import { useState } from 'src/composables/useState'; import axios from 'axios'; import { useArrayData } from 'composables/useArrayData'; @@ -30,7 +27,6 @@ const props = defineProps({ const emit = defineEmits(['update:tickedNodes']); -const { t } = useI18n(); const route = useRoute(); const state = useState(); @@ -186,16 +182,6 @@ onUnmounted(() => { </script> <template> - <VnInput - v-if="showSearchBar" - v-model="store.userParams.search" - :placeholder="t('globals.search')" - @keydown.enter.prevent="reFetch()" - > - <template #prepend> - <QIcon class="cursor-pointer" name="search" /> - </template> - </VnInput> <QTree ref="treeRef" :nodes="nodes" diff --git a/src/pages/Zone/Card/ZoneSearchbar.vue b/src/pages/Zone/Card/ZoneSearchbar.vue index 607057d0b..06168eb62 100644 --- a/src/pages/Zone/Card/ZoneSearchbar.vue +++ b/src/pages/Zone/Card/ZoneSearchbar.vue @@ -19,24 +19,14 @@ const exprBuilder = (param, value) => { agencyModeFk: value, }; case 'search': - if (value) { - if (!isNaN(value)) { - return { id: value }; - } else { - return { - name: { - like: `%${value}%`, - }, - }; - } - } + return /^\d+$/.test(value) ? { id: value } : { name: { like: `%${value}%` } }; } }; </script> <template> <VnSearchbar - data-key="ZoneList" + data-key="Zones" url="Zones" :filter="{ include: { relation: 'agencyMode', scope: { fields: ['name'] } }, diff --git a/src/pages/Zone/Card/ZoneWarehouses.vue b/src/pages/Zone/Card/ZoneWarehouses.vue index 196333d08..1d28bf391 100644 --- a/src/pages/Zone/Card/ZoneWarehouses.vue +++ b/src/pages/Zone/Card/ZoneWarehouses.vue @@ -14,7 +14,7 @@ const { t } = useI18n(); const route = useRoute(); const { openConfirmationModal } = useVnConfirm(); -const paginateRef = ref(null); +const paginateRef = ref(); const createWarehouseDialogRef = ref(null); const arrayData = useArrayData('ZoneWarehouses'); diff --git a/src/pages/Zone/ZoneDeliveryPanel.vue b/src/pages/Zone/ZoneDeliveryPanel.vue index 03f534701..d6c96b935 100644 --- a/src/pages/Zone/ZoneDeliveryPanel.vue +++ b/src/pages/Zone/ZoneDeliveryPanel.vue @@ -1,47 +1,25 @@ <script setup> -import { onMounted, ref, reactive } from 'vue'; +import { onMounted, ref, reactive, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import VnSelect from 'src/components/common/VnSelect.vue'; - import { useArrayData } from 'src/composables/useArrayData'; -import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; -import { watch } from 'vue'; +import FetchData from 'src/components/FetchData.vue'; const { t } = useI18n(); const { notify } = useNotify(); -const deliveryMethodFk = ref(null); -const deliveryMethods = ref([]); +const deliveryMethodFk = ref('delivery'); +const deliveryMethods = ref({}); +const inq = ref([]); const formData = reactive({}); const arrayData = useArrayData('ZoneDeliveryDays', { url: 'Zones/getEvents', }); -const fetchDeliveryMethods = async (filter) => { - try { - const params = { filter: JSON.stringify(filter) }; - const { data } = await axios.get('DeliveryMethods', { params }); - return data.map((deliveryMethod) => deliveryMethod.id); - } catch (err) { - console.error('Error fetching delivery methods: ', err); - } -}; - -watch( - () => deliveryMethodFk.value, - async (val) => { - let filter; - if (val === 'pickUp') filter = { where: { code: 'PICKUP' } }; - else filter = { where: { code: { inq: ['DELIVERY', 'AGENCY'] } } }; - - deliveryMethods.value = await fetchDeliveryMethods(filter); - }, - { immediate: true } -); - +const deliveryMethodsConfig = { pickUp: ['PICKUP'], delivery: ['AGENCY', 'DELIVERY'] }; const fetchData = async (params) => { try { const { data } = params @@ -62,14 +40,38 @@ const onSubmit = async () => { }; onMounted(async () => { - deliveryMethodFk.value = 'delivery'; formData.geoFk = arrayData.store?.userParams?.geoFk; formData.agencyModeFk = arrayData.store?.userParams?.agencyModeFk; if (formData.geoFk || formData.agencyModeFk) await fetchData(); }); +watch( + () => deliveryMethodFk.value, + () => { + inq.value = { + deliveryMethodFk: { inq: deliveryMethods.value[deliveryMethodFk.value] }, + }; + } +); </script> <template> + <FetchData + url="DeliveryMethods" + :fields="['id', 'name', 'deliveryMethodFk']" + @on-fetch=" + (data) => { + Object.entries(deliveryMethodsConfig).forEach(([key, value]) => { + deliveryMethods[key] = data + .filter((code) => value.includes(code.code)) + .map((method) => method.id); + }); + inq = { + deliveryMethodFk: { inq: deliveryMethods[deliveryMethodFk] }, + }; + } + " + auto-load + /> <QForm @submit="onSubmit()" class="q-pa-md"> <div class="column q-gutter-y-sm"> <QRadio @@ -90,7 +92,7 @@ onMounted(async () => { :label="t('deliveryPanel.postcode')" v-model="formData.geoFk" url="Postcodes/location" - :fields="['geoFk', 'code', 'townFk']" + :fields="['geoFk', 'code', 'townFk', 'countryFk']" sort-by="code, townFk" option-value="geoFk" option-label="code" @@ -106,26 +108,35 @@ onMounted(async () => { <QItemLabel>{{ opt.code }}</QItemLabel> <QItemLabel caption >{{ opt.town?.province?.name }}, - {{ opt.town?.province?.country?.country }}</QItemLabel + {{ opt.town?.province?.country?.name }}</QItemLabel > </QItemSection> </QItem> </template> </VnSelect> <VnSelect - :label=" - t( - deliveryMethodFk === 'delivery' - ? 'deliveryPanel.agency' - : 'deliveryPanel.warehouse' - ) - " + data-key="delivery" + v-if="deliveryMethodFk == 'delivery'" + :label="t('deliveryPanel.agency')" v-model="formData.agencyModeFk" url="AgencyModes/isActive" :fields="['id', 'name']" - :where="{ - deliveryMethodFk: { inq: deliveryMethods }, - }" + :where="inq" + sort-by="name ASC" + option-value="id" + option-label="name" + hide-selected + dense + outlined + rounded + /> + <VnSelect + v-else + :label="t('deliveryPanel.warehouse')" + v-model="formData.agencyModeFk" + url="AgencyModes/isActive" + :fields="['id', 'name']" + :where="inq" sort-by="name ASC" option-value="id" option-label="name" diff --git a/src/pages/Zone/ZoneFilterPanel.vue b/src/pages/Zone/ZoneFilterPanel.vue index c84355eb0..25c55d75c 100644 --- a/src/pages/Zone/ZoneFilterPanel.vue +++ b/src/pages/Zone/ZoneFilterPanel.vue @@ -27,6 +27,7 @@ const agencies = ref([]); :data-key="props.dataKey" :search-button="true" :hidden-tags="['search']" + search-url="table" > <template #tags="{ tag }"> <div class="q-gutter-x-xs"> diff --git a/src/pages/Zone/ZoneList.vue b/src/pages/Zone/ZoneList.vue index 0272292f6..d160ea6b5 100644 --- a/src/pages/Zone/ZoneList.vue +++ b/src/pages/Zone/ZoneList.vue @@ -1,74 +1,120 @@ <script setup> import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; -import { onMounted, computed } from 'vue'; +import { computed, ref, onMounted } from 'vue'; +import axios from 'axios'; + import { toCurrency } from 'src/filters'; - -import VnPaginate from 'src/components/ui/VnPaginate.vue'; -import ZoneSummary from 'src/pages/Zone/Card/ZoneSummary.vue'; - -import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { toTimeFormat } from 'src/filters/date'; import { useVnConfirm } from 'composables/useVnConfirm'; import useNotify from 'src/composables/useNotify.js'; +import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useStateStore } from 'stores/useStateStore'; -import axios from 'axios'; +import ZoneSummary from 'src/pages/Zone/Card/ZoneSummary.vue'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import VnInputTime from 'src/components/common/VnInputTime.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import ZoneFilterPanel from './ZoneFilterPanel.vue'; import ZoneSearchbar from './Card/ZoneSearchbar.vue'; -const stateStore = useStateStore(); const { t } = useI18n(); const router = useRouter(); const { notify } = useNotify(); const { viewSummary } = useSummaryDialog(); const { openConfirmationModal } = useVnConfirm(); +const stateStore = useStateStore(); +const tableRef = ref(); +const warehouseOptions = ref([]); -const redirectToZoneSummary = (event, { id }) => { - router.push({ name: 'ZoneSummary', params: { id } }); +const tableFilter = { + include: [ + { + relation: 'agencyMode', + scope: { + fields: ['id', 'name'], + }, + }, + ], }; const columns = computed(() => [ { - name: 'ID', - label: t('list.id'), - field: (row) => row.id, - sortable: true, align: 'left', + name: 'id', + label: t('list.id'), + chip: { + condition: () => true, + }, + isId: true, + columnFilter: { + inWhere: true, + }, }, { + align: 'left', name: 'name', label: t('list.name'), - field: (row) => row.name, - sortable: true, - align: 'left', + isTitle: true, + create: true, + columnFilter: { + optionLabel: 'name', + optionValue: 'id', + }, }, { - name: 'agency', + align: 'left', + name: 'agencyModeFk', label: t('list.agency'), - field: (row) => row?.agencyMode?.name, - sortable: true, - align: 'left', + cardVisible: true, + columnFilter: { + component: 'select', + inWhere: true, + attrs: { + url: 'AgencyModes', + }, + }, + columnField: { + component: null, + }, + format: (row, dashIfEmpty) => dashIfEmpty(row?.agencyMode?.name), }, { - name: 'close', - label: t('list.close'), - field: (row) => (row?.hour ? toTimeFormat(row?.hour) : '-'), - sortable: true, align: 'left', - }, - { name: 'price', label: t('list.price'), - field: (row) => (row?.price ? toCurrency(row.price) : '-'), - sortable: true, - align: 'left', + cardVisible: true, + format: (row) => toCurrency(row.price), + columnFilter: { + inWhere: true, + }, + }, + { + align: 'left', + name: 'hour', + label: t('list.close'), + cardVisible: true, + format: (row) => toTimeFormat(row.hour), + hidden: true, }, { - name: 'actions', - label: '', - sortable: false, align: 'right', + name: 'tableActions', + actions: [ + { + title: t('list.zoneSummary'), + icon: 'preview', + action: (row) => viewSummary(row.id, ZoneSummary), + isPrimary: true, + }, + { + title: t('globals.clone'), + icon: 'vn:clone', + action: (row) => handleClone(row.id), + isPrimary: true, + }, + ], }, ]); @@ -84,6 +130,7 @@ const handleClone = (id) => { () => clone(id) ); }; + onMounted(() => (stateStore.rightDrawer = true)); </script> @@ -91,82 +138,72 @@ onMounted(() => (stateStore.rightDrawer = true)); <ZoneSearchbar /> <RightMenu> <template #right-panel> - <ZoneFilterPanel data-key="ZoneList" :expr-builder="exprBuilder" /> + <ZoneFilterPanel data-key="Zones" /> </template> </RightMenu> - <QPage class="column items-center q-pa-md"> - <div class="vn-card-list"> - <VnPaginate - data-key="ZoneList" - url="Zones" - :filter="{ - include: { relation: 'agencyMode', scope: { fields: ['name'] } }, - }" - :limit="20" - auto-load - > - <template #body="{ rows }"> - <div class="q-pa-md"> - <QTable - :rows="rows" - :columns="columns" - row-key="clientId" - class="full-width" - @row-click="redirectToZoneSummary" - > - <template #header="props"> - <QTr :props="props" class="bg"> - <QTh - v-for="col in props.cols" - :key="col.name" - :props="props" - > - {{ t(col.label) }} - <QTooltip v-if="col.tooltip">{{ - col.tooltip - }}</QTooltip> - </QTh> - </QTr> - </template> - - <template #body-cell="props"> - <QTd :props="props"> - <QTr :props="props" class="cursor-pointer"> - {{ props.value }} - </QTr> - </QTd> - </template> - <template #body-cell-actions="props"> - <QTd :props="props" class="q-gutter-x-sm"> - <QIcon - name="vn:clone" - size="sm" - color="primary" - @click.stop="handleClone(props.row.id)" - > - <QTooltip>{{ t('globals.clone') }}</QTooltip> - </QIcon> - <QIcon - name="preview" - size="sm" - color="primary" - @click.stop=" - viewSummary(props.row.id, ZoneSummary) - " - > - <QTooltip>{{ t('Preview') }}</QTooltip> - </QIcon> - </QTd> - </template> - </QTable> - </div> - </template> - </VnPaginate> - </div> - <QPageSticky position="bottom-right" :offset="[18, 18]"> - <QBtn :to="{ path: `/zone/create` }" fab icon="add" color="primary"> - <QTooltip>{{ t('list.create') }}</QTooltip> - </QBtn> - </QPageSticky> - </QPage> + <VnTable + ref="tableRef" + data-key="Zones" + url="Zones" + :create="{ + urlCreate: 'Zones', + title: t('list.createZone'), + onDataSaved: ({ id }) => tableRef.redirect(`${id}/location`), + formInitialData: {}, + }" + :user-filter="tableFilter" + :columns="columns" + redirect="zone" + :right-search="false" + auto-load + > + <template #more-create-dialog="{ data }"> + <VnSelect + url="AgencyModes" + v-model="data.agencyModeFk" + option-value="id" + option-label="name" + :label="t('list.agency')" + /> + <VnInput + v-model="data.price" + :label="t('list.price')" + min="0" + type="number" + required="true" + /> + <VnInput + v-model="data.bonus" + :label="t('list.bonus')" + min="0" + type="number" + /> + <VnInput + v-model="data.travelingDays" + :label="t('list.travelingDays')" + type="number" + min="0" + /> + <VnInputTime v-model="data.hour" :label="t('list.close')" /> + <VnSelect + url="Warehouses" + v-model="data.warehouseFK" + option-value="id" + option-label="name" + :label="t('list.warehouse')" + :options="warehouseOptions" + /> + <QCheckbox + v-model="data.isVolumetric" + :label="t('list.isVolumetric')" + :toggle-indeterminate="false" + /> + </template> + </VnTable> </template> + +<i18n> +es: + Search zone: Buscar zona + You can search zones by id or name: Puedes buscar zonas por id o nombre +</i18n> diff --git a/src/pages/Zone/locale/en.yml b/src/pages/Zone/locale/en.yml index 31eeb2b7f..2608c071c 100644 --- a/src/pages/Zone/locale/en.yml +++ b/src/pages/Zone/locale/en.yml @@ -18,9 +18,16 @@ list: create: Create zone openSummary: Details searchZone: Search zones + searchLocation: Search locations searchInfo: Search zone by id or name confirmCloneTitle: All it's properties will be copied confirmCloneSubtitle: Do you want to clone this zone? + travelingDays: Traveling days + warehouse: Warehouse + bonus: Bonus + isVolumetric: Volumetric + createZone: Create zone + zoneSummary: Summary create: name: Name warehouse: Warehouse @@ -30,6 +37,8 @@ create: price: Price bonus: Bonus volumetric: Volumetric + itemMaxSize: Max m³ + inflation: Inflation summary: agency: Agency price: Price diff --git a/src/pages/Zone/locale/es.yml b/src/pages/Zone/locale/es.yml index c670c2c08..dd919a0c5 100644 --- a/src/pages/Zone/locale/es.yml +++ b/src/pages/Zone/locale/es.yml @@ -18,9 +18,16 @@ list: create: Crear zona openSummary: Detalles searchZone: Buscar zonas + searchLocation: Buscar localizaciones searchInfo: Buscar zonas por identificador o nombre confirmCloneTitle: Todas sus propiedades serán copiadas confirmCloneSubtitle: ¿Seguro que quieres clonar esta zona? + travelingDays: Días de viaje + warehouse: Almacén + bonus: Bonus + isVolumetric: Volumétrico + createZone: Crear zona + zoneSummary: Resumen create: name: Nombre warehouse: Almacén @@ -30,6 +37,8 @@ create: price: Precio bonus: Bonificación volumetric: Volumétrico + itemMaxSize: Medida máxima + inflation: Inflación summary: agency: Agencia price: Precio diff --git a/src/router/modules/monitor.js b/src/router/modules/monitor.js index f0db8d3f3..3353da3cf 100644 --- a/src/router/modules/monitor.js +++ b/src/router/modules/monitor.js @@ -11,7 +11,7 @@ export default { component: RouterView, redirect: { name: 'MonitorMain' }, menus: { - main: ['MonitorList'], + main: ['MonitorTickets', 'MonitorClientsActions'], card: [], }, children: [ @@ -19,16 +19,27 @@ export default { path: '', name: 'MonitorMain', component: () => import('src/components/common/VnSectionMain.vue'), - redirect: { name: 'MonitorList' }, + redirect: { name: 'MonitorTickets' }, children: [ { - path: 'list', - name: 'MonitorList', + path: 'tickets', + name: 'MonitorTickets', meta: { - title: 'list', - icon: 'grid_view', + title: 'ticketsMonitor', + icon: 'vn:ticket', }, - component: () => import('src/pages/Monitor/MonitorList.vue'), + component: () => + import('src/pages/Monitor/Ticket/MonitorTickets.vue'), + }, + { + path: 'clients-actions', + name: 'MonitorClientsActions', + meta: { + title: 'clientsActionsMonitor', + icon: 'vn:client', + }, + component: () => + import('src/pages/Monitor/MonitorClientsActions.vue'), }, ], }, diff --git a/src/router/modules/route.js b/src/router/modules/route.js index 3c5c860cf..955fc9098 100644 --- a/src/router/modules/route.js +++ b/src/router/modules/route.js @@ -7,6 +7,7 @@ export default { title: 'routes', icon: 'vn:delivery', moduleName: 'Route', + keyBinding: 'r', }, component: RouterView, redirect: { name: 'RouteMain' }, diff --git a/src/router/modules/worker.js b/src/router/modules/worker.js index f80df5e06..2a523e7fe 100644 --- a/src/router/modules/worker.js +++ b/src/router/modules/worker.js @@ -25,6 +25,7 @@ export default { 'WorkerLocker', 'WorkerBalance', 'WorkerFormation', + 'WorkerMedical', ], }, children: [ @@ -196,6 +197,15 @@ export default { }, component: () => import('src/pages/Worker/Card/WorkerFormation.vue'), }, + { + name: 'WorkerMedical', + path: 'medical', + meta: { + title: 'medical', + icon: 'medical_information', + }, + component: () => import('src/pages/Worker/Card/WorkerMedical.vue'), + }, ], }, ], diff --git a/src/router/modules/zone.js b/src/router/modules/zone.js index 889b47464..40358c58e 100644 --- a/src/router/modules/zone.js +++ b/src/router/modules/zone.js @@ -50,33 +50,6 @@ export default { }, component: () => import('src/pages/Zone/ZoneDeliveryDays.vue'), }, - { - path: 'create', - name: 'ZoneCreate', - meta: { - title: 'zoneCreate', - icon: 'create', - }, - component: () => import('src/pages/Zone/ZoneCreate.vue'), - }, - { - path: ':id/edit', - name: 'ZoneEdit', - meta: { - title: 'zoneEdit', - icon: 'edit', - }, - component: () => import('src/pages/Zone/ZoneCreate.vue'), - }, - // { - // path: 'counter', - // name: 'ZoneCounter', - // meta: { - // title: 'zoneCounter', - // icon: 'add_circle', - // }, - // component: () => import('src/pages/Zone/ZoneCounter.vue'), - // }, { name: 'ZoneUpcomingDeliveries', path: 'upcoming-deliveries', diff --git a/test/cypress/integration/zone/zoneBasicData.spec.js b/test/cypress/integration/zone/zoneBasicData.spec.js new file mode 100644 index 000000000..c6151a49b --- /dev/null +++ b/test/cypress/integration/zone/zoneBasicData.spec.js @@ -0,0 +1,21 @@ +describe('ZoneBasicData', () => { + const notification = '.q-notification__message'; + + beforeEach(() => { + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit('/#/zone/4/basic-data'); + }); + + it('should throw an error if the name is empty', () => { + cy.get('.q-card > :nth-child(1)').clear(); + cy.get('.q-btn-group > .q-btn--standard').click(); + cy.get(notification).should('contains.text', "can't be blank"); + }); + + it("should edit the basicData's zone", () => { + cy.get('.q-card > :nth-child(1)').type(' modified'); + cy.get('.q-btn-group > .q-btn--standard').click(); + cy.get(notification).should('contains.text', 'Data saved'); + }); +}); diff --git a/test/cypress/integration/zone/zoneCreate.spec.js b/test/cypress/integration/zone/zoneCreate.spec.js new file mode 100644 index 000000000..9618ea846 --- /dev/null +++ b/test/cypress/integration/zone/zoneCreate.spec.js @@ -0,0 +1,38 @@ +describe('ZoneCreate', () => { + const notification = '.q-notification__message'; + + const data = { + Name: { val: 'Zone pickup D' }, + Price: { val: '3' }, + Bonus: { val: '0' }, + 'Traveling days': { val: '0' }, + Warehouse: { val: 'Algemesi', type: 'select' }, + Volumetric: { val: 'true', type: 'checkbox' }, + }; + + beforeEach(() => { + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit('/#/zone/list'); + cy.get('.q-page-sticky > div > .q-btn').click(); + }); + + it('should throw an error if an agency has not been selected', () => { + cy.fillInForm({ + ...data, + }); + cy.get('input[aria-label="Close"]').type('10:00'); + cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.get(notification).should('contains.text', 'Agency cannot be blank'); + }); + + it('should create a zone', () => { + cy.fillInForm({ + ...data, + Agency: { val: 'inhouse pickup', type: 'select' }, + }); + cy.get('input[aria-label="Close"]').type('10:00'); + cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.get(notification).should('contains.text', 'Data created'); + }); +}); diff --git a/test/cypress/integration/zone/zoneList.spec.js b/test/cypress/integration/zone/zoneList.spec.js index f35da7e5f..92c77a2c6 100644 --- a/test/cypress/integration/zone/zoneList.spec.js +++ b/test/cypress/integration/zone/zoneList.spec.js @@ -1,15 +1,18 @@ describe('ZoneList', () => { beforeEach(() => { - cy.viewport(1920, 1080); + cy.viewport(1280, 720); cy.login('developer'); - cy.visit(`/#/zone/list`); + cy.visit('/#/zone/list'); }); - it('should open the details', () => { - cy.get(':nth-child(1) > .text-right > .material-symbols-outlined').click(); + it('should filter by agency', () => { + cy.get( + ':nth-child(1) > .column > .q-field > .q-field__inner > .q-field__control > .q-field__control-container' + ).type('{downArrow}{enter}'); }); - it('should redirect to summary', () => { - cy.waitForElement('.q-page'); - cy.get('tbody > :nth-child(1)').click(); + + it('should open the zone summary', () => { + cy.get('input[aria-label="Name"]').type('zone refund'); + cy.get('.q-scrollarea__content > .q-btn--standard > .q-btn__content').click(); }); }); diff --git a/test/cypress/integration/zone/zoneWarehouse.spec.js b/test/cypress/integration/zone/zoneWarehouse.spec.js new file mode 100644 index 000000000..3ffa3f69d --- /dev/null +++ b/test/cypress/integration/zone/zoneWarehouse.spec.js @@ -0,0 +1,34 @@ +describe('ZoneWarehouse', () => { + const data = { + Warehouse: { val: 'Algemesi', type: 'select' }, + }; + const deviceProductionField = + '.vn-row > :nth-child(1) > .q-field > .q-field__inner > .q-field__control > .q-field__control-container'; + const dataError = "ER_DUP_ENTRY: Duplicate entry '2-2' for key 'zoneFk'"; + + beforeEach(() => { + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/zone/2/warehouses`); + }); + + it('should throw an error if the warehouse chosen is already put in the zone', () => { + cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); + cy.get(deviceProductionField).click(); + cy.get(deviceProductionField).type('{upArrow}{enter}'); + cy.get('.q-notification__message').should('have.text', dataError); + }); + + it('should create a warehouse', () => { + cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); + cy.get(deviceProductionField).click(); + cy.fillInForm(data); + cy.get('.q-mt-lg > .q-btn--standard').click(); + }); + + it('should delete a warehouse', () => { + cy.get('tbody > :nth-child(2) > :nth-child(2) > .q-icon').click(); + cy.get('.q-card__actions > .q-btn--flat > .q-btn__content').click(); + cy.reload(); + }); +}); diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index 3cf909af5..a9a405313 100755 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -105,6 +105,12 @@ Cypress.Commands.add('fillInForm', (obj, form = '.q-form > .q-card') => { case 'date': cy.wrap(el).type(val.split('-').join('')); break; + case 'time': + cy.wrap(el).click(); + cy.get('.q-time .q-time__clock').contains(val.h).click(); + cy.get('.q-time .q-time__clock').contains(val.m).click(); + cy.get('.q-time .q-time__link').contains(val.x).click(); + break; default: cy.wrap(el).type(val); break;