Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into CambiosSolicitadosSuppliers
gitea/salix-front/pipeline/pr-dev This commit looks good Details

This commit is contained in:
William Buezas 2024-03-12 08:16:40 -03:00
commit 433843ea09
19 changed files with 1069 additions and 421 deletions

View File

@ -71,13 +71,9 @@ const closeForm = () => {
<span ref="closeButton" class="close-icon" v-close-popup>
<QIcon name="close" size="sm" />
</span>
<h1 class="title">
{{
t('editBuyTitle', {
buysAmount: rows.length,
})
}}
</h1>
<span class="title">{{ t('Edit') }}</span>
<span class="countLines">{{ ` ${rows.length} ` }}</span>
<span class="title">{{ t('buy(s)') }}</span>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
@ -94,23 +90,23 @@ const closeForm = () => {
</div>
</VnRow>
<div class="q-mt-lg row justify-end">
<QBtn
:label="t('globals.save')"
type="submit"
color="primary"
:disabled="isLoading"
:loading="isLoading"
/>
<QBtn
:label="t('globals.cancel')"
type="reset"
color="primary"
flat
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
v-close-popup
/>
<QBtn
:label="t('globals.save')"
type="submit"
color="primary"
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
/>
</div>
</QCard>
</QForm>
@ -129,13 +125,18 @@ const closeForm = () => {
right: 20px;
cursor: pointer;
}
.countLines {
font-size: 24px;
color: $primary;
font-weight: bold;
}
</style>
<i18n>
en:
editBuyTitle: Edit {buysAmount} buy(s)
es:
editBuyTitle: Editar {buysAmount} compra(s)
es:
Edit: Editar
buy(s): compra(s)
Field to edit: Campo a editar
Value: Valor
</i18n>
</i18n>

View File

@ -1,6 +1,7 @@
<script setup>
import { computed, ref } from 'vue';
import { toDate } from 'src/filters';
import VnInput from 'components/common/VnInput.vue';
import isValidDate from "filters/isValidDate";
const props = defineProps({
modelValue: {
@ -17,12 +18,25 @@ const props = defineProps({
},
});
const emit = defineEmits(['update:modelValue']);
const joinDateAndTime = (date, time) => {
if (!date) {
return null;
}
if (!time) {
return new Date(date).toISOString();
}
const [year, month, day] = date.split('/');
return new Date(`${year}-${month}-${day}T${time}`).toISOString();
};
const time = computed(() => (props.modelValue ? props.modelValue.split('T')?.[1] : null));
const value = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value ? new Date(value).toISOString() : null);
emit('update:modelValue', joinDateAndTime(value, time.value));
},
});
@ -40,6 +54,16 @@ const formatDate = (dateString) => {
date.getDate()
)}`;
};
const displayDate = (dateString) => {
if (!dateString || !isValidDate(dateString)) {
return '';
}
return new Date(dateString).toLocaleDateString([], {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
};
const styleAttrs = computed(() => {
return props.isOutlined
@ -53,12 +77,12 @@ const styleAttrs = computed(() => {
</script>
<template>
<QInput
<VnInput
class="vn-input-date"
rounded
readonly
:model-value="toDate(value)"
:model-value="displayDate(value)"
v-bind="{ ...$attrs, ...styleAttrs }"
readonly
@click="isPopupOpen = true"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
@ -76,7 +100,7 @@ const styleAttrs = computed(() => {
</QPopupProxy>
</QIcon>
</template>
</QInput>
</VnInput>
</template>
<style lang="scss">

View File

@ -1,8 +1,8 @@
<script setup>
import { computed, ref } from 'vue';
import { toHour } from 'src/filters';
import { useI18n } from 'vue-i18n';
import isValidDate from 'filters/isValidDate';
import VnInput from "components/common/VnInput.vue";
const props = defineProps({
modelValue: {
@ -26,8 +26,8 @@ const value = computed({
},
set(value) {
const [hours, minutes] = value.split(':');
const date = new Date();
date.setUTCHours(
const date = new Date(props.modelValue);
date.setHours(
Number.parseInt(hours) || 0,
Number.parseInt(minutes) || 0,
0,
@ -37,6 +37,7 @@ const value = computed({
},
});
const isPopupOpen = ref(false);
const onDateUpdate = (date) => {
internalValue.value = date;
};
@ -45,7 +46,7 @@ const save = () => {
value.value = internalValue.value;
};
const formatTime = (dateString) => {
if (!isValidDate(dateString)) {
if (!dateString || !isValidDate(dateString)) {
return '';
}
@ -70,16 +71,17 @@ const styleAttrs = computed(() => {
</script>
<template>
<QInput
<VnInput
class="vn-input-time"
rounded
readonly
:model-value="toHour(value)"
:model-value="formatTime(value)"
v-bind="{ ...$attrs, ...styleAttrs }"
@click="isPopupOpen = true"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
v-model="isPopupOpen"
cover
transition-show="scale"
transition-hide="scale"
@ -109,7 +111,7 @@ const styleAttrs = computed(() => {
</QPopupProxy>
</QIcon>
</template>
</QInput>
</VnInput>
</template>
<style lang="scss">

View File

@ -1036,7 +1036,6 @@ en:
claimStateFk: Claim State
workerFk: Worker
clientFk: Customer
rma: RMA
responsibility: Responsibility
packages: Packages
es:
@ -1076,7 +1075,6 @@ es:
claimStateFk: Estado de la reclamación
workerFk: Trabajador
clientFk: Cliente
rma: RMA
responsibility: Responsabilidad
packages: Bultos
</i18n>

View File

@ -162,13 +162,12 @@ watch(props, async () => {
display: flex;
margin-bottom: 9px;
& .q-checkbox__label {
margin-left: 25px;
margin-left: 31px;
color: var(--vn-text);
}
& .q-checkbox__inner {
position: absolute;
left: 0;
margin-left: 5px;
color: var(--vn-label);
}
}

View File

@ -49,6 +49,7 @@ a {
.q-icon {
float: right;
}
text-transform: uppercase;
}
.link:hover {

93
src/filters/date.js Normal file
View File

@ -0,0 +1,93 @@
/**
* Checks if a given date is valid.
*
* @param {number|string|Date} date - The date to be checked.
* @returns {boolean} True if the date is valid, false otherwise.
*
* @example
* // returns true
* isValidDate(new Date());
*
* @example
* // returns false
* isValidDate('invalid date');
*/
export function isValidDate(date) {
return date && !isNaN(new Date(date).getTime());
}
/**
* Converts a given date to a specific format.
*
* @param {number|string|Date} date - The date to be formatted.
* @returns {string} The formatted date as a string in 'dd/mm/yyyy' format. If the provided date is not valid, an empty string is returned.
*
* @example
* // returns "02/12/2022"
* toDateFormat(new Date(2022, 11, 2));
*/
export function toDateFormat(date) {
if (!isValidDate(date)) {
return '';
}
return new Date(date).toLocaleDateString('es-ES', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
}
/**
* Converts a given date to a specific time format.
*
* @param {number|string|Date} date - The date to be formatted.
* @param {boolean} [showSeconds=false] - Whether to include seconds in the output format.
* @returns {string} The formatted time as a string in 'hh:mm:ss' format. If the provided date is not valid, an empty string is returned. If showSeconds is false, seconds are not included in the output format.
*
* @example
* // returns "00:00"
* toTimeFormat(new Date(2022, 11, 2));
*
* @example
* // returns "00:00:00"
* toTimeFormat(new Date(2022, 11, 2), true);
*/
export function toTimeFormat(date, showSeconds = false) {
if (!isValidDate(date)) {
return '';
}
return new Date(date).toLocaleDateString('es-ES', {
hour: '2-digit',
minute: '2-digit',
second: showSeconds ? '2-digit' : undefined,
});
}
/**
* Converts a given date to a specific date and time format.
*
* @param {number|string|Date} date - The date to be formatted.
* @param {boolean} [showSeconds=false] - Whether to include seconds in the output format.
* @returns {string} The formatted date as a string in 'dd/mm/yyyy, hh:mm:ss' format. If the provided date is not valid, an empty string is returned. If showSeconds is false, seconds are not included in the output format.
*
* @example
* // returns "02/12/2022, 00:00"
* toDateTimeFormat(new Date(2022, 11, 2));
*
* @example
* // returns "02/12/2022, 00:00:00"
* toDateTimeFormat(new Date(2022, 11, 2), true);
*/
export function toDateTimeFormat(date, showSeconds = false) {
if (!isValidDate(date)) {
return '';
}
return new Date(date).toLocaleDateString('es-ES', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: showSeconds ? '2-digit' : undefined,
});
}

View File

@ -497,11 +497,9 @@ export default {
claims: 'Claims',
list: 'List',
createClaim: 'Create claim',
rmaList: 'RMA',
summary: 'Summary',
basicData: 'Basic Data',
lines: 'Lines',
rma: 'RMA',
photos: 'Photos',
development: 'Development',
log: 'Audit logs',
@ -518,10 +516,6 @@ export default {
code: 'Code',
records: 'records',
},
rma: {
user: 'User',
created: 'Created',
},
card: {
claimId: 'Claim ID',
assignedTo: 'Assigned',
@ -562,7 +556,6 @@ export default {
responsible: 'Responsible',
worker: 'Worker',
redelivery: 'Redelivery',
returnOfMaterial: 'RMA',
},
basicData: {
customer: 'Customer',
@ -570,7 +563,6 @@ export default {
created: 'Created',
state: 'State',
picked: 'Picked',
returnOfMaterial: 'Return of material authorization (RMA)',
},
photo: {
fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}',

View File

@ -496,11 +496,9 @@ export default {
claims: 'Reclamaciones',
list: 'Listado',
createClaim: 'Crear reclamación',
rmaList: 'RMA',
summary: 'Resumen',
basicData: 'Datos básicos',
lines: 'Líneas',
rma: 'RMA',
development: 'Trazabilidad',
photos: 'Fotos',
log: 'Historial',
@ -517,10 +515,6 @@ export default {
code: 'Código',
records: 'registros',
},
rma: {
user: 'Usuario',
created: 'Creado',
},
card: {
claimId: 'ID reclamación',
assignedTo: 'Asignada a',
@ -561,7 +555,6 @@ export default {
responsible: 'Responsable',
worker: 'Trabajador',
redelivery: 'Devolución',
returnOfMaterial: 'RMA',
},
basicData: {
customer: 'Cliente',
@ -569,7 +562,6 @@ export default {
created: 'Creada',
state: 'Estado',
picked: 'Recogida',
returnOfMaterial: 'Autorización de retorno de materiales (RMA)',
},
photo: {
fileDescription:

View File

@ -24,7 +24,6 @@ const claimFilter = {
'workerFk',
'claimStateFk',
'packages',
'rma',
'hasToPickUp',
],
include: [
@ -169,13 +168,6 @@ const statesFilter = {
type="number"
/>
</div>
<div class="col">
<VnInput
v-model="data.rma"
:label="t('claim.basicData.returnOfMaterial')"
:rules="validate('claim.rma')"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">

View File

@ -1,145 +0,0 @@
<script setup>
import axios from 'axios';
import { watch, ref, computed, onUnmounted, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import CrudModel from 'components/CrudModel.vue';
import { useState } from 'src/composables/useState';
import { toDate } from 'src/filters';
const quasar = useQuasar();
const state = useState();
const { t } = useI18n();
const selected = ref([]);
const claimRmaRef = ref();
const claim = computed(() => state.get('ClaimDescriptor'));
const claimRmaFilter = {
include: {
relation: 'worker',
scope: {
include: {
relation: 'user',
},
},
},
order: 'created DESC',
where: {
code: claim.value?.rma,
},
};
async function addRow() {
if (!claim.value.rma) {
return quasar.notify({
message: `This claim is not associated to any RMA`,
type: 'negative',
});
}
const formData = {
code: claim.value.rma,
};
await axios.post(`ClaimRmas`, formData);
await claimRmaRef.value.reload();
quasar.notify({
type: 'positive',
message: t('globals.rowAdded'),
icon: 'check',
});
}
onMounted(() => {
if (claim.value) claimRmaRef.value.reload();
});
watch(
claim,
() => {
claimRmaRef.value.reload();
},
{ deep: true }
);
</script>
<template>
<div class="column items-center">
<div class="list">
<CrudModel
data-key="ClaimRma"
url="ClaimRmas"
model="ClaimRma"
:filter="claimRmaFilter"
v-model:selected="selected"
ref="claimRmaRef"
:default-save="false"
:default-reset="false"
:default-remove="false"
>
<template #body="{ rows }">
<QCard>
<template v-for="(row, index) of rows" :key="row.id">
<QItem class="q-pa-none items-start">
<QItemSection class="q-pa-md">
<QList>
<QItem class="q-pa-none">
<QItemSection>
<QItemLabel caption>
{{ t('claim.rma.user') }}
</QItemLabel>
<QItemLabel>
{{ row?.worker?.user?.name }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem class="q-pa-none">
<QItemSection>
<QItemLabel caption>
{{ t('claim.rma.created') }}
</QItemLabel>
<QItemLabel>
{{
toDate(row.created, {
timeStyle: 'medium',
})
}}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QItemSection>
<QCardActions vertical class="justify-between">
<QBtn
flat
round
color="orange"
icon="vn:bin"
@click="claimRmaRef.remove([row])"
>
<QTooltip>{{ t('globals.remove') }}</QTooltip>
</QBtn>
</QCardActions>
</QItem>
<QSeparator v-if="index !== rows.length - 1" />
</template>
</QCard>
</template>
</CrudModel>
</div>
</div>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn fab color="primary" icon="add" @click="addRow()" />
</QPageSticky>
</template>
<style lang="scss" scoped>
.list {
width: 100%;
max-width: 60em;
}
</style>
<i18n>
es:
This claim is not associated to any RMA: Esta reclamación no está asociada a ninguna ARM
</i18n>

View File

@ -226,7 +226,7 @@ function openDialog(dmsId) {
/>
</QCard>
<QCard class="vn-three">
<a class="header" :href="`#/claim/${entityId}/notes`">
<a class="header header-link" :href="`#/claim/${entityId}/notes`">
{{ t('claim.summary.notes') }}
<QIcon name="open_in_new" />
</a>

View File

@ -1,171 +0,0 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import VnInput from 'src/components/common/VnInput.vue';
import { useArrayData } from 'src/composables/useArrayData';
import axios from 'axios';
const quasar = useQuasar();
const { t } = useI18n();
const arrayData = useArrayData('ClaimRmaList');
const isLoading = ref(false);
const input = ref();
const newRma = ref({
code: '',
crated: Date.vnNew(),
});
function onInputUpdate(value) {
newRma.value.code = value.toUpperCase();
}
async function submit() {
const formData = newRma.value;
if (formData.code === '') return;
isLoading.value = true;
await axios.post('ClaimRmas', formData);
await arrayData.refresh();
isLoading.value = false;
input.value.$el.focus();
newRma.value = {
code: '',
created: Date.vnNew(),
};
}
function confirm(id) {
quasar
.dialog({
component: VnConfirm,
componentProps: {
data: { id },
promise: remove,
},
})
.onOk(async () => await arrayData.refresh());
}
async function remove({ id }) {
await axios.delete(`ClaimRmas/${id}`);
quasar.notify({
type: 'positive',
message: t('globals.rowRemoved'),
});
}
</script>
<template>
<QPage class="column items-center q-pa-md sticky">
<QPageSticky expand position="top" :offset="[16, 16]">
<QCard class="card q-pa-md">
<QForm @submit="submit">
<VnInput
ref="input"
v-model="newRma.code"
:label="t('claim.rmaList.code')"
@update:model-value="onInputUpdate"
class="q-mb-md"
:readonly="isLoading"
:loading="isLoading"
autofocus
/>
<div class="text-caption">
{{ arrayData.totalRows }} {{ t('claim.rmaList.records') }}
</div>
</QForm>
</QCard>
</QPageSticky>
<div class="vn-card-list">
<VnPaginate
data-key="ClaimRmaList"
url="ClaimRmas"
order="id DESC"
:offset="50"
auto-load
>
<template #body="{ rows }">
<QCard class="card">
<template v-if="isLoading">
<QItem class="q-pa-none items-start">
<QItemSection class="q-pa-md">
<QList>
<QItem class="q-pa-none">
<QItemSection>
<QItemLabel caption>
<QSkeleton />
</QItemLabel>
<QItemLabel>
<QSkeleton type="text" />
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QItemSection>
<QCardActions vertical class="justify-between">
<QSkeleton
type="circle"
class="q-mb-md"
size="40px"
/>
</QCardActions>
</QItem>
<QSeparator />
</template>
<template v-for="row of rows" :key="row.id">
<QItem class="q-pa-none items-start">
<QItemSection class="q-pa-md">
<QList>
<QItem class="q-pa-none">
<QItemSection>
<QItemLabel caption>{{
t('claim.rmaList.code')
}}</QItemLabel>
<QItemLabel>{{ row.code }}</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QItemSection>
<QCardActions vertical class="justify-between">
<QBtn
flat
round
color="primary"
icon="vn:bin"
@click="confirm(row.id)"
>
<QTooltip>{{ t('globals.remove') }}</QTooltip>
</QBtn>
</QCardActions>
</QItem>
<QSeparator />
</template>
</QCard>
</template>
</VnPaginate>
</div>
</QPage>
</template>
<style lang="scss" scoped>
.sticky {
padding-top: 156px;
}
.card {
width: 100%;
max-width: 60em;
}
.q-page-sticky {
z-index: 2998;
}
</style>

View File

@ -112,7 +112,7 @@ const onDataSaved = async () => {
:filter="filterBanks"
@on-fetch="(data) => (bankOptions = data)"
auto-load
url="Banks"
url="Accountings"
/>
<fetch-data
:filter="filterClientFindOne"

View File

@ -1,5 +1,5 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { onMounted, ref, computed, reactive, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
@ -8,11 +8,16 @@ import FetchedTags from 'components/ui/FetchedTags.vue';
import EntryDescriptorProxy from './Card/EntryDescriptorProxy.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import EditTableCellValueForm from 'src/components/EditTableCellValueForm.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import EntryLatestBuysFilter from './EntryLatestBuysFilter.vue';
import ItemDescriptorProxy from '../Item/Card/ItemDescriptorProxy.vue';
import { useStateStore } from 'stores/useStateStore';
import { toDate, toCurrency } from 'src/filters';
import { useSession } from 'composables/useSession';
import { dashIfEmpty } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
const router = useRouter();
const session = useSession();
@ -21,11 +26,72 @@ const stateStore = useStateStore();
const { t } = useI18n();
const rowsFetchDataRef = ref(null);
const itemTypesOptions = ref([]);
const originsOptions = ref([]);
const itemFamiliesOptions = ref([]);
const intrastatOptions = ref([]);
const packagingsOptions = ref([]);
const editTableCellDialogRef = ref(null);
const visibleColumns = ref([]);
const allColumnNames = ref([]);
const rows = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'id':
case 'size':
case 'weightByPiece':
case 'isActive':
case 'family':
case 'minPrice':
case 'packingOut':
return { [`i.${param}`]: value };
case 'name':
case 'description':
return { [`i.${param}`]: { like: `%${value}%` } };
case 'code':
return { 'it.code': value };
case 'intrastat':
return { 'intr.description': value };
case 'origin':
return { 'ori.code': value };
case 'landing':
return { [`lb.${param}`]: value };
case 'packing':
case 'grouping':
case 'quantity':
case 'entryFk':
case 'buyingValue':
case 'freightValue':
case 'comissionValue':
case 'packageValue':
case 'isIgnored':
case 'price2':
case 'price3':
case 'ektFk':
case 'weight':
case 'packagingFk':
return { [`b.${param}`]: value };
}
};
const params = reactive({});
const arrayData = useArrayData('EntryLatestBuys', {
url: 'Buys/latestBuysFilter',
order: ['itemFk DESC'],
exprBuilder: exprBuilder,
});
const store = arrayData.store;
const rows = computed(() => store.data);
const rowsSelected = ref([]);
const getInputEvents = (col) => {
return col.columnFilter.type === 'select'
? { 'update:modelValue': () => applyColumnFilter(col) }
: {
'keyup.enter': () => applyColumnFilter(col),
};
};
const columns = computed(() => [
{
label: t('entry.latestBuys.picture'),
@ -37,12 +103,32 @@ const columns = computed(() => [
name: 'itemFk',
field: 'itemFk',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('entry.latestBuys.packing'),
field: 'packing',
name: 'packing',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
@ -50,6 +136,16 @@ const columns = computed(() => [
field: 'grouping',
name: 'grouping',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
@ -57,12 +153,32 @@ const columns = computed(() => [
field: 'quantity',
name: 'quantity',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('globals.description'),
field: 'description',
name: 'description',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
@ -70,35 +186,104 @@ const columns = computed(() => [
field: 'size',
name: 'size',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('entry.latestBuys.tags'),
name: 'tags',
align: 'left',
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('entry.latestBuys.type'),
field: 'code',
name: 'type',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelectFilter,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemTypesOptions.value,
'option-value': 'code',
'option-label': 'code',
dense: true,
},
},
},
{
label: t('entry.latestBuys.intrastat'),
field: 'intrastat',
name: 'intrastat',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelectFilter,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: intrastatOptions.value,
'option-value': 'description',
'option-label': 'description',
dense: true,
},
},
},
{
label: t('entry.latestBuys.origin'),
field: 'origin',
name: 'origin',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelectFilter,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: originsOptions.value,
'option-value': 'code',
'option-label': 'code',
dense: true,
},
},
},
{
label: t('entry.latestBuys.weightByPiece'),
field: 'weightByPiece',
name: 'weightByPiece',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
@ -106,24 +291,67 @@ const columns = computed(() => [
field: 'isActive',
name: 'isActive',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('entry.latestBuys.family'),
field: 'family',
name: 'family',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelectFilter,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemFamiliesOptions.value,
'option-value': 'code',
'option-label': 'code',
dense: true,
},
},
},
{
label: t('entry.latestBuys.entryFk'),
field: 'entryFk',
name: 'entryFk',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('entry.latestBuys.buyingValue'),
field: 'buyingValue',
name: 'buyingValue',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
},
{
@ -131,6 +359,16 @@ const columns = computed(() => [
field: 'freightValue',
name: 'freightValue',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
},
{
@ -138,6 +376,16 @@ const columns = computed(() => [
field: 'comissionValue',
name: 'comissionValue',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
},
{
@ -145,6 +393,16 @@ const columns = computed(() => [
field: 'packageValue',
name: 'packageValue',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
},
{
@ -152,12 +410,33 @@ const columns = computed(() => [
field: 'isIgnored',
name: 'isIgnored',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('entry.latestBuys.price2'),
field: 'price2',
name: 'price2',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
},
{
@ -165,6 +444,16 @@ const columns = computed(() => [
field: 'price3',
name: 'price3',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
},
{
@ -172,6 +461,16 @@ const columns = computed(() => [
field: 'minPrice',
name: 'minPrice',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
},
{
@ -179,6 +478,16 @@ const columns = computed(() => [
field: 'ektFk',
name: 'ektFk',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
@ -186,18 +495,51 @@ const columns = computed(() => [
field: 'weight',
name: 'weight',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('entry.latestBuys.packagingFk'),
field: 'packagingFk',
name: 'packagingFk',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelectFilter,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: packagingsOptions.value,
'option-value': 'id',
'option-label': 'id',
dense: true,
},
},
},
{
label: t('entry.latestBuys.packingOut'),
field: 'packingOut',
name: 'packingOut',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
@ -205,6 +547,16 @@ const columns = computed(() => [
field: 'landing',
name: 'landing',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toDate(val),
},
]);
@ -234,20 +586,55 @@ const redirectToEntryBuys = (entryFk) => {
router.push({ name: 'EntryBuys', params: { id: entryFk } });
};
const applyColumnFilter = async (col) => {
try {
params[col.field] = col.columnFilter.filterValue;
await arrayData.addFilter({ params });
} catch (err) {
console.error('Error applying column filter', err);
}
};
onMounted(async () => {
stateStore.rightDrawer = true;
const filteredColumns = columns.value.filter((col) => col.name !== 'picture');
allColumnNames.value = filteredColumns.map((col) => col.name);
await arrayData.fetch({ append: false });
});
onUnmounted(() => (stateStore.rightDrawer = false));
</script>
<template>
<FetchData
ref="rowsFetchDataRef"
url="Buys/latestBuysFilter"
:filter="{ order: 'itemFk DESC', limit: 20 }"
@on-fetch="(data) => (rows = data)"
url="ItemTypes"
:filter="{ fields: ['code'], order: 'code ASC', limit: 30 }"
auto-load
@on-fetch="(data) => (itemTypesOptions = data)"
/>
<FetchData
url="Origins"
:filter="{ fields: ['code'], order: 'code ASC', limit: 30 }"
auto-load
@on-fetch="(data) => (originsOptions = data)"
/>
<FetchData
url="ItemFamilies"
:filter="{ fields: ['code'], order: 'code ASC', limit: 30 }"
auto-load
@on-fetch="(data) => (itemFamiliesOptions = data)"
/>
<FetchData
url="Packagings"
:filter="{ fields: ['id'], order: 'id ASC', limit: 30 }"
auto-load
@on-fetch="(data) => (packagingsOptions = data)"
/>
<FetchData
url="Intrastats"
:filter="{ fields: ['description'], order: 'description ASC', limit: 30 }"
auto-load
@on-fetch="(data) => (intrastatOptions = data)"
/>
<QToolbar class="bg-vn-dark justify-end">
<div id="st-data">
@ -261,19 +648,43 @@ onMounted(async () => {
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<EntryLatestBuysFilter data-key="EntryLatestBuys" :tags="tags" />
</QScrollArea>
</QDrawer>
<QPage class="column items-center q-pa-md">
<QTable
:rows="rows"
:columns="columns"
hide-bottom
selection="multiple"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:visible-columns="visibleColumns"
v-model:selected="rowsSelected"
:no-data-label="t('globals.noResults')"
@row-click="(_, row) => redirectToEntryBuys(row.entryFk)"
>
<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.name !== 'picture'"
v-model="col.columnFilter.filterValue"
v-bind="col.columnFilter.attrs"
v-on="col.columnFilter.event(col)"
dense
/>
</QTd>
</QTr>
</template>
<template #body-cell-picture="{ row }">
<QTd>
<QImg
@ -288,9 +699,10 @@ onMounted(async () => {
</template>
<template #body-cell-itemFk="{ row }">
<QTd @click.stop>
<QBtn flat color="blue">
<QBtn flat color="primary">
{{ row.itemFk }}
</QBtn>
<ItemDescriptorProxy :id="row.itemFk" />
</QTd>
</template>
<template #body-cell-tags="{ row }">
@ -300,7 +712,7 @@ onMounted(async () => {
</template>
<template #body-cell-entryFk="{ row }">
<QTd @click.stop>
<QBtn flat color="blue">
<QBtn flat color="primary">
<EntryDescriptorProxy :id="row.entryFk" />
{{ row.entryFk }}
</QBtn>

View File

@ -0,0 +1,474 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnInput from 'components/common/VnInput.vue';
import FetchData from 'components/FetchData.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
import axios from 'axios';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const itemCategories = ref([]);
const selectedCategoryFk = ref(null);
const selectedTypeFk = ref(null);
const itemTypesOptions = ref([]);
const itemTypeWorkersOptions = ref([]);
const suppliersOptions = ref([]);
const tagOptions = ref([]);
const tagValues = ref([]);
const categoryList = computed(() => {
return (itemCategories.value || [])
.filter((category) => category.display)
.map((category) => ({
...category,
icon: `vn:${(category.icon || '').split('-')[1]}`,
}));
});
const selectedCategory = computed(() =>
(itemCategories.value || []).find(
(category) => category?.id === selectedCategoryFk.value
)
);
const selectedType = computed(() => {
return (itemTypesOptions.value || []).find(
(type) => type?.id === selectedTypeFk.value
);
});
const selectCategory = async (params, categoryId, search) => {
if (params.categoryFk === categoryId) {
resetCategory(params);
search();
return;
}
selectedCategoryFk.value = categoryId;
params.categoryFk = categoryId;
await fetchItemTypes(categoryId);
search();
};
const resetCategory = (params) => {
selectedCategoryFk.value = null;
itemTypesOptions.value = null;
if (params) {
params.categoryFk = null;
params.typeFk = null;
}
};
const applyTags = (params, search) => {
params.tags = tagValues.value
.filter((tag) => tag.selectedTag && tag.value)
.map((tag) => ({
tagFk: tag.selectedTag.id,
tagName: tag.selectedTag.name,
value: tag.value,
}));
search();
};
const fetchItemTypes = async (id) => {
try {
const filter = {
fields: ['id', 'name', 'categoryFk'],
where: { categoryFk: id },
include: 'category',
order: 'name ASC',
};
const { data } = await axios.get('ItemTypes', {
params: { filter: JSON.stringify(filter) },
});
itemTypesOptions.value = data;
} catch (err) {
console.error('Error fetching item types', err);
}
};
const getCategoryClass = (category, params) => {
if (category.id === params?.categoryFk) {
return 'active';
}
};
const getSelectedTagValues = async (tag) => {
try {
tag.value = null;
const filter = {
fields: ['value'],
order: 'value ASC',
limit: 30,
};
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get(`Tags/${tag.selectedTag.id}/filterValue`, {
params,
});
tag.valueOptions = data;
} catch (err) {
console.error('Error getting selected tag values');
}
};
const removeTag = (index, params, search) => {
(tagValues.value || []).splice(index, 1);
applyTags(params, search);
};
</script>
<template>
<FetchData
url="ItemCategories"
limit="30"
auto-load
@on-fetch="(data) => (itemCategories = data)"
/>
<FetchData
url="TicketRequests/getItemTypeWorker"
limit="30"
auto-load
:filter="{ fields: ['id', 'nickname'], order: 'nickname ASC', limit: 30 }"
@on-fetch="(data) => (itemTypeWorkersOptions = data)"
/>
<FetchData
url="Suppliers"
limit="30"
auto-load
:filter="{ fields: ['id', 'name', 'nickname'], order: 'name ASC', limit: 30 }"
@on-fetch="(data) => (suppliersOptions = data)"
/>
<FetchData
url="Tags"
:filter="{ fields: ['id', 'name', 'isFree'] }"
auto-load
limit="30"
@on-fetch="(data) => (tagOptions = data)"
/>
<VnFilterPanel
:data-key="props.dataKey"
:expr-builder="exprBuilder"
:custom-tags="['tags']"
@init="onFilterInit"
@remove="clearFilter"
>
<template #tags="{ tag, formatFn }">
<strong v-if="tag.label === 'categoryFk'">
{{ t(selectedCategory?.name || '') }}
</strong>
<strong v-else-if="tag.label === 'typeFk'">
{{ t(selectedType?.name || '') }}
</strong>
<div v-else class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #customTags="{ tags: customTags, params }">
<template v-for="tag in customTags" :key="tag.label">
<VnFilterPanelChip
v-for="chip in tag.value"
:key="chip"
removable
@remove="removeTagChip(chip, params, searchFn)"
>
<div class="q-gutter-x-xs">
<strong>{{ chip.tagName }}: </strong>
<span>"{{ chip.value }}"</span>
</div>
</VnFilterPanelChip>
</template>
</template>
<template #body="{ params, searchFn }">
<QItem class="category-filter q-mt-md">
<QBtn
dense
flat
round
v-for="category in categoryList"
:key="category.name"
:class="['category', getCategoryClass(category, params)]"
:icon="category.icon"
@click="selectCategory(params, category.id, searchFn)"
>
<QTooltip>
{{ t(category.name) }}
</QTooltip>
</QBtn>
</QItem>
<QItem class="q-my-md">
<QItemSection>
<VnSelectFilter
:label="t('params.typeFk')"
v-model="params.typeFk"
:options="itemTypesOptions"
option-value="id"
option-label="name"
dense
outlined
rounded
use-input
:disable="!selectedCategoryFk"
@update:model-value="
(value) => {
selectedTypeFk = value;
searchFn();
}
"
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption>
{{ opt.categoryName }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QItemSection>
</QItem>
<QSeparator />
<QItem class="q-my-md">
<QItemSection>
<VnSelectFilter
:label="t('params.salesPersonFk')"
v-model="params.salesPersonFk"
:options="itemTypeWorkersOptions"
option-value="id"
option-label="nickname"
dense
outlined
rounded
use-input
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem class="q-my-md">
<QItemSection>
<VnSelectFilter
:label="t('params.supplier')"
v-model="params.supplierFk"
:options="suppliersOptions"
option-value="id"
option-label="name"
dense
outlined
rounded
use-input
@update:model-value="searchFn()"
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption>
{{ opt.nickname }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QItemSection>
</QItem>
<QItem class="q-my-md">
<QItemSection>
<VnInputDate
:label="t('params.from')"
v-model="params.from"
is-outlined
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem class="q-my-md">
<QItemSection>
<VnInputDate
:label="t('params.to')"
v-model="params.to"
is-outlined
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
:label="t('params.active')"
v-model="params.active"
toggle-indeterminate
@update:model-value="searchFn()"
/>
</QItemSection>
<QItemSection>
<QCheckbox
:label="t('params.visible')"
v-model="params.visible"
toggle-indeterminate
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
:label="t('params.floramondo')"
v-model="params.floramondo"
toggle-indeterminate
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem
v-for="(value, index) in tagValues"
:key="value"
class="q-mt-md filter-value"
>
<QItemSection class="col">
<VnSelectFilter
:label="t('params.tag')"
v-model="value.selectedTag"
:options="tagOptions"
option-label="name"
dense
outlined
rounded
:emit-value="false"
use-input
:is-clearable="false"
@update:model-value="getSelectedTagValues(value)"
/>
</QItemSection>
<QItemSection class="col">
<VnSelectFilter
v-if="!value?.selectedTag?.isFree && value.valueOptions"
:label="t('params.value')"
v-model="value.value"
:options="value.valueOptions || []"
option-value="value"
option-label="value"
dense
outlined
rounded
emit-value
use-input
:disable="!value"
:is-clearable="false"
class="filter-input"
@update:model-value="applyTags(params, searchFn)"
/>
<VnInput
v-else
v-model="value.value"
:label="t('params.value')"
:disable="!value"
is-outlined
class="filter-input"
:is-clearable="false"
@keyup.enter="applyTags(params, searchFn)"
/>
</QItemSection>
<QIcon
name="delete"
class="filter-icon"
@click="removeTag(index, params, searchFn)"
/>
</QItem>
<QItem class="q-mt-lg">
<QIcon
name="add_circle"
class="filter-icon"
@click="tagValues.push({})"
/>
</QItem>
</template>
</VnFilterPanel>
</template>
<style lang="scss" scoped>
.category-filter {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
.category {
padding: 8px;
width: 60px;
height: 60px;
font-size: 1.4rem;
background-color: var(--vn-light-gray);
&.active {
background-color: $primary;
}
}
}
.filter-icon {
font-size: 24px;
color: $primary;
padding: 0 4px;
cursor: pointer;
}
.filter-input {
flex-shrink: 1;
min-width: 0;
}
.filter-value {
display: flex;
align-items: center;
}
</style>
<i18n>
en:
params:
supplier: Supplier
from: From
to: To
active: Is active
visible: Is visible
floramondo: Is floramondo
salesPersonFk: Buyer
categoryFk: Category
typeFk: Type
tag: Tag
value: Value
es:
params:
supplier: Proveedor
from: Desde
to: Hasta
active: Activo
visible: Visible
floramondo: Floramondo
salesPersonFk: Comprador
categoryFk: Categoría
typeFk: Tipo
tag: Etiqueta
value: Valor
Plant: Planta
Flower: Flor
Handmade: Confección
Green: Verde
Accessories: Complemento
Fruit: Fruta
</i18n>

View File

@ -75,7 +75,12 @@ async function insert() {
}
</script>
<template>
<FetchData url="Banks" auto-load limit="30" @on-fetch="(data) => (banks = data)" />
<FetchData
url="Accountings"
auto-load
limit="30"
@on-fetch="(data) => (banks = data)"
/>
<CrudModel
v-if="invoiceIn"
ref="invoiceInFormRef"

View File

@ -147,7 +147,7 @@ async function changeState(value) {
</div>
</QCard>
<QCard class="vn-one">
<a class="header link" :href="ticketUrl + 'basic-data/step-one'">
<a class="header header-link" :href="ticketUrl + 'basic-data/step-one'">
{{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" />
</a>
@ -193,7 +193,7 @@ async function changeState(value) {
/>
</QCard>
<QCard class="vn-one">
<a class="header link" :href="ticketUrl + 'basic-data/step-one'">
<a class="header header-link" :href="ticketUrl + 'basic-data/step-one'">
{{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" />
</a>
@ -236,7 +236,7 @@ async function changeState(value) {
/>
</QCard>
<QCard class="vn-one">
<a class="header link" :href="ticketUrl + 'observation'">
<a class="header header-link" :href="ticketUrl + 'observation'">
{{ t('ticket.pageTitles.notes') }}
<QIcon name="open_in_new" />
</a>
@ -258,7 +258,7 @@ async function changeState(value) {
</VnLv>
</QCard>
<QCard class="vn-max">
<a class="header link" :href="ticketUrl + 'sale'">
<a class="header header-link" :href="ticketUrl + 'sale'">
{{ t('ticket.summary.saleLines') }}
<QIcon name="open_in_new" />
</a>
@ -396,7 +396,7 @@ async function changeState(value) {
class="vn-max"
v-if="ticket.packagings.length > 0 || ticket.services.length > 0"
>
<a class="header link" :href="ticketUrl + 'package'">
<a class="header header-link" :href="ticketUrl + 'package'">
{{ t('globals.packages') }}
<QIcon name="open_in_new" />
</a>
@ -417,7 +417,7 @@ async function changeState(value) {
</template>
</QTable>
<a class="header link q-mt-xl" :href="ticketUrl + 'service'">
<a class="header header-link q-mt-xl" :href="ticketUrl + 'service'">
{{ t('ticket.summary.service') }}
<QIcon name="open_in_new" />
</a>

View File

@ -11,11 +11,10 @@ export default {
component: RouterView,
redirect: { name: 'ClaimMain' },
menus: {
main: ['ClaimList', 'ClaimRmaList'],
main: ['ClaimList'],
card: [
'ClaimBasicData',
'ClaimLines',
'ClaimRma',
'ClaimPhotos',
'ClaimNotes',
'ClaimDevelopment',
@ -39,16 +38,6 @@ export default {
},
component: () => import('src/pages/Claim/ClaimList.vue'),
},
{
name: 'ClaimRmaList',
path: 'rma',
meta: {
title: 'rmaList',
icon: 'vn:barcode',
roles: ['claimManager'],
},
component: () => import('src/pages/Claim/ClaimRmaList.vue'),
},
],
},
{
@ -85,16 +74,6 @@ export default {
},
component: () => import('src/pages/Claim/Card/ClaimLines.vue'),
},
{
name: 'ClaimRma',
path: 'rma',
meta: {
title: 'rma',
icon: 'vn:barcode',
roles: ['claimManager'],
},
component: () => import('src/pages/Claim/Card/ClaimRma.vue'),
},
{
name: 'ClaimPhotos',
path: 'photos',