Merge pull request 'Ticket expedition' (!499) from hyervoni/salix-front-mindshore:feature/TicketExpedition into dev
gitea/salix-front/pipeline/head This commit looks good Details

Reviewed-on: #499
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
Reviewed-by: Alex Moreno <alexm@verdnatura.es>
This commit is contained in:
Javier Segarra 2024-07-08 08:06:15 +00:00
commit 22faaf8186
9 changed files with 644 additions and 37 deletions

View File

@ -52,7 +52,7 @@ const toggleMarkAll = (val) => {
const getConfig = async (url, filter) => {
const response = await axios.get(url, {
params: { filter: filter },
params: { filter: JSON.stringify(filter) },
});
return response.data && response.data.length > 0 ? response.data[0] : null;
};
@ -60,7 +60,7 @@ const getConfig = async (url, filter) => {
const fetchViewConfigData = async () => {
try {
const userConfigFilter = {
where: { tableCode: $props.tableCode, userFk: user.id },
where: { tableCode: $props.tableCode, userFk: user.value.id },
};
const userConfig = await getConfig('UserConfigViews', userConfigFilter);
@ -74,8 +74,14 @@ const fetchViewConfigData = async () => {
const defaultConfig = await getConfig('DefaultViewConfigs', defaultConfigFilter);
if (defaultConfig) {
// Si el backend devuelve una configuración por defecto la usamos
setUserConfigViewData(defaultConfig.columns);
return;
} else {
// Si no hay configuración por defecto mostramos todas las columnas
const defaultColumns = {};
$props.allColumns.forEach((col) => (defaultColumns[col] = true));
setUserConfigViewData(defaultColumns);
}
} catch (err) {
console.err('Error fetching config view data', err);

View File

@ -550,6 +550,7 @@ ticket:
observation: Notes
ticketAdvance: Advance tickets
futureTickets: Future tickets
expedition: Expedition
purchaseRequest: Purchase request
weeklyTickets: Weekly tickets
services: Service

View File

@ -548,6 +548,7 @@ ticket:
observation: Notas
ticketAdvance: Adelantar tickets
futureTickets: Tickets a futuro
expedition: Expedición
purchaseRequest: Petición de compra
weeklyTickets: Tickets programados
services: Servicios

View File

@ -0,0 +1,78 @@
<script setup>
import { reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnInput from 'src/components/common/VnInput.vue';
import VnRow from 'components/ui/VnRow.vue';
import FormModelPopup from 'components/FormModelPopup.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const $props = defineProps({
ticket: {
type: Boolean,
default: false,
},
withRoute: {
type: Boolean,
default: false,
},
selectedExpeditions: {
type: Array,
default: () => [],
},
});
const { t } = useI18n();
const router = useRouter();
const { notify } = useNotify();
const newTicketFormData = reactive({});
const createTicket = async () => {
try {
const expeditionIds = $props.selectedExpeditions.map(
(expedition) => expedition.id
);
const params = {
clientId: $props.ticket.clientFk,
landed: newTicketFormData.landed,
warehouseId: $props.ticket.warehouseFk,
addressId: $props.ticket.addressFk,
agencyModeId: $props.ticket.agencyModeFk,
routeId: newTicketFormData.routeFk,
expeditionIds: expeditionIds,
};
const { data } = await axios.post('Expeditions/moveExpeditions', params);
notify(t('globals.dataSaved'), 'positive');
router.push({ name: 'TicketSummary', params: { id: data.id } });
} catch (error) {
console.error(error);
}
};
</script>
<template>
<FormModelPopup
model="expeditionNewTicket"
:form-initial-data="newTicketFormData"
:save-fn="createTicket"
>
<template #form-inputs="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<VnInputDate :label="t('expedition.landed')" v-model="data.landed" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
v-if="withRoute"
:label="t('expedition.routeId')"
v-model="data.routeFk"
/>
</VnRow>
</template>
</FormModelPopup>
</template>

View File

@ -0,0 +1,474 @@
<script setup>
import { onMounted, ref, computed, onUnmounted, reactive, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import TicketEditManaProxy from './TicketEditMana.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import ExpeditionNewTicket from './ExpeditionNewTicket.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency, toPercentage } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
import { useVnConfirm } from 'composables/useVnConfirm';
import useNotify from 'src/composables/useNotify.js';
import { toDateTimeFormat } from 'src/filters/date';
import axios from 'axios';
const route = useRoute();
const stateStore = useStateStore();
const { t } = useI18n();
const { notify } = useNotify();
const { openConfirmationModal } = useVnConfirm();
const editPriceProxyRef = ref(null);
const newTicketDialogRef = ref(null);
const logsTableDialogRef = ref(null);
const expeditionsLogsData = ref([]);
const selectedExpeditions = ref([]);
const allColumnNames = ref([]);
const visibleColumns = ref([]);
const newTicketWithRoute = ref(false);
const itemsOptions = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'expeditionFk':
return { id: value };
case 'packageItemName':
return { packagingItemFk: value };
}
};
const expeditionsFilter = computed(() => ({
where: { ticketFk: route.params.id },
order: ['created DESC'],
}));
const expeditionsArrayData = useArrayData('ticketExpeditions', {
url: 'Expeditions/filter',
filter: expeditionsFilter.value,
exprBuilder: exprBuilder,
});
const expeditionsStore = expeditionsArrayData.store;
const ticketExpeditions = computed(() => expeditionsStore.data);
const ticketArrayData = useArrayData('ticketData');
const ticketStore = ticketArrayData.store;
const ticketData = computed(() => ticketStore.data);
const refetchExpeditions = async () => {
await expeditionsArrayData.applyFilter({
params: { filter: JSON.stringify(expeditionsFilter.value) },
});
};
watch(
() => route.params.id,
async () => await refetchExpeditions(),
{ immediate: true }
);
const params = reactive({});
const applyColumnFilter = async (col) => {
try {
const paramKey = col.columnFilter?.filterParamKey || col.field;
params[paramKey] = col.columnFilter.filterValue;
await expeditionsArrayData.addFilter({ params });
} catch (err) {
console.error('Error applying column filter', err);
}
};
const getInputEvents = (col) => {
return col.columnFilter.type === 'select'
? { 'update:modelValue': () => applyColumnFilter(col) }
: {
'keyup.enter': () => applyColumnFilter(col),
};
};
const columns = computed(() => [
{
label: t('expedition.id'),
name: 'id',
field: 'id',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterParamKey: 'expeditionFk',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('expedition.item'),
name: 'item',
align: 'left',
columnFilter: {
component: VnInput,
type: 'text',
filterParamKey: 'packageItemName',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('expedition.name'),
name: 'name',
field: 'packageItemName',
align: 'left',
columnFilter: {
component: VnSelect,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemsOptions.value,
'option-value': 'id',
'option-label': 'name',
dense: true,
},
},
},
{
label: t('expedition.packageType'),
name: 'packageType',
field: 'freightItemName',
align: 'left',
columnFilter: {
component: VnInput,
type: 'text',
// filterParamKey: 'expeditionFk',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('expedition.counter'),
name: 'counter',
field: 'counter',
align: 'left',
columnFilter: null,
},
{
label: t('expedition.externalId'),
name: 'externalId',
field: 'externalId',
align: 'left',
columnFilter: null,
},
{
label: t('expedition.created'),
name: 'created',
field: 'created',
align: 'left',
columnFilter: null,
format: (value) => toDateTimeFormat(value),
},
{
label: t('expedition.state'),
name: 'state',
field: 'state',
align: 'left',
columnFilter: null,
},
{
label: '',
name: 'history',
align: 'left',
columnFilter: null,
},
]);
const logTableColumns = computed(() => [
{
label: t('expedition.state'),
name: 'state',
field: 'state',
align: 'left',
sortable: true,
},
{
label: t('expedition.name'),
name: 'name',
align: 'name',
columnFilter: null,
},
{
label: t('expedition.created'),
name: 'created',
field: 'created',
align: 'left',
columnFilter: null,
format: (value) => toDateTimeFormat(value),
},
]);
const showNewTicketDialog = (withRoute = false) => {
newTicketWithRoute.value = withRoute;
newTicketDialogRef.value.show();
};
const deleteExpedition = async () => {
try {
const expeditionIds = selectedExpeditions.value.map(
(expedition) => expedition.id
);
const params = { expeditionIds };
await axios.post('Expeditions/deleteExpeditions', params);
await refetchExpeditions();
selectedExpeditions.value = [];
notify(t('expedition.expeditionRemoved'), 'positive');
} catch (error) {
console.error(error);
}
};
const showLog = async (expedition) => {
await getExpeditionState(expedition);
logsTableDialogRef.value.show();
};
const getExpeditionState = async (expedition) => {
try {
const filter = {
where: { expeditionFk: expedition.id },
order: ['created DESC'],
};
const { data } = await axios.get(`ExpeditionStates/filter`, {
params: { filter: JSON.stringify(filter) },
});
expeditionsLogsData.value = data;
} catch (error) {
console.error(error);
}
};
onMounted(async () => {
stateStore.rightDrawer = true;
const filteredColumns = columns.value.filter((col) => col.name !== 'history');
allColumnNames.value = filteredColumns.map((col) => col.name);
// await expeditionsArrayData.fetch({ append: false });
});
onUnmounted(() => (stateStore.rightDrawer = false));
</script>
<template>
<FetchData
url="Items"
auto-load
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
@on-fetch="(data) => (itemsOptions = data)"
/>
<VnSubToolbar>
<template #st-data>
<TableVisibleColumns
:all-columns="allColumnNames"
table-code="expeditionIndex"
labels-traductions-path="expedition"
@on-config-saved="visibleColumns = [...$event, 'history']"
/>
</template>
<template #st-actions>
<QBtnGroup push class="q-gutter-x-sm" flat>
<QBtnDropdown
ref="btnDropdownRef"
color="primary"
:label="t('expedition.move')"
:disable="!selectedExpeditions.length"
>
<template #label>
<QTooltip>{{ t('Select lines to see the options') }}</QTooltip>
</template>
<QList>
<QItem
clickable
v-close-popup
v-ripple
@click="showNewTicketDialog(false)"
>
<QItemSection>
<QItemLabel>{{
t('expedition.newTicketWithoutRoute')
}}</QItemLabel>
</QItemSection>
</QItem>
<QItem
clickable
v-close-popup
v-ripple
@click="showNewTicketDialog(true)"
>
<QItemSection>
<QItemLabel>{{
t('expedition.newTicketWithRoute')
}}</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QBtnDropdown>
<QBtn
:disable="!selectedExpeditions.length"
icon="delete"
color="primary"
@click="
openConfirmationModal(
'',
t('expedition.removeExpeditionSubtitle'),
deleteExpedition
)
"
/>
</QBtnGroup>
</template>
</VnSubToolbar>
<QTable
:rows="ticketExpeditions"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
selection="multiple"
v-model:selected="selectedExpeditions"
:visible-columns="visibleColumns"
:no-data-label="t('globals.noResults')"
>
<template #top-row="{ cols }">
<QTr>
<QTd />
<QTd v-for="(col, index) in cols" :key="index" style="max-width: 100px">
<component
:is="col.columnFilter.component"
v-if="col.columnFilter"
v-model="col.columnFilter.filterValue"
v-bind="col.columnFilter.attrs"
v-on="col.columnFilter.event(col)"
dense
/>
</QTd>
</QTr>
</template>
<template #body-cell-item="{ row }">
<QTd auto-width @click.stop>
<QBtn flat color="primary">{{ row.packagingItemFk }}</QBtn>
<ItemDescriptorProxy :id="row.packagingItemFk" />
</QTd>
</template>
<template #body-cell-available="{ row }">
<QTd @click.stop>
<QBadge :color="row.available < 0 ? 'alert' : 'transparent'" dense>
{{ row.available }}
</QBadge>
</QTd>
</template>
<template #body-cell-price="{ row }">
<QTd>
<template v-if="isTicketEditable && row.id">
<QBtn flat color="primary" dense @click="onOpenEditPricePopover(row)">
{{ toCurrency(row.price) }}
</QBtn>
<TicketEditManaProxy
ref="editPriceProxyRef"
:mana="mana"
:new-price="getNewPrice"
@save="updatePrice(row)"
>
<VnInput
v-model.number="edit.price"
:label="t('ticketSale.price')"
type="number"
/>
</TicketEditManaProxy>
</template>
<span v-else>{{ toCurrency(row.price) }}</span>
</QTd>
</template>
<template #body-cell-discount="{ row }">
<QTd>
<template v-if="!isLocked && row.id">
<QBtn
flat
color="primary"
dense
@click="onOpenEditDiscountPopover(row)"
>
{{ toPercentage(row.discount / 100) }}
</QBtn>
<TicketEditManaProxy
:mana="mana"
:new-price="getNewPrice"
@save="changeDiscount(row)"
>
<VnInput
v-model.number="edit.discount"
:label="t('ticketSale.discount')"
type="number"
/>
</TicketEditManaProxy>
</template>
<span v-else>{{ toPercentage(row.discount / 100) }}</span>
</QTd>
</template>
<template #body-cell-history="{ row }">
<QTd>
<QBtn
@click.stop="showLog(row)"
color="primary"
icon="history"
size="md"
flat
>
<QTooltip class="text-no-wrap">
{{ t('expedition.historyAction') }}
</QTooltip>
</QBtn>
</QTd>
</template>
</QTable>
<QDialog ref="newTicketDialogRef" transition-show="scale" transition-hide="scale">
<ExpeditionNewTicket
:ticket="ticketData"
:with-route="newTicketWithRoute"
:selected-expeditions="selectedExpeditions"
/>
</QDialog>
<QDialog ref="logsTableDialogRef" transition-show="scale" transition-hide="scale">
<QTable
ref="tableRef"
data-key="TicketExpeditionLog"
:rows="expeditionsLogsData"
:columns="logTableColumns"
class="q-pa-sm"
>
<template #body-cell-name="{ row }">
<QTd auto-width>
<QBtn flat dense color="primary">{{ row.name }}</QBtn>
<WorkerDescriptorProxy :id="row.workerFk" />
</QTd>
</template>
</QTable>
</QDialog>
</template>

View File

@ -774,6 +774,5 @@ es:
Continue anyway?: ¿Continuar de todas formas?
You are going to delete lines of the ticket: Vas a eliminar lineas del ticket
Add item: Añadir artículo
Select lines to see the options: Selecciona líneas para ver las opciones
Transfer lines: Transferir líneas
</i18n>

View File

@ -93,6 +93,25 @@ futureTickets:
moveTicketSuccess: Tickets moved successfully!
searchInfo: Search future tickets by date
futureTicket: Future tickets
expedition:
id: Expedition
item: Item
name: Name
packageType: Package type
counter: Counter
externalId: externalId
created: Created
state: State
historyAction: Status log
newTicketWithRoute: New ticket with route
newTicketWithoutRoute: New ticket without route
landed: Landed
routeId: Route id
deleteExpedition: Delete expedition
expeditionRemoved: Expedition removed
removeExpeditionSubtitle: Are you sure you want to delete this expedition?
worker: Worker
move: Move
basicData:
next: Next
back: Back

View File

@ -164,5 +164,25 @@ ticketSale:
shipped: F. Envío
agency: Agencia
address: Consignatario
expedition:
id: Expedición
item: Artículo
name: Nombre
packageType: Package type
counter: Contador
externalId: externalId
created: Fecha creación
state: Estado
historyAction: Historial de estados
newTicketWithRoute: Nuevo ticket con ruta
newTicketWithoutRoute: Nuevo ticket sin ruta
landed: F. entrega
routeId: Id ruta
deleteExpedition: Eliminar expedición
expeditionRemoved: Expedición eliminada
removeExpeditionSubtitle: ¿Está seguro de eliminar esta expedición?
worker: Trabajador
move: Mover
Search ticket: Buscar tickets
You can search by ticket id or alias: Puedes buscar por id o alias del ticket
Select lines to see the options: Selecciona líneas para ver las opciones

View File

@ -14,15 +14,16 @@ export default {
main: ['TicketList', 'TicketAdvance', 'TicketWeekly', 'TicketFuture'],
card: [
'TicketBasicData',
'TicketBoxing',
'TicketSms',
'TicketPurchaseRequest',
'TicketSale',
'TicketLog',
'TicketPurchaseRequest',
'TicketExpedition',
'TicketService',
'TicketTracking',
'TicketVolume',
'TicketNotes',
'TicketTracking',
'TicketBoxing',
'TicketSms',
],
},
children: [
@ -70,8 +71,8 @@ export default {
component: () => import('src/pages/Ticket/TicketFuture.vue'),
},
{
path: 'advance',
name: 'TicketAdvance',
path: 'advance',
meta: {
title: 'ticketAdvance',
icon: 'keyboard_double_arrow_left',
@ -142,12 +143,48 @@ export default {
},
component: () => import('src/pages/Ticket/Card/TicketLog.vue'),
},
{
path: 'observation',
name: 'TicketNotes',
meta: {
title: 'notes',
icon: 'vn:notes',
},
component: () => import('src/pages/Ticket/Card/TicketNotes.vue'),
},
{
path: 'volume',
name: 'TicketVolume',
meta: {
title: 'volume',
icon: 'vn:volume',
},
component: () => import('src/pages/Ticket/Card/TicketVolume.vue'),
},
{
path: 'expedition',
name: 'TicketExpedition',
meta: {
title: 'expedition',
icon: 'vn:package',
},
component: () => import('src/pages/Ticket/Card/TicketExpedition.vue'),
},
{
path: 'service',
name: 'TicketService',
meta: {
title: 'services',
icon: 'vn:services',
},
component: () => import('src/pages/Ticket/Card/TicketService.vue'),
},
{
path: 'boxing',
name: 'TicketBoxing',
meta: {
title: 'boxing',
icon: 'vn:package',
icon: 'science',
},
component: () => import('src/pages/Ticket/Card/TicketBoxing.vue'),
},
@ -160,34 +197,6 @@ export default {
},
component: () => import('src/pages/Ticket/Card/TicketSms.vue'),
},
{
path: 'service',
name: 'TicketService',
meta: {
title: 'services',
icon: 'vn:services',
},
component: () => import('src/pages/Ticket/Card/TicketService.vue'),
},
{
path: 'volume',
name: 'TicketVolume',
meta: {
title: 'volume',
icon: 'vn:volume',
},
component: () => import('src/pages/Ticket/Card/TicketVolume.vue'),
},
{
path: 'observation',
name: 'TicketNotes',
meta: {
title: 'notes',
icon: 'vn:notes',
},
component: () => import('src/pages/Ticket/Card/TicketNotes.vue'),
},
],
},
],