refactor: refs #6897 enhance localization entries, clean up unused code, and improve component structure

This commit is contained in:
Pablo Natek 2025-02-03 13:16:57 +01:00
parent 84c92b8a98
commit b258c4eaac
30 changed files with 1008 additions and 515 deletions

View File

@ -64,6 +64,10 @@ const $props = defineProps({
type: Function,
default: null,
},
beforeSaveFn: {
type: Function,
default: null,
},
goTo: {
type: String,
default: '',
@ -149,7 +153,7 @@ function filter(value, update, filterOptions) {
(ref) => {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
},
);
}
@ -176,7 +180,11 @@ async function saveChanges(data) {
hasChanges.value = false;
return;
}
const changes = data || getChanges();
let changes = data || getChanges();
if ($props.beforeSaveFn) {
changes = await $props.beforeSaveFn(changes, getChanges);
}
try {
await axios.post($props.saveUrl || $props.url + '/crud', changes);
} finally {
@ -215,7 +223,7 @@ async function remove(data) {
if (preRemove.length) {
newData = newData.filter(
(form) => !preRemove.some((index) => index == form.$index)
(form) => !preRemove.some((index) => index == form.$index),
);
const changes = getChanges();
if (!changes.creates?.length && !changes.updates?.length)
@ -374,6 +382,8 @@ watch(formUrl, async () => {
@click="onSubmit"
:disable="!hasChanges"
:title="t('globals.save')"
v-shortcut="'s'"
shortcut="s"
data-cy="crudModelDefaultSaveBtn"
/>
<slot name="moreAfterActions" />

View File

@ -97,7 +97,7 @@ const $props = defineProps({
});
const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed(
() => $props.model ?? `formModel_${route?.meta?.title ?? route.name}`
() => $props.model ?? `formModel_${route?.meta?.title ?? route.name}`,
).value;
const componentIsRendered = ref(false);
const arrayData = useArrayData(modelValue);
@ -148,7 +148,7 @@ onMounted(async () => {
JSON.stringify(newVal) !== JSON.stringify(originalData.value);
isResetting.value = false;
},
{ deep: true }
{ deep: true },
);
}
});
@ -156,7 +156,7 @@ onMounted(async () => {
if (!$props.url)
watch(
() => arrayData.store.data,
(val) => updateAndEmit('onFetch', val)
(val) => updateAndEmit('onFetch', val),
);
watch(
@ -165,7 +165,7 @@ watch(
originalData.value = null;
reset();
await fetch();
}
},
);
onBeforeRouteLeave((to, from, next) => {
@ -254,7 +254,7 @@ function filter(value, update, filterOptions) {
(ref) => {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
},
);
}

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, computed } from 'vue';
import { ref, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue';
@ -15,23 +15,30 @@ defineProps({
type: String,
default: '',
},
showSaveAndContinueBtn: {
type: Boolean,
default: false,
},
});
const { t } = useI18n();
const formModelRef = ref(null);
const closeButton = ref(null);
const isSaveAndContinue = ref(false);
const onDataSaved = (formData, requestResponse) => {
if (closeButton.value) closeButton.value.click();
if (closeButton.value && isSaveAndContinue) closeButton.value.click();
emit('onDataSaved', formData, requestResponse);
};
const isLoading = computed(() => formModelRef.value?.isLoading);
const reset = computed(() => formModelRef.value?.reset);
defineExpose({
isLoading,
onDataSaved,
isSaveAndContinue,
reset,
});
</script>
@ -51,6 +58,19 @@ defineExpose({
<p>{{ subtitle }}</p>
<slot name="form-inputs" :data="data" :validate="validate" />
<div class="q-mt-lg row justify-end">
<QBtn
v-if="showSaveAndContinueBtn"
:label="t('globals.isSaveAndContinue')"
:title="t('globals.isSaveAndContinue')"
type="submit"
color="primary"
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
data-cy="FormModelPopup_isSaveAndContinue"
z-max
@click="() => (isSaveAndContinue = true)"
/>
<QBtn
:label="t('globals.cancel')"
:title="t('globals.cancel')"
@ -59,10 +79,15 @@ defineExpose({
flat
:disabled="isLoading"
:loading="isLoading"
@click="emit('onDataCanceled')"
v-close-popup
data-cy="FormModelPopup_cancel"
v-close-popup
z-max
@click="
() => {
isSaveAndContinue = false;
emit('onDataCanceled');
}
"
/>
<QBtn
:label="t('globals.save')"
@ -74,6 +99,7 @@ defineExpose({
:loading="isLoading"
data-cy="FormModelPopup_save"
z-max
@click="() => (isSaveAndContinue = false)"
/>
</div>
</template>

View File

@ -12,6 +12,7 @@ import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import VnComponent from 'components/common/VnComponent.vue';
import VnUserLink from 'components/ui/VnUserLink.vue';
import VnSelectEnum from '../common/VnSelectEnum.vue';
const model = defineModel(undefined, { required: true });
const emit = defineEmits(['blur']);
@ -59,6 +60,7 @@ const defaultSelect = {
row: $props.row,
disable: !$props.isEditable,
class: 'fit',
'emit-value': false,
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
@ -139,6 +141,10 @@ const defaultComponents = {
component: markRaw(VnSelect),
...defaultSelect,
},
selectEnum: {
component: markRaw(VnSelectEnum),
...defaultSelect,
},
icon: {
component: markRaw(QIcon),
},

View File

@ -1,6 +1,6 @@
<script setup>
import { markRaw, computed } from 'vue';
import { QCheckbox } from 'quasar';
import { QCheckbox, QToggle } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
/* basic input */
@ -34,7 +34,7 @@ defineExpose({ addFilter, props: $props });
const model = defineModel(undefined, { required: true });
const arrayData = useArrayData(
$props.dataKey,
$props.searchUrl ? { searchUrl: $props.searchUrl } : null
$props.searchUrl ? { searchUrl: $props.searchUrl } : null,
);
const columnFilter = computed(() => $props.column?.columnFilter);
@ -51,7 +51,7 @@ const defaultAttrs = {
};
const forceAttrs = {
label: $props.showTitle ? '' : columnFilter.value?.label ?? $props.column.label,
label: $props.showTitle ? '' : (columnFilter.value?.label ?? $props.column.label),
};
const selectComponent = {
@ -117,10 +117,19 @@ const components = {
},
select: selectComponent,
rawSelect: selectComponent,
toggle: {
component: markRaw(QToggle),
event: updateEvent,
attrs: {
class: $props.showTitle ? 'q-py-sm' : 'q-px-md q-py-xs fit',
'toggle-indeterminate': true,
size: 'sm',
},
forceAttrs,
},
};
async function addFilter(value, name) {
console.log('test');
value ??= undefined;
if (value && typeof value === 'object') value = model.value;
value = value === '' ? undefined : value;
@ -133,19 +142,8 @@ async function addFilter(value, name) {
await arrayData.addFilter({ params: { [field]: value } });
}
function alignRow() {
switch ($props.column.align) {
case 'left':
return 'justify-start items-start';
case 'right':
return 'justify-end items-end';
default:
return 'flex-center';
}
}
const showFilter = computed(
() => $props.column?.columnFilter !== false && $props.column.name != 'tableActions'
() => $props.column?.columnFilter !== false && $props.column.name != 'tableActions',
);
const onTabPressed = async () => {
@ -153,12 +151,7 @@ const onTabPressed = async () => {
};
</script>
<template>
<div
v-if="showFilter"
class="full-width"
:class="alignRow()"
style="max-height: 45px; overflow: hidden"
>
<div v-if="showFilter" class="full-width flex-center" style="overflow: hidden">
<VnTableColumn
:column="$props.column"
default="input"
@ -170,10 +163,6 @@ const onTabPressed = async () => {
</div>
</template>
<style lang="scss">
/* label.test {
padding-bottom: 0px !important;
background-color: red;
} */
label.test > .q-field__inner > .q-field__control {
padding: inherit;
}

View File

@ -3,6 +3,7 @@ import {
ref,
onBeforeMount,
onMounted,
onUnmounted,
computed,
watch,
h,
@ -29,6 +30,7 @@ import VnVisibleColumn from 'src/components/VnTable/VnVisibleColumn.vue';
import VnLv from 'components/ui/VnLv.vue';
import VnTableOrder from 'src/components/VnTable/VnOrder.vue';
import VnTableFilter from './VnTableFilter.vue';
import { getColAlign } from 'src/composables/getColAlign';
const arrayData = useArrayData(useAttrs()['data-key']);
const $props = defineProps({
@ -56,10 +58,6 @@ const $props = defineProps({
type: [Function, Boolean],
default: null,
},
rowCtrlClick: {
type: [Function, Boolean],
default: null,
},
redirect: {
type: String,
default: null,
@ -150,6 +148,7 @@ const showForm = ref(false);
const splittedColumns = ref({ columns: [] });
const columnsVisibilitySkipped = ref();
const createForm = ref();
const createRef = ref(null);
const tableRef = ref();
const params = ref(useFilterParams($attrs['data-key']).params);
const orders = ref(useFilterParams($attrs['data-key']).orders);
@ -159,6 +158,7 @@ const editingRow = ref(null);
const editingField = ref(null);
const isTableMode = computed(() => mode.value == TABLE_MODE);
const showRightIcon = computed(() => $props.rightSearch || $props.rightSearchIcon);
const originalCreateData = $props?.create?.formInitialData;
const tableModes = [
{
icon: 'view_column',
@ -178,7 +178,8 @@ onBeforeMount(() => {
const urlParams = route.query[$props.searchUrl];
hasParams.value = urlParams && Object.keys(urlParams).length !== 0;
});
onMounted(() => {
onMounted(async () => {
if ($props.isEditable) document.addEventListener('click', clickHandler);
mode.value =
quasar.platform.is.mobile && !$props.disableOption?.card
? CARD_MODE
@ -199,6 +200,9 @@ onMounted(() => {
};
}
});
onUnmounted(async () => {
if ($props.isEditable) document.removeEventListener('click', clickHandler);
});
watch(
() => $props.columns,
@ -250,16 +254,6 @@ const rowClickFunction = computed(() => {
return () => {};
});
const rowCtrlClickFunction = computed(() => {
if ($props.rowCtrlClick != undefined) return $props.rowCtrlClick;
if ($props.redirect)
return (evt, { id }) => {
stopEventPropagation(evt);
window.open(`/#/${$props.redirect}/${id}`, '_blank');
};
return () => {};
});
function redirectFn(id) {
router.push({ path: `/${$props.redirect}/${id}` });
}
@ -281,10 +275,6 @@ function columnName(col) {
return name;
}
function getColAlign(col) {
return 'text-' + (col.align ?? 'left');
}
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
defineExpose({
create: createForm,
@ -336,126 +326,114 @@ function hasEditableFormat(column) {
if (isEditableColumn(column)) return 'editable-text';
}
const handleClick = async (event) => {
const clickHandler = async (event) => {
const clickedElement = event.target.closest('td');
if (!clickedElement) return;
const isDateElement = event.target.closest('.q-date');
const isTimeElement = event.target.closest('.q-time');
const rowIndex = clickedElement.getAttribute('data-row-index');
console.log('HandleRowIndex: ', rowIndex);
const colField = clickedElement.getAttribute('data-col-field');
console.log('HandleColField: ', colField);
if (isDateElement || isTimeElement) return;
if (rowIndex !== null && colField) {
console.log('handleClick STARTEDEDITING');
const column = $props.columns.find((col) => col.name === colField);
console.log('isEditableColumn(column): ', isEditableColumn(column));
if (!isEditableColumn(column)) return;
await startEditing(Number(rowIndex), colField, clickedElement);
if (column.component !== 'checkbox') console.log();
if (clickedElement === null) {
destroyInput(editingRow.value, editingField.value);
return;
}
const rowIndex = clickedElement.getAttribute('data-row-index');
const colField = clickedElement.getAttribute('data-col-field');
const column = $props.columns.find((col) => col.name === colField);
if (editingRow.value != null && editingField.value != null) {
if (editingRow.value == rowIndex && editingField.value == colField) {
return;
} else {
destroyInput(editingRow.value, editingField.value);
if (isEditableColumn(column))
await renderInput(Number(rowIndex), colField, clickedElement);
return;
}
}
if (isEditableColumn(column))
await renderInput(Number(rowIndex), colField, clickedElement);
};
async function startEditing(rowId, field, clickedElement) {
console.log('startEditing: ', field);
if (rowId === editingRow.value && field === editingField.value) return;
editingRow.value = rowId;
async function handleTabKey(event, rowIndex, colField) {
if (editingRow.value == rowIndex && editingField.value == colField)
destroyInput(editingRow.value, editingField.value);
const direction = event.shiftKey ? -1 : 1;
const { nextRowIndex, nextColumnName } = await handleTabNavigation(
rowIndex,
colField,
direction,
);
if (nextRowIndex < 0 || nextRowIndex >= arrayData.store.data.length) return;
event.preventDefault();
await renderInput(nextRowIndex, nextColumnName, null);
}
const selectRegex = /select/;
async function renderInput(rowId, field, clickedElement) {
editingField.value = field;
editingRow.value = rowId;
const column = $props.columns.find((col) => col.name === field);
console.log('LaVerdaderacolumn: ', column);
const row = CrudModelRef.value.formData[rowId];
const oldValue = CrudModelRef.value.formData[rowId][column?.name];
console.log('changes: ', CrudModelRef.value.getChanges());
if (!clickedElement)
clickedElement = document.querySelector(
`[data-row-index="${rowId}"][data-col-field="${field}"]`
`[data-row-index="${rowId}"][data-col-field="${field}"]`,
);
Array.from(clickedElement.childNodes).forEach((child) => {
child.style.visibility = 'hidden';
child.style.position = 'absolute';
child.style.position = 'relative';
});
console.log('row[column.name]: ', row[column.name]);
const isSelect = selectRegex.test(column?.component);
if (isSelect) column.attrs = { ...column.attrs, 'emit-value': false };
const node = h(VnColumn, {
row: row,
class: 'temp-input',
column: column,
modelValue: row[column.name],
componentProp: 'columnField',
autofocus: true,
focusOnMount: true,
eventHandlers: {
'update:modelValue': (value) => {
console.log('update:modelValue: ', value);
row[column.name] = value;
column?.cellEvent?.['update:modelValue']?.(value, oldValue, row);
},
onMouseDown: (event) => {
console.log('mouseDown: ', field);
if (column.component === 'checkbox') event.stopPropagation();
},
blur: () => {
/* const focusElement = document.activeElement;
const rowIndex = focusElement.getAttribute('data-row-index');
const colField = focusElement.getAttribute('data-col-field');
console.log('rowIndex: ', rowIndex);
console.log('colField: ', colField);
console.log('editingField.value: ', editingField.value);
console.log('editingRow.value: ', editingRow.value);
handleBlur(rowId, field, clickedElement);
column?.cellEvent?.blur?.(row); */
'update:modelValue': async (value) => {
if (isSelect) {
row[column.name] = value[column.name.attrs?.optionValue ?? 'id'];
row[column?.name + 'textValue'] =
value[column.name.attrs?.optionLabel ?? 'name'];
await column?.cellEvent?.['update:modelValue']?.(
value,
oldValue,
row,
);
} else row[column.name] = value;
await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row);
},
keyup: async (event) => {
console.log('keyup: ', field);
if (event.key === 'Enter') handleBlur(rowId, field, clickedElement);
},
keydown: async (event) => {
switch (event.key) {
case 'Tab':
console.log('TabTest: ', field);
await handleTabKey(event, rowId, field);
event.stopPropagation();
if (column.component === 'checkbox')
handleBlur(rowId, field, clickedElement);
break;
case 'Escape':
console.log('Escape: ', field);
stopEditing(rowId, field, clickedElement);
destroyInput(rowId, field, clickedElement);
break;
default:
break;
}
},
click: (event) => {
/* event.stopPropagation();
console.log('click: ', field);
if (column.component === 'checkbox') {
const allowNull = column?.toggleIndeterminate ?? true;
const currentValue = row[column.name];
let newValue;
if (allowNull) {
if (currentValue === null) {
newValue = true;
} else if (currentValue) {
newValue = false;
} else {
newValue = null;
}
} else {
newValue = !currentValue;
}
row[column.name] = newValue;
column?.cellEvent?.['update:modelValue']?.(newValue, row);
}
column?.cellEvent?.['click']?.(event, row); */
column?.cellEvent?.['click']?.(event, row);
},
},
});
@ -463,11 +441,15 @@ async function startEditing(rowId, field, clickedElement) {
node.appContext = app._context;
render(node, clickedElement);
if (column.component === 'checkbox') node.el?.querySelector('span > div').focus();
if (['checkbox', 'toggle', undefined].includes(column?.component))
node.el?.querySelector('span > div').focus();
}
function stopEditing(rowIndex, field, clickedElement) {
console.log('stopEditing: ', field);
function destroyInput(rowIndex, field, clickedElement) {
if (!clickedElement)
clickedElement = document.querySelector(
`[data-row-index="${rowIndex}"][data-col-field="${field}"]`,
);
if (clickedElement) {
render(null, clickedElement);
Array.from(clickedElement.childNodes).forEach((child) => {
@ -481,8 +463,7 @@ function stopEditing(rowIndex, field, clickedElement) {
}
function handleBlur(rowIndex, field, clickedElement) {
console.log('handleBlur: ', field);
stopEditing(rowIndex, field, clickedElement);
destroyInput(rowIndex, field, clickedElement);
}
async function handleTabNavigation(rowIndex, colName, direction) {
@ -501,7 +482,6 @@ async function handleTabNavigation(rowIndex, colName, direction) {
} while (iterations < totalColumns);
if (iterations >= totalColumns) {
console.warn('No editable columns found.');
return;
}
@ -510,61 +490,37 @@ async function handleTabNavigation(rowIndex, colName, direction) {
} else if (direction === -1 && newColumnIndex >= currentColumnIndex) {
rowIndex--;
}
console.log('next: ', columns[newColumnIndex].name, 'rowIndex: ', rowIndex);
return { nextRowIndex: rowIndex, nextColumnName: columns[newColumnIndex].name };
}
async function handleTabKey(event, rowIndex, colName) {
const direction = event.shiftKey ? -1 : 1;
const { nextRowIndex, nextColumnName } = await handleTabNavigation(
rowIndex,
colName,
direction
);
if (nextRowIndex < 0 || nextRowIndex >= arrayData.store.data.length) return;
event.preventDefault();
await startEditing(nextRowIndex, nextColumnName, null);
}
function getCheckboxIcon(value) {
switch (typeof value) {
case 'boolean':
return value ? 'check' : 'close';
case 'string':
return value.toLowerCase() === 'partial'
? 'indeterminate_check_box'
: 'unknown_med';
case 'number':
return value === 0 ? 'close' : 'check';
case 'object':
return value === null ? 'help_outline' : 'unknown_med';
case 'undefined':
return 'help_outline';
return 'indeterminate_check_box';
default:
return 'indeterminate_check_box';
}
}
function getToggleIcon(value) {
if (value === null) return 'help_outline';
return value ? 'toggle_on' : 'toggle_off';
}
/* function getCheckboxIcon(value) {
switch (typeof value) {
case 'boolean':
return value ? 'check_box' : 'check_box_outline_blank';
case 'string':
return value.toLowerCase() === 'partial'
? 'indeterminate_check_box'
: 'unknown_med';
case 'number':
return value === 0 ? 'check_box_outline_blank' : 'check_box';
case 'object':
return value === null ? 'help_outline' : 'unknown_med';
case 'undefined':
return 'help_outline';
default:
return 'indeterminate_check_box';
function formatColumnValue(col, row, dashIfEmpty) {
if (col?.format) {
if (selectRegex.test(col?.component) && row[col?.name + 'textValue']) {
return dashIfEmpty(row[col?.name + 'textValue']);
} else {
return col.format(row, dashIfEmpty);
}
} else {
return dashIfEmpty(row[col?.name]);
}
} */
}
</script>
<template>
<QDrawer
@ -628,7 +584,6 @@ function getCheckboxIcon(value) {
@row-click="(_, row) => rowClickFunction && rowClickFunction(row)"
@update:selected="emit('update:selected', $event)"
@selection="(details) => handleSelection(details, rows)"
v-on="isEditable ? { click: handleClick } : {}"
>
<template #top-left v-if="!$props.withoutHeader">
<slot name="top-left"> </slot>
@ -647,6 +602,14 @@ function getCheckboxIcon(value) {
dense
:options="tableModes.filter((mode) => !mode.disable)"
/>
<QBtn
v-if="showRightIcon"
icon="filter_alt"
class="bg-vn-section-color q-ml-sm"
dense
@click="stateStore.toggleRightDrawer()"
/>
</template>
<template #header-cell="{ col }">
<QTh
@ -665,7 +628,7 @@ function getCheckboxIcon(value) {
<VnTableOrder
v-model="orders[col.orderBy ?? col.name]"
:name="col.orderBy ?? col.name"
:label="col?.label"
:label="col?.labelAbbreviation ?? col?.label"
:data-key="$attrs['data-key']"
:search-url="searchUrl"
/>
@ -707,15 +670,14 @@ function getCheckboxIcon(value) {
position: 'relative',
}"
:class="[
getColAlign(col),
col.columnClass,
'body-cell no-margin no-padding',
'body-cell no-margin no-padding text-center',
]"
:data-row-index="rowIndex"
:data-col-field="col?.name"
>
<div
class="no-padding no-margin"
class="no-padding no-margin peter"
style="
overflow: hidden;
text-overflow: ellipsis;
@ -729,7 +691,18 @@ function getCheckboxIcon(value) {
:row-index="rowIndex"
>
<QIcon
v-if="col?.component === 'checkbox'"
v-if="col?.component === 'toggle'"
:name="
col?.getIcon
? col.getIcon(row[col?.name])
: getToggleIcon(row[col?.name])
"
style="color: var(--vn-text-color)"
:class="hasEditableFormat(col)"
size="17px"
/>
<QIcon
v-else-if="col?.component === 'checkbox'"
:name="getCheckboxIcon(row[col?.name])"
style="color: var(--vn-text-color)"
:class="hasEditableFormat(col)"
@ -741,11 +714,7 @@ function getCheckboxIcon(value) {
:style="col?.style ? col.style(row) : null"
style="bottom: 0"
>
{{
col?.format
? col.format(row, dashIfEmpty)
: dashIfEmpty(row[col?.name])
}}
{{ formatColumnValue(col, row, dashIfEmpty) }}
</span>
</slot>
</div>
@ -898,7 +867,6 @@ function getCheckboxIcon(value) {
v-for="col of cols.filter((cols) => cols.visible ?? true)"
:key="col?.id"
class="text-center"
:class="getColAlign(col)"
>
<slot :name="`column-footer-${col.name}`" />
</QTh>
@ -944,32 +912,53 @@ function getCheckboxIcon(value) {
{{ createForm?.title }}
</QTooltip>
</QPageSticky>
<QDialog v-model="showForm" transition-show="scale" transition-hide="scale">
<QDialog
v-model="showForm"
transition-show="scale"
transition-hide="scale"
:full-width="create?.isFullWidth ?? false"
@before-hide="
() => {
if (createRef.isSaveAndContinue) {
showForm = true;
createForm.formInitialData = { ...create.formInitialData };
}
}
"
>
<FormModelPopup
ref="createRef"
v-bind="createForm"
:model="$attrs['data-key'] + 'Create'"
@on-data-saved="(_, res) => createForm.onDataSaved(res)"
>
<template #form-inputs="{ data }">
<div class="grid-create">
<slot
v-for="column of splittedColumns.create"
:key="column.name"
:name="`column-create-${column.name}`"
:data="data"
:column-name="column.name"
:label="column.label"
>
<VnColumn
:column="column"
:row="{}"
default="input"
v-model="data[column.name]"
:show-label="true"
component-prop="columnCreate"
/>
</slot>
<slot name="more-create-dialog" :data="data" />
<div :class="create?.containerClass">
<div>
<slot name="previous-create-dialog" :data="data" />
</div>
<div class="grid-create" :style="create?.columnGridStyle">
<slot
v-for="column of splittedColumns.create"
:key="column.name"
:name="`column-create-${column.name}`"
:data="data"
:column-name="column.name"
:label="column.label"
>
<VnColumn
:column="column"
:row="{}"
default="input"
v-model="data[column.name]"
:show-label="true"
component-prop="columnCreate"
/>
</slot>
</div>
<div>
<slot name="more-create-dialog" :data="data" />
</div>
</div>
</template>
</FormModelPopup>
@ -1021,6 +1010,7 @@ es:
.body-cell {
padding-left: 2px !important;
padding-right: 2px !important;
position: relative;
}
.bg-chip-secondary {
background-color: var(--vn-page-color);
@ -1047,11 +1037,14 @@ es:
.grid-create {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, max-content));
max-width: 100%;
grid-gap: 20px;
margin: 0 auto;
}
.form-container {
display: flex;
flex-wrap: wrap;
gap: 16px; /* Espacio entre los divs */
}
.flex-one {
display: flex;
flex-flow: row wrap;
@ -1162,4 +1155,11 @@ es:
.q-table__middle.q-virtual-scroll.q-virtual-scroll--vertical.scroll {
background-color: var(--vn-section-color);
}
.temp-input {
top: 0;
position: absolute;
width: 100%;
height: 100%;
display: flex;
}
</style>

View File

@ -29,25 +29,29 @@ function columnName(col) {
<VnFilterPanel v-bind="$attrs" :search-button="true" :disable-submit-event="true">
<template #body="{ params, orders }">
<div
class="row no-wrap flex-center"
class="container"
v-for="col of columns.filter((c) => c.columnFilter ?? true)"
:key="col.id"
>
<VnFilter
ref="tableFilterRef"
:column="col"
:data-key="$attrs['data-key']"
v-model="params[columnName(col)]"
:search-url="searchUrl"
/>
<VnTableOrder
v-if="col?.columnFilter !== false && col?.name !== 'tableActions'"
v-model="orders[col.orderBy ?? col.name]"
:name="col.orderBy ?? col.name"
:data-key="$attrs['data-key']"
:search-url="searchUrl"
:vertical="true"
/>
<div class="filter">
<VnFilter
ref="tableFilterRef"
:column="col"
:data-key="$attrs['data-key']"
v-model="params[columnName(col)]"
:search-url="searchUrl"
/>
</div>
<div class="order">
<VnTableOrder
v-if="col?.columnFilter !== false && col?.name !== 'tableActions'"
v-model="orders[col.orderBy ?? col.name]"
:name="col.orderBy ?? col.name"
:data-key="$attrs['data-key']"
:search-url="searchUrl"
:vertical="true"
/>
</div>
</div>
<slot
name="moreFilterPanel"
@ -67,3 +71,21 @@ function columnName(col) {
</template>
</VnFilterPanel>
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
align-items: center;
height: 45px;
gap: 10px;
}
.filter {
width: 70%;
height: 40px;
text-align: center;
}
.order {
width: 10%;
}
</style>

View File

@ -58,7 +58,6 @@ function toValueAttrs(attrs) {
v-on="mix(toComponent).event ?? {}"
v-model="model"
@blur="emit('blur')"
@mouse-down="() => console.log('mouse-down')"
/>
</span>
</template>

View File

@ -140,7 +140,7 @@ const handleUppercase = () => {
hide-bottom-space
:data-cy="$attrs.dataCy ?? $attrs.label + '_input'"
>
<template #prepend>
<template #prepend v-if="$slots.prepend">
<slot name="prepend" />
</template>
<template #append>
@ -165,15 +165,15 @@ const handleUppercase = () => {
}
"
></QIcon>
<QIcon
name="match_case"
size="xs"
v-if="!$attrs.disabled && !($attrs.readonly) && $props.uppercase"
v-if="!$attrs.disabled && !$attrs.readonly && $props.uppercase"
@click="handleUppercase"
class="uppercase-icon"
/>
<slot name="append" v-if="$slots.append && !$attrs.disabled" />
<QIcon v-if="info" name="info">
<QTooltip max-width="350px">
@ -194,4 +194,4 @@ const handleUppercase = () => {
inputMin: Debe ser mayor a {value}
maxLength: El valor excede los {value} carácteres
inputMax: Debe ser menor a {value}
</i18n>
</i18n>

View File

@ -171,7 +171,8 @@ onMounted(() => {
});
const arrayDataKey =
$props.dataKey ?? ($props.url?.length > 0 ? $props.url : $attrs.name ?? $attrs.label);
$props.dataKey ??
($props.url?.length > 0 ? $props.url : ($attrs.name ?? $attrs.label));
const arrayData = useArrayData(arrayDataKey, {
url: $props.url,
@ -220,7 +221,7 @@ async function fetchFilter(val) {
optionFilterValue.value ??
(new RegExp(/\d/g).test(val)
? optionValue.value
: optionFilter.value ?? optionLabel.value);
: (optionFilter.value ?? optionLabel.value));
let defaultWhere = {};
if ($props.filterOptions.length) {
@ -239,7 +240,7 @@ async function fetchFilter(val) {
const { data } = await arrayData.applyFilter(
{ filter: filterOptions },
{ updateRouter: false }
{ updateRouter: false },
);
setOptions(data);
return data;
@ -272,7 +273,7 @@ async function filterHandler(val, update) {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
}
},
);
}
@ -308,7 +309,7 @@ function handleKeyDown(event) {
if (inputValue) {
const matchingOption = myOptions.value.find(
(option) =>
option[optionLabel.value].toLowerCase() === inputValue.toLowerCase()
option[optionLabel.value].toLowerCase() === inputValue.toLowerCase(),
);
if (matchingOption) {
@ -320,11 +321,11 @@ function handleKeyDown(event) {
}
const focusableElements = document.querySelectorAll(
'a:not([disabled]), button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'
'a:not([disabled]), button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])',
);
const currentIndex = Array.prototype.indexOf.call(
focusableElements,
event.target
event.target,
);
if (currentIndex >= 0 && currentIndex < focusableElements.length - 1) {
focusableElements[currentIndex + 1].focus();

View File

@ -0,0 +1,49 @@
<script setup>
import VnSelectDialog from './VnSelectDialog.vue';
import FilterTravelForm from 'src/components/FilterTravelForm.vue';
import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters';
const { t } = useI18n();
const $props = defineProps({
data: {
type: Object,
required: true,
},
onFilterTravelSelected: {
type: Function,
required: true,
},
});
</script>
<template>
<VnSelectDialog
:label="t('entry.basicData.travel')"
v-bind="$attrs"
url="Travels/filter"
:fields="['id', 'warehouseInName']"
option-value="id"
option-label="warehouseInName"
map-options
hide-selected
:required="true"
action-icon="filter_alt"
>
<template #form>
<FilterTravelForm @travel-selected="onFilterTravelSelected(data, $event)" />
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.agencyModeName }} -
{{ scope.opt?.warehouseInName }}
({{ toDate(scope.opt?.shipped) }})
{{ scope.opt?.warehouseOutName }}
({{ toDate(scope.opt?.landed) }})
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectDialog>
</template>

View File

@ -114,7 +114,7 @@ async function clearFilters() {
arrayData.resetPagination();
// 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
@ -162,13 +162,13 @@ const formatTags = (tags) => {
const tags = computed(() => {
const filteredTags = tagsList.value.filter(
(tag) => !($props.customTags || []).includes(tag.label)
(tag) => !($props.customTags || []).includes(tag.label),
);
return formatTags(filteredTags);
});
const customTags = computed(() =>
tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label))
tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label)),
);
async function remove(key) {
@ -191,7 +191,9 @@ const getLocale = (label) => {
if (te(globalLocale)) return t(globalLocale);
else if (te(t(`params.${param}`)));
else {
const camelCaseModuleName = route.meta.moduleName.charAt(0).toLowerCase() + route.meta.moduleName.slice(1);
const camelCaseModuleName =
route.meta.moduleName.charAt(0).toLowerCase() +
route.meta.moduleName.slice(1);
return t(`${camelCaseModuleName}.params.${param}`);
}
};
@ -290,6 +292,9 @@ const getLocale = (label) => {
/>
</template>
<style scoped lang="scss">
.q-field__label.no-pointer-events.absolute.ellipsis {
margin-left: 6px !important;
}
.list {
width: 256px;
}

View File

@ -0,0 +1,48 @@
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import axios from 'axios';
import VnConfirm from 'components/ui/VnConfirm.vue';
export async function checkEntryLock(entryFk, userFk) {
const { t } = useI18n();
const quasar = useQuasar();
const { push } = useRouter();
const { data } = await axios.get(`Entries/${entryFk}`, {
params: {
filter: JSON.stringify({
fields: ['id', 'locked', 'lockerUserFk'],
include: { relation: 'user', scope: { fields: ['id', 'nickname'] } },
}),
},
});
const entryConfig = await axios.get('EntryConfigs/findOne');
if (data?.lockerUserFk && data?.locked) {
const now = new Date().getTime();
const lockedTime = new Date(data.locked).getTime();
const timeDiff = (now - lockedTime) / 1000;
const isMaxTimeLockExceeded = entryConfig.data.maxLockTime > timeDiff;
if (data?.lockerUserFk !== userFk && isMaxTimeLockExceeded) {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('entry.lock.title'),
message: t('entry.lock.message', {
userName: data?.user?.nickname,
time: timeDiff / 60,
}),
},
})
.onOk(
async () =>
await axios.patch(`Entries/${entryFk}`, {
locked: Date.vnNow(),
lockerUserFk: userFk,
}),
)
.onCancel(() => push({ path: `summary` }));
}
}
}

View File

@ -0,0 +1,19 @@
export function getColAlign(col) {
let align;
switch (col.component) {
case 'number':
align = 'right';
break;
case 'date':
case 'checkbox':
align = 'center';
break;
default:
align = col?.align;
}
if (/^is[A-Z]/.test(col.name) || /^has[A-Z]/.test(col.name)) align = 'center';
return 'text-' + (align ?? 'center');
}

View File

@ -315,9 +315,6 @@ input::-webkit-inner-spin-button {
max-width: fit-content;
}
.row > .column:has(.q-checkbox) {
max-width: fit-content;
}
.q-field__inner {
.q-field__control {
min-height: auto !important;

View File

@ -1,7 +1,7 @@
globals:
lang:
es: Spanish
en: English
en: English
language: Language
quantity: Quantity
entity: Entity
@ -33,6 +33,7 @@ globals:
reset: Reset
close: Close
cancel: Cancel
isSaveAndContinue: Save and continue
clone: Clone
confirm: Confirm
assign: Assign
@ -476,6 +477,27 @@ entry:
isRaid: Raid
invoiceNumber: Invoice
reference: Ref/Alb/Guide
params:
isExcludedFromAvailable: Excluir del inventario
isOrdered: Pedida
isConfirmed: Lista para etiquetar
isReceived: Recibida
isRaid: Redada
landed: Fecha
supplierFk: Proveedor
invoiceNumber: Nº Factura
reference: Ref/Alb/Guía
agencyModeId: Agencia
isBooked: Asentado
companyFk: Empresa
travelFk: Envio
evaNotes: Notas
warehouseOutFk: Origen
warehouseInFk: Destino
entryTypeDescription: Tipo entrada
invoiceAmount: Importe
dated: Fecha
ticket:
params:
ticketFk: Ticket ID

View File

@ -33,6 +33,7 @@ globals:
reset: Restaurar
close: Cerrar
cancel: Cancelar
isSaveAndContinue: Guardar y continuar
clone: Clonar
confirm: Confirmar
assign: Asignar

View File

@ -1,30 +1,31 @@
<script setup>
import { ref } from 'vue';
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useRole } from 'src/composables/useRole';
import { useState } from 'src/composables/useState';
import { checkEntryLock } from 'src/composables/checkEntryLock';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import FilterTravelForm from 'src/components/FilterTravelForm.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import { toDate } from 'src/filters';
import VnSelectTravelExtended from 'src/components/common/VnSelectTravelExtended.vue';
const route = useRoute();
const { t } = useI18n();
const { hasAny } = useRole();
const isAdministrative = () => hasAny(['administrative']);
const state = useState();
const user = state.getUser().fn();
const companiesOptions = ref([]);
const currenciesOptions = ref([]);
const onFilterTravelSelected = (formData, id) => {
formData.travelFk = id;
};
onMounted(() => {
checkEntryLock(route.params.id, user.id);
});
</script>
<template>
@ -52,37 +53,11 @@ const onFilterTravelSelected = (formData, id) => {
>
<template #form="{ data }">
<VnRow>
<VnSelectDialog
:label="t('entry.basicData.travel')"
<VnSelectTravelExtended
:data="data"
v-model="data.travelFk"
url="Travels/filter"
:fields="['id', 'warehouseInName']"
option-value="id"
option-label="warehouseInName"
map-options
hide-selected
:required="true"
action-icon="filter_alt"
>
<template #form>
<FilterTravelForm
@travel-selected="onFilterTravelSelected(data, $event)"
/>
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.agencyModeName }} -
{{ scope.opt?.warehouseInName }}
({{ toDate(scope.opt?.shipped) }})
{{ scope.opt?.warehouseOutName }}
({{ toDate(scope.opt?.landed) }})
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectDialog>
:onFilterTravelSelected="(data, result) => (data.travelFk = result)"
/>
<VnSelect
:label="t('globals.supplier')"
v-model="data.supplierFk"

View File

@ -2,14 +2,21 @@
import { useStateStore } from 'stores/useStateStore';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { h, onMounted, ref } from 'vue';
import { onMounted, ref } from 'vue';
import { useState } from 'src/composables/useState';
import FetchData from 'src/components/FetchData.vue';
import VnTable from 'src/components/VnTable/VnTable.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import FetchedTags from 'src/components/ui/FetchedTags.vue';
import VnColor from 'src/components/common/VnColor.vue';
import { QCheckbox } from 'quasar';
import VnSelect from 'src/components/common/VnSelect.vue';
import ItemDescriptor from 'src/pages/Item/Card/ItemDescriptor.vue';
import axios from 'axios';
import VnSelectEnum from 'src/components/common/VnSelectEnum.vue';
import { checkEntryLock } from 'src/composables/checkEntryLock';
const $props = defineProps({
id: {
type: Number,
@ -21,28 +28,67 @@ const $props = defineProps({
},
});
const { t } = useI18n();
const state = useState();
const user = state.getUser().fn();
const stateStore = useStateStore();
const { t } = useI18n();
const route = useRoute();
const selectedRows = ref([]);
const entityId = ref($props.id ?? route.params.id);
console.log('entityId: ', entityId.value);
const entryBuysRef = ref();
const footerFetchDataRef = ref();
const footer = ref({});
const columns = [
{
align: 'center',
labelAbbreviation: 'NV',
label: t('Ignore'),
toolTip: t('Ignored for available'),
name: 'isIgnored',
component: 'checkbox',
toggleIndeterminate: false,
create: true,
width: '25px',
},
{
label: t('Buyer'),
name: 'workerFk',
component: 'select',
attrs: {
url: 'Workers/search',
fields: ['id', 'nickname'],
optionLabel: 'nickname',
optionValue: 'id',
},
visible: false,
},
{
label: t('Family'),
name: 'itemTypeFk',
component: 'select',
attrs: {
url: 'itemTypes',
fields: ['id', 'name'],
optionLabel: 'name',
optionValue: 'id',
},
visible: false,
},
{
name: 'id',
isId: true,
visible: false,
isEditable: false,
columnFilter: false,
},
{
align: 'center',
label: 'Nv',
name: 'isIgnored',
component: 'checkbox',
toggleIndeterminate: false,
width: '35px',
name: 'entryFk',
isId: true,
visible: false,
isEditable: false,
disable: true,
create: true,
columnFilter: false,
},
{
align: 'center',
@ -50,26 +96,47 @@ const columns = [
name: 'itemFk',
component: 'input',
isEditable: false,
create: true,
width: '45px',
width: '40px',
},
{
label: '',
labelAbbreviation: '',
label: 'Color',
name: 'hex',
columnSearch: false,
isEditable: false,
width: '5px',
component: 'select',
attrs: {
url: 'Inks',
fields: ['id', 'name'],
},
},
{
align: 'center',
label: t('Article'),
name: 'name',
width: '100px',
component: 'select',
attrs: {
url: 'Items',
fields: ['id', 'name'],
optionLabel: 'name',
optionValue: 'id',
},
width: '85px',
isEditable: false,
},
{
align: 'center',
label: t('Siz.'),
label: t('Article'),
name: 'itemFk',
visible: false,
create: true,
columnFilter: false,
},
{
align: 'center',
labelAbbreviation: t('Siz.'),
label: t('Size'),
toolTip: t('Size'),
name: 'size',
width: '35px',
@ -80,15 +147,17 @@ const columns = [
},
{
align: 'center',
label: t('Sti.'),
labelAbbreviation: t('Sti.'),
label: t('Printed Stickers/Stickers'),
toolTip: t('Printed Stickers/Stickers'),
name: 'stickers',
component: 'number',
create: true,
attrs: {
positive: false,
},
cellEvent: {
'update:modelValue': (value, oldValue, row) => {
'update:modelValue': async (value, oldValue, row) => {
row['quantity'] = value * row['packing'];
row['amount'] = row['quantity'] * row['buyingValue'];
},
@ -105,7 +174,8 @@ const columns = [
fields: ['id', 'volume'],
optionLabel: 'id',
},
width: '60px',
create: true,
width: '40px',
},
{
align: 'center',
@ -117,12 +187,14 @@ const columns = [
},
{
align: 'center',
label: 'Pack',
labelAbbreviation: 'Pack',
label: 'Packing',
toolTip: 'Packing',
name: 'packing',
component: 'number',
create: true,
cellEvent: {
'update:modelValue': (value, oldValue, row) => {
console.log('oldValue: ', oldValue);
'update:modelValue': async (value, oldValue, row) => {
const oldPacking = oldValue === 1 || oldValue === null ? 1 : oldValue;
row['weight'] = (row['weight'] * value) / oldPacking;
row['quantity'] = row['stickers'] * value;
@ -134,42 +206,44 @@ const columns = [
if (row.groupingMode === 'grouping')
return { color: 'var(--vn-label-color)' };
},
/* append: {
name: 'groupingMode',
h: (row) =>
h(QCheckbox, {
'data-name': 'groupingMode',
modelValue: row['groupingMode'] === 'packing',
size: 'sm',
'onUpdate:modelValue': (value) => {
console.log('entra');
if (value) row['groupingMode'] = 'packing';
else row['groupingMode'] = 'grouping';
},
onClick: (event) => {
console.log('eventOnClick: ', event);
},
}),
}, */
},
{
align: 'center',
label: 'Group',
labelAbbreviation: 'GM',
label: t('Grouping selector'),
toolTip: t('Grouping selector'),
name: 'groupingMode',
component: 'toggle',
attrs: {
'toggle-indeterminate': true,
trueValue: 'grouping',
falseValue: 'packing',
indeterminateValue: null,
},
width: '35px',
size: 'xs',
width: '30px',
create: true,
rightFilter: false,
getIcon: (value) => {
switch (value) {
case 'grouping':
return 'toggle_on';
case 'packing':
return 'toggle_off';
default:
return 'minimize';
}
},
},
{
align: 'center',
label: 'Group',
labelAbbreviation: 'Group',
label: 'Grouping',
toolTip: 'Grouping',
name: 'grouping',
component: 'number',
width: '35px',
create: true,
style: (row) => {
if (row.groupingMode === 'packing') return { color: 'var(--vn-label-color)' };
},
@ -183,34 +257,37 @@ const columns = [
positive: false,
},
cellEvent: {
'update:modelValue': (value, oldValue, row) => {
'update:modelValue': async (value, oldValue, row) => {
row['amount'] = value * row['buyingValue'];
},
},
width: '50px',
width: '45px',
create: true,
style: getQuantityStyle,
},
{
align: 'center',
label: t('Cost'),
labelAbbreviation: t('Cost'),
label: t('Buying value'),
toolTip: t('Buying value'),
name: 'buyingValue',
create: true,
component: 'number',
attrs: {
positive: false,
},
cellEvent: {
'update:modelValue': (value, oldValue, row) => {
'update:modelValue': async (value, oldValue, row) => {
row['amount'] = row['quantity'] * value;
},
},
width: '50px',
width: '45px',
},
{
align: 'center',
label: t('Amount'),
name: 'amount',
width: '50px',
width: '45px',
component: 'number',
attrs: {
positive: false,
@ -220,11 +297,13 @@ const columns = [
},
{
align: 'center',
label: t('Pack.'),
labelAbbreviation: t('Pack.'),
label: t('Package'),
toolTip: t('Package'),
name: 'price2',
component: 'number',
width: '35px',
create: true,
},
{
align: 'center',
@ -232,48 +311,45 @@ const columns = [
name: 'price3',
component: 'number',
cellEvent: {
'update:modelValue': (value, row) => {
/*
Call db.execV("UPDATE vn.item SET " & _
"typeFk = # " & _
",producerFk = # " & _
",minPrice = # " & _
",box = # " & _
",hasMinPrice = # " & _
",comment = # " & _
"WHERE id = # " _
, Me.tipo_id _
, Me.producer_id _
, Me.PVP _
, Me.caja _
, Me.Min _
, Nz(Me.reference, 0) _
, Me.Id_Article _
)
Me.Tarifa2 = Me.Tarifa2 * (Me.Tarifa3 / Me.Tarifa3.OldValue)
Call actualizar_compra
Me.sincro = True
*/
'update:modelValue': async (value, oldValue, row) => {
row['price2'] = row['price2'] * (value / oldValue);
},
},
width: '35px',
create: true,
},
{
align: 'center',
label: 'Min.',
labelAbbreviation: 'Min.',
label: t('Minimum price'),
toolTip: t('Minimum price'),
name: 'minPrice',
component: 'number',
isEditable: false,
cellEvent: {
'update:modelValue': async (value, oldValue, row) => {
await axios.patch(`Items/${row['itemFk']}`, {
minPrice: value,
});
},
},
width: '35px',
style: (row) => {
if (row?.hasMinPrice)
return { backgroundColor: 'var(--q-positive)', color: 'black' };
if (!row?.hasMinPrice) return { color: 'var(--vn-label-color)' };
},
},
{
align: 'center',
label: t('P.Sen'),
labelAbbreviation: 'CM',
label: t('Check min price'),
toolTip: t('Check min price'),
name: 'hasMinPrice',
component: 'checkbox',
width: '25px',
},
{
align: 'center',
labelAbbreviation: t('P.Sen'),
label: t('Packing sent'),
toolTip: t('Packing sent'),
name: 'packingOut',
component: 'number',
@ -282,16 +358,18 @@ const columns = [
},
{
align: 'center',
label: t('Com.'),
labelAbbreviation: t('Com.'),
label: t('Comment'),
toolTip: t('Comment'),
name: 'comment',
component: 'input',
isEditable: false,
width: '55px',
width: '50px',
},
{
align: 'center',
label: 'Prod.',
labelAbbreviation: 'Prod.',
label: t('Producer'),
toolTip: t('Producer'),
name: 'subName',
isEditable: false,
@ -309,7 +387,8 @@ const columns = [
},
{
align: 'center',
label: 'Comp.',
labelAbbreviation: 'Comp.',
label: t('Company'),
toolTip: t('Company'),
name: 'company_name',
component: 'input',
@ -327,97 +406,162 @@ function getAmountStyle(row) {
return { color: 'var(--vn-label-color)' };
}
async function beforeSave(data, getChanges) {
try {
const changes = data.updates;
if (!changes) return data;
const patchPromises = [];
for (const change of changes) {
let patchData = {};
if ('hasMinPrice' in change.data) {
patchData.hasMinPrice = change.data?.hasMinPrice;
delete change.data.hasMinPrice;
}
if ('minPrice' in change.data) {
patchData.minPrice = change.data?.minPrice;
delete change.data.minPrice;
}
if (Object.keys(patchData).length > 0) {
const promise = axios
.get('Buys/findOne', {
params: {
filter: {
fields: ['itemFk'],
where: { id: change.where.id },
},
},
})
.then((buy) => {
return axios.patch(`Items/${buy.data.itemFk}`, patchData);
})
.catch((error) => {
console.error('Error processing change: ', change, error);
});
patchPromises.push(promise);
}
}
await Promise.all(patchPromises);
data.updates = changes.filter((change) => Object.keys(change.data).length > 0);
return data;
} catch (error) {
console.error('Error in beforeSave:', error);
throw error;
}
}
function invertQuantitySign(rows, sign) {
for (const row of rows) {
row.quantity = row.quantity * sign;
}
}
function setIsChecked(rows, value) {
for (const row of rows) {
row.isChecked = value;
}
footerFetchDataRef.value.fetch();
}
async function setBuyUltimate(itemFk, data) {
if (!itemFk) return;
const buyUltimate = await axios.get(`Entries/getBuyUltimate`, {
params: {
itemFk,
warehouseFk: user.warehouseFk,
date: Date.vnNew(),
},
});
const buyUltimateData = buyUltimate.data[0];
const allowedKeys = columns
.filter((col) => col.create === true)
.map((col) => col.name);
allowedKeys.forEach((key) => {
if (buyUltimateData.hasOwnProperty(key) && key !== 'entryFk') {
data[key] = buyUltimateData[key];
}
});
}
onMounted(() => {
console.log('viewMode: ', $props.editableMode);
stateStore.rightDrawer = false;
if ($props.editableMode) checkEntryLock(entityId.value, user.id);
});
</script>
<template>
<QToggle
toggle-indeterminate
toggle-order="ft"
v-model="cyan"
label="'ft' order + toggle-indeterminate"
color="cyan"
/>
<Teleport to="#st-data" v-if="stateStore?.isSubToolbarShown() && editableMode">
<QBtnGroup push style="column-gap: 1px">
<QBtn icon="calculate" color="primary" flat @click="console.log('calculate')">
<QTooltip>{{ t('tableActions.openBucketCalculator') }}</QTooltip>
</QBtn>
<QBtnDropdown
icon="box_edit"
color="primary"
flat
tool-tip="test"
@click="console.log('request_quote')"
:title="t('tableActions.setSaleMode')"
>
<div>
<QList>
<QItem clickable v-close-popup @click="setSaleMode('packing')">
<QItemSection>
<QItemLabel>Packing</QItemLabel>
</QItemSection>
</QItem>
<QItem clickable v-close-popup @click="setSaleMode('packing')">
<QItemSection>
<QItemLabel>Grouping</QItemLabel>
</QItemSection>
</QItem>
<QItem label="Grouping" />
</QList>
</div>
</QBtnDropdown>
<QBtn
icon="invert_colors"
color="primary"
flat
@click="console.log('price_check')"
>
<QTooltip>{{ t('tableActions.openCalculator') }}</QTooltip>
</QBtn>
<QBtn
icon="exposure_neg_1"
color="primary"
flat
@click="console.log('request_quote')"
title="test"
:title="t('Invert quantity value')"
:disable="!selectedRows.length"
>
<QTooltip>{{ t('tableActions.invertQuantitySign') }}</QTooltip>
</QBtn>
<QBtn
<QList>
<QItem>
<QItemSection>
<QBtn flat @click="invertQuantitySign(selectedRows, -1)">
<span style="font-size: medium">-1</span>
</QBtn>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QBtn flat @click="invertQuantitySign(selectedRows, 1)">
<span style="font-size: medium">1</span>
</QBtn>
</QItemSection>
</QItem>
</QList>
</QBtnDropdown>
<QBtnDropdown
icon="price_check"
color="primary"
flat
@click="console.log('request_quote')"
:title="t('Check buy amount')"
:disable="!selectedRows.length"
>
<QTooltip>{{ t('tableActions.checkAmount') }}</QTooltip>
</QBtn>
<QBtn
icon="price_check"
color="primary"
flat
@click="console.log('request_quote')"
>
<QTooltip>{{ t('tableActions.setMinPrice') }}</QTooltip>
</QBtn>
<QTooltip>{{}}</QTooltip>
<QList>
<QItem>
<QItemSection>
<QBtn
icon="check"
flat
@click="setIsChecked(selectedRows, true)"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QBtn
icon="close"
flat
@click="setIsChecked(selectedRows, false)"
/>
</QItemSection>
</QItem>
</QList>
</QBtnDropdown>
</QBtnGroup>
</Teleport>
<FetchData
ref="footerFetchDataRef"
:url="`Entries/${entityId}/getBuyList`"
:params="{ groupBy: 'GROUP BY b.entryFk' }"
@on-fetch="
(data) => {
console.log('data: ', data);
footer = data[0];
}
"
@on-fetch="(data) => (footer = data[0])"
auto-load
/>
<VnTable
ref="tableRef"
ref="entryBuysRef"
data-key="EntryBuys"
:url="`Entries/${entityId}/getBuyList`"
save-url="Buys/crud"
@ -431,19 +575,40 @@ onMounted(() => {
}
: {}
"
:create="
editableMode
? {
urlCreate: 'Buys',
title: t('Create buy'),
onDataSaved: () => {
entryBuysRef.reload();
footerFetchDataRef.fetch();
},
formInitialData: { entryFk: entityId, isIgnored: false },
isFullWidth: true,
containerClass: 'form-container',
showSaveAndContinueBtn: true,
columnGridStyle: {
'max-width': '50%',
flex: 1,
},
}
: null
"
:is-editable="editableMode"
:without-header="!editableMode"
:with-filters="editableMode"
:right-search="false"
:right-search="editableMode"
:row-click="false"
:columns="columns"
:beforeSaveFn="beforeSave"
class="buyList"
table-height="84vh"
auto-load
footer
>
<template #column-hex="{ row }">
<VnColor :colors="row?.hexJson" style="height: 100%" />
<VnColor :colors="row?.hexJson" style="height: 100%; min-width: 2000px" />
</template>
<template #column-name="{ row }">
<span class="link">
@ -456,9 +621,9 @@ onMounted(() => {
</template>
<template #column-stickers="{ row }">
<span :class="editableMode ? 'editable-text' : ''">
<span style="color: var(--vn-label-color)">{{
row.printedStickers
}}</span>
<span style="color: var(--vn-label-color)">
{{ row.printedStickers }}
</span>
<span>/{{ row.stickers }}</span>
</span>
</template>
@ -483,6 +648,36 @@ onMounted(() => {
{{ footer?.amount }}
</span>
</template>
<template #column-create-itemFk="{ data }">
<VnSelect
url="Items"
v-model="data.itemFk"
:label="t('Article')"
:fields="['id', 'name']"
option-label="name"
option-value="id"
@update:modelValue="
async (value) => {
setBuyUltimate(value, data);
}
"
:required="true"
/>
</template>
<template #column-create-groupingMode="{ data }">
<VnSelectEnum
:label="t('Grouping mode')"
v-model="data.groupingMode"
schema="vn"
table="buy"
column="groupingMode"
option-value="groupingMode"
option-label="groupingMode"
/>
</template>
<template #previous-create-dialog="{ data }">
<ItemDescriptor :id="data.itemFk" />
</template>
</VnTable>
</template>
<i18n>
@ -508,4 +703,18 @@ es:
Producer: Productor
Company: Compañia
Tags: Etiquetas
Grouping mode: Modo de agrupación
C.min: P.min
Ignore: Ignorar
Ignored for available: Ignorado para disponible
Grouping selector: Selector de grouping
Check min price: Marcar precio mínimo
Create buy: Crear compra
Invert quantity value: Invertir valor de cantidad
Check buy amount: Marcar como correcta la cantidad de compra
</i18n>
<style lang="scss" scoped>
.test {
justify-content: center;
}
</style>

View File

@ -10,6 +10,9 @@ import filter from './EntryFilter.js';
import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue';
import axios from 'axios';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
const quasar = useQuasar();
const { push } = useRouter();
const $props = defineProps({
@ -56,17 +59,24 @@ const getEntryRedirectionFilter = (entry) => {
function showEntryReport() {
openReport(`Entries/${entityId.value}/entry-order-pdf`);
}
function recalculateRates() {
console.log('recalculateRates');
async function recalculateRates(entity) {
const entryConfig = await axios.get('EntryConfigs/findOne');
if (entryConfig.data?.inventorySupplierFk === entity.supplierFk) {
quasar.notify({
type: 'negative',
message: t('Cannot recalculate prices because this is an inventory entry'),
});
return;
}
await axios.post(`Entries/${entityId.value}/recalcEntryPrices`);
}
async function cloneEntry() {
console.log('cloneEntry');
await axios
.post(`Entries/${entityId.value}/cloneEntry`)
.then((response) => push(`/entry/${response.data[0].vNewEntryFk}`));
}
async function deleteEntry() {
console.log('deleteEntry');
await axios.post(`Entries/${entityId.value}/deleteEntry`).then(() => push(`/entry/`));
}
</script>
@ -118,6 +128,10 @@ async function deleteEntry() {
:label="t('entry.summary.invoiceAmount')"
:value="entity?.invoiceAmount"
/>
<VnLv
:label="t('entry.summary.entryType')"
:value="entity?.entryType?.description"
/>
</template>
<template #icons="{ entity }">
<QCardActions class="q-gutter-x-md">
@ -163,21 +177,6 @@ async function deleteEntry() {
>
<QTooltip>{{ t('Supplier card') }}</QTooltip>
</QBtn>
<QBtn
:to="{
name: 'TravelMain',
query: {
params: JSON.stringify({
agencyModeFk: entity.travel?.agencyModeFk,
}),
},
}"
size="md"
icon="local_airport"
color="primary"
>
<QTooltip>{{ t('All travels with current agency') }}</QTooltip>
</QBtn>
<QBtn
:to="{
name: 'EntryMain',
@ -207,4 +206,5 @@ es:
shipped: Enviado
landed: Recibido
This entry is deleted: Esta entrada está eliminada
Cannot recalculate prices because this is an inventory entry: No se pueden recalcular los precios porque es una entrada de inventario
</i18n>

View File

@ -46,5 +46,11 @@ export default {
fields: ['id', 'code'],
},
},
{
relation: 'entryType',
scope: {
fields: ['code', 'description'],
},
},
],
};

View File

@ -167,7 +167,12 @@ onMounted(async () => {
:url="`#/entry/{{ entityId }}/buys`"
:text="t('entry.summary.buys')"
/>
<EntryBuys v-if="entityId" :id="entityId" :editable-mode="false" />
<EntryBuys
v-if="entityId"
:id="entityId"
:editable-mode="false"
:isEditable="false"
/>
</QCard>
</template>
</CardSummary>

View File

@ -9,6 +9,7 @@ import { onBeforeMount } from 'vue';
import EntryFilter from './EntryFilter.vue';
import VnTable from 'components/VnTable/VnTable.vue';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
import VnSelectTravelExtended from 'src/components/common/VnSelectTravelExtended.vue';
import { toDate } from 'src/filters';
const { t } = useI18n();
@ -274,45 +275,56 @@ onBeforeMount(async () => {
<template #advanced-menu>
<EntryFilter :data-key="dataKey" />
</template>
</VnSection>
<VnTable
v-if="defaultEntry.defaultSupplierFk"
ref="tableRef"
:data-key="dataKey"
url="Entries/filter"
:filter="entryQueryFilter"
:create="{
urlCreate: 'Entries',
title: t('Create entry'),
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {
supplierFk: defaultEntry.defaultSupplierFk,
dated: Date.vnNew(),
companyFk: user?.companyFk,
},
}"
order="id DESC"
:columns="columns"
redirect="entry"
:right-search="false"
>
<template #column-landed="{ row }">
<QBadge
v-if="row?.travelFk"
v-bind="getBadgeAttrs(row)"
class="q-pa-sm"
style="font-size: 14px"
<template #body>
<VnTable
v-if="defaultEntry.defaultSupplierFk"
ref="tableRef"
:data-key="dataKey"
url="Entries/filter"
:filter="entryQueryFilter"
:create="{
urlCreate: 'Entries',
title: t('Create entry'),
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {
supplierFk: defaultEntry.defaultSupplierFk,
dated: Date.vnNew(),
companyFk: user?.companyFk,
},
}"
order="id DESC"
:columns="columns"
redirect="entry"
:right-search="false"
>
{{ toDate(row.landed) }}
</QBadge>
<template #column-landed="{ row }">
<QBadge
v-if="row?.travelFk"
v-bind="getBadgeAttrs(row)"
class="q-pa-sm"
style="font-size: 14px"
>
{{ toDate(row.landed) }}
</QBadge>
</template>
<template #column-supplierFk="{ row }">
<span class="link" @click.stop>
{{ row.supplierName }}
<SupplierDescriptorProxy :id="row.supplierFk" />
</span>
</template>
<template #column-create-travelFk="{ data }">
<VnSelectTravelExtended
:data="data"
v-model="data.travelFk"
:onFilterTravelSelected="
(data, result) => (data.travelFk = result)
"
/>
</template>
</VnTable>
</template>
<template #column-supplierFk="{ row }">
<span class="link" @click.stop>
{{ row.supplierName }}
<SupplierDescriptorProxy :id="row.supplierFk" />
</span>
</template>
</VnTable>
</VnSection>
</template>
<i18n>

View File

@ -1,4 +1,7 @@
entry:
lock:
title: Lock entry
message: This entry has been locked by {userName} for {time} minutes. Do you want to unlock it?
list:
newEntry: New entry
tableVisibleColumns:
@ -20,11 +23,13 @@ entry:
entryTypeDescription: Entry type
invoiceAmount: Import
travelFk: Travel
dated: Dated
inventoryEntry: Inventory entry
summary:
commission: Commission
currency: Currency
invoiceNumber: Invoice number
invoiceAmount: Invoice amount
ordered: Ordered
booked: Booked
excludedFromAvailable: Inventory
@ -42,6 +47,7 @@ entry:
buyingValue: Buying value
import: Import
pvp: PVP
entryType: Entry type
basicData:
travel: Travel
currency: Currency
@ -78,11 +84,48 @@ entry:
landing: Landing
isExcludedFromAvailable: Es inventory
params:
toShipped: To
fromShipped: From
daysOnward: Days onward
daysAgo: Days ago
warehouseInFk: Warehouse in
isExcludedFromAvailable: Exclude from inventory
isOrdered: Ordered
isConfirmed: Ready to label
isReceived: Received
isIgnored: Ignored
isRaid: Raid
landed: Date
supplierFk: Supplier
reference: Ref/Alb/Guide
invoiceNumber: Invoice
agencyModeId: Agency
isBooked: Booked
companyFk: Company
evaNotes: Notes
warehouseOutFk: Origin
warehouseInFk: Destiny
entryTypeDescription: Entry type
invoiceAmount: Import
travelFk: Travel
dated: Dated
itemFk: Item id
hex: Color
name: Item name
size: Size
stickers: Stickers
packagingFk: Packaging
weight: Kg
groupingMode: Grouping selector
grouping: Grouping
quantity: Quantity
buyingValue: Buying value
price2: Package
price3: Box
minPrice: Minumum price
hasMinPrice: Has minimum price
packingOut: Packing out
comment: Comment
subName: Supplier name
tags: Tags
company_name: Company name
itemTypeFk: Item type
workerFk: Worker id
search: Search entries
searchInfo: You can search by entry reference
entryFilter:

View File

@ -1,4 +1,8 @@
entry:
lock:
title: Entrada bloqueada
message: Esta entrada ha sido bloqueada por {userName} hace {time} minutos. ¿Quieres desbloquearla?
list:
newEntry: Nueva entrada
tableVisibleColumns:
@ -20,11 +24,13 @@ entry:
warehouseInFk: Destino
entryTypeDescription: Tipo entrada
invoiceAmount: Importe
dated: Fecha
inventoryEntry: Es inventario
summary:
commission: Comisión
currency: Moneda
invoiceNumber: Núm. factura
invoiceAmount: Importe
ordered: Pedida
booked: Contabilizada
excludedFromAvailable: Inventario
@ -43,6 +49,7 @@ entry:
buyingValue: Coste
import: Importe
pvp: PVP
entryType: Tipo entrada
basicData:
travel: Envío
currency: Moneda
@ -78,14 +85,52 @@ entry:
packingOut: Embalaje envíos
landing: Llegada
isExcludedFromAvailable: Es inventario
params:
toShipped: Hasta
fromShipped: Desde
warehouseInFk: Alm. entrada
daysOnward: Días adelante
daysAgo: Días atras
search: Buscar entradas
searchInfo: Puedes buscar por referencia de entrada
params:
isExcludedFromAvailable: Excluir del inventario
isOrdered: Pedida
isConfirmed: Lista para etiquetar
isReceived: Recibida
isRaid: Redada
isIgnored: Ignorado
landed: Fecha
supplierFk: Proveedor
invoiceNumber: Nº Factura
reference: Ref/Alb/Guía
agencyModeId: Agencia
isBooked: Asentado
companyFk: Empresa
travelFk: Envio
evaNotes: Notas
warehouseOutFk: Origen
warehouseInFk: Destino
entryTypeDescription: Tipo entrada
invoiceAmount: Importe
dated: Fecha
itemFk: Id artículo
hex: Color
name: Nombre artículo
size: Medida
stickers: Etiquetas
packagingFk: Embalaje
weight: Kg
groupinMode: Selector de grouping
grouping: Grouping
quantity: Quantity
buyingValue: Precio de compra
price2: Paquete
price3: Caja
minPrice: Precio mínimo
hasMinPrice: Tiene precio mínimo
packingOut: Packing out
comment: Referencia
subName: Nombre proveedor
tags: Etiquetas
company_name: Nombre empresa
itemTypeFk: Familia
workerFk: Comprador
entryFilter:
params:
invoiceNumber: Núm. factura

View File

@ -272,7 +272,6 @@ const createInvoiceInCorrection = async () => {
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
{{ console.log('opt: ', opt) }}
<QItemSection>
<QItemLabel
>{{ opt.id }} -

View File

@ -28,6 +28,7 @@ const cols = computed(() => [
name: 'isBooked',
label: t('invoicein.isBooked'),
columnFilter: false,
component: 'checkbox',
},
{
align: 'left',
@ -176,7 +177,9 @@ const cols = computed(() => [
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.nickname }}</QItemLabel>
<QItemLabel caption> #{{ scope.opt?.id }}, {{ scope.opt?.name }} </QItemLabel>
<QItemLabel caption>
#{{ scope.opt?.id }}, {{ scope.opt?.name }}
</QItemLabel>
</QItemSection>
</QItem>
</template>

View File

@ -157,7 +157,7 @@ const openTab = (id) =>
openConfirmationModal(
$t('globals.deleteConfirmTitle'),
$t('salesOrdersTable.deleteConfirmMessage'),
removeOrders
removeOrders,
)
"
>

View File

@ -50,7 +50,6 @@ const columns = computed(() => [
name: 'isAnyVolumeAllowed',
component: 'checkbox',
cardVisible: true,
disable: true,
},
{
align: 'right',
@ -80,7 +79,8 @@ const columns = computed(() => [
url="Agencies"
order="name"
:columns="columns"
:right-search="false"
is-editable="false"
:right-search="true"
:use-model="true"
redirect="agency"
default-mode="card"

View File

@ -68,7 +68,7 @@ const columns = computed(() => [
},
useLike: false,
cardVisible: true,
format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef),
format: (row, dashIfEmpty) => dashIfEmpty(row.workerUserName),
},
{
align: 'center',
@ -87,6 +87,7 @@ const columns = computed(() => [
},
},
columnClass: 'expand',
format: (row, dashIfEmpty) => dashIfEmpty(row.agencyName),
},
{
align: 'center',
@ -108,6 +109,7 @@ const columns = computed(() => [
columnFilter: {
inWhere: true,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.vehiclePlateNumber),
},
{
align: 'center',
@ -117,7 +119,7 @@ const columns = computed(() => [
cardVisible: true,
create: true,
component: 'date',
format: ({ created }) => toDate(created),
format: ({ dated }) => toDate(dated),
},
{
align: 'center',
@ -127,7 +129,7 @@ const columns = computed(() => [
cardVisible: true,
create: true,
component: 'date',
format: ({ from }) => toDate(from),
format: ({ from }) => from,
},
{
align: 'center',
@ -152,7 +154,7 @@ const columns = computed(() => [
label: t('route.hourStarted'),
component: 'time',
columnFilter: false,
format: ({ hourStarted }) => toHour(hourStarted),
format: ({ started }) => toHour(started),
},
{
align: 'center',
@ -160,7 +162,7 @@ const columns = computed(() => [
label: t('route.hourFinished'),
component: 'time',
columnFilter: false,
format: ({ hourFinished }) => toHour(hourFinished),
format: ({ finished }) => toHour(finished),
},
{
align: 'center',