0
0
Fork 0

Merge pull request 'refs #6681 - PR3 - Resolve conflicts' (!166) from hyervoni-PR-3 into dev

Reviewed-on: verdnatura/salix-front#166
Reviewed-by: Jorge Penades <jorgep@verdnatura.es>
This commit is contained in:
Alex Moreno 2024-01-29 08:29:43 +00:00
commit f715c615d2
74 changed files with 7616 additions and 204 deletions

2
Jenkinsfile vendored
View File

@ -96,4 +96,4 @@ pipeline {
} }
} }
} }
} }

View File

@ -7,6 +7,13 @@ import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import FormModelPopup from './FormModelPopup.vue'; import FormModelPopup from './FormModelPopup.vue';
const props = defineProps({
showEntityField: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['onDataSaved']); const emit = defineEmits(['onDataSaved']);
const { t } = useI18n(); const { t } = useI18n();
@ -73,7 +80,7 @@ const onDataSaved = (data) => {
:rules="validate('bankEntity.countryFk')" :rules="validate('bankEntity.countryFk')"
/> />
</div> </div>
<div class="col"> <div v-if="showEntityField" class="col">
<QInput :label="t('id')" v-model="data.id" /> <QInput :label="t('id')" v-model="data.id" />
</div> </div>
</VnRow> </VnRow>

View File

@ -19,8 +19,8 @@ const cityFormData = reactive({
const provincesOptions = ref([]); const provincesOptions = ref([]);
const onDataSaved = () => { const onDataSaved = (dataSaved) => {
emit('onDataSaved'); emit('onDataSaved', dataSaved);
}; };
</script> </script>

View File

@ -28,16 +28,24 @@ const countriesOptions = ref([]);
const provincesOptions = ref([]); const provincesOptions = ref([]);
const townsLocationOptions = ref([]); const townsLocationOptions = ref([]);
const onDataSaved = () => { const onDataSaved = (dataSaved) => {
emit('onDataSaved'); emit('onDataSaved', dataSaved);
}; };
const onCityCreated = async () => { const onCityCreated = async ({ name, provinceFk }, formData) => {
await townsFetchDataRef.value.fetch(); await townsFetchDataRef.value.fetch();
formData.townFk = townsLocationOptions.value.find((town) => town.name === name).id;
formData.provinceFk = provinceFk;
formData.countryFk = provincesOptions.value.find(
(province) => province.id === provinceFk
).countryFk;
}; };
const onProvinceCreated = async () => { const onProvinceCreated = async ({ name }, formData) => {
await provincesFetchDataRef.value.fetch(); await provincesFetchDataRef.value.fetch();
formData.provinceFk = provincesOptions.value.find(
(province) => province.name === name
).id;
}; };
</script> </script>
@ -88,7 +96,9 @@ const onProvinceCreated = async () => {
:roles-allowed-to-create="['deliveryAssistant']" :roles-allowed-to-create="['deliveryAssistant']"
> >
<template #form> <template #form>
<CreateNewCityForm @on-data-saved="onCityCreated($event)" /> <CreateNewCityForm
@on-data-saved="onCityCreated($event, data)"
/>
</template> </template>
</VnSelectCreate> </VnSelectCreate>
</div> </div>
@ -107,7 +117,7 @@ const onProvinceCreated = async () => {
> >
<template #form> <template #form>
<CreateNewProvinceForm <CreateNewProvinceForm
@on-data-saved="onProvinceCreated($event)" @on-data-saved="onProvinceCreated($event, data)"
/> />
</template> </template>
</VnSelectCreate> </VnSelectCreate>
@ -131,7 +141,7 @@ const onProvinceCreated = async () => {
es: es:
New postcode: Nuevo código postal New postcode: Nuevo código postal
Please, ensure you put the correct data!: ¡Por favor, asegúrese de poner los datos correctos! Please, ensure you put the correct data!: ¡Por favor, asegúrese de poner los datos correctos!
City: Ciudad City: Población
Province: Provincia Province: Provincia
Country: País Country: País
Postcode: Código postal Postcode: Código postal

View File

@ -19,8 +19,8 @@ const provinceFormData = reactive({
const autonomiesOptions = ref([]); const autonomiesOptions = ref([]);
const onDataSaved = () => { const onDataSaved = (dataSaved) => {
emit('onDataSaved'); emit('onDataSaved', dataSaved);
}; };
</script> </script>

View File

@ -225,15 +225,19 @@ function getDifferences(obj1, obj2) {
delete obj2.$index; delete obj2.$index;
for (let key in obj1) { for (let key in obj1) {
if (obj2[key] && obj1[key] !== obj2[key]) { if (obj2[key] && JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {
diff[key] = obj2[key]; diff[key] = obj2[key];
} }
} }
for (let key in obj2) { for (let key in obj2) {
if (obj1[key] === undefined || obj1[key] !== obj2[key]) { if (
obj1[key] === undefined ||
JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])
) {
diff[key] = obj2[key]; diff[key] = obj2[key];
} }
} }
return diff; return diff;
} }

View File

@ -128,13 +128,14 @@ async function save() {
try { try {
const body = $props.mapper ? $props.mapper(formData.value) : formData.value; const body = $props.mapper ? $props.mapper(formData.value) : formData.value;
let response
if ($props.urlCreate) { if ($props.urlCreate) {
await axios.post($props.urlCreate, body); response = await axios.post($props.urlCreate, body);
notify('globals.dataCreated', 'positive'); notify('globals.dataCreated', 'positive');
} else { } else {
await axios.patch($props.urlUpdate || $props.url, body); response = await axios.patch($props.urlUpdate || $props.url, body);
} }
emit('onDataSaved', formData.value); emit('onDataSaved', formData.value, response);
originalData.value = JSON.parse(JSON.stringify(formData.value)); originalData.value = JSON.parse(JSON.stringify(formData.value));
hasChanges.value = false; hasChanges.value = false;
} catch (err) { } catch (err) {

View File

@ -42,8 +42,8 @@ const { t } = useI18n();
const closeButton = ref(null); const closeButton = ref(null);
const isLoading = ref(false); const isLoading = ref(false);
const onDataSaved = () => { const onDataSaved = (dataSaved) => {
emit('onDataSaved'); emit('onDataSaved', dataSaved);
closeForm(); closeForm();
}; };
@ -59,7 +59,7 @@ const closeForm = () => {
:default-actions="false" :default-actions="false"
:url-create="urlCreate" :url-create="urlCreate"
:model="model" :model="model"
@on-data-saved="onDataSaved()" @on-data-saved="onDataSaved($event)"
> >
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<span ref="closeButton" class="close-icon" v-close-popup> <span ref="closeButton" class="close-icon" v-close-popup>

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
const emit = defineEmits(['update:modelValue', 'update:options']); const emit = defineEmits(['update:modelValue', 'update:options', 'keyup.enter']);
const $props = defineProps({ const $props = defineProps({
modelValue: { modelValue: {
@ -32,6 +32,10 @@ const styleAttrs = computed(() => {
} }
: {}; : {};
}); });
const onEnterPress = () => {
emit('keyup.enter');
};
</script> </script>
<template> <template>
@ -41,6 +45,7 @@ const styleAttrs = computed(() => {
v-bind="{ ...$attrs, ...styleAttrs }" v-bind="{ ...$attrs, ...styleAttrs }"
type="text" type="text"
:class="{ required: $attrs.required }" :class="{ required: $attrs.required }"
@keyup.enter="onEnterPress()"
> >
<template v-if="$slots.prepend" #prepend> <template v-if="$slots.prepend" #prepend>
<slot name="prepend" /> <slot name="prepend" />

View File

@ -0,0 +1,108 @@
<script setup>
import {computed, ref} from 'vue';
import { toHour} from 'src/filters';
import {useI18n} from "vue-i18n";
import isValidDate from "filters/isValidDate";
const props = defineProps({
modelValue: {
type: String,
default: null,
},
readonly: {
type: Boolean,
default: false,
},
isOutlined: {
type: Boolean,
default: false,
},
});
const { t } = useI18n();
const emit = defineEmits(['update:modelValue']);
const value = computed({
get() {
return props.modelValue;
},
set(value) {
const [hours, minutes] = value.split(':')
const date = new Date()
date.setUTCHours(Number.parseInt(hours) || 0, Number.parseInt(minutes) || 0, 0, 0)
emit('update:modelValue', value ? date.toISOString() : null);
},
});
const onDateUpdate = (date) => {
internalValue.value = date;
};
const save = () => {
value.value = internalValue.value;
};
const formatTime = (dateString) => {
if (!isValidDate(dateString)){
return ''
}
const date = new Date(dateString || '');
return `${date.getUTCHours().toString().padStart(2, '0')}:${date.getUTCMinutes().toString().padStart(2, '0')}`;
};
const internalValue = ref(formatTime(value))
const styleAttrs = computed(() => {
return props.isOutlined
? {
dense: true,
outlined: true,
rounded: true,
}
: {};
});
</script>
<template>
<QInput
class="vn-input-time"
rounded
readonly
:model-value="toHour(value)"
v-bind="{ ...$attrs, ...styleAttrs }"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
:no-parent-event="props.readonly"
>
<QTime
:format24h="false"
:model-value="formatTime(value)"
@update:model-value="onDateUpdate"
>
<div class="row items-center justify-end q-gutter-sm">
<QBtn :label="t('Cancel')" color="primary" flat v-close-popup />
<QBtn label="Ok" color="primary" flat @click="save" v-close-popup />
</div>
</QTime>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</template>
<style lang="scss">
.vn-input-time.q-field--standard.q-field--readonly .q-field__control:before {
border-bottom-style: solid;
}
.vn-input-time.q-field--outlined.q-field--readonly .q-field__control:before {
border-style: solid;
}
</style>
<i18n>
es:
Cancel: Cancelar
</i18n>

View File

@ -9,6 +9,7 @@ const $props = defineProps({
isSelected: { type: Boolean, default: false }, isSelected: { type: Boolean, default: false },
title: { type: String, default: null }, title: { type: String, default: null },
showCheckbox: { type: Boolean, default: false }, showCheckbox: { type: Boolean, default: false },
hasInfoIcons: { type: Boolean, default: false },
}); });
const emit = defineEmits(['toggleCardCheck']); const emit = defineEmits(['toggleCardCheck']);
@ -39,6 +40,9 @@ const toggleCardCheck = (item) => {
</div> </div>
</slot> </slot>
<div class="card-list-body"> <div class="card-list-body">
<div v-if="hasInfoIcons" class="column q-mr-md q-gutter-y-xs">
<slot name="info-icons" />
</div>
<div class="list-items row flex-wrap-wrap"> <div class="list-items row flex-wrap-wrap">
<slot name="list-items" /> <slot name="list-items" />
</div> </div>

View File

@ -17,9 +17,9 @@ a {
// Removes chrome autofill background // Removes chrome autofill background
input:-webkit-autofill, input:-webkit-autofill,
select:-webkit-autofill { select:-webkit-autofill {
color: var(--vn-text) ; color: var(--vn-text);
font-family: $typography-font-family; font-family: $typography-font-family;
-webkit-text-fill-color: var(--vn-text) ; -webkit-text-fill-color: var(--vn-text);
-webkit-background-clip: text !important; -webkit-background-clip: text !important;
background-clip: text !important; background-clip: text !important;
} }
@ -48,6 +48,10 @@ body.body--dark {
background-color: var(--vn-dark); background-color: var(--vn-dark);
} }
.color-vn-text {
color: var(--vn-text);
}
.vn-card { .vn-card {
background-color: var(--vn-gray); background-color: var(--vn-gray);
color: var(--vn-text); color: var(--vn-text);

View File

@ -8,11 +8,13 @@ import toPercentage from './toPercentage';
import toLowerCamel from './toLowerCamel'; import toLowerCamel from './toLowerCamel';
import dashIfEmpty from './dashIfEmpty'; import dashIfEmpty from './dashIfEmpty';
import dateRange from './dateRange'; import dateRange from './dateRange';
import toHour from './toHour';
export { export {
toLowerCase, toLowerCase,
toLowerCamel, toLowerCamel,
toDate, toDate,
toHour,
toDateString, toDateString,
toDateHour, toDateHour,
toRelativeDate, toRelativeDate,

View File

@ -0,0 +1,3 @@
export default function isValidDate(date) {
return !isNaN(new Date(date).getTime());
}

16
src/filters/toHour.js Normal file
View File

@ -0,0 +1,16 @@
import isValidDate from 'filters/isValidDate';
export default function toHour(date) {
if (!isValidDate(date)) {
return '--:--';
}
const dateHour = new Date(date);
let hours = dateHour.getUTCHours();
hours = hours % 12;
hours = hours ? hours : 12;
let minutes = dateHour.getUTCMinutes();
minutes = minutes < 10 ? minutes.toString().padStart(2, '0') : minutes;
return `${hours}:${minutes} ${dateHour.getUTCHours() >= 12 ? 'PM' : 'AM'}`;
}

View File

@ -261,12 +261,89 @@ export default {
pageTitles: { pageTitles: {
entries: 'Entries', entries: 'Entries',
list: 'List', list: 'List',
createEntry: 'New entry',
summary: 'Summary', summary: 'Summary',
create: 'Create', basicData: 'Basic data',
buys: 'Buys',
notes: 'Notes',
log: 'Log',
}, },
list: { list: {
newEntry: 'New entry', newEntry: 'New entry',
landed: 'Landed',
invoiceNumber: 'Invoice number',
supplier: 'Supplier',
booked: 'Booked',
confirmed: 'Confirmed',
ordered: 'Ordered',
},
summary: {
commission: 'Commission',
currency: 'Currency',
company: 'Company',
reference: 'Reference',
invoiceNumber: 'Invoice number',
ordered: 'Ordered',
confirmed: 'Confirmed',
booked: 'Booked',
raid: 'Raid',
excludedFromAvailable: 'Inventory',
travelReference: 'Reference',
travelAgency: 'Agency',
travelShipped: 'Shipped',
travelWarehouseOut: 'Warehouse Out',
travelDelivered: 'Delivered',
travelLanded: 'Landed',
travelWarehouseIn: 'Warehouse In',
travelReceived: 'Received',
buys: 'Buys',
quantity: 'Quantity',
stickers: 'Stickers',
package: 'Package',
weight: 'Weight',
packing: 'Packing',
grouping: 'Grouping',
buyingValue: 'Buying value',
import: 'Import',
pvp: 'PVP',
item: 'Item',
},
basicData: {
supplier: 'Supplier',
travel: 'Travel',
reference: 'Reference',
invoiceNumber: 'Invoice number',
company: 'Company',
currency: 'Currency',
commission: 'Commission',
observation: 'Observation',
ordered: 'Ordered',
confirmed: 'Confirmed',
booked: 'Booked',
raid: 'Raid',
excludedFromAvailable: 'Inventory',
},
buys: {
groupingPrice: 'Grouping price',
packingPrice: 'Packing price',
reference: 'Reference',
observations: 'Observations',
item: 'Item',
description: 'Description',
size: 'Size',
packing: 'Packing',
grouping: 'Grouping',
buyingValue: 'Buying value',
packagingFk: 'Box',
file: 'File',
},
notes: {
observationType: 'Observation type',
description: 'Description',
},
descriptor: {
agency: 'Agency',
landed: 'Landed',
warehouseOut: 'Warehouse Out',
}, },
}, },
ticket: { ticket: {
@ -807,6 +884,10 @@ export default {
pageTitles: { pageTitles: {
routes: 'Routes', routes: 'Routes',
cmrsList: 'External CMRs list', cmrsList: 'External CMRs list',
RouteList: 'List',
create: 'Create',
basicData: 'Basic Data',
summary: 'Summary',
}, },
cmr: { cmr: {
list: { list: {
@ -877,6 +958,72 @@ export default {
create: { create: {
supplierName: 'Supplier name', supplierName: 'Supplier name',
}, },
basicData: {
alias: 'Alias',
workerFk: 'Responsible',
isSerious: 'Verified',
isActive: 'Active',
isPayMethodChecked: 'PayMethod checked',
note: 'Notes',
},
fiscalData: {
name: 'Social name *',
nif: 'Tax number *',
account: 'Account',
sageTaxTypeFk: 'Sage tax type',
sageWithholdingFk: 'Sage withholding',
sageTransactionTypeFk: 'Sage transaction type',
supplierActivityFk: 'Supplier activity',
healthRegister: 'Health register',
street: 'Street',
postcode: 'Postcode',
city: 'City *',
provinceFk: 'Province',
country: 'Country',
isTrucker: 'Trucker',
isVies: 'Vies',
},
billingData: {
payMethodFk: 'Billing data',
payDemFk: 'Payment deadline',
payDay: 'Pay day',
},
accounts: {
iban: 'Iban',
bankEntity: 'Bank entity',
beneficiary: 'Beneficiary',
},
contacts: {
name: 'Name',
phone: 'Phone',
mobile: 'Mobile',
email: 'Email',
observation: 'Notes',
},
addresses: {
street: 'Street',
postcode: 'Postcode',
phone: 'Phone',
name: 'Name',
city: 'City',
province: 'Province',
mobile: 'Mobile',
},
agencyTerms: {
agencyFk: 'Agency',
minimumM3: 'Minimum M3',
packagePrice: 'Package Price',
kmPrice: 'Km Price',
m3Price: 'M3 Price',
routePrice: 'Route price',
minimumKm: 'Minimum Km',
addRow: 'Add row',
},
consumption: {
entry: 'Entry',
date: 'Date',
reference: 'Reference',
},
}, },
travel: { travel: {
pageTitles: { pageTitles: {

View File

@ -261,10 +261,88 @@ export default {
entries: 'Entradas', entries: 'Entradas',
list: 'Listado', list: 'Listado',
summary: 'Resumen', summary: 'Resumen',
create: 'Crear', basicData: 'Datos básicos',
buys: 'Compras',
notes: 'Notas',
log: 'Historial',
}, },
list: { list: {
newEntry: 'Nueva entrada', newEntry: 'Nueva entrada',
landed: 'F. entrega',
invoiceNumber: 'Núm. factura',
supplier: 'Proveedor',
booked: 'Asentado',
confirmed: 'Confirmado',
ordered: 'Pedida',
},
summary: {
commission: 'Comisión',
currency: 'Moneda',
company: 'Empresa',
reference: 'Referencia',
invoiceNumber: 'Núm. factura',
ordered: 'Pedida',
confirmed: 'Confirmado',
booked: 'Asentado',
raid: 'Redada',
excludedFromAvailable: 'Inventario',
travelReference: 'Referencia',
travelAgency: 'Agencia',
travelShipped: 'F. envio',
travelWarehouseOut: 'Alm. salida',
travelDelivered: 'Enviada',
travelLanded: 'F. entrega',
travelWarehouseIn: 'Alm. entrada',
travelReceived: 'Recibida',
buys: 'Compras',
quantity: 'Cantidad',
stickers: 'Etiquetas',
package: 'Embalaje',
weight: 'Peso',
packing: 'Packing',
grouping: 'Grouping',
buyingValue: 'Coste',
import: 'Importe',
pvp: 'PVP',
item: 'Artículo',
},
basicData: {
supplier: 'Proveedor',
travel: 'Envío',
reference: 'Referencia',
invoiceNumber: 'Núm. factura',
company: 'Empresa',
currency: 'Moneda',
observation: 'Observación',
commission: 'Comisión',
ordered: 'Pedida',
confirmed: 'Confirmado',
booked: 'Asentado',
raid: 'Redada',
excludedFromAvailable: 'Inventario',
},
buys: {
groupingPrice: 'Precio grouping',
packingPrice: 'Precio packing',
reference: 'Referencia',
observations: 'Observaciónes',
item: 'Artículo',
description: 'Descripción',
size: 'Medida',
packing: 'Packing',
grouping: 'Grouping',
buyingValue: 'Coste',
packagingFk: 'Embalaje',
file: 'Fichero',
},
notes: {
observationType: 'Tipo de observación',
description: 'Descripción',
},
descriptor: {
agency: 'Agencia',
landed: 'F. entrega',
warehouseOut: 'Alm. salida',
}, },
}, },
ticket: { ticket: {
@ -806,6 +884,10 @@ export default {
pageTitles: { pageTitles: {
routes: 'Rutas', routes: 'Rutas',
cmrsList: 'Listado de CMRs externos', cmrsList: 'Listado de CMRs externos',
RouteList: 'Listado',
create: 'Crear',
basicData: 'Datos básicos',
summary: 'Summary',
}, },
cmr: { cmr: {
list: { list: {
@ -876,6 +958,72 @@ export default {
create: { create: {
supplierName: 'Nombre del proveedor', supplierName: 'Nombre del proveedor',
}, },
basicData: {
alias: 'Alias',
workerFk: 'Responsable',
isSerious: 'Verificado',
isActive: 'Activo',
isPayMethodChecked: 'Método de pago validado',
note: 'Notas',
},
fiscalData: {
name: 'Razón social *',
nif: 'NIF/CIF *',
account: 'Cuenta',
sageTaxTypeFk: 'Tipo de impuesto sage',
sageWithholdingFk: 'Retención sage',
sageTransactionTypeFk: 'Tipo de transacción sage',
supplierActivityFk: 'Actividad proveedor',
healthRegister: 'Pasaporte sanitario',
street: 'Calle',
postcode: 'Código postal',
city: 'Población *',
provinceFk: 'Provincia',
country: 'País',
isTrucker: 'Transportista',
isVies: 'Vies',
},
billingData: {
payMethodFk: 'Forma de pago',
payDemFk: 'Plazo de pago',
payDay: 'Día de pago',
},
accounts: {
iban: 'Iban',
bankEntity: 'Entidad bancaria',
beneficiary: 'Beneficiario',
},
contacts: {
name: 'Nombre',
phone: 'Teléfono',
mobile: 'Móvil',
email: 'Email',
observation: 'Notas',
},
addresses: {
street: 'Dirección',
postcode: 'Código postal',
phone: 'Teléfono',
name: 'Nombre',
city: 'Población',
province: 'Provincia',
mobile: 'Móvil',
},
agencyTerms: {
agencyFk: 'Agencia',
minimumM3: 'M3 mínimos',
packagePrice: 'Precio bulto',
kmPrice: 'Precio Km',
m3Price: 'Precio M3',
routePrice: 'Precio ruta',
minimumKm: 'Km mínimos',
addRow: 'Añadir fila',
},
consumption: {
entry: 'Entrada',
date: 'Fecha',
reference: 'Referencia',
},
}, },
travel: { travel: {
pageTitles: { pageTitles: {

View File

@ -1,3 +1,130 @@
<script setup>
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import axios from 'axios';
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 VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelectCreate from 'src/components/common/VnSelectCreate.vue';
import CreateBankEntityForm from 'src/components/CreateBankEntityForm.vue';
const { t } = useI18n();
const route = useRoute();
const payMethods = ref([]);
const bankEntitiesOptions = ref([]);
const bankEntitiesRef = ref(null);
const filter = {
fields: ['id', 'bic', 'name'],
order: 'bic ASC',
limit: 30,
};
const getBankEntities = () => {
bankEntitiesRef.value.fetch();
};
</script>
<template> <template>
<div class="flex justify-center">Billing data</div> <fetch-data @on-fetch="(data) => (payMethods = data)" auto-load url="PayMethods" />
<fetch-data
ref="bankEntitiesRef"
@on-fetch="(data) => (bankEntitiesOptions = data)"
:filter="filter"
auto-load
url="BankEntities"
/>
<FormModel
:url-update="`Clients/${route.params.id}`"
:url="`Clients/${route.params.id}/getCard`"
auto-load
model="customer"
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Billing data')"
:options="payMethods"
hide-selected
option-label="name"
option-value="id"
v-model="data.payMethod"
/>
</div>
<div class="col">
<VnInput
:label="t('Due day')"
:rules="validate('client.socialName')"
v-model="data.dueDay"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput :label="t('IBAN')" v-model="data.iban" />
</div>
<div class="col">
<VnSelectCreate
:label="t('Swift / BIC')"
:options="bankEntitiesOptions"
:roles-allowed-to-create="['salesAssistant', 'hr']"
:rules="validate('Worker.bankEntity')"
hide-selected
option-label="name"
option-value="id"
v-model="data.bankEntityFk"
>
<template #form>
<CreateBankEntityForm @on-data-saved="getBankEntities()" />
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection v-if="scope.opt">
<QItemLabel
>{{ scope.opt.bic }}
{{ scope.opt.name }}</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelectCreate>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox :label="t('Received LCR')" v-model="data.hasLcr" />
</div>
<div class="col">
<QCheckbox
:label="t('VNL core received')"
v-model="data.hasCoreVnl"
/>
</div>
<div class="col">
<QCheckbox :label="t('VNL B2B received')" v-model="data.hasSepaVnl" />
</div>
</VnRow>
</template>
</FormModel>
</template> </template>
<i18n>
es:
Billing data: Forma de pago
Due day: Vencimiento
IBAN: IBAN
Swift / BIC: Swift / BIC
Received LCR: Recibido LCR
VNL core received: Recibido core VNL
VNL B2B received: Recibido B2B VNL
</i18n>

View File

@ -1,3 +1,176 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const provincesLocation = ref([]);
const consigneeFilter = {
fields: [
'id',
'isDefaultAddress',
'isActive',
'nickname',
'street',
'city',
'provinceFk',
'phone',
'mobile',
'isEqualizated',
'isLogifloraAllowed',
'postalCode',
],
order: ['isDefaultAddress DESC', 'isActive DESC', 'nickname ASC'],
include: [
{
relation: 'observations',
scope: {
include: 'observationType',
},
},
{
relation: 'province',
scope: {
fields: ['id', 'name'],
},
},
],
};
const setProvince = (provinceFk) => {
const result = provincesLocation.value.filter(
(province) => province.id === provinceFk
);
return result[0]?.name || '';
};
const toCustomerConsigneeCreate = () => {
router.push({ name: 'CustomerConsigneeCreate' });
};
const toCustomerConsigneeEdit = (consigneeId) => {
router.push({
name: 'CustomerConsigneeEdit',
params: {
id: route.params.id,
consigneeId,
},
});
};
</script>
<template> <template>
<div class="flex justify-center">Consignees</div> <FetchData
@on-fetch="(data) => (provincesLocation = data)"
auto-load
url="Provinces/location"
/>
<QCard class="q-pa-lg">
<VnPaginate
data-key="CustomerConsignees"
:url="`Clients/${route.params.id}/addresses`"
order="id"
auto-load
:filter="consigneeFilter"
>
<template #body="{ rows }">
<QCard
v-for="(item, index) in rows"
:key="index"
:class="{
'consignees-card': true,
'q-mb-md': index < rows.length - 1,
}"
@click="toCustomerConsigneeEdit(item.id)"
>
<div class="q-ml-xs q-mr-md flex items-center">
<QIcon name="star" size="md" color="primary" />
</div>
<div>
<div class="text-weight-bold q-mb-sm">
{{ item.nickname }} - #{{ item.id }}
</div>
<div>{{ item.street }}</div>
<div>
{{ item.postalCode }} - {{ item.city }},
{{ setProvince(item.provinceFk) }}
</div>
<div class="flex">
<QCheckbox
:label="t('Is equalizated')"
v-model="item.isEqualizated"
class="q-mr-lg"
disable
/>
<QCheckbox
:label="t('Is logiflora allowed')"
v-model="item.isLogifloraAllowed"
disable
/>
</div>
</div>
<QSeparator
class="q-mx-lg"
v-if="item.observations.length"
vertical
/>
<div v-if="item.observations.length">
<div
:key="index"
class="flex q-mb-sm"
v-for="(observation, index) in item.observations"
>
<div class="text-weight-bold q-mr-sm">
{{ observation.observationType.description }}:
</div>
<div>{{ observation.description }}</div>
</div>
</div>
</QCard>
<QPageSticky :offset="[18, 18]">
<QBtn
@click.stop="toCustomerConsigneeCreate()"
color="primary"
fab
icon="add"
/>
<QTooltip>
{{ t('New consignee') }}
</QTooltip>
</QPageSticky>
</template>
</VnPaginate>
</QCard>
</template> </template>
<style lang="scss" scoped>
.consignees-card {
border: 2px solid var(--vn-light-gray);
border-radius: 10px;
padding: 10px;
display: flex;
cursor: pointer;
&:hover {
background-color: var(--vn-light-gray);
}
}
</style>
<i18n>
es:
Is equalizated: Recargo de equivalencia
Is logiflora allowed: Compra directa en Holanda
New consignee: Nuevo consignatario
</i18n>

View File

@ -1,3 +1,148 @@
<script setup>
import { ref, computed, onBeforeMount, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { date, QBtn } from 'quasar';
import { toCurrency, toDate } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const stateStore = useStateStore();
const arrayData = ref(null);
const workerId = ref(0);
onBeforeMount(async () => {
const filter = {
include: [
{
relation: 'worker',
scope: {
fields: ['id'],
include: { relation: 'user', scope: { fields: ['name'] } },
},
},
],
where: { clientFk: `${route.params.id}` },
order: ['created DESC'],
limit: 20,
};
arrayData.value = useArrayData('CustomerCreditsCard', {
url: 'ClientCredits',
filter,
});
await arrayData.value.fetch({ append: false });
stateStore.rightDrawer = true;
});
onMounted(() => {
const filteredColumns = columns.value.filter(
(col) => col.name !== 'actions' && col.name !== 'customerStatus'
);
allColumnNames.value = filteredColumns.map((col) => col.name);
});
const rows = computed(() => arrayData.value.store.data);
const allColumnNames = ref([]);
const tableColumnComponents = {
created: {
component: 'span',
props: () => {},
event: () => {},
},
employee: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: (prop) => {
selectWorkerId(prop.row.clientFk);
},
},
amount: {
component: 'span',
props: () => {},
event: () => {},
},
};
const columns = computed(() => [
{
align: 'left',
field: 'created',
label: t('Since'),
name: 'created',
format: (value) => date.formatDate(value, 'DD/MM/YYYY hh:mm:ss'),
},
{
align: 'left',
field: (value) => value.worker.user.name,
label: t('Employee'),
name: 'employee',
},
{
align: 'left',
field: 'amount',
label: t('Credit'),
name: 'amount',
format: (value) => toCurrency(value),
},
]);
const selectWorkerId = (id) => {
workerId.value = id;
};
const toCustomerCreditCreate = () => {
router.push({ name: 'CustomerCreditCreate' });
};
</script>
<template> <template>
<div class="flex justify-center">Credits</div> <QPage class="column items-center q-pa-md">
<QTable
:columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
>
<template #body-cell="props">
<QTd :props="props">
<QTr :props="props" class="cursor-pointer">
<component
:is="tableColumnComponents[props.col.name].component"
@click="tableColumnComponents[props.col.name].event(props)"
class="rounded-borders q-pa-sm"
v-bind="tableColumnComponents[props.col.name].props(props)"
>
{{ props.value }}
<WorkerDescriptorProxy :id="workerId" />
</component>
</QTr>
</QTd>
</template>
</QTable>
</QPage>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerCreditCreate()" color="primary" fab icon="add" />
<QTooltip>
{{ t('New credit') }}
</QTooltip>
</QPageSticky>
</template> </template>
<i18n>
es:
Since: Desde
Employee: Empleado
Credit: Credito
New credit: Nuevo credito
</i18n>

View File

@ -1,3 +1,295 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
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 VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import CustomerCreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue';
import VnSelectCreate from 'src/components/common/VnSelectCreate.vue';
const { t } = useI18n();
const route = useRoute();
const townsFetchDataRef = ref(null);
const postcodeFetchDataRef = ref(null);
const typesTaxes = ref([]);
const typesTransactions = ref([]);
const citiesLocationOptions = ref([]);
const provincesLocationOptions = ref([]);
const countriesOptions = ref([]);
const postcodesOptions = ref([]);
const onPostcodeCreated = async ({ code, provinceFk, townFk, countryFk }, formData) => {
await postcodeFetchDataRef.value.fetch();
await townsFetchDataRef.value.fetch();
formData.postcode = code;
formData.provinceFk = provinceFk;
formData.city = citiesLocationOptions.value.find((town) => town.id === townFk).name;
formData.countryFk = countryFk;
};
</script>
<template> <template>
<div class="flex justify-center">Fiscal data</div> <fetch-data auto-load @on-fetch="(data) => (typesTaxes = data)" url="SageTaxTypes" />
<fetch-data
auto-load
@on-fetch="(data) => (typesTransactions = data)"
url="SageTransactionTypes"
/>
<FetchData
ref="townsFetchDataRef"
@on-fetch="(data) => (citiesLocationOptions = data)"
auto-load
url="Towns/location"
/>
<FetchData
@on-fetch="(data) => (provincesLocationOptions = data)"
auto-load
url="Provinces/location"
/>
<FetchData
@on-fetch="(data) => (countriesOptions = data)"
auto-load
url="Countries"
/>
<FetchData
ref="postcodeFetchDataRef"
url="Postcodes/location"
@on-fetch="(data) => (postcodesOptions = data)"
auto-load
/>
<FormModel
:url-update="`Clients/${route.params.id}/updateFiscalData`"
:url="`Clients/${route.params.id}/getCard`"
auto-load
model="customer"
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Social name')"
:required="true"
:rules="validate('client.socialName')"
v-model="data.socialName"
/>
</div>
<div class="col">
<VnInput :label="t('Tax number')" v-model="data.fi" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput :label="t('Street')" v-model="data.street" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Sage tax type')"
:options="typesTaxes"
hide-selected
option-label="vat"
option-value="id"
v-model="data.sageTaxTypeFk"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('Sage transaction type')"
:options="typesTransactions"
hide-selected
option-label="vat"
option-value="id"
v-model="data.sageTransactionTypeFk"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectCreate
:label="t('Postcode')"
:options="postcodesOptions"
:roles-allowed-to-create="['deliveryAssistant']"
:rules="validate('Worker.postcode')"
hide-selected
option-label="code"
option-value="code"
v-model="data.postcode"
>
<template #form>
<CustomerCreateNewPostcode
@on-data-saved="onPostcodeCreated($event, data)"
/>
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection v-if="scope.opt">
<QItemLabel>{{ scope.opt.code }}</QItemLabel>
<QItemLabel caption
>{{ scope.opt.code }} -
{{ scope.opt.town.name }} ({{
scope.opt.town.province.name
}},
{{
scope.opt.town.province.country.country
}})</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelectCreate>
</div>
<div class="col">
<VnSelectFilter
:label="t('City')"
:options="citiesLocationOptions"
hide-selected
option-label="name"
option-value="name"
v-model="data.city"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt.name }}</QItemLabel>
<QItemLabel caption>
{{
`${scope.opt.name}, ${scope.opt.province.name} (${scope.opt.province.country.country})`
}}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Province')"
:options="provincesLocationOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.provinceFk"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt.name} (${scope.opt.country.country})`
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<VnSelectFilter
:label="t('Country')"
:options="countriesOptions"
hide-selected
option-label="country"
option-value="id"
v-model="data.countryFk"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox :label="t('Active')" v-model="data.isActive" />
</div>
<div class="col">
<QCheckbox :label="t('Frozen')" v-model="data.isFreezed" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox :label="t('Has to invoice')" v-model="data.hasToInvoice" />
</div>
<div class="col">
<QCheckbox :label="t('Vies')" v-model="data.isVies" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox
:label="t('Notify by email')"
v-model="data.isToBeMailed"
/>
</div>
<div class="col">
<QCheckbox
:label="t('Invoice by address')"
v-model="data.hasToInvoiceByAddress"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox
:label="t('Is equalizated')"
v-model="data.isEqualizated"
/>
</div>
<div class="col">
<QCheckbox
:label="t('Verified data')"
v-model="data.isTaxDataChecked"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox
:label="t('Incoterms authorization')"
v-model="data.hasIncoterms"
/>
</div>
<div class="col">
<QCheckbox
:label="t('Electronic invoice')"
v-model="data.hasElectronicInvoice"
/>
</div>
</VnRow>
</template>
</FormModel>
</template> </template>
<i18n>
es:
Social name: Razón social
Tax number: NIF / CIF
Street: Dirección fiscal
Sage tax type: Tipo de impuesto Sage
Sage transaction type: Tipo de transacción Sage
Postcode: Código postal
City: Población
Province: Provincia
Country: País
Active: Activo
Frozen: Congelado
Has to invoice: Factura
Vies: Vies
Notify by email: Notificar vía e-mail
Invoice by address: Facturar por consignatario
Is equalizated: Recargo de equivalencia
Verified data: Datos comprobados
Incoterms authorization: Autorización incoterms
Electronic invoice: Factura electrónica
</i18n>

View File

@ -1,3 +1,96 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { date } from 'quasar';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const noteFilter = {
order: 'created DESC',
where: {
clientFk: `${route.params.id}`,
},
};
const toCustomerNoteCreate = () => {
router.push({ name: 'CustomerNoteCreate' });
};
</script>
<template> <template>
<div class="flex justify-center">Notes</div> <QCard class="q-pa-lg">
<VnPaginate
data-key="CustomerNotes"
:url="'clientObservations'"
auto-load
:filter="noteFilter"
>
<template #body="{ rows }">
<div v-if="rows.length">
<QCard
v-for="(item, index) in rows"
:key="index"
:class="{
'q-pa-md': true,
'q-rounded': true,
'custom-border': true,
'q-mb-md': index < rows.length - 1,
}"
>
<div class="flex justify-between">
<p class="label-color">{{ item.worker.user.nickname }}</p>
<p class="label-color">
{{
date.formatDate(item?.created, 'DD-MM-YYYY HH:mm:ss')
}}
</p>
</div>
<h6 class="q-mt-xs q-mb-none">{{ item.text }}</h6>
</QCard>
</div>
<div v-else>
<h5 class="flex justify-center label-color">
{{ t('globals.noResults') }}
</h5>
</div>
<QPageSticky :offset="[18, 18]">
<QBtn
@click.stop="toCustomerConsigneeCreate()"
color="primary"
fab
icon="add"
/>
<QTooltip>
{{ t('New consignee') }}
</QTooltip>
</QPageSticky>
</template>
</VnPaginate>
</QCard>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerNoteCreate()" color="primary" fab icon="add" />
<QTooltip>
{{ t('New consignee') }}
</QTooltip>
</QPageSticky>
</template> </template>
<style lang="scss">
.custom-border {
border: 2px solid var(--vn-light-gray);
border-radius: 10px;
padding: 10px;
}
.label-color {
color: var(--vn-label);
}
</style>

View File

@ -30,6 +30,7 @@ const newClientForm = reactive({
}); });
const postcodeFetchDataRef = ref(null); const postcodeFetchDataRef = ref(null);
const townsFetchDataRef = ref(null);
const workersOptions = ref([]); const workersOptions = ref([]);
const businessTypesOptions = ref([]); const businessTypesOptions = ref([]);
const citiesLocationOptions = ref([]); const citiesLocationOptions = ref([]);
@ -37,8 +38,13 @@ const provincesLocationOptions = ref([]);
const countriesOptions = ref([]); const countriesOptions = ref([]);
const postcodesOptions = ref([]); const postcodesOptions = ref([]);
const onPostcodeCreated = async () => { const onPostcodeCreated = async ({ code, provinceFk, townFk, countryFk }, formData) => {
postcodeFetchDataRef.value.fetch(); await postcodeFetchDataRef.value.fetch();
await townsFetchDataRef.value.fetch();
formData.postcode = code;
formData.provinceFk = provinceFk;
formData.city = citiesLocationOptions.value.find((town) => town.id === townFk).name;
formData.countryFk = countryFk;
}; };
</script> </script>
@ -60,6 +66,7 @@ const onPostcodeCreated = async () => {
url="BusinessTypes" url="BusinessTypes"
/> />
<FetchData <FetchData
ref="townsFetchDataRef"
@on-fetch="(data) => (citiesLocationOptions = data)" @on-fetch="(data) => (citiesLocationOptions = data)"
auto-load auto-load
url="Towns/location" url="Towns/location"
@ -78,7 +85,6 @@ const onPostcodeCreated = async () => {
<VnSubToolbar /> <VnSubToolbar />
<FormModel <FormModel
:form-initial-data="newClientForm" :form-initial-data="newClientForm"
:observe-form-changes="false"
model="client" model="client"
url-create="Clients/createWithUser" url-create="Clients/createWithUser"
> >
@ -145,7 +151,7 @@ const onPostcodeCreated = async () => {
> >
<template #form> <template #form>
<CustomerCreateNewPostcode <CustomerCreateNewPostcode
@on-data-saved="onPostcodeCreated($event)" @on-data-saved="onPostcodeCreated($event, data)"
/> />
</template> </template>
<template #option="scope"> <template #option="scope">

View File

@ -2,7 +2,7 @@
import { ref, computed, onBeforeMount } from 'vue'; import { ref, computed, onBeforeMount } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { QBtn, QCheckbox } from 'quasar'; import { QBtn, QCheckbox, useQuasar } from 'quasar';
import { toCurrency, toDate } from 'filters/index'; import { toCurrency, toDate } from 'filters/index';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
@ -12,34 +12,19 @@ import CustomerNotificationsFilter from './CustomerDefaulterFilter.vue';
import CustomerBalanceDueTotal from './CustomerBalanceDueTotal.vue'; import CustomerBalanceDueTotal from './CustomerBalanceDueTotal.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDefaulterAddObservation from './CustomerDefaulterAddObservation.vue';
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
const quasar = useQuasar();
const arrayData = ref(null); const arrayData = ref(null);
const balanceDueTotal = ref(0); const balanceDueTotal = ref(0);
const customerId = ref(0);
onBeforeMount(async () => {
arrayData.value = useArrayData('CustomerDefaulter', {
url: 'Defaulters/filter',
limit: 0,
});
await arrayData.value.fetch({ append: false });
balanceDueTotal.value = arrayData.value.store.data.reduce(
(accumulator, currentValue) => {
return accumulator + (currentValue['amount'] || 0);
},
0
);
console.log(balanceDueTotal.value);
stateStore.rightDrawer = true;
});
const rows = computed(() => arrayData.value.store.data);
const selected = ref([]); const selected = ref([]);
const workerId = ref(0); const workerId = ref(0);
const customerId = ref(0);
const rows = computed(() => arrayData.value.store.data);
const tableColumnComponents = { const tableColumnComponents = {
client: { client: {
@ -49,11 +34,10 @@ const tableColumnComponents = {
}, },
isWorker: { isWorker: {
component: QCheckbox, component: QCheckbox,
props: ({ value }) => ({ props: ({ row }) => ({
disable: true, disable: true,
'model-value': Boolean(value), 'model-value': Boolean(row.selected),
}), }),
event: () => {},
}, },
salesperson: { salesperson: {
component: QBtn, component: QBtn,
@ -171,6 +155,25 @@ const columns = computed(() => [
}, },
]); ]);
onBeforeMount(() => {
getArrayData();
});
const getArrayData = async () => {
arrayData.value = useArrayData('CustomerDefaulter', {
url: 'Defaulters/filter',
limit: 0,
});
await arrayData.value.fetch({ append: false });
balanceDueTotal.value = arrayData.value.store.data.reduce(
(accumulator, currentValue) => {
return accumulator + (currentValue['amount'] || 0);
},
0
);
stateStore.rightDrawer = true;
};
const selectCustomerId = (id) => { const selectCustomerId = (id) => {
workerId.value = 0; workerId.value = 0;
customerId.value = id; customerId.value = id;
@ -180,6 +183,20 @@ const selectWorkerId = (id) => {
customerId.value = 0; customerId.value = 0;
workerId.value = id; workerId.value = id;
}; };
const viewAddObservation = (rowsSelected) => {
quasar.dialog({
component: CustomerDefaulterAddObservation,
componentProps: {
clients: rowsSelected,
promise: refreshData,
},
});
};
const refreshData = () => {
getArrayData();
};
</script> </script>
<template> <template>
@ -190,11 +207,17 @@ const selectWorkerId = (id) => {
</QDrawer> </QDrawer>
<QToolbar class="bg-vn-dark"> <QToolbar class="bg-vn-dark">
<div id="st-data"> <div id="st-data" class="row">
<CustomerBalanceDueTotal :amount="balanceDueTotal" /> <CustomerBalanceDueTotal :amount="balanceDueTotal" />
<div class="flex items-center q-ml-lg">
<QBtn
color="primary"
icon="vn:notes"
:disabled="!selected.length"
@click.stop="viewAddObservation(selected)"
/>
</div>
</div> </div>
<QSpace />
<div id="st-actions"></div>
</QToolbar> </QToolbar>
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">
@ -204,7 +227,7 @@ const selectWorkerId = (id) => {
:rows="rows" :rows="rows"
class="full-width q-mt-md" class="full-width q-mt-md"
hide-bottom hide-bottom
row-key="id" row-key="clientFk"
selection="multiple" selection="multiple"
v-model:selected="selected" v-model:selected="selected"
> >

View File

@ -0,0 +1,100 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { useQuasar } from 'quasar';
import VnRow from 'components/ui/VnRow.vue';
const $props = defineProps({
clients: {
type: Array,
required: true,
},
promise: {
type: Function,
required: true,
},
});
const { t } = useI18n();
const quasar = useQuasar();
const newObservation = ref(null);
const onSubmit = async () => {
try {
const data = $props.clients.map((item) => {
return { clientFk: item.clientFk, text: newObservation.value };
});
await axios.post('ClientObservations', data);
const payload = {
defaulters: $props.clients,
observation: newObservation.value,
};
await axios.post('Defaulters/observationEmail', payload);
await $props.promise();
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
} catch (error) {
quasar.notify({
message: t(`${error.message}`),
type: 'negative',
});
}
};
</script>
<template>
<QDialog ref="dialogRef">
<QCard class="q-pa-md q-mb-md">
<QCardSection>
<QForm @submit="onSubmit()" class="q-pa-sm">
<div>
{{
t('Add observation to all selected clients', {
numberClients: t($props.clients.length),
})
}}
</div>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
:label="t('Message')"
type="textarea"
v-model="newObservation"
/>
</div>
</VnRow>
<div class="q-mt-lg row justify-end">
<QBtn
:label="t('globals.cancel')"
color="primary"
flat
class="q-mr-md"
v-close-popup
/>
<QBtn
:label="t('globals.save')"
type="submit"
color="primary"
v-close-popup
/>
</div>
</QForm>
</QCardSection>
</QCard>
</QDialog>
</template>
<i18n>
es:
Add observation to all selected clients: Añadir observación a { numberClients } cliente(s) seleccionado(s)
Message: Mensaje
</i18n>

View File

@ -485,7 +485,6 @@ const selectCustomerId = (id) => {
}; };
const selectSalesPersonId = (id) => { const selectSalesPersonId = (id) => {
console.log('selectedSalesPersonId:: ', selectedSalesPersonId.value);
selectedSalesPersonId.value = id; selectedSalesPersonId.value = id;
}; };
</script> </script>

View File

@ -0,0 +1,265 @@
<script setup>
import { onBeforeMount, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
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 VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelectCreate from 'src/components/common/VnSelectCreate.vue';
import CustomerCreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue';
import CustomsNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCustomsAgent.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const formInitialData = reactive({ isDefaultAddress: false });
const townsFetchDataRef = ref(null);
const postcodeFetchDataRef = ref(null);
const urlCreate = ref('');
const postcodesOptions = ref([]);
const citiesLocationOptions = ref([]);
const provincesLocationOptions = ref([]);
const agencyModes = ref([]);
const incoterms = ref([]);
const customsAgents = ref([]);
onBeforeMount(() => {
urlCreate.value = `Clients/${route.params.id}/createAddress`;
getCustomsAgents();
});
const onPostcodeCreated = async ({ code, provinceFk, townFk }, formData) => {
await postcodeFetchDataRef.value.fetch();
await townsFetchDataRef.value.fetch();
formData.postalCode = code;
formData.provinceFk = provinceFk;
formData.city = citiesLocationOptions.value.find((town) => town.id === townFk).name;
};
const getCustomsAgents = async () => {
const { data } = await axios.get('CustomsAgents');
customsAgents.value = data;
};
const refreshData = () => {
getCustomsAgents();
};
const toCustomerConsignees = () => {
router.push({
name: 'CustomerConsignees',
params: {
id: route.params.id,
},
});
};
</script>
<template>
<FetchData
@on-fetch="(data) => (postcodesOptions = data)"
auto-load
ref="postcodeFetchDataRef"
url="Postcodes/location"
/>
<FetchData
@on-fetch="(data) => (citiesLocationOptions = data)"
auto-load
ref="townsFetchDataRef"
url="Towns/location"
/>
<FetchData
@on-fetch="(data) => (provincesLocationOptions = data)"
auto-load
url="Provinces/location"
/>
<fetch-data
@on-fetch="(data) => (agencyModes = data)"
auto-load
url="AgencyModes/isActive"
/>
<fetch-data @on-fetch="(data) => (incoterms = data)" auto-load url="Incoterms" />
<FormModel
:form-initial-data="formInitialData"
:observe-form-changes="false"
:url-create="urlCreate"
@on-data-saved="toCustomerConsignees()"
model="client"
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox :label="t('Default')" v-model="data.isDefaultAddress" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput :label="t('Consignee')" v-model="data.nickname" />
</div>
<div class="col">
<VnInput :label="t('Street address')" v-model="data.street" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectCreate
:label="t('Postcode')"
:options="postcodesOptions"
:roles-allowed-to-create="['deliveryAssistant']"
:rules="validate('Worker.postcode')"
hide-selected
option-label="code"
option-value="code"
v-model="data.postalCode"
>
<template #form>
<CustomerCreateNewPostcode
@on-data-saved="onPostcodeCreated($event, data)"
/>
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection v-if="scope.opt">
<QItemLabel>{{ scope.opt.code }}</QItemLabel>
<QItemLabel caption>
{{ scope.opt.code }} -
{{ scope.opt.town.name }}
({{ scope.opt.town.province.name }},
{{ scope.opt.town.province.country.country }})
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectCreate>
</div>
<div class="col">
<!-- ciudades -->
<VnSelectFilter
:label="t('City')"
:options="citiesLocationOptions"
hide-selected
option-label="name"
option-value="name"
v-model="data.city"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt.name }}</QItemLabel>
<QItemLabel caption>
{{
`${scope.opt.name}, ${scope.opt.province.name} (${scope.opt.province.country.country})`
}}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Province')"
:options="provincesLocationOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.provinceFk"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt.name} (${scope.opt.country.country})`
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<VnSelectFilter
:label="t('Agency')"
:options="agencyModes"
hide-selected
option-label="name"
option-value="id"
v-model="data.agencyModeFk"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput :label="t('Phone')" v-model="data.phone" />
</div>
<div class="col">
<VnInput :label="t('Mobile')" v-model="data.mobile" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Incoterms')"
:options="incoterms"
hide-selected
option-label="name"
option-value="code"
v-model="data.incotermsFk"
/>
</div>
<div class="col">
<VnSelectCreate
:label="t('Customs agent')"
:options="customsAgents"
hide-selected
option-label="fiscalName"
option-value="id"
v-model="data.customsAgentFk"
>
<template #form>
<CustomsNewCustomsAgent @on-data-saved="refreshData()" />
</template>
</VnSelectCreate>
</div>
</VnRow>
</template>
</FormModel>
</template>
<style lang="scss" scoped>
.add-icon {
cursor: pointer;
background-color: $primary;
border-radius: 50px;
}
</style>
<i18n>
es:
Default: Predeterminado
Consignee: Consignatario
Street address: Dirección postal
Postcode: Código postal
City: Población
Province: Provincia
Agency: Agencia
Phone: Teléfono
Mobile: Movíl
Incoterms: Incoterms
Customs agent: Agente de aduanas
</i18n>

View File

@ -0,0 +1,371 @@
<script setup>
import { onBeforeMount, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import axios from 'axios';
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 VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelectCreate from 'src/components/common/VnSelectCreate.vue';
import CustomerCreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue';
import CustomsNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCustomsAgent.vue';
const { t } = useI18n();
const route = useRoute();
const townsFetchDataRef = ref(null);
const postcodeFetchDataRef = ref(null);
const urlUpdate = ref('');
const postcodesOptions = ref([]);
const citiesLocationOptions = ref([]);
const provincesLocationOptions = ref([]);
const agencyModes = ref([]);
const incoterms = ref([]);
const customsAgents = ref([]);
const observationTypes = ref([]);
const notes = ref([]);
onBeforeMount(() => {
urlUpdate.value = `Clients/${route.params.id}/updateAddress/${route.params.consigneeId}`;
});
const onPostcodeCreated = async ({ code, provinceFk, townFk }, formData) => {
await postcodeFetchDataRef.value.fetch();
await townsFetchDataRef.value.fetch();
formData.postalCode = code;
formData.provinceFk = provinceFk;
formData.city = citiesLocationOptions.value.find((town) => town.id === townFk).name;
};
const getData = async (observations) => {
observationTypes.value = observations;
if (observationTypes.value.length) {
const filter = {
fields: ['id', 'addressFk', 'observationTypeFk', 'description'],
where: { addressFk: `${route.params.consigneeId}` },
};
const { data } = await axios.get('AddressObservations', {
params: { filter: JSON.stringify(filter) },
});
if (data.length) {
notes.value = data
.map((observation) => {
const type = observationTypes.value.find(
(type) => type.id === observation.observationTypeFk
);
return type
? {
$isNew: false,
$oldData: null,
$orgIndex: null,
addressFk: `${route.params.consigneeId}`,
description: observation.description,
observationTypeFk: type.id,
}
: null;
})
.filter((item) => item !== null);
}
}
};
const addNote = () => {
notes.value.push({
$isNew: true,
$oldData: null,
$orgIndex: null,
addressFk: `${route.params.consigneeId}`,
description: '',
observationTypeFk: '',
});
};
const deleteNote = (index) => {
notes.value.splice(index, 1);
};
const onDataSaved = () => {
const payload = {
creates: notes.value,
};
axios.post('AddressObservations/crud', payload);
};
</script>
<template>
<FetchData
ref="postcodeFetchDataRef"
@on-fetch="(data) => (postcodesOptions = data)"
auto-load
url="Postcodes/location"
/>
<FetchData
ref="townsFetchDataRef"
@on-fetch="(data) => (citiesLocationOptions = data)"
auto-load
url="Towns/location"
/>
<FetchData
@on-fetch="(data) => (provincesLocationOptions = data)"
auto-load
url="Provinces/location"
/>
<fetch-data
@on-fetch="(data) => (agencyModes = data)"
auto-load
url="AgencyModes/isActive"
/>
<fetch-data @on-fetch="(data) => (incoterms = data)" auto-load url="Incoterms" />
<fetch-data
@on-fetch="(data) => (customsAgents = data)"
auto-load
url="CustomsAgents"
/>
<fetch-data @on-fetch="getData" auto-load url="ObservationTypes" />
<FormModel
:observe-form-changes="false"
:url-update="urlUpdate"
:url="`Addresses/${route.params.consigneeId}`"
@on-data-saved="onDataSaved()"
auto-load
model="client"
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox :label="t('Enabled')" v-model="data.isActive" />
</div>
<div class="col">
<QCheckbox
:label="t('Is equalizated')"
v-model="data.isEqualizated"
/>
</div>
<div class="col">
<QCheckbox
:label="t('Is Loginflora allowed')"
v-model="data.isLogifloraAllowed"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput :label="t('Consignee')" v-model="data.nickname" />
</div>
<div class="col">
<VnInput :label="t('Street address')" v-model="data.street" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectCreate
:label="t('Postcode')"
:options="postcodesOptions"
:roles-allowed-to-create="['deliveryAssistant']"
:rules="validate('Worker.postcode')"
hide-selected
option-label="code"
option-value="code"
v-model="data.postalCode"
>
<template #form>
<CustomerCreateNewPostcode
@on-data-saved="onPostcodeCreated($event, data)"
/>
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection v-if="scope.opt">
<QItemLabel>{{ scope.opt.code }}</QItemLabel>
<QItemLabel caption>
{{ scope.opt.code }} -
{{ scope.opt.town.name }}
({{ scope.opt.town.province.name }},
{{ scope.opt.town.province.country.country }})
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectCreate>
</div>
<div class="col">
<!-- ciudades -->
<VnSelectFilter
:label="t('City')"
:options="citiesLocationOptions"
hide-selected
option-label="name"
option-value="name"
v-model="data.city"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt.name }}</QItemLabel>
<QItemLabel caption>
{{
`${scope.opt.name}, ${scope.opt.province.name} (${scope.opt.province.country.country})`
}}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Province')"
:options="provincesLocationOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.provinceFk"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt.name} (${scope.opt.country.country})`
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<VnSelectFilter
:label="t('Agency')"
:options="agencyModes"
hide-selected
option-label="name"
option-value="id"
v-model="data.agencyModeFk"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput :label="t('Phone')" v-model="data.phone" />
</div>
<div class="col">
<VnInput :label="t('Mobile')" v-model="data.mobile" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Incoterms')"
:options="incoterms"
hide-selected
option-label="name"
option-value="code"
v-model="data.incotermsFk"
/>
</div>
<div class="col">
<VnSelectCreate
:label="t('Customs agent')"
:options="customsAgents"
hide-selected
option-label="fiscalName"
option-value="id"
v-model="data.customsAgentFk"
>
<template #form>
<CustomsNewCustomsAgent />
</template>
</VnSelectCreate>
</div>
</VnRow>
<h4 class="q-mb-xs">{{ t('Notes') }}</h4>
<VnRow
:key="index"
class="row q-gutter-md q-mb-md"
v-for="(note, index) in notes"
>
<div class="col">
<VnSelectFilter
:label="t('Observation type')"
:options="observationTypes"
hide-selected
option-label="description"
option-value="id"
v-model="note.observationTypeFk"
/>
</div>
<div class="col">
<VnInput :label="t('Description')" v-model="note.description" />
</div>
<div class="flex items-center">
<QIcon
@click.stop="deleteNote(index)"
class="cursor-pointer"
color="primary"
name="delete"
size="sm"
>
<QTooltip>
{{ t('Remove') }}
</QTooltip>
</QIcon>
</div>
</VnRow>
<QIcon
@click.stop="addNote()"
class="cursor-pointer add-icon q-mt-md"
name="add"
size="sm"
>
<QTooltip>
{{ t('Add note') }}
</QTooltip>
</QIcon>
</template>
</FormModel>
</template>
<style lang="scss" scoped>
.add-icon {
background-color: $primary;
border-radius: 50px;
}
</style>
<i18n>
es:
Enabled: Activo
Is equalizated: Recargo de equivalencia
Is Loginflora allowed: Compra directa en Holanda
Consignee: Consignatario
Street address: Dirección postal
Postcode: Código postal
City: Población
Province: Provincia
Agency: Agencia
Phone: Teléfono
Mobile: Movíl
Incoterms: Incoterms
Customs agent: Agente de aduanas
Notes: Notas
Observation type: Tipo de observación
Description: Descripción
Add note: Añadir nota
Remove note: Eliminar nota
</i18n>

View File

@ -0,0 +1,51 @@
<script setup>
import { reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const initialData = reactive({
credit: null,
});
const toCustomerCredits = () => {
router.push({
name: 'CustomerCredits',
params: {
id: route.params.id,
},
});
};
</script>
<template>
<FormModel
:form-initial-data="initialData"
:observe-form-changes="false"
:url-update="`/Clients/${route.params.id}`"
@on-data-saved="toCustomerCredits()"
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
:label="t('Credit')"
type="number"
v-model.number="data.credit"
/>
</div>
</VnRow>
</template>
</FormModel>
</template>
<i18n>
es:
Credit: Crédito
</i18n>

View File

@ -0,0 +1,65 @@
<script setup>
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FormModelPopup from 'src/components/FormModelPopup.vue';
const emit = defineEmits(['onDataSaved']);
const { t } = useI18n();
const initialData = reactive({
nif: null,
fiscalName: null,
street: null,
phone: null,
});
const onDataSaved = (dataSaved) => {
emit('onDataSaved', dataSaved);
};
</script>
<template>
<FormModelPopup
:title="t('New customs agent')"
url-create="CustomsAgents"
model="customer"
:form-initial-data="initialData"
@on-data-saved="onDataSaved($event)"
>
<template #form-inputs="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput :label="t('NIF')" :required="true" v-model="data.nif" />
</div>
<div class="col">
<VnInput
:label="t('Fiscal name')"
:required="true"
v-model="data.fiscalName"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput :label="t('Street')" v-model="data.street" />
</div>
<div class="col">
<VnInput :label="t('Phone')" v-model="data.phone" />
</div>
</VnRow>
</template>
</FormModelPopup>
</template>
<i18n>
es:
New customs agent: Nuevo agente de aduanas
NIF: NIF
Fiscal name: Nombre fiscal
Street: Dirección fiscal
Phone: Teléfono
</i18n>

View File

@ -0,0 +1,51 @@
<script setup>
import { onMounted, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const initialData = reactive({
text: null,
});
onMounted(() => {
initialData.clientFk = `${route.params.id}`;
});
const toCustomerNotes = () => {
router.push({
name: 'CustomerNotes',
params: {
id: route.params.id,
},
});
};
</script>
<template>
<FormModel
:form-initial-data="initialData"
:observe-form-changes="false"
url-create="ClientObservations"
@on-data-saved="toCustomerNotes()"
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput :label="t('Note')" type="textarea" v-model="data.text" />
</div>
</VnRow>
</template>
</FormModel>
</template>
<i18n>
es:
Note: Nota
</i18n>

View File

@ -0,0 +1,202 @@
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
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 VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import { toDate } from 'src/filters';
const route = useRoute();
const { t } = useI18n();
const suppliersOptions = ref([]);
const travelsOptions = ref([]);
const companiesOptions = ref([]);
const currenciesOptions = ref([]);
</script>
<template>
<FetchData
url="Suppliers"
:filter="{ fields: ['id', 'nickname'] }"
order="nickname"
@on-fetch="(data) => (suppliersOptions = data)"
auto-load
/>
<FetchData
url="Travels/filter"
:filter="{ fields: ['id', 'warehouseInName'] }"
order="id"
@on-fetch="(data) => (travelsOptions = data)"
auto-load
/>
<FetchData
ref="companiesRef"
url="Companies"
:filter="{ fields: ['id', 'code'] }"
order="code"
@on-fetch="(data) => (companiesOptions = data)"
auto-load
/>
<FetchData
ref="currenciesRef"
url="Currencies"
:filter="{ fields: ['id', 'code'] }"
order="code"
@on-fetch="(data) => (currenciesOptions = data)"
auto-load
/>
<FormModel
:url="`Entries/${route.params.id}`"
:url-update="`Entries/${route.params.id}`"
model="entry"
auto-load
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('entry.basicData.supplier')"
v-model="data.supplierFk"
:options="suppliersOptions"
option-value="id"
option-label="nickname"
hide-selected
:required="true"
map-options
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption>
{{ scope.opt?.nickname }}, {{ scope.opt?.id }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<VnSelectFilter
:label="t('entry.basicData.travel')"
v-model="data.travelFk"
:options="travelsOptions"
option-value="id"
option-label="warehouseInName"
map-options
hide-selected
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel
>{{ scope.opt?.agencyModeName }} -
{{ scope.opt?.warehouseInName }} ({{
toDate(scope.opt?.shipped)
}}) &#x2192; {{ scope.opt?.warehouseOutName }} ({{
toDate(scope.opt?.landed)
}})</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.reference"
:label="t('entry.basicData.reference')"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.invoiceNumber"
:label="t('entry.basicData.invoiceNumber')"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('entry.basicData.company')"
v-model="data.companyFk"
:options="companiesOptions"
option-value="id"
option-label="code"
map-options
hide-selected
:required="true"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('entry.basicData.currency')"
v-model="data.currencyFk"
:options="currenciesOptions"
option-value="id"
option-label="code"
/>
</div>
<div class="col">
<QInput
:label="t('entry.basicData.commission')"
v-model="data.commission"
type="number"
autofocus
min="0"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
:label="t('entry.basicData.observation')"
type="textarea"
v-model="data.observation"
fill-input
autogrow
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox
v-model="data.isOrdered"
:label="t('entry.basicData.ordered')"
/>
</div>
<div class="col">
<QCheckbox
v-model="data.isConfirmed"
:label="t('entry.basicData.confirmed')"
/>
</div>
<div class="col">
<QCheckbox
v-model="data.isExcludedFromAvailable"
:label="t('entry.basicData.excludedFromAvailable')"
/>
</div>
<div class="col">
<QCheckbox v-model="data.isRaid" :label="t('entry.basicData.raid')" />
</div>
<div class="col">
<QCheckbox
v-model="data.isBooked"
:label="t('entry.basicData.booked')"
/>
</div>
</VnRow>
</template>
</FormModel>
</template>

View File

@ -0,0 +1,409 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import FetchData from 'src/components/FetchData.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency } from 'src/filters';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const quasar = useQuasar();
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const stateStore = useStateStore();
const { notify } = useNotify();
const rowsSelected = ref([]);
const entryBuysPaginateRef = ref(null);
const packagingsOptions = ref(null);
const tableColumnComponents = computed(() => ({
item: {
component: () => 'span',
props: () => {},
event: () => ({}),
},
quantity: {
component: () => VnInput,
props: (col) => ({
type: 'number',
min: 0,
label: col.label,
class: 'input-number',
}),
event: (props) => ({
'keyup.enter': () => saveChange(props.row),
}),
},
packagingFk: {
component: () => VnSelectFilter,
props: () => ({
'option-value': 'id',
'option-label': 'id',
'emit-value': true,
'map-options': true,
'use-input': true,
'hide-selected': true,
options: packagingsOptions.value,
}),
event: (props) => ({
'update:modelValue': () => saveChange(props.row),
}),
},
stickers: {
component: () => VnInput,
props: (col) => ({
type: 'number',
min: 0,
label: col.label,
class: 'input-number',
}),
event: (props) => ({
'keyup.enter': () => saveChange(props.row),
}),
},
weight: {
component: () => VnInput,
props: (col) => ({
type: 'number',
min: 0,
label: col.label,
}),
event: (props) => ({
'keyup.enter': () => saveChange(props.row),
}),
},
packing: {
component: () => VnInput,
props: (col) => ({
type: 'number',
min: 0,
label: col.label,
}),
event: (props) => ({
'keyup.enter': () => saveChange(props.row),
}),
},
grouping: {
component: () => VnInput,
props: (col) => ({
type: 'number',
min: 0,
label: col.label,
}),
event: (props) => ({
'keyup.enter': () => saveChange(props.row),
}),
},
buyingValue: {
component: () => VnInput,
props: (col) => ({
type: 'number',
min: 0,
label: col.label,
}),
event: (props) => ({
'keyup.enter': () => saveChange(props.row),
}),
},
price2: {
component: () => VnInput,
props: (col) => ({
type: 'number',
min: 0,
label: col.label,
}),
event: (props) => ({
'keyup.enter': () => saveChange(props.row),
}),
},
price3: {
component: () => VnInput,
props: (col) => ({
type: 'number',
min: 0,
label: col.label,
}),
event: (props) => ({
'keyup.enter': () => saveChange(props.row),
}),
},
import: {
component: () => 'span',
props: () => {},
event: () => ({}),
},
}));
const entriesTableColumns = computed(() => {
return [
{
label: t('entry.summary.item'),
field: 'id',
name: 'item',
align: 'left',
},
{
label: t('entry.summary.quantity'),
field: 'quantity',
name: 'quantity',
align: 'left',
},
{
label: t('entry.summary.package'),
field: 'packagingFk',
name: 'packagingFk',
align: 'left',
},
{
label: t('entry.summary.stickers'),
field: 'stickers',
name: 'stickers',
align: 'left',
},
{
label: t('entry.summary.weight'),
field: 'weight',
name: 'weight',
align: 'left',
},
{
label: t('entry.summary.packing'),
field: 'packing',
name: 'packing',
align: 'left',
},
{
label: t('entry.summary.grouping'),
field: 'grouping',
name: 'grouping',
align: 'left',
},
{
label: t('entry.summary.buyingValue'),
field: 'buyingValue',
name: 'buyingValue',
align: 'left',
format: (value) => toCurrency(value),
},
{
label: t('entry.buys.groupingPrice'),
field: 'price2',
name: 'price2',
align: 'left',
},
{
label: t('entry.buys.packingPrice'),
field: 'price3',
name: 'price3',
align: 'left',
},
{
label: t('entry.summary.import'),
name: 'import',
align: 'left',
format: (_, row) => toCurrency(row.buyingValue * row.quantity),
},
];
});
const saveChange = async (rowData) => {
await axios.patch(`Buys/${rowData.id}`, rowData);
};
const openRemoveDialog = async () => {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('Confirm deletion'),
message: t(
`Are you sure you want to delete this buy${
rowsSelected.value.length > 1 ? 's' : ''
}?`
),
data: rowsSelected.value,
},
})
.onOk(async () => {
try {
await deleteBuys();
const notifyMessage = t(
`Buy${rowsSelected.value.length > 1 ? 's' : ''} deleted`
);
notify(notifyMessage, 'positive');
} catch (err) {
console.error('Error deleting buys');
}
});
};
const deleteBuys = async () => {
await axios.post('Buys/deleteBuys', { buys: rowsSelected.value });
entryBuysPaginateRef.value.fetch();
};
const importBuys = () => {
router.push({ name: 'EntryBuysImport' });
};
</script>
<template>
<FetchData
ref="expensesRef"
url="Packagings"
:filter="{ fields: ['id'], where: { freightItemFk: true }, order: 'id ASC' }"
auto-load
@on-fetch="(data) => (packagingsOptions = data)"
/>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<QBtnGroup push style="column-gap: 10px">
<slot name="moreBeforeActions" />
<QBtn
:label="t('globals.remove')"
color="primary"
icon="delete"
flat
@click="openRemoveDialog()"
:disable="!rowsSelected?.length"
:title="t('globals.remove')"
/>
</QBtnGroup>
</Teleport>
<VnPaginate
ref="entryBuysPaginateRef"
data-key="EntryBuys"
:url="`Entries/${route.params.id}/getBuys`"
auto-load
>
<template #body="{ rows }">
<QTable
:rows="rows"
:columns="entriesTableColumns"
selection="multiple"
row-key="id"
hide-bottom
class="full-width q-mt-md"
:grid="$q.screen.lt.md"
v-model:selected="rowsSelected"
>
<template #body="props">
<QTr>
<QTd>
<QCheckbox v-model="props.selected" />
</QTd>
<QTd v-for="col in props.cols" :key="col.name">
<component
:is="tableColumnComponents[col.name].component()"
v-bind="tableColumnComponents[col.name].props(col)"
v-model="props.row[col.field]"
v-on="tableColumnComponents[col.name].event(props)"
>
<template
v-if="col.name === 'item' || col.name === 'import'"
>
{{ col.value }}
</template>
</component>
</QTd>
</QTr>
<QTr no-hover>
<QTd />
<QTd>
<span>{{ props.row.item.itemType.code }}</span>
</QTd>
<QTd>
<span>{{ props.row.item.id }}</span>
</QTd>
<QTd>
<span>{{ props.row.item.size }}</span>
</QTd>
<QTd>
<span>{{ toCurrency(props.row.item.minPrice) }}</span>
</QTd>
<QTd colspan="7">
<span>{{ props.row.item.concept }}</span>
<span v-if="props.row.item.subName" class="subName">
{{ props.row.item.subName }}
</span>
<fetched-tags :item="props.row.item" :max-length="5" />
</QTd>
</QTr>
<!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys -->
<QTr v-if="props.rowIndex !== rows.length - 1" class="separation-row">
<QTd colspan="12" style="height: 24px" />
</QTr>
</template>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard bordered flat>
<QCardSection>
<QCheckbox v-model="props.selected" dense />
</QCardSection>
<QSeparator />
<QList dense>
<QItem v-for="col in props.cols" :key="col.name">
<component
:is="tableColumnComponents[col.name].component()"
v-bind="
tableColumnComponents[col.name].props(col)
"
v-model="props.row[col.field]"
v-on="
tableColumnComponents[col.name].event(props)
"
class="full-width"
>
<template
v-if="
col.name === 'item' ||
col.name === 'import'
"
>
{{ col.label + ': ' + col.value }}
</template>
</component>
</QItem>
</QList>
</QCard>
</div>
</template>
</QTable>
</template>
</VnPaginate>
<QPageSticky :offset="[20, 20]">
<QBtn fab icon="upload" color="primary" @click="importBuys()" />
<QTooltip class="text-no-wrap">
{{ t('Import buys') }}
</QTooltip>
</QPageSticky>
</template>
<style lang="scss" scoped>
.separation-row {
background-color: var(--vn-gray) !important;
}
.grid-style-transition {
transition: transform 0.28s, background-color 0.28s;
}
</style>
<i18n>
es:
Import buys: Importar compras
Buy deleted: Compra eliminada
Buys deleted: Compras eliminadas
Confirm deletion: Confirmar eliminación
Are you sure you want to delete this buy?: Seguro que quieres eliminar esta compra?
Are you sure you want to delete this buys?: Seguro que quieres eliminar estas compras?
</i18n>

View File

@ -0,0 +1,282 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import VnInput from 'src/components/common/VnInput.vue';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import { useStateStore } from 'stores/useStateStore';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import { toCurrency } from 'filters/index';
const stateStore = useStateStore();
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const { notify } = useNotify();
const importData = ref({
file: null,
invoice: null,
buys: [],
observation: null,
ref: null,
});
const lastItemBuysOptions = ref([]);
const packagingsOptions = ref([]);
const columns = computed(() => [
{
label: t('entry.buys.item'),
name: 'item',
field: 'itemFk',
options: lastItemBuysOptions.value,
optionValue: 'id',
optionLabel: 'name',
align: 'left',
},
{
label: t('entry.buys.description'),
name: 'description',
field: 'description',
align: 'left',
},
{
label: t('entry.buys.size'),
name: 'size',
field: 'size',
align: 'left',
},
{
label: t('entry.buys.packing'),
name: 'packing',
field: 'packing',
align: 'left',
},
{
label: t('entry.buys.grouping'),
name: 'grouping',
field: 'grouping',
align: 'left',
},
{
label: t('entry.buys.buyingValue'),
name: 'buyingValue',
field: 'buyingValue',
align: 'left',
format: (val) => toCurrency(val),
},
{
label: t('entry.buys.packagingFk'),
name: 'packagingFk',
field: 'packagingFk',
options: packagingsOptions.value,
optionValue: 'id',
optionLabel: 'id',
align: 'left',
},
]);
const onFileChange = (e) => {
importData.value.file = e;
const reader = new FileReader();
reader.onload = (e) => fillData(e.target.result);
reader.readAsText(importData.value.file);
};
const fillData = async (rawData) => {
const data = JSON.parse(rawData);
const [invoice] = data.invoices;
importData.value.observation = invoice.tx_awb;
const companyName = invoice.tx_company;
const boxes = invoice.boxes;
const buys = [];
for (let box of boxes) {
const boxVolume = box.nu_length * box.nu_width * box.nu_height;
for (let product of box.products) {
const packing = product.nu_stems_bunch * product.nu_bunches;
buys.push({
description: product.nm_product,
companyName: companyName,
size: product.nu_length,
packing: packing,
grouping: product.nu_stems_bunch,
buyingValue: parseFloat(product.mny_rate_stem),
volume: boxVolume,
});
}
}
const boxesId = boxes.map((box) => box.id_box);
importData.value.ref = boxesId.join(', ');
await fetchBuys(buys);
};
const fetchBuys = async (buys) => {
try {
const params = { buys };
const { data } = await axios.post(
`Entries/${route.params.id}/importBuysPreview`,
params
);
importData.value.buys = data;
} catch (err) {
console.error('Error fetching buys');
}
};
const onSubmit = async () => {
try {
const params = importData.value;
const hasAnyEmptyRow = params.buys.some((buy) => {
return buy.itemFk === null;
});
if (hasAnyEmptyRow) {
notify(t('Some of the imported buys does not have an item'), 'negative');
return;
}
await axios.post(`Entries/${route.params.id}/importBuys`, params);
notify('globals.dataSaved', 'positive');
redirectToBuysView();
} catch (err) {
console.error('Error importing buys', err);
}
};
const redirectToBuysView = () => {
router.push({ name: 'EntryBuys' });
};
</script>
<template>
<FetchData
:url="`Entries/${route.params.id}/lastItemBuys`"
:filter="{ fields: ['id', 'name'] }"
order="id DESC"
@on-fetch="(data) => (lastItemBuysOptions = data)"
auto-load
/>
<FetchData
url="Packagings"
:filter="{ fields: ['id'], where: { isBox: true } }"
order="id ASC"
@on-fetch="(data) => (packagingsOptions = data)"
auto-load
/>
<QForm>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<div>
<QBtnGroup push class="q-gutter-x-sm">
<QBtn
:label="t('globals.cancel')"
color="primary"
icon="restart_alt"
flat
@click="redirectToBuysView()"
/>
<QBtn
:label="t('globals.save')"
color="primary"
icon="save"
type="submit"
:disable="!importData.file"
@click="onSubmit()"
/>
</QBtnGroup>
</div>
</Teleport>
<QCard class="q-pa-lg">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QFile
:label="t('entry.buys.file')"
:multiple="false"
v-model="importData.file"
@update:model-value="onFileChange($event)"
class="required"
>
<template #append>
<QIcon name="vn:attach" class="cursor-pointer">
<QTooltip>{{ t('Select a file') }}</QTooltip>
</QIcon>
</template>
</QFile>
</div>
</VnRow>
<div v-if="importData.file">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('entry.buys.reference')"
v-model="importData.ref"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('entry.buys.observations')"
v-model="importData.observation"
/>
</div>
</VnRow>
<VnRow>
<QTable
:columns="columns"
:rows="importData.buys"
:pagination="{ rowsPerPage: 0 }"
hide-pagination
>
<template #body-cell-item="{ row, col }">
<QTd auto-width>
<VnSelectFilter
v-model="row[col.field]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
hide-selected
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.id }} -
{{ scope.opt?.name }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QTd>
</template>
<template #body-cell-packagingFk="{ row, col }">
<QTd auto-width>
<VnSelectFilter
v-model="row[col.field]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
hide-selected
/>
</QTd>
</template>
</QTable>
</VnRow>
</div>
</QCard>
</QForm>
</template>
<i18n>
es:
Select a file: Selecciona un fichero
Some of the imported buys does not have an item: Algunas de las compras importadas no tienen un artículo
</i18n>

View File

@ -1,15 +1,29 @@
<script setup> <script setup>
import { useStateStore } from 'stores/useStateStore'; import { useI18n } from 'vue-i18n';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import LeftMenu from 'components/LeftMenu.vue'; import LeftMenu from 'components/LeftMenu.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import EntryDescriptor from './EntryDescriptor.vue';
import { useStateStore } from 'stores/useStateStore';
const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
</script> </script>
<template> <template>
<!-- Entry searchbar --> <template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="EntryList"
:label="t('Search entries')"
:info="t('You can search by entry reference')"
/>
</Teleport>
</template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256"> <QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit"> <QScrollArea class="fit">
<!-- EntryDescriptor --> <EntryDescriptor />
<QSeparator /> <QSeparator />
<LeftMenu source="card" /> <LeftMenu source="card" />
</QScrollArea> </QScrollArea>
@ -22,3 +36,9 @@ const stateStore = useStateStore();
</QPage> </QPage>
</QPageContainer> </QPageContainer>
</template> </template>
<i18n>
es:
Search entries: Buscar entradas
You can search by entry reference: Puedes buscar por referencia de la entrada
</i18n>

View File

@ -0,0 +1,202 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
import { toDate } from 'src/filters';
import { usePrintService } from 'composables/usePrintService';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
summary: {
type: Object,
default: null,
},
});
const route = useRoute();
const { t } = useI18n();
const { openReport } = usePrintService();
const entryFilter = {
include: [
{
relation: 'travel',
scope: {
fields: ['id', 'landed', 'agencyModeFk', 'warehouseOutFk'],
include: [
{
relation: 'agency',
scope: {
fields: ['name'],
},
},
{
relation: 'warehouseOut',
scope: {
fields: ['name'],
},
},
{
relation: 'warehouseIn',
scope: {
fields: ['name'],
},
},
],
},
},
{
relation: 'supplier',
scope: {
fields: ['id', 'nickname'],
},
},
],
};
const entityId = computed(() => {
return $props.id || route.params.id;
});
const data = ref(useCardDescription());
const setData = (entity) =>
(data.value = useCardDescription(entity.supplier.nickname, entity.id));
const getEntryRedirectionFilter = (entry) => {
let entryTravel = entry && entry.travel;
if (!entryTravel || !entryTravel.landed) return null;
const date = new Date(entryTravel.landed);
date.setHours(0, 0, 0, 0);
const from = new Date(date.getTime());
from.setDate(from.getDate() - 10);
const to = new Date(date.getTime());
to.setDate(to.getDate() + 10);
return JSON.stringify({
supplierFk: entry.supplierFk,
from,
to,
});
};
const showEntryReport = () => {
openReport(`Entries/${route.params.id}/entry-order-pdf`);
};
</script>
<template>
<CardDescriptor
module="Entry"
:url="`Entries/${entityId}`"
:filter="entryFilter"
:title="data.title"
:subtitle="data.subtitle"
@on-fetch="setData"
data-key="entryData"
>
<template #menu="{ entity }">
<QItem v-ripple clickable @click="showEntryReport(entity)">
<QItemSection>{{ t('Show entry report') }}</QItemSection>
</QItem>
<QItem v-ripple clickable>
<QItemSection>
<RouterLink :to="{ name: 'EntryList' }" class="color-vn-text">
{{ t('Go to module index') }}
</RouterLink>
</QItemSection>
</QItem>
</template>
<template #body="{ entity }">
<VnLv
:label="t('entry.descriptor.agency')"
:value="entity.travel.agency.name"
/>
<VnLv
:label="t('entry.descriptor.landed')"
:value="toDate(entity.travel.landed)"
/>
<VnLv
:label="t('entry.descriptor.warehouseOut')"
:value="entity.travel.warehouseOut.name"
/>
</template>
<template #icons="{ entity }">
<QCardActions class="q-gutter-x-md">
<QIcon
v-if="entity.isExcludedFromAvailable"
name="vn:inventory"
color="primary"
size="xs"
>
<QTooltip>{{ t('Inventory entry') }}</QTooltip>
</QIcon>
<QIcon v-if="entity.isRaid" name="vn:web" color="primary" size="xs">
<QTooltip>{{ t('Virtual entry') }}</QTooltip>
</QIcon>
</QCardActions>
</template>
<template #actions="{ entity }">
<QCardActions>
<QBtn
:to="`/supplier/${entity.supplier.id}`"
size="md"
icon="vn:supplier"
color="primary"
>
<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',
query: {
params: getEntryRedirectionFilter(entity),
},
}"
size="md"
icon="vn:entry"
color="primary"
>
<QTooltip>{{ t('All entries with current supplier') }}</QTooltip>
</QBtn>
</QCardActions>
</template>
</CardDescriptor>
</template>
<i18n>
es:
Supplier card: Ficha del proveedor
All travels with current agency: Todos los envíos con la agencia actual
All entries with current supplier: Todas las entradas con el proveedor actual
Show entry report: Ver informe del pedido
Go to module index: Ir al índice del modulo
Inventory entry: Es inventario
Virtual entry: Es una redada
</i18n>

View File

@ -0,0 +1,16 @@
<script setup>
import EntryDescriptor from './EntryDescriptor.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
</script>
<template>
<QPopupProxy>
<EntryDescriptor v-if="$props.id" :id="$props.id" />
</QPopupProxy>
</template>

View File

@ -0,0 +1,6 @@
<script setup>
import VnLog from 'src/components/common/VnLog.vue';
</script>
<template>
<VnLog model="Entry" url="/EntryLogs"></VnLog>
</template>

View File

@ -0,0 +1,99 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import CrudModel from 'components/CrudModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
const route = useRoute();
const { t } = useI18n();
const entryObservationsRef = ref(null);
const entryObservationsOptions = ref([]);
onMounted(() => {
if (entryObservationsRef.value) entryObservationsRef.value.reload();
});
</script>
<template>
<FetchData
url="ObservationTypes"
@on-fetch="(data) => (entryObservationsOptions = data)"
auto-load
/>
<CrudModel
data-key="EntryAccount"
url="EntryObservations"
model="EntryAccount"
:filter="{
fields: ['id', 'entryFk', 'observationTypeFk', 'description'],
where: { entryFk: route.params.id },
}"
ref="entryObservationsRef"
:default-remove="false"
:data-required="{ entryFk: route.params.id }"
>
<template #body="{ rows }">
<QCard class="q-pa-md">
<VnRow
v-for="(row, index) in rows"
:key="index"
class="row q-gutter-md q-mb-md"
>
<div class="col-3">
<VnSelectFilter
:label="t('entry.notes.observationType')"
v-model="row.observationTypeFk"
:options="entryObservationsOptions"
option-label="description"
option-value="id"
hide-selected
/>
</div>
<div class="col">
<VnInput
:label="t('entry.notes.description')"
v-model="row.description"
/>
</div>
<div class="col-1 row justify-center items-center">
<QIcon
name="delete"
size="sm"
class="cursor-pointer"
color="primary"
@click="entryObservationsRef.remove([row])"
>
<QTooltip>
{{ t('Remove note') }}
</QTooltip>
</QIcon>
</div>
</VnRow>
<VnRow>
<QIcon
name="add"
size="sm"
class="cursor-pointer"
color="primary"
@click="entryObservationsRef.insert()"
>
<QTooltip>
{{ t('Add note') }}
</QTooltip>
</QIcon>
</VnRow>
</QCard>
</template>
</CrudModel>
</template>
<i18n>
es:
Add note: Añadir nota
Remove note: Quitar nota
</i18n>

View File

@ -0,0 +1,359 @@
<script setup>
import { onMounted, ref, computed, onUpdated } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import VnRow from 'components/ui/VnRow.vue';
import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
import { toDate, toCurrency } from 'src/filters';
import { getUrl } from 'src/composables/getUrl';
import axios from 'axios';
onUpdated(() => summaryRef.value.fetch());
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const entityId = computed(() => $props.id || route.params.id);
const summaryRef = ref();
const entry = ref();
const entryBuys = ref([]);
const entryUrl = ref();
onMounted(async () => {
entryUrl.value = (await getUrl('entry/')) + entityId.value;
});
const tableColumnComponents = {
quantity: {
component: () => 'span',
},
stickers: {
component: () => 'span',
},
packagingFk: {
component: () => 'span',
},
weight: {
component: () => 'span',
},
packing: {
component: () => 'span',
},
grouping: {
component: () => 'span',
},
buyingValue: {
component: () => 'span',
},
amount: {
component: () => 'span',
},
pvp: {
component: () => 'span',
},
};
const entriesTableColumns = computed(() => {
return [
{
label: t('entry.summary.quantity'),
field: 'quantity',
name: 'quantity',
align: 'left',
},
{
label: t('entry.summary.stickers'),
field: 'stickers',
name: 'stickers',
align: 'left',
},
{
label: t('entry.summary.package'),
field: 'packagingFk',
name: 'packagingFk',
align: 'left',
},
{
label: t('entry.summary.weight'),
field: 'weight',
name: 'weight',
align: 'left',
},
{
label: t('entry.summary.packing'),
field: 'packing',
name: 'packing',
align: 'left',
},
{
label: t('entry.summary.grouping'),
field: 'grouping',
name: 'grouping',
align: 'left',
},
{
label: t('entry.summary.buyingValue'),
field: 'buyingValue',
name: 'buyingValue',
align: 'left',
format: (value) => toCurrency(value),
},
{
label: t('entry.summary.import'),
name: 'amount',
align: 'left',
format: (_, row) => toCurrency(row.buyingValue * row.quantity),
},
{
label: t('entry.summary.pvp'),
name: 'pvp',
align: 'left',
format: (_, row) => toCurrency(row.price2) + ' / ' + toCurrency(row.price3),
},
];
});
async function setEntryData(data) {
if (data) entry.value = data;
await fetchEntryBuys();
}
const fetchEntryBuys = async () => {
try {
const { data } = await axios.get(`Entries/${entry.value.id}/getBuys`);
if (data) entryBuys.value = data;
} catch (err) {
console.error('Error fetching entry buys');
}
};
</script>
<template>
<CardSummary
ref="summaryRef"
:url="`Entries/${entityId}/getEntry`"
@on-fetch="(data) => setEntryData(data)"
>
<template #header-left>
<a class="header link" :href="entryUrl">
<QIcon name="open_in_new" color="white" size="sm" />
</a>
</template>
<template #header>
<span>{{ entry.id }} - {{ entry.supplier.nickname }}</span>
</template>
<template #body>
<QCard class="vn-one">
<a class="header link" :href="entryUrl">
{{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" color="primary" />
</a>
<VnRow>
<div class="col">
<VnLv
:label="t('entry.summary.commission')"
:value="entry.commission"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.currency')"
:value="entry.currency.name"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.company')"
:value="entry.company.code"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.reference')"
:value="entry.reference"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.invoiceNumber')"
:value="entry.invoiceNumber"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.ordered')"
:value="entry.isOrdered"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.confirmed')"
:value="entry.isConfirmed"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.booked')"
:value="entry.isBooked"
/>
</div>
<div class="col">
<VnLv :label="t('entry.summary.raid')" :value="entry.isRaid" />
</div>
<div class="col">
<VnLv
:label="t('entry.summary.excludedFromAvailable')"
:value="entry.isExcludedFromAvailable"
/>
</div>
</VnRow>
</QCard>
<QCard class="vn-one">
<a class="header link" :href="entryUrl">
{{ t('Travel data') }}
<QIcon name="open_in_new" color="primary" />
</a>
<VnRow>
<div class="col">
<VnLv :label="t('entry.summary.travelReference')">
<template #value>
<span class="link">
{{ entry.travel.ref }}
<TravelDescriptorProxy :id="entry.travel.id" />
</span>
</template>
</VnLv>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelAgency')"
:value="entry.travel.agency.name"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelShipped')"
:value="toDate(entry.travel.shipped)"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelWarehouseOut')"
:value="entry.travel.warehouseOut.name"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelDelivered')"
:value="entry.travel.isDelivered"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelLanded')"
:value="toDate(entry.travel.landed)"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelWarehouseIn')"
:value="entry.travel.warehouseIn.name"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelReceived')"
:value="entry.travel.isReceived"
/>
</div>
</VnRow>
</QCard>
<QCard class="vn-two" style="min-width: 100%">
<a class="header">
{{ t('entry.summary.buys') }}
</a>
<QTable
:rows="entryBuys"
:columns="entriesTableColumns"
hide-bottom
row-key="index"
class="full-width q-mt-md"
>
<template #body="{ cols, row, rowIndex }">
<QTr no-hover>
<QTd v-for="col in cols" :key="col.name">
<component
:is="tableColumnComponents[col.name].component()"
>
<template
v-if="
col.name !== 'observation' &&
col.name !== 'isConfirmed'
"
>{{ col.value }}</template
>
<QTooltip v-if="col.toolTip">{{
col.toolTip
}}</QTooltip>
</component>
</QTd>
</QTr>
<QTr no-hover>
<QTd>
<span>{{ row.item.itemType.code }}</span>
</QTd>
<QTd>
<span>{{ row.item.id }}</span>
</QTd>
<QTd>
<span>{{ row.item.size }}</span>
</QTd>
<QTd>
<span>{{ toCurrency(row.item.minPrice) }}</span>
</QTd>
<QTd colspan="6">
<span>{{ row.item.concept }}</span>
<span v-if="row.item.subName" class="subName">
{{ row.item.subName }}
</span>
<fetched-tags :item="row.item" :max-length="5" />
</QTd>
</QTr>
<!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys -->
<QTr
v-if="rowIndex !== entryBuys.length - 1"
class="separation-row"
>
<QTd colspan="10" style="height: 24px" />
</QTr>
</template>
</QTable>
</QCard>
</template>
</CardSummary>
</template>
<style lang="scss" scoped>
.separation-row {
background-color: var(--vn-gray) !important;
}
</style>
<i18n>
es:
Travel data: 'Datos envío'
</i18n>

View File

@ -0,0 +1,29 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import EntrySummary from './EntrySummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
</script>
<template>
<QDialog ref="dialogRef" @hide="onDialogHide">
<EntrySummary v-if="$props.id" :id="$props.id" />
</QDialog>
</template>
<style lang="scss">
.q-dialog .summary .header {
position: sticky;
z-index: $z-max;
top: 0;
}
</style>

View File

@ -0,0 +1,246 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FetchData from 'components/FetchData.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const currenciesOptions = ref([]);
const companiesOptions = ref([]);
const suppliersOptions = ref([]);
</script>
<template>
<FetchData
ref="companiesRef"
url="Companies"
:filter="{ fields: ['id', 'code'] }"
order="code"
@on-fetch="(data) => (companiesOptions = data)"
auto-load
/>
<FetchData
ref="currenciesRef"
url="Currencies"
:filter="{ fields: ['id', 'name'] }"
order="code"
@on-fetch="(data) => (currenciesOptions = data)"
auto-load
/>
<FetchData
url="Suppliers"
:filter="{ fields: ['id', 'nickname', 'name'] }"
order="nickname"
@on-fetch="(data) => (suppliersOptions = data)"
auto-load
/>
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params }">
<QList dense class="list q-gutter-y-sm q-mt-sm">
<QItem>
<QItemSection>
<VnInput
v-model="params.search"
:label="t('params.search')"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.reference"
:label="t('params.reference')"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.invoiceNumber"
:label="t('params.invoiceNumber')"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.travelFk"
:label="t('params.travelFk')"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelectFilter
:label="t('params.companyFk')"
v-model="params.companyFk"
:options="companiesOptions"
option-value="id"
option-label="code"
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelectFilter
:label="t('params.currencyFk')"
v-model="params.currencyFk"
:options="currenciesOptions"
option-value="id"
option-label="name"
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelectFilter
:label="t('params.supplierFk')"
v-model="params.supplierFk"
:options="suppliersOptions"
option-value="id"
option-label="name"
hide-selected
dense
outlined
rounded
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
scope.opt?.name + ': ' + scope.opt?.nickname
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
:label="t('params.created')"
is-outlined
v-model="params.created"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
:label="t('params.from')"
is-outlined
v-model="params.from"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
:label="t('params.to')"
is-outlined
v-model="params.to"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
:label="t('params.isBooked')"
v-model="params.isBooked"
toggle-indeterminate
/>
</QItemSection>
<QItemSection>
<QCheckbox
:label="t('params.isConfirmed')"
v-model="params.isConfirmed"
toggle-indeterminate
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
:label="t('params.isOrdered')"
v-model="params.isOrdered"
toggle-indeterminate
/>
</QItemSection>
</QItem>
</QList>
</template>
</VnFilterPanel>
</template>
<style scoped>
.list {
width: 256px;
}
.list * {
max-width: 100%;
}
</style>
<i18n>
en:
params:
search: General search
reference: Reference
invoiceNumber: Invoice number
travelFk: Travel
companyFk: Company
currencyFk: Currency
supplierFk: Supplier
from: From
to: To
created: Created
isBooked: Booked
isConfirmed: Confirmed
isOrdered: Ordered
es:
params:
search: Búsqueda general
reference: Referencia
invoiceNumber: Núm. factura
travelFk: Envío
companyFk: Empresa
currencyFk: Moneda
supplierFk: Proveedor
from: Desde
to: Hasta
created: Fecha creación
isBooked: Asentado
isConfirmed: Confirmado
isOrdered: Pedida
</i18n>

View File

@ -1,22 +1,144 @@
<script setup> <script setup>
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
import EntrySummaryDialog from './Card/EntrySummaryDialog.vue';
import EntryFilter from './EntryFilter.vue';
import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'src/filters/index';
const stateStore = useStateStore();
const router = useRouter(); const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
function navigate(id) {
router.push({ path: `/entry/${id}` });
}
const redirectToCreateView = () => { const redirectToCreateView = () => {
router.push({ name: 'EntryCreate' }); router.push({ name: 'EntryCreate' });
}; };
function viewSummary(id) {
quasar.dialog({
component: EntrySummaryDialog,
componentProps: {
id,
},
});
}
onMounted(async () => {
stateStore.rightDrawer = true;
});
</script> </script>
<template> <template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<EntryFilter data-key="EntryList" />
</QScrollArea>
</QDrawer>
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">
<QPageSticky :offset="[20, 20]"> <div class="card-list">
<QBtn fab icon="add" color="primary" @click="redirectToCreateView()" /> <VnPaginate
<QTooltip> data-key="EntryList"
{{ t('entry.list.newEntry') }} url="Entries/filter"
</QTooltip> order="landed DESC, id DESC"
</QPageSticky> auto-load
>
<template #body="{ rows }">
<CardList
v-for="row of rows"
:key="row.id"
:title="row.reference"
@click="navigate(row.id)"
:id="row.id"
:has-info-icons="!!row.isExcludedFromAvailable || !!row.isRaid"
>
<template #info-icons>
<QIcon
v-if="row.isExcludedFromAvailable"
name="vn:inventory"
color="primary"
size="xs"
>
<QTooltip>{{ t('Inventory entry') }}</QTooltip>
</QIcon>
<QIcon
v-if="row.isRaid"
name="vn:web"
color="primary"
size="xs"
>
<QTooltip>{{ t('Virtual entry') }}</QTooltip>
</QIcon>
</template>
<template #list-items>
<VnLv
:label="t('entry.list.landed')"
:value="toDate(row.landed)"
/>
<VnLv
:label="t('entry.list.booked')"
:value="!!row.isBooked"
/>
<VnLv
:label="t('entry.list.invoiceNumber')"
:value="row.invoiceNumber"
/>
<VnLv
:label="t('entry.list.confirmed')"
:value="!!row.isConfirmed"
/>
<VnLv
:label="t('entry.list.supplier')"
:value="row.supplierName"
/>
<VnLv
:label="t('entry.list.ordered')"
:value="!!row.isOrdered"
/>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id)"
color="primary"
type="submit"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
</QPage> </QPage>
<QPageSticky :offset="[20, 20]">
<QBtn fab icon="add" color="primary" @click="redirectToCreateView()" />
<QTooltip>
{{ t('entry.list.newEntry') }}
</QTooltip>
</QPageSticky>
</template> </template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
</style>
<i18n>
es:
Search entries: Buscar entradas
You can search by entry reference: Puedes buscar por referencia de la entrada
Inventory entry: Es inventario
Virtual entry: Es una redada
</i18n>

View File

@ -1,11 +1,24 @@
<script setup> <script setup>
import { useStateStore } from 'stores/useStateStore'; import { useI18n } from 'vue-i18n';
import LeftMenu from 'src/components/LeftMenu.vue'; import LeftMenu from 'src/components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore';
const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="EntryList"
:label="t('Search entries')"
:info="t('You can search by entry reference')"
/>
</Teleport>
</template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256"> <QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit text-grey-8"> <QScrollArea class="fit text-grey-8">
<LeftMenu /> <LeftMenu />
@ -15,3 +28,9 @@ const stateStore = useStateStore();
<RouterView></RouterView> <RouterView></RouterView>
</QPageContainer> </QPageContainer>
</template> </template>
<i18n>
es:
Search entries: Buscar entradas
You can search by entry reference: Puedes buscar por referencia de la entrada
</i18n>

View File

@ -0,0 +1,22 @@
<script setup>
import { useStateStore } from 'stores/useStateStore';
import LeftMenu from 'components/LeftMenu.vue';
import RouteDescriptor from 'pages/Route/Card/RouteDescriptor.vue';
// import ShelvingDescriptor from 'pages/Shelving/Card/ShelvingDescriptor.vue';
const stateStore = useStateStore();
</script>
<template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<RouteDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<RouterView></RouterView>
</QPage>
</QPageContainer>
</template>

View File

@ -0,0 +1,104 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'components/ui/VnLv.vue';
import useCardDescription from 'composables/useCardDescription';
import { dashIfEmpty, toDate } from 'src/filters';
import RouteDescriptorMenu from 'pages/Route/Card/RouteDescriptorMenu.vue';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const route = useRoute();
const { t } = useI18n();
const entityId = computed(() => {
return $props.id || route.params.id;
});
const filter = {
fields: [
'id',
'workerFk',
'agencyModeFk',
'created',
'm3',
'warehouseFk',
'description',
'vehicleFk',
'kmStart',
'kmEnd',
'started',
'finished',
'cost',
'zoneFk',
'isOk',
],
include: [
{ relation: 'agencyMode', scope: { fields: ['id', 'name'] } },
{
relation: 'vehicle',
scope: { fields: ['id', 'm3'] },
},
{ relation: 'zone', scope: { fields: ['id', 'name'] } },
{
relation: 'worker',
scope: {
fields: ['id'],
include: {
relation: 'user',
scope: {
fields: ['id'],
include: { relation: 'emailUser', scope: { fields: ['email'] } },
},
},
},
},
],
};
const data = ref(useCardDescription());
const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id));
</script>
<template>
<CardDescriptor
module="Route"
:url="`Routes/${entityId}`"
:filter="filter"
:title="data.title"
:subtitle="data.subtitle"
data-key="Routes"
@on-fetch="setData"
>
<template #body="{ entity }">
<VnLv :label="t('Date')" :value="toDate(entity?.created)" />
<VnLv :label="t('Agency')" :value="entity?.agencyMode?.name" />
<VnLv :label="t('Zone')" :value="entity?.zone?.name" />
<VnLv
:label="t('Volume')"
:value="`${dashIfEmpty(entity?.m3)} / ${dashIfEmpty(
entity?.vehicle?.m3
)} `"
/>
<VnLv :label="t('Description')" :value="entity?.description" />
</template>
<template #menu="{ entity }">
<RouteDescriptorMenu :route="entity" />
</template>
</CardDescriptor>
</template>
<i18n>
es:
Date: Fecha
Agency: Agencia
Zone: Zona
Volume: Volumen
Description: Descripción
</i18n>

View File

@ -0,0 +1,63 @@
<script setup>
import axios from 'axios';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnConfirm from 'components/ui/VnConfirm.vue';
const props = defineProps({
route: {
type: Object,
required: true,
},
});
const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
function confirmRemove() {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('confirmDeletion'),
message: t('confirmDeletionMessage'),
promise: remove,
},
})
.onOk(async () => await router.push({ name: 'RouteList' }));
}
async function remove() {
if (!props.route.id) {
return;
}
await axios.delete(`Routes/${props.route.id}`);
quasar.notify({
message: t('globals.dataDeleted'),
type: 'positive',
});
}
// TODO: Add reports
</script>
<template>
<QItem @click="confirmRemove" v-ripple clickable>
<QItemSection avatar>
<QIcon name="delete" />
</QItemSection>
<QItemSection>{{ t('deleteRoute') }}</QItemSection>
</QItem>
</template>
<i18n>
en:
confirmDeletion: Confirm deletion
confirmDeletionMessage: Are you sure you want to delete this route?
deleteRoute: Delete route
es:
confirmDeletion: Confirmar eliminación,
confirmDeletionMessage: Seguro que quieres eliminar esta ruta?
deleteRoute: Eliminar ruta
</i18n>

View File

@ -0,0 +1,15 @@
<script setup>
import RouteDescriptor from 'pages/Route/Card/RouteDescriptor.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
</script>
<template>
<QPopupProxy>
<RouteDescriptor v-if="$props.id" :id="$props.id" />
</QPopupProxy>
</template>

View File

@ -0,0 +1,234 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const emit = defineEmits(['search']);
const workerList = ref([]);
const agencyList = ref([]);
const vehicleList = ref([]);
const warehouseList = ref([]);
</script>
<template>
<FetchData
url="Workers/search"
:filter="{ fields: ['id', 'nickname'] }"
sort-by="nickname ASC"
limit="30"
@on-fetch="(data) => (workerList = data)"
auto-load
/>
<FetchData
url="AgencyModes/isActive"
:filter="{ fields: ['id', 'name'] }"
sort-by="name ASC"
limit="30"
@on-fetch="(data) => (agencyList = data)"
auto-load
/>
<FetchData
url="Vehicles"
:filter="{ fields: ['id', 'numberPlate'] }"
sort-by="numberPlate ASC"
limit="30"
@on-fetch="(data) => (vehicleList = data)"
auto-load
/>
<FetchData url="Warehouses" @on-fetch="(data) => (warehouseList = data)" auto-load />
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
@search="emit('search')"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params }">
<QList dense>
<QItem class="q-my-sm">
<QItemSection v-if="workerList">
<VnSelectFilter
:label="t('Worker')"
v-model="params.workerFk"
:options="workerList"
option-value="id"
option-label="nickname"
dense
outlined
rounded
emit-value
map-options
use-input
:input-debounce="0"
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption>
{{ opt.nickname }},{{ opt.code }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection v-if="agencyList">
<VnSelectFilter
:label="t('Agency')"
v-model="params.agencyModeFk"
:options="agencyList"
option-value="id"
option-label="name"
dense
outlined
rounded
emit-value
map-options
use-input
:input-debounce="0"
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInputDate
v-model="params.from"
:label="t('From')"
is-outlined
:disable="Boolean(params.scopeDays)"
@update:model-value="params.scopeDays = null"
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInputDate
v-model="params.to"
:label="t('To')"
is-outlined
:disable="Boolean(params.scopeDays)"
@update:model-value="params.scopeDays = null"
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput
v-model="params.scopeDays"
type="number"
:label="t('Days Onward')"
is-outlined
clearable
:disable="Boolean(params.from || params.to)"
@update:model-value="
params.to = null;
params.from = null;
"
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection v-if="vehicleList">
<VnSelectFilter
:label="t('Vehicle')"
v-model="params.vehicleFk"
:options="vehicleList"
option-value="id"
option-label="numberPlate"
dense
outlined
rounded
emit-value
map-options
use-input
:input-debounce="0"
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput v-model="params.m3" label="m³" is-outlined clearable />
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection v-if="vehicleList">
<VnSelectFilter
:label="t('Warehouse')"
v-model="params.warehouseFk"
:options="warehouseList"
option-value="id"
option-label="name"
dense
outlined
rounded
emit-value
map-options
use-input
:input-debounce="0"
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput
v-model="params.description"
:label="t('Description')"
is-outlined
clearable
/>
</QItemSection>
</QItem>
</QList>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
warehouseFk: Warehouse
description: Description
m3:
vehicleFk: Vehicle
agencyModeFk: Agency
workerFk: Worker
from: From
to: To
es:
params:
warehouseFk: Almacén
description: Descripción
m3:
vehicleFk: Vehículo
agencyModeFk: Agencia
workerFk: Trabajador
from: Desde
to: Hasta
Warehouse: Almacén
Description: Descripción
Vehicle: Vehículo
Agency: Agencia
Worker: Trabajador
From: Desde
To: Hasta
</i18n>

View File

@ -0,0 +1,214 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue';
import axios from 'axios';
import VnInputTime from 'components/common/VnInputTime.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const shelvingId = route.params?.id || null;
const isNew = Boolean(!shelvingId);
const defaultInitialData = {
agencyModeFk: null,
created: null,
description: '',
vehicleFk: null,
workerFk: null,
};
const workerList = ref([]);
const agencyList = ref([]);
const vehicleList = ref([]);
const routeFilter = {
fields: [
'id',
'workerFk',
'agencyModeFk',
'created',
'm3',
'warehouseFk',
'description',
'vehicleFk',
'kmStart',
'kmEnd',
'started',
'finished',
'cost',
'zoneFk',
'isOk',
],
include: [
{ relation: 'agencyMode', scope: { fields: ['id', 'name'] } },
{
relation: 'vehicle',
scope: { fields: ['id', 'm3'] },
},
{ relation: 'zone', scope: { fields: ['id', 'name'] } },
{
relation: 'worker',
scope: {
fields: ['id'],
include: {
relation: 'user',
scope: {
fields: ['id'],
include: { relation: 'emailUser', scope: { fields: ['email'] } },
},
},
},
},
],
};
const onSave = (data, response) => {
if (isNew) {
axios.post(`Routes/${response.data?.id}/updateWorkCenter`);
router.push({ name: 'RouteSummary', params: { id: response.data?.id } });
}
};
</script>
<template>
<VnSubToolbar />
<FetchData
url="Workers/search"
:filter="{ fields: ['id', 'nickname'] }"
sort-by="nickname ASC"
limit="30"
@on-fetch="(data) => (workerList = data)"
auto-load
/>
<FetchData
url="AgencyModes/isActive"
:filter="{ fields: ['id', 'name'] }"
sort-by="name ASC"
limit="30"
@on-fetch="(data) => (agencyList = data)"
auto-load
/>
<FetchData
url="Vehicles"
:filter="{ fields: ['id', 'numberPlate'] }"
sort-by="numberPlate ASC"
limit="30"
@on-fetch="(data) => (vehicleList = data)"
auto-load
/>
<FormModel
:url="isNew ? null : `Routes/${shelvingId}`"
:url-create="isNew ? 'Routes' : null"
:observe-form-changes="!isNew"
:filter="routeFilter"
model="route"
:auto-load="!isNew"
:form-initial-data="defaultInitialData"
@on-data-saved="onSave"
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Worker')"
v-model="data.workerFk"
:options="workerList"
option-value="id"
option-label="nickname"
emit-value
map-options
use-input
:input-debounce="0"
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption>
{{ opt.nickname }},{{ opt.code }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<VnSelectFilter
:label="t('Vehicle')"
v-model="data.vehicleFk"
:options="vehicleList"
option-value="id"
option-label="numberPlate"
emit-value
map-options
use-input
:input-debounce="0"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Agency')"
v-model="data.agencyModeFk"
:options="agencyList"
option-value="id"
option-label="name"
emit-value
map-options
use-input
:input-debounce="0"
/>
</div>
<div class="col">
<VnInputDate v-model="data.created" :label="t('Created')" />
</div>
</VnRow>
<template v-if="!isNew">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.kmStart"
:label="t('Km Start')"
clearable
/>
</div>
<div class="col">
<VnInput v-model="data.kmEnd" :label="t('Km End')" clearable />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInputTime
v-model="data.started"
:label="t('Hour started')"
clearable
/>
</div>
<div class="col">
<VnInputTime
v-model="data.finished"
:label="t('Hour finished')"
clearable
/>
</div>
</VnRow>
</template>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.description"
:label="t('Description')"
clearable
/>
</div>
</VnRow>
</template>
</FormModel>
</template>

View File

@ -0,0 +1,20 @@
<script setup>
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import {useI18n} from "vue-i18n";
const { t } = useI18n();
</script>
<template>
<VnSearchbar
data-key="RouteList"
:label="t('Search route')"
:info="t('You can search by route reference')"
/>
</template>
<style scoped lang="scss"></style>
<i18n>
es:
Search route: Buscar rutas
You can search by route reference: Puedes buscar por referencia de la ruta
</i18n>

View File

@ -0,0 +1,314 @@
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'components/ui/VnLv.vue';
import { QIcon } from 'quasar';
import { dashIfEmpty, toCurrency, toDate, toHour } from 'src/filters';
import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue';
import axios from 'axios';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
const $props = defineProps({
id: {
type: Number,
default: 0,
},
});
const route = useRoute();
const stateStore = useStateStore();
const { t } = useI18n();
const entityId = computed(() => $props.id || route.params.id);
const isDialog = Boolean($props.id);
const hideRightDrawer = () => {
if (!isDialog) {
stateStore.rightDrawer = false;
}
};
onMounted(hideRightDrawer);
onUnmounted(hideRightDrawer);
const getTotalPackages = (tickets) => {
return (tickets || []).reduce((sum, ticket) => sum + ticket.packages, 0);
};
const ticketColumns = ref([
{
name: 'order',
label: t('route.summary.order'),
field: (row) => dashIfEmpty(row?.priority),
sortable: false,
align: 'center',
},
{
name: 'street',
label: t('route.summary.street'),
field: (row) => row?.street,
sortable: false,
align: 'left',
},
{
name: 'city',
label: t('route.summary.city'),
field: (row) => row?.city,
sortable: false,
align: 'left',
},
{
name: 'pc',
label: t('route.summary.pc'),
field: (row) => row?.postalCode,
sortable: false,
align: 'center',
},
{
name: 'client',
label: t('route.summary.client'),
field: (row) => row?.nickname,
sortable: false,
align: 'left',
},
{
name: 'warehouse',
label: t('route.summary.warehouse'),
field: (row) => row?.warehouseName,
sortable: false,
align: 'left',
},
{
name: 'packages',
label: t('route.summary.packages'),
field: (row) => row?.packages,
sortable: false,
align: 'center',
},
{
name: 'volume',
label: t('route.summary.m3'),
field: (row) => row?.volume,
sortable: false,
align: 'center',
},
{
name: 'packaging',
label: t('route.summary.packaging'),
field: (row) => row?.ipt,
sortable: false,
align: 'center',
},
{
name: 'ticket',
label: t('route.summary.ticket'),
field: (row) => row?.id,
sortable: false,
align: 'right',
},
{
name: 'observations',
label: '',
field: (row) => row?.ticketObservation,
sortable: false,
align: 'left',
},
]);
const openBuscaman = async (route, ticket) => {
if (!route.vehicleFk) throw new Error(`The route doesn't have a vehicle`);
const response = await axios.get(`Routes/${route.vehicleFk}/getDeliveryPoint`);
if (!response.data)
throw new Error(`The route's vehicle doesn't have a delivery point`);
const address = `${response.data}+to:${ticket.postalCode} ${ticket.city} ${ticket.street}`;
window.open(
'https://gps.buscalia.com/usuario/localizar.aspx?bmi=true&addr=' +
encodeURI(address),
'_blank'
);
};
</script>
<template>
<div class="q-pa-md">
<CardSummary ref="summary" :url="`Routes/${entityId}/summary`">
<template #header-left>
<RouterLink :to="{ name: `RouteSummary`, params: { id: entityId } }">
<QIcon name="open_in_new" color="white" size="sm" />
</RouterLink>
</template>
<template #header="{ entity }">
<span>{{ `${entity?.route.id} - ${entity?.route?.description}` }}</span>
</template>
<template #body="{ entity }">
<QCard class="vn-one">
<VnLv :label="t('ID')" :value="entity?.route.id" />
<VnLv
:label="t('route.summary.date')"
:value="toDate(entity?.route.created)"
/>
<VnLv
:label="t('route.summary.agency')"
:value="entity?.route?.agencyMode?.name"
/>
<VnLv
:label="t('route.summary.vehicle')"
:value="entity.route?.vehicle?.numberPlate"
/>
<VnLv :label="t('route.summary.driver')">
<template #value>
<span class="link">
{{ dashIfEmpty(entity?.route?.worker?.user?.name) }}
<WorkerDescriptorProxy :id="entity.route?.workerFk" />
</span>
</template>
</VnLv>
<VnLv
:label="t('route.summary.cost')"
:value="toCurrency(entity.route?.cost)"
/>
</QCard>
<QCard class="vn-one">
<VnLv
:label="t('route.summary.started')"
:value="toHour(entity?.route.started)"
/>
<VnLv
:label="t('route.summary.finished')"
:value="toHour(entity?.route.finished)"
/>
<VnLv
:label="t('route.summary.kmStart')"
:value="dashIfEmpty(entity?.route?.kmStart)"
/>
<VnLv
:label="t('route.summary.kmEnd')"
:value="dashIfEmpty(entity?.route?.kmEnd)"
/>
<VnLv
:label="t('route.summary.volume')"
:value="`${dashIfEmpty(entity?.route?.m3)} / ${dashIfEmpty(
entity?.route?.vehicle?.m3
)} `"
/>
<VnLv
:label="t('route.summary.packages')"
:value="getTotalPackages(entity.tickets)"
/>
</QCard>
<QCard class="vn-one">
<div class="header">
{{ t('route.summary.description') }}
</div>
<p>
{{ dashIfEmpty(entity?.route?.description) }}
</p>
</QCard>
<QCard class="vn-max">
<div class="header">
{{ t('route.summary.tickets') }}
</div>
<QTable
:columns="ticketColumns"
:rows="entity?.tickets"
:rows-per-page-options="[0]"
row-key="id"
flat
hide-pagination
>
<template #body-cell-city="{ value, row }">
<QTd auto-width>
<span
class="text-primary cursor-pointer"
@click="openBuscaman(entity?.route, row)"
>
{{ value }}
</span>
</QTd>
</template>
<template #body-cell-client="{ value, row }">
<QTd auto-width>
<span class="text-primary cursor-pointer">
{{ value }}
<CustomerDescriptorProxy :id="row?.clientFk" />
</span>
</QTd>
</template>
<template #body-cell-ticket="{ value, row }">
<QTd auto-width>
<span class="text-primary cursor-pointer">
{{ value }}
<TicketDescriptorProxy :id="row?.id" />
</span>
</QTd>
</template>
<template #body-cell-observations="{ value }">
<QTd auto-width>
<QIcon
v-if="value"
name="vn:notes"
color="primary"
class="cursor-pointer"
>
<QTooltip>{{ value }}</QTooltip>
</QIcon>
</QTd>
</template>
</QTable>
</QCard>
</template>
</CardSummary>
</div>
</template>
<i18n>
en:
route:
summary:
date: Date
agency: Agency
vehicle: Vehicle
driver: Driver
cost: Cost
started: Started time
finished: Finished time
kmStart: Km start
kmEnd: Km end
volume: Volume
packages: Packages
description: Description
tickets: Tickets
order: Order
street: Street
city: City
pc: PC
client: Client
warehouse: Warehouse
m3:
packaging: Packaging
ticket: Ticket
es:
route:
summary:
date: Fecha
agency: Agencia
vehicle: Vehículo
driver: Conductor
cost: Costo
started: Hora inicio
finished: Hora fin
kmStart: Km inicio
kmEnd: Km fin
volume: Volumen
packages: Bultos
description: Descripción
tickets: Tickets
order: Orden
street: Dirección fiscal
city: Población
pc: CP
client: Cliente
warehouse: Almacén
packaging: Encajado
</i18n>

View File

@ -0,0 +1,29 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import RouteSummary from 'pages/Route/Card/RouteSummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
</script>
<template>
<QDialog ref="dialogRef" @hide="onDialogHide">
<RouteSummary v-if="$props.id" :id="$props.id" />
</QDialog>
</template>
<style lang="scss">
.q-dialog .route .header {
position: sticky;
z-index: $z-max;
top: 0;
}
</style>

View File

@ -0,0 +1,530 @@
<script setup>
import VnPaginate from 'components/ui/VnPaginate.vue';
import { useStateStore } from 'stores/useStateStore';
import { useI18n } from 'vue-i18n';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { dashIfEmpty, toDate, toHour } from 'src/filters';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import FetchData from 'components/FetchData.vue';
import { useValidator } from 'composables/useValidator';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import axios from 'axios';
import RouteSearchbar from 'pages/Route/Card/RouteSearchbar.vue';
import RouteFilter from 'pages/Route/Card/RouteFilter.vue';
import { useQuasar } from 'quasar';
import RouteSummaryDialog from 'pages/Route/Card/RouteSummaryDialog.vue';
const stateStore = useStateStore();
const { t } = useI18n();
const { validate } = useValidator();
const quasar = useQuasar();
const to = Date.vnNew();
to.setDate(to.getDate() + 1);
to.setHours(0, 0, 0, 0);
const from = Date.vnNew();
from.setDate(from.getDate());
from.setHours(0, 0, 0, 0);
const params = ref({ from, to });
onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => (stateStore.rightDrawer = false));
const selectedRows = ref([]);
const columns = computed(() => [
{
name: 'ID',
label: t('ID'),
field: (row) => row.id,
sortable: true,
align: 'left',
},
{
name: 'worker',
label: t('Worker'),
field: (row) => row.workerUserName,
sortable: true,
align: 'left',
},
{
name: 'agency',
label: t('Agency'),
field: (row) => row.agencyName,
sortable: true,
align: 'left',
},
{
name: 'vehicle',
label: t('Vehicle'),
field: (row) => row.vehiclePlateNumber,
sortable: true,
align: 'left',
},
{
name: 'date',
label: t('Date'),
field: (row) => row.created,
sortable: true,
align: 'left',
},
{
name: 'volume',
label: 'm³',
field: (row) => dashIfEmpty(row.m3),
sortable: true,
align: 'left',
},
{
name: 'description',
label: t('Description'),
field: (row) => row.description,
sortable: true,
align: 'left',
},
{
name: 'started',
label: t('Hour started'),
field: (row) => toHour(row.started),
sortable: true,
align: 'left',
},
{
name: 'finished',
label: t('Hour finished'),
field: (row) => toHour(row.finished),
sortable: true,
align: 'left',
},
{
name: 'actions',
label: '',
sortable: false,
align: 'right',
},
]);
const refreshKey = ref(0);
const workers = ref([]);
const agencyList = ref([]);
const vehicleList = ref([]);
const updateRoute = async (route) => {
try {
return await axios.patch(`Routes/${route.id}`, route);
} catch (err) {
return err;
}
};
const updateVehicle = (row, vehicle) => {
row.vehicleFk = vehicle.id;
row.vehiclePlateNumber = vehicle.numberPlate;
updateRoute(row);
};
const updateAgency = (row, agency) => {
row.agencyModeFk = agency.id;
row.agencyName = agency.name;
updateRoute(row);
};
const updateWorker = (row, worker) => {
row.workerFk = worker.id;
row.workerUserName = worker.name;
updateRoute(row);
};
const confirmationDialog = ref(false);
const startingDate = ref(null);
const cloneRoutes = () => {
axios.post('Routes/clone', {
created: startingDate.value,
ids: selectedRows.value.map((row) => row?.id),
});
refreshKey.value++;
startingDate.value = null;
};
const markAsServed = () => {
selectedRows.value.forEach((row) => {
if (row?.id) {
axios.patch(`Routes/${row?.id}`, { isOk: true });
}
});
refreshKey.value++;
startingDate.value = null;
};
function previewRoute(id) {
if (!id) {
return;
}
quasar.dialog({
component: RouteSummaryDialog,
componentProps: {
id,
},
});
}
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<RouteSearchbar />
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDialog v-model="confirmationDialog">
<QCard style="min-width: 350px">
<QCardSection>
<p class="text-h6 q-ma-none">{{ t('Select the starting date') }}</p>
</QCardSection>
<QCardSection class="q-pt-none">
<VnInputDate
:label="t('Stating date')"
v-model="startingDate"
autofocus
/>
</QCardSection>
<!-- TODO: Add report -->
<QCardActions align="right">
<QBtn flat :label="t('Cancel')" v-close-popup class="text-primary" />
<QBtn color="primary" v-close-popup @click="cloneRoutes">
{{ t('Clone') }}
</QBtn>
</QCardActions>
</QCard>
</QDialog>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<RouteFilter data-key="RouteList" />
</QScrollArea>
</QDrawer>
<FetchData
url="Workers/activeWithInheritedRole"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<FetchData url="AgencyModes" @on-fetch="(data) => (agencyList = data)" auto-load />
<FetchData url="Vehicles" @on-fetch="(data) => (vehicleList = data)" auto-load />
<QPage class="column items-center">
<QToolbar class="bg-vn-dark justify-end">
<div id="st-actions" class="q-pa-sm">
<QBtn
icon="vn:clone"
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="confirmationDialog = true"
>
<QTooltip>{{ t('Clone Selected Routes') }}</QTooltip>
</QBtn>
<QBtn
icon="check"
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="markAsServed"
>
<QTooltip>{{ t('Mark as served') }}</QTooltip>
</QBtn>
</div>
</QToolbar>
<div class="route-list">
<VnPaginate
:key="refreshKey"
data-key="RouteList"
url="Routes/filter"
:order="['created DESC', 'id DESC']"
:limit="20"
auto-load
>
<template #body="{ rows }">
<div class="q-pa-md">
<QTable
v-model:selected="selectedRows"
:columns="columns"
:rows="rows"
flat
row-key="id"
selection="multiple"
:rows-per-page-options="[0]"
hide-pagination
>
<template #body-cell-worker="props">
<QTd :props="props">
{{ props.row?.workerUserName }}
<QPopupEdit
:model-value="props.row.workerFk"
v-slot="scope"
buttons
@update:model-value="
(worker) => updateWorker(props.row, worker)
"
>
<VnSelectFilter
:label="t('Worker')"
v-model="scope.value"
:options="workers"
option-value="id"
option-label="name"
hide-selected
autofocus
:emit-value="false"
:rules="validate('Route.workerFk')"
:is-clearable="false"
@keyup.enter="scope.set"
@focus="($event) => $event.target.select()"
>
<template #option="{ opt, itemProps }">
<QItem
v-bind="itemProps"
class="q-pa-xs row items-center"
>
<QItemSection
class="col-9 justify-center"
>
<span>{{ opt.name }}</span>
<span class="text-grey">{{
opt.nickname
}}</span>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QPopupEdit>
</QTd>
</template>
<template #body-cell-agency="props">
<QTd :props="props">
{{ props.row?.agencyName }}
<QPopupEdit
:model-value="props.row.agencyModeFk"
v-slot="scope"
buttons
@update:model-value="
(agency) => updateAgency(props.row, agency)
"
>
<VnSelectFilter
:label="t('Agency')"
v-model="scope.value"
:options="agencyList"
option-value="id"
option-label="name"
hide-selected
autofocus
:emit-value="false"
:rules="validate('route.agencyFk')"
:is-clearable="false"
@keyup.enter="scope.set"
@focus="($event) => $event.target.select()"
/>
</QPopupEdit>
</QTd>
</template>
<template #body-cell-vehicle="props">
<QTd :props="props">
{{ props.row?.vehiclePlateNumber }}
<QPopupEdit
:model-value="props.row.vehicleFk"
v-slot="scope"
buttons
@update:model-value="
(vehicle) => updateVehicle(props.row, vehicle)
"
>
<VnSelectFilter
:label="t('Vehicle')"
v-model="scope.value"
:options="vehicleList"
option-value="id"
option-label="numberPlate"
hide-selected
autofocus
:emit-value="false"
:rules="validate('route.vehicleFk')"
:is-clearable="false"
@keyup.enter="scope.set"
@focus="($event) => $event.target.select()"
/>
</QPopupEdit>
</QTd>
</template>
<template #body-cell-date="props">
<QTd :props="props">
{{ toDate(props.row?.created) }}
<QPopupEdit
v-model="props.row.created"
v-slot="scope"
@update:model-value="updateRoute(props.row)"
buttons
>
<VnInputDate
v-model="scope.value"
autofocus
:label="t('Date')"
:rules="validate('route.created')"
:is-clearable="false"
@keyup.enter="scope.set"
@focus="($event) => $event.target.select()"
/>
</QPopupEdit>
</QTd>
</template>
<template #body-cell-description="props">
<QTd :props="props">
{{ props.row?.description }}
<QPopupEdit
v-model="props.row.description"
v-slot="scope"
@update:model-value="updateRoute(props.row)"
buttons
>
<VnInput
v-model="scope.value"
autofocus
:label="t('Description')"
:rules="validate('route.description')"
:is-clearable="false"
@keyup.enter="scope.set"
@focus="($event) => $event.target.select()"
/>
</QPopupEdit>
</QTd>
</template>
<template #body-cell-started="props">
<QTd :props="props">
{{ toHour(props.row.started) }}
<QPopupEdit
v-model="props.row.started"
v-slot="scope"
buttons
@update:model-value="updateRoute(props.row)"
>
<VnInputTime
v-model="scope.value"
autofocus
:label="t('Hour started')"
:rules="validate('route.started')"
:is-clearable="false"
@keyup.enter="scope.set"
@focus="($event) => $event.target.select()"
/>
</QPopupEdit>
</QTd>
</template>
<template #body-cell-finished="props">
<QTd :props="props">
{{ toHour(props.row.finished) }}
<QPopupEdit
v-model="props.row.finished"
v-slot="scope"
buttons
@update:model-value="updateRoute(props.row)"
>
<VnInputTime
v-model="scope.value"
autofocus
:label="t('Hour finished')"
:rules="validate('route.finished')"
:is-clearable="false"
@keyup.enter="scope.set"
@focus="($event) => $event.target.select()"
/>
</QPopupEdit>
</QTd>
</template>
<template #body-cell-actions="props">
<QTd :props="props">
<div class="table-actions">
<QIcon
name="vn:ticketAdd"
size="xs"
color="primary"
>
<QTooltip>{{ t('Add ticket') }}</QTooltip>
</QIcon>
<QIcon
name="preview"
size="xs"
color="primary"
@click="previewRoute(props?.row?.id)"
>
<QTooltip>{{ t('Preview') }}</QTooltip>
</QIcon>
</div>
</QTd>
</template>
</QTable>
</div>
</template>
</VnPaginate>
</div>
<QPageSticky :offset="[20, 20]">
<RouterLink :to="{ name: 'RouteCreate' }">
<QBtn fab icon="add" color="primary" />
<QTooltip>
{{ t('newRoute') }}
</QTooltip>
</RouterLink>
</QPageSticky>
</QPage>
</template>
<style lang="scss" scoped>
.route-list {
width: 100%;
}
.table-actions {
display: flex;
align-items: center;
gap: 12px;
i {
cursor: pointer;
}
}
</style>
<i18n>
en:
newRoute: New Route
es:
ID: ID
Worker: Trabajador
Agency: Agencia
Vehicle: Vehículo
Date: Fecha
Description: Descripción
Hour started: Hora inicio
Hour finished: Hora fin
newRoute: Nueva Ruta
Clone Selected Routes: Clonar rutas seleccionadas
Select the starting date: Seleccione la fecha de inicio
Stating date: Fecha de inicio
Cancel: Cancelar
Clone: Clonar
Mark as served: Marcar como servidas
</i18n>

View File

@ -1 +1,177 @@
<template>Supplier accounts</template> <script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import CrudModel from 'components/CrudModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import CreateBankEntityForm from 'src/components/CreateBankEntityForm.vue';
import VnSelectCreate from 'src/components/common/VnSelectCreate.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import { useQuasar } from 'quasar';
const quasar = useQuasar();
const { notify } = useNotify();
const route = useRoute();
const { t } = useI18n();
const supplier = ref(null);
const supplierAccountRef = ref(null);
const wireTransferFk = ref(null);
const bankEntitiesOptions = ref([]);
const onBankEntityCreated = (data) => {
bankEntitiesOptions.value.push(data);
};
const onChangesSaved = () => {
if (supplier.value.payMethodFk !== wireTransferFk.value)
quasar
.dialog({
message: t('Do you want to change the pay method to wire transfer?'),
ok: {
push: true,
color: 'primary',
},
cancel: true,
})
.onOk(async () => {
await setWireTransfer();
});
};
const setWireTransfer = async () => {
try {
const params = {
id: route.params.id,
payMethodFk: wireTransferFk.value,
};
await axios.patch(`Suppliers/${route.params.id}`, params);
notify('globals.dataSaved', 'positive');
} catch (err) {
console.error('Error setting wire transfer', err);
}
};
onMounted(() => {
if (supplierAccountRef.value) supplierAccountRef.value.reload();
});
</script>
<template>
<FetchData
url="BankEntities"
@on-fetch="(data) => (bankEntitiesOptions = data)"
auto-load
/>
<FetchData
url="payMethods/findOne"
@on-fetch="(data) => (wireTransferFk = data.id)"
:filter="{ where: { code: 'wireTransfer' } }"
auto-load
/>
<FetchData
:url="`Suppliers/${route.params.id}`"
@on-fetch="(data) => (supplier = data)"
auto-load
/>
<CrudModel
data-key="SupplierAccount"
url="SupplierAccounts"
model="SupplierAccounts"
:filter="{
fields: ['id', 'supplierFk', 'iban', 'bankEntityFk', 'beneficiary'],
where: { supplierFk: route.params.id },
}"
ref="supplierAccountRef"
:default-remove="false"
:data-required="{ supplierFk: route.params.id }"
@save-changes="onChangesSaved()"
>
<template #body="{ rows }">
<QCard class="q-pa-md">
<VnRow
v-for="(row, index) in rows"
:key="index"
class="row q-gutter-md q-mb-md"
>
<div class="col">
<VnInput
:label="t('supplier.accounts.iban')"
v-model="row.iban"
/>
</div>
<div class="col">
<VnSelectCreate
:label="t('worker.create.bankEntity')"
v-model="row.bankEntityFk"
:options="bankEntitiesOptions"
option-label="name"
option-value="id"
hide-selected
>
<template #form>
<CreateBankEntityForm
@on-data-saved="onBankEntityCreated($event)"
:show-entity-field="false"
/>
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection v-if="scope.opt">
<QItemLabel
>{{ scope.opt.bic }}
{{ scope.opt.name }}</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelectCreate>
</div>
<div class="col">
<VnInput
:label="t('supplier.accounts.beneficiary')"
v-model="row.beneficiary"
/>
</div>
<div class="col-1 row justify-center items-center">
<QIcon
name="delete"
size="sm"
class="cursor-pointer"
color="primary"
@click="supplierAccountRef.remove([row])"
>
<QTooltip>
{{ t('Remove account') }}
</QTooltip>
</QIcon>
</div>
</VnRow>
<VnRow>
<QIcon
name="add"
size="sm"
class="cursor-pointer"
color="primary"
@click="supplierAccountRef.insert()"
>
<QTooltip>
{{ t('Add account') }}
</QTooltip>
</QIcon>
</VnRow>
</QCard>
</template>
</CrudModel>
</template>
<i18n>
es:
Do you want to change the pay method to wire transfer?: ¿Quieres modificar la forma de pago a transferencia?
Add account: Añadir cuenta
Remove account: Remover cuenta
</i18n>

View File

@ -1 +1,97 @@
<template>Supplier addresses</template> <script setup>
import { useI18n } from 'vue-i18n';
import { useRouter, useRoute } from 'vue-router';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import CardList from 'src/components/ui/CardList.vue';
import VnLv from 'src/components/ui/VnLv.vue';
const router = useRouter();
const route = useRoute();
const { t } = useI18n();
const addressesFilter = {
fields: [
'id',
'nickname',
'street',
'city',
'provinceFk',
'phone',
'mobile',
'postalCode',
],
order: ['nickname ASC'],
include: [
{
relation: 'province',
scope: {
fields: ['id', 'name'],
},
},
],
};
const redirectToCreateView = () => {
router.push({ name: 'SupplierAddressesCreate' });
};
const redirectToUpdateView = (addressData) => {
const stringifiedAddressData = JSON.stringify(addressData);
router.push({
name: 'SupplierAddressesCreate',
query: { addressData: stringifiedAddressData },
});
};
</script>
<template>
<QPage class="column items-center q-pa-md">
<div class="card-list">
<VnPaginate
data-key="SupplierAddress"
:url="`Suppliers/${route.params.id}/addresses`"
:filter="addressesFilter"
auto-load
>
<template #body="{ rows }">
<CardList
v-for="row of rows"
:key="row.id"
:title="row.nickname"
:id="row.id"
@click="redirectToUpdateView(row)"
>
<template #list-items>
<VnLv
:label="t('supplier.addresses.street')"
:value="row.street"
/>
<VnLv
:label="t('supplier.addresses.postcode')"
:value="`${row.postalCode} - ${row.city}, ${row.province.name}`"
/>
<VnLv
:label="t('supplier.addresses.phone')"
:value="`${row.phone}, ${row.mobile}`"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
<QPageSticky :offset="[20, 20]">
<QBtn fab icon="add" color="primary" @click="redirectToCreateView()" />
<QTooltip>
{{ t('supplier.list.newSupplier') }}
</QTooltip>
</QPageSticky>
</QPage>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
</style>

View File

@ -0,0 +1,182 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { reactive, ref, onMounted, onBeforeMount } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import FormModel from 'components/FormModel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelectCreate from 'src/components/common/VnSelectCreate.vue';
import VnRow from 'components/ui/VnRow.vue';
import CustomerCreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const provincesOptions = ref([]);
const postcodesOptions = ref([]);
const townsLocationOptions = ref([]);
const viewAction = ref();
const updateAddressId = ref(null);
const newAddressForm = reactive({
nickname: null,
street: null,
postalCode: null,
city: null,
provinceFk: null,
phone: null,
mobile: null,
});
const onDataSaved = () => {
router.push({ name: 'SupplierAddresses' });
};
const updateAddressForm = (addressData) => {
for (let key in newAddressForm) {
if (key in addressData) {
newAddressForm[key] = addressData[key];
}
}
};
onBeforeMount(() => {
viewAction.value = route.query.addressData ? 'update' : 'create';
if (viewAction.value === 'create') newAddressForm.supplierFk = route.params.id;
});
onMounted(() => {
if (viewAction.value === 'update' && route.query.addressData) {
const addressData = JSON.parse(route.query.addressData);
updateAddressId.value = addressData.id;
updateAddressForm(addressData);
}
});
</script>
<template>
<FetchData
ref="postcodeFetchDataRef"
url="Postcodes/location"
@on-fetch="(data) => (postcodesOptions = data)"
auto-load
/>
<FetchData
ref="provincesFetchDataRef"
@on-fetch="(data) => (provincesOptions = data)"
auto-load
url="Provinces"
/>
<FetchData
ref="townsFetchDataRef"
@on-fetch="(data) => (townsLocationOptions = data)"
auto-load
url="Towns/location"
/>
<QPage>
<FormModel
model="supplierAddresses"
:form-initial-data="newAddressForm"
:url-update="
viewAction !== 'create' ? `SupplierAddresses/${updateAddressId}` : null
"
:url-create="viewAction === 'create' ? 'SupplierAddresses' : null"
:observe-form-changes="viewAction === 'create'"
@on-data-saved="onDataSaved()"
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.nickname"
:label="t('supplier.addresses.name')"
/>
</div>
<div class="col">
<VnInput
v-model="data.street"
:label="t('supplier.addresses.street')"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectCreate
v-model="data.postalCode"
:label="t('supplier.addresses.postcode')"
:rules="validate('supplierAddress.postcode')"
:roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions"
option-label="code"
option-value="code"
hide-selected
>
<template #form>
<CustomerCreateNewPostcode
@on-data-saved="onPostcodeCreated($event)"
/>
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection v-if="scope.opt">
<QItemLabel>{{ scope.opt.code }}</QItemLabel>
<QItemLabel caption
>{{ scope.opt.code }} -
{{ scope.opt.town.name }} ({{
scope.opt.town.province.name
}},
{{
scope.opt.town.province.country.country
}})</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelectCreate>
</div>
<div class="col">
<VnSelectFilter
:label="t('supplier.addresses.city')"
:options="townsLocationOptions"
v-model="data.city"
hide-selected
option-label="name"
option-value="id"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('supplier.addresses.province')"
:options="provincesOptions"
hide-selected
map-options
option-label="name"
option-value="id"
v-model="data.provinceFk"
/>
</div>
<div class="col">
<VnInput
v-model="data.phone"
:label="t('supplier.addresses.phone')"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.mobile"
:label="t('supplier.addresses.mobile')"
/>
</div>
<div class="col" />
</VnRow>
</template>
</FormModel>
</QPage>
</template>

View File

@ -1 +1,139 @@
<template>Supplier agency term</template> <script setup>
import { ref, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import CrudModel from 'components/CrudModel.vue';
import VnRow from 'components/ui/VnRow.vue';
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const supplierAgencyTermRef = ref(null);
const agenciesOptions = ref(null);
const supplierAgencyFilter = {
include: {
relation: 'agency',
scope: {
fields: ['id', 'name'],
},
},
where: { supplierFk: route.params.id },
};
const redirectToCreateView = () => {
router.push({ name: 'SupplierAgencyTermCreate' });
};
onMounted(() => {
if (supplierAgencyTermRef.value) supplierAgencyTermRef.value.reload();
});
</script>
<template>
<FetchData
url="Suppliers/freeAgencies"
@on-fetch="(data) => (agenciesOptions = data)"
auto-load
/>
<CrudModel
ref="supplierAgencyTermRef"
data-key="SupplierAgencyTerm"
save-url="SupplierAgencyTerms/crud"
url="SupplierAgencyTerms"
model="SupplierAgencyTerm"
primary-key="agencyFk"
:filter="supplierAgencyFilter"
:default-remove="false"
:data-required="{
supplierFk: route.params.id,
}"
>
<template #body="{ rows }">
<QCard class="q-pa-md">
<VnRow
v-for="(row, index) in rows"
:key="index"
class="row q-gutter-md q-mb-md"
>
<div class="col">
<QField :label="t('supplier.agencyTerms.agencyFk')" stack-label>
<template #control>
<div tabindex="0">
{{ row.agency?.name }}
</div>
</template>
</QField>
</div>
<div class="col">
<QInput
:label="t('supplier.agencyTerms.minimumM3')"
v-model.number="row.minimumM3"
type="number"
/>
</div>
<div class="col">
<QInput
:label="t('supplier.agencyTerms.packagePrice')"
v-model.number="row.packagePrice"
type="number"
/>
</div>
<div class="col">
<QInput
:label="t('supplier.agencyTerms.kmPrice')"
v-model.number="row.kmPrice"
type="number"
/>
</div>
<div class="col">
<QInput
:label="t('supplier.agencyTerms.m3Price')"
v-model.number="row.m3Price"
type="number"
/>
</div>
<div class="col">
<QInput
:label="t('supplier.agencyTerms.routePrice')"
v-model.number="row.routePrice"
type="number"
/>
</div>
<div class="col">
<QInput
:label="t('supplier.agencyTerms.minimumKm')"
v-model.number="row.minimumKm"
type="number"
/>
</div>
<div class="col-1 row justify-center items-center">
<QIcon
name="delete"
size="sm"
class="cursor-pointer"
color="primary"
@click="supplierAgencyTermRef.remove([row])"
>
<QTooltip>
{{ t('Remove row') }}
</QTooltip>
</QIcon>
</div>
</VnRow>
</QCard>
</template>
</CrudModel>
<QPageSticky :offset="[20, 20]">
<QBtn fab icon="add" color="primary" @click="redirectToCreateView()" />
<QTooltip>
{{ t('supplier.agencyTerms.addRow') }}
</QTooltip>
</QPageSticky>
</template>
<i18n>
es:
Remove row: Eliminar fila
</i18n>

View File

@ -0,0 +1,113 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { reactive, ref } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
const agenciesOptions = ref(null);
const newAgencyTermForm = reactive({
agencyFk: null,
minimumM3: null,
packagePrice: null,
kmPrice: null,
m3Price: null,
routePrice: null,
minimumKm: null,
supplierFk: route.params.id,
});
const onDataSaved = () => {
router.push({ name: 'SupplierAgencyTerm' });
};
</script>
<template>
<FetchData
url="Suppliers/freeAgencies"
@on-fetch="(data) => (agenciesOptions = data)"
auto-load
/>
<QPage>
<FormModel
model="supplierAgencyTerm"
:form-initial-data="newAgencyTermForm"
url-create="SupplierAgencyTerms"
:observe-form-changes="true"
@on-data-saved="onDataSaved()"
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('supplier.agencyTerms.agencyFk')"
v-model="data.agencyFk"
:options="agenciesOptions"
option-label="name"
option-value="id"
hide-selected
rounded
/>
</div>
<div class="col">
<QInput
:label="t('supplier.agencyTerms.minimumM3')"
v-model.number="data.minimumM3"
type="number"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
:label="t('supplier.agencyTerms.packagePrice')"
v-model.number="data.packagePrice"
type="number"
/>
</div>
<div class="col">
<QInput
:label="t('supplier.agencyTerms.kmPrice')"
v-model.number="data.kmPrice"
type="number"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
:label="t('supplier.agencyTerms.m3Price')"
v-model.number="data.m3Price"
type="number"
/>
</div>
<div class="col">
<QInput
:label="t('supplier.agencyTerms.routePrice')"
v-model.number="data.routePrice"
type="number"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
:label="t('supplier.agencyTerms.minimumKm')"
v-model.number="data.minimumKm"
type="number"
/>
</div>
<div class="col" />
</VnRow>
</template>
</FormModel>
</QPage>
</template>

View File

@ -1 +1,97 @@
<template>Supplier basic data</template> <script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
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 VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
const route = useRoute();
const { t } = useI18n();
const workersOptions = ref([]);
</script>
<template>
<FetchData
url="Workers/search"
:filter="{ fields: ['id', 'nickname'], order: 'nickname ASC', limit: 30 }"
@on-fetch="(data) => (workersOptions = data)"
auto-load
/>
<FormModel
:url="`Suppliers/${route.params.id}`"
:url-update="`Suppliers/${route.params.id}`"
model="supplier"
auto-load
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.nickname"
:label="t('supplier.basicData.alias')"
:rules="validate('supplier.nickname')"
clearable
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('supplier.basicData.workerFk')"
v-model="data.workerFk"
:options="workersOptions"
option-value="id"
option-label="name"
hide-selected
map-options
:rules="validate('supplier.workerFk')"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption>
{{ scope.opt?.nickname }}, {{ scope.opt?.id }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox
v-model="data.isSerious"
:label="t('supplier.basicData.isSerious')"
/>
</div>
<div class="col">
<QCheckbox
v-model="data.isActive"
:label="t('supplier.basicData.isActive')"
/>
</div>
<div class="col">
<QCheckbox
v-model="data.isPayMethodChecked"
:label="t('supplier.basicData.isPayMethodChecked')"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
:label="t('supplier.basicData.note')"
type="textarea"
v-model="data.note"
fill-input
autogrow
/>
</div>
</VnRow>
</template>
</FormModel>
</template>

View File

@ -1 +1,68 @@
<template>Supplier billing data</template> <script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
const route = useRoute();
const { t } = useI18n();
const paymethodsOptions = ref([]);
const payDemsOptions = ref([]);
</script>
<template>
<FetchData
url="Paymethods"
@on-fetch="(data) => (paymethodsOptions = data)"
auto-load
/>
<FetchData url="PayDems" @on-fetch="(data) => (payDemsOptions = data)" auto-load />
<FormModel
:url="`Suppliers/${route.params.id}`"
:url-update="`Suppliers/${route.params.id}`"
model="supplier"
auto-load
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('supplier.billingData.payMethodFk')"
v-model="data.payMethodFk"
:options="paymethodsOptions"
option-value="id"
option-label="name"
hide-selected
map-options
:rules="validate('supplier.payMethodFk')"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('supplier.billingData.payDemFk')"
v-model="data.payDemFk"
:options="payDemsOptions"
option-value="id"
option-label="payDem"
hide-selected
map-options
:rules="validate('supplier.payDemFk')"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
:label="t('supplier.billingData.payDay')"
type="number"
v-model="data.payDay"
/>
</div>
</VnRow>
</template>
</FormModel>
</template>

View File

@ -29,7 +29,6 @@ const { t } = useI18n();
<QPageContainer> <QPageContainer>
<QPage> <QPage>
<VnSubToolbar /> <VnSubToolbar />
<div class="q-pa-md"><RouterView></RouterView></div> <div class="q-pa-md"><RouterView></RouterView></div>
</QPage> </QPage>
</QPageContainer> </QPageContainer>
@ -63,3 +62,8 @@ const { t } = useI18n();
} }
} }
</style> </style>
<i18n>
es:
Search suppliers: Buscar proveedores
</i18n>

View File

@ -1 +1,192 @@
<template>Supplier consumption</template> <script setup>
import { useRoute } from 'vue-router';
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import FetchData from 'components/FetchData.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import { toDate, toDateString } from 'src/filters';
import { dashIfEmpty } from 'src/filters';
import { usePrintService } from 'composables/usePrintService';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
const { t } = useI18n();
const route = useRoute();
const { openReport, sendEmail } = usePrintService();
const quasar = useQuasar();
const { notify } = useNotify();
const suppliersConsumption = ref();
const userParams = computed(() => {
const minDate = Date.vnNew();
minDate.setHours(0, 0, 0, 0);
minDate.setMonth(minDate.getMonth() - 2);
const maxDate = Date.vnNew();
maxDate.setHours(23, 59, 59, 59);
return {
from: toDateString(minDate),
to: toDateString(maxDate),
};
});
const reportParams = computed(() => ({
recipientId: Number(route.params.id),
...userParams.value,
}));
const rows = computed(() => suppliersConsumption.value || []);
const openReportPdf = () => {
openReport(`Suppliers/${route.params.id}/campaign-metrics-pdf`, reportParams.value);
};
const getSupplierContacts = async () => {
try {
const params = {
filter: {
where: {
supplierFk: route.params.id,
email: { neq: null },
},
limit: 1,
},
};
const { data } = await axios.get('SupplierContacts', params);
if (data && data.length) return data[0].email;
} catch (err) {
notify('This supplier does not have a contact with an email address', 'negative');
console.error('Error fetching supplierContacts: ', err);
}
};
const openSendEmailDialog = async () => {
const email = await getSupplierContacts();
quasar.dialog({
component: SendEmailDialog,
componentProps: {
data: {
address: email,
},
promise: sendCampaignMetricsEmail,
},
});
};
const sendCampaignMetricsEmail = ({ address }) => {
sendEmail(`Suppliers/${route.params.id}/campaign-metrics-email`, {
recipient: address,
...reportParams.value,
});
};
const calculateTotal = (buysArray) => {
return buysArray.reduce((accumulator, { total }) => accumulator + total, 0);
};
</script>
<template>
<FetchData
url="Suppliers/consumption"
@on-fetch="(data) => (suppliersConsumption = data)"
:filter="{
where: { supplierFk: route.params.id },
order: ['itemTypeFk', 'itemName', 'itemSize'],
}"
:params="userParams"
auto-load
/>
<QToolbar class="bg-vn-dark justify-end">
<div id="st-data">
<QBtn
v-if="userParams.from && userParams.to"
color="primary"
icon-right="picture_as_pdf"
no-caps
class="q-mr-md"
@click="openReportPdf()"
>
<QTooltip>
{{ t('Open as PDF') }}
</QTooltip>
</QBtn>
<QBtn
v-if="userParams.from && userParams.to"
color="primary"
icon-right="email"
no-caps
@click="openSendEmailDialog()"
>
<QTooltip>
{{ t('Send to email') }}
</QTooltip>
</QBtn>
</div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<QPage class="column items-center q-pa-md">
<QTable
:rows="rows"
hide-bottom
row-key="id"
hide-header
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
>
<template #body="{ row }">
<QTr>
<QTd no-hover class="label">{{
t('supplier.consumption.entry')
}}</QTd>
<QTd no-hover>{{ row.id }}</QTd>
<QTd no-hover class="label">{{ t('supplier.consumption.date') }}</QTd>
<QTd no-hover>{{ toDate(row.shipped) }}</QTd>
<QTd no-hover class="label">{{
t('supplier.consumption.reference')
}}</QTd>
<QTd no-hover>{{ row.invoiceNumber }}</QTd>
</QTr>
<QTr v-for="(buy, index) in row.buys" :key="index">
<QTd no-hover> {{ buy.itemName }}</QTd>
<QTd no-hover>
<span>{{ buy.subName }}</span>
<fetched-tags :item="buy" :max-length="5" />
</QTd>
<QTd no-hover> {{ dashIfEmpty(buy.quantity) }}</QTd>
<QTd no-hover> {{ dashIfEmpty(buy.price) }}</QTd>
<QTd colspan="2" no-hover> {{ dashIfEmpty(buy.total) }}</QTd>
</QTr>
<QTr>
<QTd colspan="6" no-hover>
<span class="label">{{ t('Total entry') }}: </span>
<span>{{ calculateTotal(row.buys) }}</span>
</QTd>
</QTr>
</template>
</QTable>
</QPage>
</template>
<style scoped lang="scss">
.label {
color: var(--vn-label);
}
</style>
<i18n>
es:
Total entry: Total entrada
Open as PDF: Abrir como PDF
Send to email: Enviar por email
This supplier does not have a contact with an email address: Este proveedor no tiene un email de contacto
</i18n>

View File

@ -1 +1,118 @@
<template>Supplier contacts</template> <script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CrudModel from 'components/CrudModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
const route = useRoute();
const { t } = useI18n();
const supplierContactRef = ref(null);
onMounted(() => {
if (supplierContactRef.value) supplierContactRef.value.reload();
});
</script>
<template>
<CrudModel
data-key="SupplierContact"
url="SupplierContacts"
model="SupplierContact"
:filter="{
where: { supplierFk: route.params.id },
}"
ref="supplierContactRef"
:default-remove="false"
:data-required="{ supplierFk: route.params.id }"
>
<template #body="{ rows }">
<QCard class="q-pa-md">
<QCardSection
v-for="(row, index) in rows"
:key="index"
class="border q-pa-md q-mb-md"
>
<VnRow class="row q-gutter-md">
<div class="col">
<VnInput
:label="t('supplier.contacts.name')"
v-model="row.name"
/>
</div>
<div class="col">
<VnInput
:label="t('supplier.contacts.phone')"
v-model="row.phone"
/>
</div>
<div class="col">
<VnInput
:label="t('supplier.contacts.mobile')"
v-model="row.mobile"
/>
</div>
<div class="col">
<VnInput
:label="t('supplier.contacts.email')"
v-model="row.email"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md">
<div class="col">
<QInput
:label="t('supplier.contacts.observation')"
type="textarea"
v-model="row.observation"
fill-input
autogrow
/>
</div>
<div class="col-1 row justify-center items-center">
<QIcon
name="delete"
size="sm"
class="cursor-pointer"
color="primary"
@click="supplierContactRef.remove([row])"
>
<QTooltip>
{{ t('Remove contact') }}
</QTooltip>
</QIcon>
</div>
</VnRow>
</QCardSection>
<VnRow>
<QIcon
name="add"
size="sm"
class="cursor-pointer"
color="primary"
@click="supplierContactRef.insert()"
>
<QTooltip>
{{ t('Add contact') }}
</QTooltip>
</QIcon>
</VnRow>
</QCard>
</template>
</CrudModel>
</template>
<style lang="scss" scoped>
.border {
border-radius: 0px !important;
border: 1px solid var(--vn-text) !important;
}
</style>
<i18n>
es:
Add contact: Añadir contacto
Remove contact: Remover contacto
</i18n>

View File

@ -1 +1,298 @@
<template>Supplier fiscal data</template> <script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
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 VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelectCreate from 'src/components/common/VnSelectCreate.vue';
import CustomerCreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue';
const route = useRoute();
const { t } = useI18n();
const postcodeFetchDataRef = ref(null);
const townsFetchDataRef = ref(null);
const sageTaxTypesOptions = ref([]);
const sageWithholdingsOptions = ref([]);
const sageTransactionTypesOptions = ref([]);
const supplierActivitiesOptions = ref([]);
const postcodesOptions = ref([]);
const provincesLocationOptions = ref([]);
const townsLocationOptions = ref([]);
const countriesOptions = ref([]);
const onPostcodeCreated = async ({ code, provinceFk, townFk, countryFk }, formData) => {
await postcodeFetchDataRef.value.fetch();
await townsFetchDataRef.value.fetch();
formData.postCode = code;
formData.provinceFk = provinceFk;
formData.city = townsLocationOptions.value.find((town) => town.id === townFk).name;
formData.countryFk = countryFk;
};
</script>
<template>
<FetchData
url="SageTaxTypes"
auto-load
@on-fetch="(data) => (sageTaxTypesOptions = data)"
/>
<FetchData
url="SageWithholdings"
auto-load
@on-fetch="(data) => (sageWithholdingsOptions = data)"
/>
<FetchData
url="sageTransactionTypes"
auto-load
@on-fetch="(data) => (sageTransactionTypesOptions = data)"
/>
<FetchData
url="SupplierActivities"
auto-load
@on-fetch="(data) => (supplierActivitiesOptions = data)"
/>
<FetchData
ref="postcodeFetchDataRef"
url="Postcodes/location"
@on-fetch="(data) => (postcodesOptions = data)"
auto-load
/>
<FetchData
ref="townsFetchDataRef"
@on-fetch="(data) => (townsLocationOptions = data)"
auto-load
url="Towns/location"
/>
<FetchData
@on-fetch="(data) => (provincesLocationOptions = data)"
auto-load
url="Provinces/location"
/>
<FetchData
@on-fetch="(data) => (countriesOptions = data)"
auto-load
url="Countries"
/>
<FormModel
:url="`Suppliers/${route.params.id}`"
:url-update="`Suppliers/${route.params.id}/updateFiscalData`"
model="supplier"
auto-load
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.name"
:label="t('supplier.fiscalData.name')"
clearable
/>
</div>
<div class="col">
<VnInput
v-model="data.nif"
:label="t('supplier.fiscalData.nif')"
clearable
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.account"
:label="t('supplier.fiscalData.account')"
clearable
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('supplier.fiscalData.sageTaxTypeFk')"
v-model="data.sageTaxTypeFk"
:options="sageTaxTypesOptions"
option-value="id"
option-label="vat"
hide-selected
map-options
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('supplier.fiscalData.sageWithholdingFk')"
v-model="data.sageWithholdingFk"
:options="sageWithholdingsOptions"
option-value="id"
option-label="withholding"
hide-selected
map-options
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('supplier.fiscalData.sageTransactionTypeFk')"
v-model="data.sageTransactionTypeFk"
:options="sageTransactionTypesOptions"
option-value="id"
option-label="transaction"
hide-selected
map-options
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('supplier.fiscalData.supplierActivityFk')"
v-model="data.supplierActivityFk"
:options="supplierActivitiesOptions"
option-value="code"
option-label="name"
hide-selected
map-options
/>
</div>
<div class="col">
<VnInput
v-model="data.healthRegister"
:label="t('supplier.fiscalData.healthRegister')"
clearable
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.street"
:label="t('supplier.fiscalData.street')"
clearable
/>
</div>
<div class="col">
<VnSelectCreate
:label="t('supplier.fiscalData.postcode')"
v-model="data.postCode"
:options="postcodesOptions"
:rules="validate('supplier.postCode')"
:roles-allowed-to-create="['deliveryAssistant']"
option-label="code"
option-value="code"
hide-selected
>
<template #form>
<CustomerCreateNewPostcode
@on-data-saved="onPostcodeCreated($event, data)"
/>
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection v-if="scope.opt">
<QItemLabel>{{ scope.opt.code }}</QItemLabel>
<QItemLabel caption
>{{ scope.opt.code }} -
{{ scope.opt.town.name }} ({{
scope.opt.town.province.name
}},
{{
scope.opt.town.province.country.country
}})</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelectCreate>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('supplier.fiscalData.city')"
:options="townsLocationOptions"
v-model="data.city"
option-value="name"
option-label="name"
hide-selected
:rules="validate('supplier.city')"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt.name }}</QItemLabel>
<QItemLabel caption
>{{ scope.opt.name }},
{{ scope.opt.province.name }} ({{
scope.opt.province.country.country
}})</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<VnSelectFilter
:label="t('supplier.fiscalData.provinceFk')"
:options="provincesLocationOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.provinceFk"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt.name} (${scope.opt.country.country})`
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('supplier.fiscalData.country')"
:options="countriesOptions"
hide-selected
option-label="country"
option-value="id"
v-model="data.countryFk"
:rules="validate('postcode.countryFk')"
/>
</div>
<div class="col flex justify-around">
<QCheckbox
v-model="data.isTrucker"
:label="t('supplier.fiscalData.isTrucker')"
/>
<div class="row items-center">
<QCheckbox
v-model="data.isVies"
:label="t('supplier.fiscalData.isVies')"
/>
<QIcon name="info" size="xs" class="cursor-pointer q-ml-sm">
<QTooltip>
{{
t(
'When activating it, do not enter the country code in the ID field.'
)
}}
</QTooltip>
</QIcon>
</div>
</div>
</VnRow>
</template>
</FormModel>
</template>
<i18n>
es:
When activating it, do not enter the country code in the ID field.: Al activarlo, no informar el código del país en el campo nif
</i18n>

View File

@ -1 +1,6 @@
<template>Supplier log</template> <script setup>
import VnLog from 'src/components/common/VnLog.vue';
</script>
<template>
<VnLog model="Supplier" url="/SupplierLogs"></VnLog>
</template>

View File

@ -182,4 +182,3 @@ const isAdministrative = computed(() => {
</template> </template>
</CardSummary> </CardSummary>
</template> </template>
<style lang="scss" scoped></style>

View File

@ -6,7 +6,6 @@ import { useI18n } from 'vue-i18n';
import { QCheckbox, QIcon } from 'quasar'; import { QCheckbox, QIcon } from 'quasar';
import CardSummary from 'components/ui/CardSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import VnRow from 'components/ui/VnRow.vue';
import travelService from 'src/services/travel.service'; import travelService from 'src/services/travel.service';
import { toDate, toCurrency } from 'src/filters'; import { toDate, toCurrency } from 'src/filters';
@ -218,75 +217,47 @@ const openEntryDescriptor = () => {};
</template> </template>
<template #body> <template #body>
<QCard class="vn-one row justify-around" style="min-width: 100%"> <QCard class="vn-one">
<VnRow> <VnLv :label="t('globals.shipped')" :value="toDate(travel.shipped)" />
<div class="col"> <VnLv
<VnLv :label="t('globals.wareHouseOut')"
:label="t('globals.shipped')" :value="travel.warehouseOut?.name"
:value="toDate(travel.shipped)" />
<VnLv :label="t('travel.summary.delivered')" class="q-mb-xs">
<template #value>
<QCheckbox
v-model="travel.isDelivered"
disable
dense
class="full-width q-my-xs"
/> />
</div> </template>
<div class="col"> </VnLv>
<VnLv
:label="t('globals.wareHouseOut')"
:value="travel.warehouseOut?.name"
/>
</div>
<div class="col">
<VnLv :label="t('travel.summary.delivered')" class="q-mb-xs">
<template #value>
<QCheckbox
v-model="travel.isDelivered"
disable
dense
class="full-width q-my-xs"
/>
</template>
</VnLv>
</div>
</VnRow>
<VnRow>
<div class="col">
<VnLv
:label="t('globals.landed')"
:value="toDate(travel.landed)"
/>
</div>
<div class="col">
<VnLv
:label="t('globals.wareHouseIn')"
:value="travel.warehouseIn?.name"
/>
</div>
<div class="col">
<VnLv :label="t('travel.summary.received')" class="q-mb-xs">
<template #value>
<QCheckbox
v-model="travel.isReceived"
disable
dense
class="full-width q-mb-xs"
/>
</template>
</VnLv>
</div>
</VnRow>
<VnRow>
<div class="col">
<VnLv :label="t('globals.agency')" :value="travel.agency?.name" />
</div>
<div class="col">
<VnLv :label="t('globals.reference')" :value="travel.ref" />
</div>
<div class="col">
<VnLv label="m³" :value="travel.m3" />
</div>
<div class="col">
<VnLv :label="t('globals.totalEntries')" :value="travel.m3" />
</div>
</VnRow>
</QCard> </QCard>
<QCard class="vn-two" v-if="entriesTableRows.length > 0"> <QCard class="vn-one">
<VnLv :label="t('globals.landed')" :value="toDate(travel.landed)" />
<VnLv
:label="t('globals.wareHouseIn')"
:value="travel.warehouseIn?.name"
/>
<VnLv :label="t('travel.summary.received')" class="q-mb-xs">
<template #value>
<QCheckbox
v-model="travel.isReceived"
disable
dense
class="full-width q-mb-xs"
/>
</template>
</VnLv>
</QCard>
<QCard class="vn-one">
<VnLv :label="t('globals.agency')" :value="travel.agency?.name" />
<VnLv :label="t('globals.reference')" :value="travel.ref" />
<VnLv label="m³" :value="travel.m3" />
<VnLv :label="t('globals.totalEntries')" :value="travel.m3" />
</QCard>
<QCard class="full-width" v-if="entriesTableRows.length > 0">
<a class="header" :href="travelUrl + 'entry'"> <a class="header" :href="travelUrl + 'entry'">
{{ t('travel.summary.entries') }} {{ t('travel.summary.entries') }}
<QIcon name="open_in_new" color="primary" /> <QIcon name="open_in_new" color="primary" />

View File

@ -9,6 +9,7 @@ import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.v
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import ExtraCommunityFilter from './ExtraCommunityFilter.vue'; import ExtraCommunityFilter from './ExtraCommunityFilter.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import EntryDescriptorProxy from '../Entry/Card/EntryDescriptorProxy.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
@ -231,7 +232,7 @@ const navigateToTravelId = (id) => {
}; };
const stopEventPropagation = (event, col) => { const stopEventPropagation = (event, col) => {
if (!['ref', 'id', 'supplier'].includes(col.name)) return; if (!['ref', 'id', 'cargoSupplierNickname'].includes(col.name)) return;
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
}; };
@ -262,7 +263,16 @@ onMounted(async () => {
<div id="st-data"></div> <div id="st-data"></div>
<QSpace /> <QSpace />
<div id="st-actions"> <div id="st-actions">
<QBtn color="primary" icon-right="archive" no-caps @click="openReportPdf()" /> <QBtn
color="primary"
icon-right="picture_as_pdf"
no-caps
@click="openReportPdf()"
>
<QTooltip>
{{ t('Open as PDF') }}
</QTooltip>
</QBtn>
</div> </div>
</QToolbar> </QToolbar>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
@ -350,10 +360,9 @@ onMounted(async () => {
:props="props" :props="props"
class="secondary-row" class="secondary-row"
> >
<QTd <QTd>
><QBtn flat color="blue" class="col-content">{{ entry.id }}</QBtn> <QBtn flat color="blue" class="col-content">{{ entry.id }} </QBtn>
<!-- Cuando se cree el modulo relacionado a entries, crear su descriptor y colocarlo acá --> <EntryDescriptorProxy :id="entry.id" />
<!-- <EntryDescriptorProxy :id="entry.id"/> -->
</QTd> </QTd>
<QTd <QTd
><QBtn flat color="blue" class="col-content">{{ ><QBtn flat color="blue" class="col-content">{{
@ -415,4 +424,5 @@ es:
physicKg: KG físico physicKg: KG físico
shipped: F. envío shipped: F. envío
landed: F. llegada landed: F. llegada
Open as PDF: Abrir como PDF
</i18n> </i18n>

View File

@ -50,6 +50,7 @@ const newWorkerForm = ref({
}); });
const postcodeFetchDataRef = ref(null); const postcodeFetchDataRef = ref(null);
const townsFetchDataRef = ref(null);
const provincesOptions = ref([]); const provincesOptions = ref([]);
const townsOptions = ref([]); const townsOptions = ref([]);
const companiesOptions = ref([]); const companiesOptions = ref([]);
@ -66,8 +67,12 @@ const onBankEntityCreated = (data) => {
bankEntitiesOptions.value.push(data); bankEntitiesOptions.value.push(data);
}; };
const onPostcodeCreated = async () => { const onPostcodeCreated = async ({ code, provinceFk, townFk }, formData) => {
postcodeFetchDataRef.value.fetch(); await postcodeFetchDataRef.value.fetch();
await townsFetchDataRef.value.fetch();
formData.postcode = code;
formData.provinceFk = provinceFk;
formData.city = townsOptions.value.find((town) => town.id === townFk).name;
}; };
onMounted(async () => { onMounted(async () => {
@ -96,6 +101,7 @@ onMounted(async () => {
auto-load auto-load
/> />
<FetchData <FetchData
ref="townsFetchDataRef"
url="Towns/location" url="Towns/location"
@on-fetch="(data) => (townsOptions = data)" @on-fetch="(data) => (townsOptions = data)"
:filter="townsFilter" :filter="townsFilter"

View File

@ -134,6 +134,12 @@ export default {
component: () => component: () =>
import('src/pages/Supplier/Card/SupplierAddresses.vue'), import('src/pages/Supplier/Card/SupplierAddresses.vue'),
}, },
{
path: 'address/create',
name: 'SupplierAddressesCreate',
component: () =>
import('src/pages/Supplier/Card/SupplierAddressesCreate.vue'),
},
{ {
path: 'consumption', path: 'consumption',
name: 'SupplierConsumption', name: 'SupplierConsumption',
@ -154,6 +160,12 @@ export default {
component: () => component: () =>
import('src/pages/Supplier/Card/SupplierAgencyTerm.vue'), import('src/pages/Supplier/Card/SupplierAgencyTerm.vue'),
}, },
{
path: 'agency-term/create',
name: 'SupplierAgencyTermCreate',
component: () =>
import('src/pages/Supplier/Card/SupplierAgencyTermCreate.vue'),
},
], ],
}, },
], ],

View File

@ -152,32 +152,105 @@ export default {
}, },
{ {
path: 'consignees', path: 'consignees',
name: 'CustomerConsignees', name: 'ConsigneesCard',
meta: { redirect: { name: 'CustomerConsignees' },
title: 'consignees', children: [
icon: 'vn:delivery', {
}, path: '',
component: () => name: 'CustomerConsignees',
import('src/pages/Customer/Card/CustomerConsignees.vue'), meta: {
icon: 'vn:delivery',
title: 'consignees',
},
component: () =>
import('src/pages/Customer/Card/CustomerConsignees.vue'),
},
{
path: 'create',
name: 'CustomerConsigneeCreate',
meta: {
title: 'consignee-create',
},
component: () =>
import(
'src/pages/Customer/components/CustomerConsigneeCreate.vue'
),
},
{
path: ':consigneeId',
name: 'CustomerConsigneeEditCard',
redirect: { name: 'CustomerConsigneeEdit' },
children: [
{
path: 'edit',
name: 'CustomerConsigneeEdit',
meta: {
title: 'consignee-edit',
},
component: () =>
import(
'src/pages/Customer/components/CustomerConsigneeEdit.vue'
),
},
],
},
],
}, },
{ {
path: 'notes', path: 'notes',
name: 'CustomerNotes', name: 'NotesCard',
meta: { redirect: { name: 'CustomerNotes' },
title: 'notes', children: [
icon: 'vn:notes', {
}, path: '',
component: () => import('src/pages/Customer/Card/CustomerNotes.vue'), name: 'CustomerNotes',
meta: {
title: 'notes',
icon: 'vn:notes',
},
component: () =>
import('src/pages/Customer/Card/CustomerNotes.vue'),
},
{
path: 'create',
name: 'CustomerNoteCreate',
meta: {
title: 'note-create',
},
component: () =>
import(
'src/pages/Customer/components/CustomerNoteCreate.vue'
),
},
],
}, },
{ {
path: 'credits', path: 'credits',
name: 'CustomerCredits', name: 'CreditsCard',
meta: { redirect: { name: 'CustomerCredits' },
title: 'credits', children: [
icon: 'vn:credit', {
}, path: '',
component: () => name: 'CustomerCredits',
import('src/pages/Customer/Card/CustomerCredits.vue'), meta: {
title: 'credits',
icon: 'vn:credit',
},
component: () =>
import('src/pages/Customer/Card/CustomerCredits.vue'),
},
{
path: 'create',
name: 'CustomerCreditCreate',
meta: {
title: 'credit-create',
},
component: () =>
import(
'src/pages/Customer/components/CustomerCreditCreate.vue'
),
},
],
}, },
{ {
path: 'greuges', path: 'greuges',

View File

@ -11,7 +11,7 @@ export default {
redirect: { name: 'EntryMain' }, redirect: { name: 'EntryMain' },
menus: { menus: {
main: ['EntryList'], main: ['EntryList'],
card: [], card: ['EntryBasicData', 'EntryBuys', 'EntryNotes', 'EntryLog'],
}, },
children: [ children: [
{ {
@ -39,23 +39,63 @@ export default {
}, },
], ],
}, },
// { {
// name: 'EntryCard', name: 'EntryCard',
// path: ':id', path: ':id',
// component: () => import('src/pages/Entry/Card/EntryCard.vue'), component: () => import('src/pages/Entry/Card/EntryCard.vue'),
// redirect: { name: 'EntrySummary' }, redirect: { name: 'EntrySummary' },
// children: [ children: [
// { {
// name: 'EntrySummary', name: 'EntrySummary',
// path: 'summary', path: 'summary',
// meta: { meta: {
// title: 'summary', title: 'summary',
// icon: 'launch', icon: 'launch',
// }, },
// component: () => component: () => import('src/pages/Entry/Card/EntrySummary.vue'),
// import('src/pages/Entry/Card/EntrySummary.vue'), },
// }, {
// ], path: 'basic-data',
// }, name: 'EntryBasicData',
meta: {
title: 'basicData',
icon: 'vn:settings',
},
component: () => import('src/pages/Entry/Card/EntryBasicData.vue'),
},
{
path: 'buys',
name: 'EntryBuys',
meta: {
title: 'buys',
icon: 'vn:lines',
},
component: () => import('src/pages/Entry/Card/EntryBuys.vue'),
},
{
path: 'buys/import',
name: 'EntryBuysImport',
component: () => import('src/pages/Entry/Card/EntryBuysImport.vue'),
},
{
path: 'notes',
name: 'EntryNotes',
meta: {
title: 'notes',
icon: 'vn:notes',
},
component: () => import('src/pages/Entry/Card/EntryNotes.vue'),
},
{
path: 'log',
name: 'EntryLog',
meta: {
title: 'log',
icon: 'vn:History',
},
component: () => import('src/pages/Entry/Card/EntryLog.vue'),
},
],
},
], ],
}; };

View File

@ -10,15 +10,15 @@ export default {
component: RouterView, component: RouterView,
redirect: { name: 'RouteMain' }, redirect: { name: 'RouteMain' },
menus: { menus: {
main: ['CmrList'], main: ['RouteList', 'CmrList'],
card: [], card: ['RouteBasicData'],
}, },
children: [ children: [
{ {
path: '/route', path: '/route',
name: 'RouteMain', name: 'RouteMain',
component: () => import('src/pages/Route/RouteMain.vue'), component: () => import('src/pages/Route/RouteMain.vue'),
redirect: { name: 'CmrList' }, redirect: { name: 'RouteList' },
children: [ children: [
{ {
path: 'cmr', path: 'cmr',
@ -29,6 +29,49 @@ export default {
}, },
component: () => import('src/pages/Route/Cmr/CmrList.vue'), component: () => import('src/pages/Route/Cmr/CmrList.vue'),
}, },
{
path: 'list',
name: 'RouteList',
meta: {
title: 'RouteList',
icon: 'view_list',
},
component: () => import('src/pages/Route/RouteList.vue'),
},
{
path: 'create',
name: 'RouteCreate',
meta: {
title: 'create',
},
component: () => import('src/pages/Route/Card/RouteForm.vue'),
},
],
},
{
name: 'RouteCard',
path: ':id',
component: () => import('src/pages/Route/Card/RouteCard.vue'),
redirect: { name: 'RouteSummary' },
children: [
{
name: 'RouteBasicData',
path: 'basic-data',
meta: {
title: 'basicData',
icon: 'vn:settings',
},
component: () => import('pages/Route/Card/RouteForm.vue'),
},
{
name: 'RouteSummary',
path: 'summary',
meta: {
title: 'summary',
icon: 'open_in_new',
},
component: () => import('pages/Route/Card/RouteSummary.vue'),
},
], ],
}, },
], ],