salix-front/src/pages/Travel/ExtraCommunity.vue

725 lines
22 KiB
Vue

<script setup>
import { onMounted, ref, computed, watch } from 'vue';
import { QBtn } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import ExtraCommunityFilter from './ExtraCommunityFilter.vue';
import VnInput from 'src/components/common/VnInput.vue';
import EntryDescriptorProxy from '../Entry/Card/EntryDescriptorProxy.vue';
import { useStateStore } from 'stores/useStateStore';
import { useArrayData } from 'composables/useArrayData';
import { toDate, toCurrency } from 'src/filters';
import { usePrintService } from 'composables/usePrintService';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import axios from 'axios';
import RightMenu from 'src/components/common/RightMenu.vue';
import VnPopup from 'src/components/common/VnPopup.vue';
const stateStore = useStateStore();
const { t } = useI18n();
const { openReport } = usePrintService();
const route = useRoute();
const tableParams = ref();
const shippedFrom = ref(Date.vnNew());
const landedTo = ref(Date.vnNew());
const arrayData = useArrayData('ExtraCommunity', {
url: 'Travels/extraCommunityFilter',
limit: 0,
order: [
'landed ASC',
'shipped ASC',
'travelFk',
'loadPriority',
'agencyModeFk',
'supplierName',
'evaNotes',
],
userParams: {
continent: 'AM',
shippedFrom: shippedFrom.value,
landedTo: landedTo.value,
},
});
const rows = ref([]);
const originalRowDataCopy = ref([]);
const draggedRowIndex = ref(null);
const targetRowIndex = ref(null);
const entryRowIndex = ref(null);
const draggedEntry = ref(null);
const travelKgPercentages = ref([]);
const tableColumnComponents = {
id: {
component: QBtn,
attrs: { flat: true, color: 'primary', dense: true },
},
cargoSupplierNickname: {
component: QBtn,
attrs: {
flat: true,
color: 'primary',
dense: true,
class: 'supplier-name-button',
},
},
agencyModeName: {
component: 'span',
attrs: {},
},
invoiceAmount: {
component: 'span',
attrs: {},
},
ref: {
component: VnInput,
attrs: { dense: true },
event: (val, field, rowIndex) => ({
'keyup.enter': () => saveFieldValue(val, field, rowIndex),
blur: () => saveFieldValue(val, field, rowIndex),
}),
},
stickers: {
component: 'span',
attrs: {},
},
percentage: {
component: 'span',
attrs: {},
},
kg: {
component: VnInput,
attrs: { dense: true, type: 'number', min: 0, class: 'input-number' },
event: (val, field, rowIndex) => ({
'keyup.enter': () => saveFieldValue(val, field, rowIndex),
blur: () => saveFieldValue(val, field, rowIndex),
}),
},
loadedKg: {
component: 'span',
attrs: {},
},
volumeKg: {
component: 'span',
attrs: {},
},
warehouseOutName: {
component: 'span',
attrs: {},
},
shipped: {
component: 'span',
attrs: {},
},
warehouseInName: {
component: 'span',
attrs: {},
},
landed: {
component: 'span',
attrs: {},
},
notes: {
component: 'span',
attrs: {},
},
isCustomInspectionRequired: {
component: 'span',
attrs: {},
},
};
const columns = computed(() => [
{
label: 'id',
field: 'id',
name: 'id',
align: 'center',
showValue: true,
sortable: true,
},
{
label: t('extraCommunity.cargoShip'),
field: 'cargoSupplierNickname',
name: 'cargoSupplierNickname',
align: 'left',
showValue: true,
sortable: true,
},
{
label: t('globals.agency'),
field: 'agencyModeName',
name: 'agencyModeName',
align: 'left',
showValue: true,
sortable: true,
},
{
label: t('globals.amount'),
name: 'invoiceAmount',
field: 'entries',
align: 'left',
showValue: true,
sortable: true,
format: (value) =>
toCurrency(
value
? value.reduce((sum, entry) => {
return sum + (entry.invoiceAmount || 0);
}, 0)
: 0,
),
},
{
label: t('globals.reference'),
field: 'ref',
name: 'ref',
align: 'left',
showValue: false,
sortable: true,
style: 'min-width: 170px;',
},
{
label: t('globals.packages'),
field: 'stickers',
name: 'stickers',
align: 'left',
showValue: true,
sortable: true,
},
{
label: '%',
field: '',
name: 'percentage',
align: 'center',
showValue: false,
sortable: true,
},
{
label: t('extraCommunity.kg'),
field: 'kg',
name: 'kg',
align: 'left',
showValue: false,
sortable: true,
},
{
label: t('extraCommunity.physicKg'),
field: 'loadedKg',
name: 'loadedKg',
align: 'left',
showValue: true,
sortable: true,
},
{
label: 'KG Vol.',
field: 'volumeKg',
name: 'volumeKg',
align: 'left',
showValue: true,
sortable: true,
},
{
label: t('globals.warehouseOut'),
field: 'warehouseOutName',
name: 'warehouseOutName',
align: 'left',
showValue: true,
sortable: true,
},
{
label: t('extraCommunity.shipped'),
field: 'shipped',
name: 'shipped',
align: 'left',
showValue: true,
sortable: true,
format: (value) => toDate(value),
},
{
label: t('globals.warehouseIn'),
field: 'warehouseInName',
name: 'warehouseInName',
align: 'left',
showValue: true,
sortable: true,
},
{
label: t('extraCommunity.landed'),
field: 'landed',
name: 'landed',
align: 'left',
showValue: true,
sortable: true,
format: (value) => toDate(value),
},
{
label: t('extraCommunity.notes'),
field: '',
name: 'notes',
align: 'center',
showValue: false,
sortable: true,
},
]);
async function getData() {
await arrayData.fetch({ append: false });
}
const onStoreDataChange = () => {
const newData = JSON.parse(JSON.stringify(arrayData.store.data)) || [];
rows.value = newData;
// el objetivo de esto es guardar una copia de los valores iniciales de todas las rows para corroborar si la data cambio antes de guardar los cambios
originalRowDataCopy.value = JSON.parse(JSON.stringify(newData));
};
watch(
arrayData.store,
() => {
if (!arrayData.store.data) return;
onStoreDataChange();
},
{ deep: true, immediate: true },
);
const openReportPdf = () => {
const params = {
...arrayData.store.userParams,
limit: arrayData.store.limit,
};
openReport('Travels/extra-community-pdf', params);
};
const saveFieldValue = async (val, field, index) => {
// Evitar la solicitud de guardado si el valor no ha cambiado
if (originalRowDataCopy.value[index][field] == val) return;
const id = rows.value[index].id;
const params = { [field]: val };
await axios.patch(`Travels/${id}`, params);
// Actualizar la copia de los datos originales con el nuevo valor
originalRowDataCopy.value[index][field] = val;
await arrayData.fetch({ append: false });
};
const stopEventPropagation = (event, col) => {
// Detener la propagación del evento de los siguientes elementos para evitar el click sobre la row que dispararía la función navigateToTravelId
if (!['ref', 'id', 'cargoSupplierNickname', 'kg'].includes(col.name)) return;
event.preventDefault();
event.stopPropagation();
};
onMounted(async () => {
stateStore.rightDrawer = true;
shippedFrom.value.setDate(shippedFrom.value.getDate() - 2);
shippedFrom.value.setHours(0, 0, 0, 0);
landedTo.value.setDate(landedTo.value.getDate() + 7);
landedTo.value.setHours(23, 59, 59, 59);
const { data } = await axios.get('TravelKgPercentages', {
params: { filter: JSON.stringify({ order: 'value DESC' }) },
});
travelKgPercentages.value = data;
await getData();
});
// Handler del evento @dragstart (inicio del drag) y guarda información inicial
const handleDragStart = (event, rowIndex, entryIndex) => {
draggedRowIndex.value = rowIndex;
entryRowIndex.value = entryIndex;
event.dataTransfer.effectAllowed = 'move';
};
// Handler del evento @dragenter (cuando haces drag sobre une elemento y lo arrastras sobre un posible target de drop) y actualiza el targetIndex
const handleDragEnter = (_, targetIndex) => {
targetRowIndex.value = targetIndex;
};
const saveRowDrop = async (targetRowIndex) => {
const entryId = draggedEntry.value.id;
const travelId = rows.value[targetRowIndex].id;
await axios.patch(`Entries/${entryId}`, { travelFk: travelId });
};
const moveRow = async (draggedRowIndex, targetRowIndex, entryIndex) => {
try {
if (draggedRowIndex === targetRowIndex) return;
// Remover entry de la row original
draggedEntry.value = rows.value[draggedRowIndex].entries.splice(entryIndex, 1)[0];
//Si la row de destino por alguna razón no tiene la propiedad entry la creamos
if (!rows.value[targetRowIndex].entries) rows.value[targetRowIndex].entries = [];
// Añadir entry a la row de destino
rows.value[targetRowIndex].entries.push(draggedEntry.value);
await saveRowDrop(targetRowIndex);
} catch (err) {
cleanDragAndDropData();
console.error('Error moving row', err);
}
};
// Handler de cuando haces un drop tanto dentro como fuera de la tabla para limpiar acciones y data
const handleDragEnd = () => {
stopScroll();
cleanDragAndDropData();
};
// Handler del evento @drop (cuando soltas el elemento draggeado sobre un target)
const handleDrop = () => {
if (
!draggedRowIndex.value &&
draggedRowIndex.value !== 0 &&
!targetRowIndex.value &&
draggedRowIndex.value !== 0
)
return;
moveRow(draggedRowIndex.value, targetRowIndex.value, entryRowIndex.value);
handleDragEnd();
};
const cleanDragAndDropData = () => {
draggedRowIndex.value = null;
targetRowIndex.value = null;
entryRowIndex.value = null;
draggedEntry.value = null;
};
const scrollInterval = ref(null);
const startScroll = (direction) => {
// Iniciar el scroll en la dirección especificada
if (!scrollInterval.value) {
scrollInterval.value = requestAnimationFrame(() => scroll(direction));
}
};
const stopScroll = () => {
if (scrollInterval.value) {
cancelAnimationFrame(scrollInterval.value);
scrollInterval.value = null;
}
};
const scroll = (direction) => {
// Controlar el desplazamiento en la dirección especificada
const yOffset = direction === 'up' ? -2 : 2;
window.scrollBy(0, yOffset);
const windowHeight = window.innerHeight;
const documentHeight = document.body.offsetHeight;
// Verificar si se alcanzaron los límites de la ventana para detener el desplazamiento
if (
(direction === 'up' && window.scrollY > 0) ||
(direction === 'down' && windowHeight + window.scrollY < documentHeight)
) {
scrollInterval.value = requestAnimationFrame(() => scroll(direction));
} else {
stopScroll();
}
};
// Handler del scroll mientras se hace el drag de una row
const handleDragScroll = (event) => {
// Obtener la posición y dimensiones del cursor
const y = event.clientY;
const windowHeight = window.innerHeight;
// Verificar si el cursor está cerca del borde superior o inferior de la ventana
const nearTop = y < 150;
const nearBottom = y > windowHeight - 100;
if (nearTop) {
startScroll('up');
} else if (nearBottom) {
startScroll('down');
} else {
stopScroll();
}
};
const getColor = (percentage) => {
for (const { value, className } of travelKgPercentages.value)
if (percentage > value) return className;
};
const filteredEntries = (entries) => {
if (!tableParams?.value?.entrySupplierFk) return entries;
return entries?.filter(
(entry) => entry.supplierFk === tableParams?.value?.entrySupplierFk,
);
};
watch(route, () => {
tableParams.value = JSON.parse(route.query.table);
});
</script>
<template>
<VnSearchbar
data-key="ExtraCommunity"
:limit="20"
:label="t('extraCommunity.searchExtraCommunity')"
/>
<RightMenu>
<template #right-panel>
<ExtraCommunityFilter data-key="ExtraCommunity" />
</template>
</RightMenu>
<VnSubToolbar class="justify-end">
<template #st-actions>
<QBtn
color="primary"
icon-right="picture_as_pdf"
no-caps
@click="openReportPdf()"
>
<QTooltip>
{{ t('Open as PDF') }}
</QTooltip>
</QBtn>
</template>
</VnSubToolbar>
<QPage class="column items-center q-pa-md">
<QTable
:rows="rows"
:columns="columns"
row-key="clientId"
class="full-width"
table-style="user-select: none;"
@drag="handleDragScroll($event)"
@dragend="handleDragEnd($event)"
:separator="!targetRowIndex && targetRowIndex !== 0 ? 'horizontal' : 'none'"
>
<template #body="props">
<QTr
:props="props"
class="cursor-pointer bg-travel"
@click="$router.push({ path: `/travel/${props.row.id}` })"
@dragenter="handleDragEnter($event, props.rowIndex)"
@dragover.prevent
@drop="handleDrop()"
:class="{
'dashed-border --top --left --right':
targetRowIndex === props.rowIndex,
'--bottom':
targetRowIndex === props.rowIndex &&
(!props.row.entries || props.row.entries.length === 0),
}"
>
<QTd
v-for="col in props.cols"
:key="col.name"
:props="props"
@click="stopEventPropagation($event, col)"
:style="col.style"
>
<component
:is="tableColumnComponents[col.name].component"
v-bind="tableColumnComponents[col.name].attrs"
v-model="rows[props.rowIndex][col.field]"
v-on="
tableColumnComponents[col.name].event
? tableColumnComponents[col.name].event(
rows[props.rowIndex][col.field],
col.field,
props.rowIndex,
)
: {}
"
>
<QChip
v-if="col.name === 'percentage'"
:label="
props.row.percentageKg
? `${props.row.percentageKg}%`
: '-'
"
class="text-left q-py-xs q-px-sm"
:color="getColor(props.row.percentageKg)"
/>
<span
v-else-if="col.showValue"
:class="[
'text-left',
{
'supplier-name':
col.name === 'cargoSupplierNickname',
},
{
link: ['id', 'cargoSupplierNickname'].includes(
col.name,
),
},
]"
v-text="col.value"
/>
<!-- Main Row Descriptors -->
<TravelDescriptorProxy
v-if="col.name === 'id'"
:id="props.row.id"
/>
<SupplierDescriptorProxy
v-if="col.name === 'cargoSupplierNickname'"
:id="props.row.cargoSupplierFk"
/>
</component>
</QTd>
</QTr>
<QTr
v-for="(entry, index) in filteredEntries(props.row.entries)"
:key="index"
:props="props"
class="bg-vn-secondary-row cursor-pointer"
@dragstart="handleDragStart($event, props.rowIndex, index)"
@dragenter="handleDragEnter($event, props.rowIndex)"
@dragover.prevent
@drop="handleDrop()"
:draggable="true"
:class="{
'dragged-row':
entryRowIndex === index && props.rowIndex === draggedRowIndex,
'dashed-border --left --right': targetRowIndex === props.rowIndex,
'--bottom':
targetRowIndex === props.rowIndex &&
index === props.row.entries.length - 1,
}"
>
<QTd>
<QBtn dense flat class="link">{{ entry.id }} </QBtn>
<EntryDescriptorProxy :id="entry.id" />
</QTd>
<QTd>
<QBtn flat class="link" dense>{{ entry.supplierName }}</QBtn>
<SupplierDescriptorProxy :id="entry.supplierFk" />
</QTd>
<QTd>
<QIcon
v-if="entry.isCustomInspectionRequired"
name="warning"
color="negative"
size="md"
:title="t('extraCommunity.requiresInspection')"
>
</QIcon>
</QTd>
<QTd>
<span>{{ toCurrency(entry.invoiceAmount) }}</span>
</QTd>
<QTd>
<span>{{ entry.reference }}</span>
</QTd>
<QTd>
<span>{{ entry.stickers }}</span>
</QTd>
<QTd />
<QTd></QTd>
<QTd>
<span>{{ entry.loadedkg }}</span>
</QTd>
<QTd>
<span>{{ entry.volumeKg }}</span>
</QTd>
<QTd />
<QTd />
<QTd />
<QTd />
<QTd>
<QBtn
v-if="entry.evaNotes"
icon="comment"
size="md"
flat
color="primary"
>
<VnPopup
:title="t('globals.observations')"
:content="entry.evaNotes"
/>
</QBtn>
</QTd>
</QTr>
</template>
</QTable>
</QPage>
</template>
<style scoped lang="scss">
.q-chip {
color: var(--vn-text-color);
}
:deep(.q-table) {
border-collapse: collapse;
th {
padding: 0;
}
tbody tr td {
&:nth-child(1) {
max-width: 65px;
}
&:nth-child(4) {
padding: 0;
}
}
}
.q-td :deep(input) {
font-weight: bold;
}
.bg-travel {
background-color: var(--vn-page-color);
border-bottom: 2px solid $primary;
}
.dashed-border {
&.--left {
border-left: 1px dashed #ccc;
}
&.--right {
border-right: 1px dashed #ccc;
}
&.--top {
border-top: 1px dashed #ccc;
}
&.--bottom {
border-bottom: 1px dashed #ccc;
}
}
.dragged-row {
background-color: $primary-light;
}
.supplier-name {
display: flex;
max-width: 150px;
@media (max-width: $breakpoint-md-max) {
max-width: 100px;
}
}
.supplier-name-button {
white-space: normal;
width: max-content;
}
</style>