Items summary #281

Merged
alexm merged 8 commits from :feature/ItemsSummary into dev 2024-04-25 12:01:08 +00:00
151 changed files with 19973 additions and 7714 deletions
Showing only changes of commit 861e4385bb - Show all commits

View File

@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2420.01]
## [2418.01] ## [2418.01]
## [2416.01] - 2024-04-18 ## [2416.01] - 2024-04-18
@ -22,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- (Tickets) => Se añade la opción de clonar ticket. #6951 - (Tickets) => Se añade la opción de clonar ticket. #6951
- (Parking) => Se añade la sección Parking. #5186 - (Parking) => Se añade la sección Parking. #5186
- (Rutas) => Se añade el campo "servida" a la tabla y se añade también a los filtros. #7130
### Changed ### Changed
### Fixed ### Fixed

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "24.18.0", "version": "24.20.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",
@ -32,6 +32,7 @@
"@intlify/unplugin-vue-i18n": "^0.8.1", "@intlify/unplugin-vue-i18n": "^0.8.1",
"@pinia/testing": "^0.1.2", "@pinia/testing": "^0.1.2",
"@quasar/app-vite": "^1.7.3", "@quasar/app-vite": "^1.7.3",
"@quasar/quasar-app-extension-qcalendar": "4.0.0-beta.15",
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0", "@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0",
"@vue/test-utils": "^2.4.4", "@vue/test-utils": "^2.4.4",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
{ {
"@quasar/testing-unit-vitest": { "@quasar/testing-unit-vitest": {
"options": [ "options": ["scripts"]
"scripts" },
] "@quasar/qcalendar": {}
}
} }

View File

@ -1,7 +1,6 @@
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import messages from 'src/i18n'; import messages from 'src/i18n';
import { locales } from 'src/i18n/handle';
const i18n = createI18n({ const i18n = createI18n({
locale: navigator.language || navigator.userLanguage, locale: navigator.language || navigator.userLanguage,
@ -13,9 +12,8 @@ const i18n = createI18n({
legacy: false, legacy: false,
}); });
export default boot(async ({ app }) => { export default boot(({ app }) => {
// Set i18n instance on app // Set i18n instance on app
await locales();
app.use(i18n); app.use(i18n);
}); });

View File

@ -24,6 +24,10 @@ const $props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
limit: {
type: Number,
default: 20,
},
saveUrl: { saveUrl: {
type: String, type: String,
default: null, default: null,
@ -76,6 +80,7 @@ defineExpose({
reset, reset,
hasChanges, hasChanges,
saveChanges, saveChanges,
getChanges,
}); });
async function fetch(data) { async function fetch(data) {
@ -260,6 +265,7 @@ watch(formUrl, async () => {
<template> <template>
<VnPaginate <VnPaginate
:url="url" :url="url"
:limit="limit"
v-bind="$attrs" v-bind="$attrs"
@on-fetch="fetch" @on-fetch="fetch"
:skeleton="false" :skeleton="false"

View File

@ -81,6 +81,7 @@ const emit = defineEmits(['onFetch', 'onDataSaved']);
const componentIsRendered = ref(false); const componentIsRendered = ref(false);
onMounted(async () => { onMounted(async () => {
originalData.value = $props.formInitialData;
nextTick(() => { nextTick(() => {
componentIsRendered.value = true; componentIsRendered.value = true;
}); });
@ -126,7 +127,7 @@ const isLoading = ref(false);
// Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas // Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas
const isResetting = ref(false); const isResetting = ref(false);
const hasChanges = ref(!$props.observeFormChanges); const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({ ...$props.formInitialData }); const originalData = ref({});
const formData = computed(() => state.get($props.model)); const formData = computed(() => state.get($props.model));
const formUrl = computed(() => $props.url); const formUrl = computed(() => $props.url);
const defaultButtons = computed(() => ({ const defaultButtons = computed(() => ({
@ -154,14 +155,18 @@ const startFormWatcher = () => {
}; };
async function fetch() { async function fetch() {
const { data } = await axios.get($props.url, { try {
params: { filter: JSON.stringify($props.filter) }, const { data } = await axios.get($props.url, {
}); params: { filter: JSON.stringify($props.filter) },
});
state.set($props.model, data);
originalData.value = data && JSON.parse(JSON.stringify(data));
state.set($props.model, data); emit('onFetch', state.get($props.model));
originalData.value = data && JSON.parse(JSON.stringify(data)); } catch (error) {
state.set($props.model, {});
emit('onFetch', state.get($props.model)); originalData.value = {};
}
} }
async function save() { async function save() {

View File

@ -90,11 +90,18 @@ defineExpose({
type="reset" type="reset"
color="primary" color="primary"
flat flat
class="q-ml-sm"
:disabled="isLoading" :disabled="isLoading"
:loading="isLoading" :loading="isLoading"
v-close-popup v-close-popup
/> />
<QBtn
:label="t('globals.save')"
type="submit"
color="primary"
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
/>
</div> </div>
</template> </template>
</FormModel> </FormModel>

View File

@ -56,14 +56,6 @@ const closeForm = () => {
<p>{{ subtitle }}</p> <p>{{ subtitle }}</p>
<slot name="form-inputs" /> <slot name="form-inputs" />
<div class="q-mt-lg row justify-end"> <div class="q-mt-lg row justify-end">
<QBtn
v-if="defaultSubmitButton"
:label="customSubmitButtonLabel || t('globals.save')"
type="submit"
color="primary"
:disabled="isLoading"
:loading="isLoading"
/>
<QBtn <QBtn
v-if="defaultCancelButton" v-if="defaultCancelButton"
:label="t('globals.cancel')" :label="t('globals.cancel')"
@ -74,6 +66,14 @@ const closeForm = () => {
:loading="isLoading" :loading="isLoading"
v-close-popup v-close-popup
/> />
<QBtn
v-if="defaultSubmitButton"
:label="customSubmitButtonLabel || t('globals.save')"
type="submit"
color="primary"
:disabled="isLoading"
:loading="isLoading"
/>
<slot name="customButtons" /> <slot name="customButtons" />
</div> </div>
</QCard> </QCard>

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { onMounted, ref } from 'vue'; import { onMounted, ref, reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { QSeparator, useQuasar } from 'quasar'; import { QSeparator, useQuasar } from 'quasar';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
@ -22,6 +22,8 @@ const props = defineProps({
}, },
}); });
const expansionItemElements = reactive({});
onMounted(async () => { onMounted(async () => {
await navigation.fetchPinned(); await navigation.fetchPinned();
getRoutes(); getRoutes();
@ -108,6 +110,10 @@ async function togglePinned(item, event) {
type: 'positive', type: 'positive',
}); });
} }
const handleItemExpansion = (itemName) => {
expansionItemElements[itemName].scrollToLastElement();
};
</script> </script>
<template> <template>
@ -213,8 +219,12 @@ async function togglePinned(item, event) {
:icon="item.icon" :icon="item.icon"
:label="t(item.title)" :label="t(item.title)"
:content-inset-level="0.5" :content-inset-level="0.5"
@after-show="handleItemExpansion(item.name)"
> >
<LeftMenuItemGroup :item="item" /> <LeftMenuItemGroup
:ref="(el) => (expansionItemElements[item.name] = el)"
:item="item"
/>
</QExpansionItem> </QExpansionItem>
</QList> </QList>
</template> </template>

View File

@ -1,6 +1,7 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed, ref } from 'vue';
import LeftMenuItem from './LeftMenuItem.vue'; import LeftMenuItem from './LeftMenuItem.vue';
import { elementIsVisibleInViewport } from 'src/composables/elementIsVisibleInViewport';
const props = defineProps({ const props = defineProps({
item: { item: {
@ -13,10 +14,27 @@ const props = defineProps({
}, },
}); });
const groupEnd = ref(null);
const scrollToLastElement = () => {
if (groupEnd.value && !elementIsVisibleInViewport(groupEnd.value)) {
groupEnd.value.scrollIntoView({
behavior: 'smooth',
block: 'end',
});
}
};
const item = computed(() => props.item); // eslint-disable-line vue/no-dupe-keys const item = computed(() => props.item); // eslint-disable-line vue/no-dupe-keys
defineExpose({
scrollToLastElement,
});
</script> </script>
<template> <template>
<template v-for="section in item.children" :key="section.name"> <template v-for="section in item.children" :key="section.name">
<LeftMenuItem :item="section" /> <LeftMenuItem :item="section" />
</template> </template>
<div ref="groupEnd" />
</template> </template>

View File

@ -187,7 +187,7 @@ const columns = computed(() => [
downloadFile( downloadFile(
prop.row.id, prop.row.id,
$props.downloadModel, $props.downloadModel,
null, undefined,
prop.row.download prop.row.download
), ),
}, },

View File

@ -26,7 +26,7 @@ const value = computed({
emit('update:modelValue', value); emit('update:modelValue', value);
}, },
}); });
const hover = ref(false);
const styleAttrs = computed(() => { const styleAttrs = computed(() => {
return $props.isOutlined return $props.isOutlined
? { ? {
@ -41,6 +41,10 @@ const onEnterPress = () => {
emit('keyup.enter'); emit('keyup.enter');
}; };
const handleValue = (val = null) => {
value.value = val;
};
const focus = () => { const focus = () => {
vnInputRef.value.focus(); vnInputRef.value.focus();
}; };
@ -51,20 +55,33 @@ defineExpose({
</script> </script>
<template> <template>
<QInput <div
ref="vnInputRef" @mouseover="hover = true"
v-model="value" @mouseleave="hover = false"
v-bind="{ ...$attrs, ...styleAttrs }"
type="text"
:class="{ required: $attrs.required }"
@keyup.enter="onEnterPress()"
:rules="$attrs.required ? [requiredFieldRule] : null" :rules="$attrs.required ? [requiredFieldRule] : null"
> >
<template v-if="$slots.prepend" #prepend> <QInput
<slot name="prepend" /> ref="vnInputRef"
</template> v-model="value"
<template v-if="$slots.append" #append> v-bind="{ ...$attrs, ...styleAttrs }"
<slot name="append" /> :type="$attrs.type"
</template> :class="{ required: $attrs.required }"
</QInput> @keyup.enter="onEnterPress()"
:clearable="false"
>
<template v-if="$slots.prepend" #prepend>
<slot name="prepend" />
</template>
<template #append>
<slot name="append" v-if="$slots.append" />
<QIcon
name="close"
size="xs"
v-if="hover && value"
@click="handleValue(null)"
></QIcon>
</template>
</QInput>
</div>
</template> </template>

View File

@ -1,7 +1,6 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import VnInput from 'components/common/VnInput.vue'; import isValidDate from 'filters/isValidDate';
import isValidDate from "filters/isValidDate";
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -17,6 +16,8 @@ const props = defineProps({
default: false, default: false,
}, },
}); });
const hover = ref(false);
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const joinDateAndTime = (date, time) => { const joinDateAndTime = (date, time) => {
@ -77,30 +78,39 @@ const styleAttrs = computed(() => {
</script> </script>
<template> <template>
<VnInput <div @mouseover="hover = true" @mouseleave="hover = false">
class="vn-input-date" <QInput
:model-value="displayDate(value)" class="vn-input-date"
v-bind="{ ...$attrs, ...styleAttrs }" readonly
readonly :model-value="displayDate(value)"
@click="isPopupOpen = true" v-bind="{ ...$attrs, ...styleAttrs }"
> @click="isPopupOpen = true"
<template #append> >
<QIcon name="event" class="cursor-pointer"> <template #append>
<QPopupProxy <QIcon
v-model="isPopupOpen" name="close"
cover size="xs"
transition-show="scale" v-if="hover && value"
transition-hide="scale" @click="onDateUpdate(null)"
:no-parent-event="props.readonly" ></QIcon>
> <QIcon name="event" class="cursor-pointer">
<QDate <QPopupProxy
:model-value="formatDate(value)" v-model="isPopupOpen"
@update:model-value="onDateUpdate" cover
/> transition-show="scale"
</QPopupProxy> transition-hide="scale"
</QIcon> :no-parent-event="props.readonly"
</template> >
</VnInput> <QDate
:today-btn="true"
:model-value="formatDate(value)"
@update:model-value="onDateUpdate"
/>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</div>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@ -2,7 +2,6 @@
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import isValidDate from 'filters/isValidDate'; import isValidDate from 'filters/isValidDate';
import VnInput from "components/common/VnInput.vue";
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -20,6 +19,7 @@ const props = defineProps({
}); });
const { t } = useI18n(); const { t } = useI18n();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const value = computed({ const value = computed({
get() { get() {
return props.modelValue; return props.modelValue;
@ -27,12 +27,7 @@ const value = computed({
set(value) { set(value) {
const [hours, minutes] = value.split(':'); const [hours, minutes] = value.split(':');
const date = new Date(props.modelValue); const date = new Date(props.modelValue);
date.setHours( date.setHours(Number.parseInt(hours) || 0, Number.parseInt(minutes) || 0, 0, 0);
Number.parseInt(hours) || 0,
Number.parseInt(minutes) || 0,
0,
0
);
emit('update:modelValue', value ? date.toISOString() : null); emit('update:modelValue', value ? date.toISOString() : null);
}, },
}); });
@ -71,7 +66,7 @@ const styleAttrs = computed(() => {
</script> </script>
<template> <template>
<VnInput <QInput
class="vn-input-time" class="vn-input-time"
readonly readonly
:model-value="formatTime(value)" :model-value="formatTime(value)"
@ -79,7 +74,7 @@ const styleAttrs = computed(() => {
@click="isPopupOpen = true" @click="isPopupOpen = true"
> >
<template #append> <template #append>
<QIcon name="event" class="cursor-pointer"> <QIcon name="schedule" class="cursor-pointer">
<QPopupProxy <QPopupProxy
v-model="isPopupOpen" v-model="isPopupOpen"
cover cover
@ -111,7 +106,7 @@ const styleAttrs = computed(() => {
</QPopupProxy> </QPopupProxy>
</QIcon> </QIcon>
</template> </template>
</VnInput> </QInput>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@ -21,7 +21,8 @@ const props = defineProps({
}, },
template: { template: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
locale: { locale: {
type: String, type: String,
@ -49,7 +50,7 @@ updateMessage();
function updateMessage() { function updateMessage() {
const params = props.data; const params = props.data;
const key = `templates['${props.template}']`; const key = props.template ? `templates['${props.template}']` : '';
message.value = t(key, params, { locale: locale.value }); message.value = t(key, params, { locale: locale.value });
} }
@ -104,15 +105,14 @@ async function send() {
map-options map-options
:input-debounce="0" :input-debounce="0"
rounded rounded
outlined
dense dense
/> />
</QCardSection> </QCardSection>
<QCardSection class="q-pb-xs"> <QCardSection class="q-pb-xs">
<VnInput :label="t('Phone')" v-model="phone" is-outlined /> <VnInput :label="t('Phone')" v-model="phone" />
</QCardSection> </QCardSection>
<QCardSection class="q-pb-xs"> <QCardSection class="q-pb-xs">
<VnInput v-model="subject" :label="t('Subject')" is-outlined /> <VnInput v-model="subject" :label="t('Subject')" />
</QCardSection> </QCardSection>
<QCardSection class="q-mb-md" q-input> <QCardSection class="q-mb-md" q-input>
<QInput <QInput
@ -125,7 +125,6 @@ async function send() {
:bottom-slots="true" :bottom-slots="true"
:rules="[(value) => value.length < maxLength || 'Error!']" :rules="[(value) => value.length < maxLength || 'Error!']"
stack-label stack-label
outlined
autofocus autofocus
> >
<template #append> <template #append>
@ -135,6 +134,11 @@ async function send() {
@click="message = ''" @click="message = ''"
class="cursor-pointer" class="cursor-pointer"
/> />
<QIcon name="info" class="cursor-pointer">
<QTooltip>
{{ t('messageTooltip') }}
</QTooltip>
</QIcon>
</template> </template>
<template #counter> <template #counter>
<QChip :color="color" dense> <QChip :color="color" dense>
@ -184,18 +188,20 @@ en:
es: Spanish es: Spanish
fr: French fr: French
pt: Portuguese pt: Portuguese
messageTooltip: Special characters like accents counts as multiple
es: es:
Send SMS: Enviar SMS Send SMS: Enviar SMS
Language: Idioma Language: Idioma
Phone: Móvil Phone: Móvil
Subject: Asunto Subject: Asunto
Message: Mensaje Message: Mensaje
messageTooltip: Carácteres especiales como acentos cuentan como varios
templates: templates:
pendingPayment: 'Su pedido está pendiente de pago. pendingPayment: 'Su pedido está pendiente de pago.
Por favor, entre en la página web y efectue el pago con tarjeta. Muchas gracias.' Por favor, entre en la página web y efectue el pago con tarjeta. Muchas gracias.'
minAmount: 'Es necesario un importe mínimo de 50 (Sin IVA) en su pedido minAmount: 'Es necesario un importe mínimo de 50 (Sin IVA) en su pedido
{ orderId } del día { shipped } para recibirlo sin portes adicionales.' { orderId } con llegada { landing } para recibirlo sin portes adicionales.'
orderChanges: 'Pedido {orderId} día { shipped }: { changes }' orderChanges: 'Pedido {orderId} con llegada estimada día { landing }: { changes }'
en: Inglés en: Inglés
es: Español es: Español
fr: Francés fr: Francés
@ -207,12 +213,14 @@ fr:
Phone: Mobile Phone: Mobile
Subject: Affaire Subject: Affaire
Message: Message Message: Message
messageTooltip: Les caractères spéciaux comme les accents comptent comme plusieurs
templates: templates:
pendingPayment: 'Votre commande est en attente de paiement. pendingPayment: 'Verdnatura : Commande en attente de règlement. Veuillez régler votre commande avant 9h.
Veuillez vous connecter sur le site web et effectuer le paiement par carte. Merci beaucoup.' Sinon elle sera décalée en fonction de vos jours de livraison . Merci'
minAmount: 'Un montant minimum de 50 (TVA non incluse) est requis pour votre commande minAmount: 'Verdnatura vous rappelle :
{ orderId } du { shipped } afin de la recevoir sans frais de port supplémentaires.' Montant minimum nécessaire de 50 euros pour recevoir la commande { orderId } livraison { landing }.
orderChanges: 'Commande { orderId } du { shipped }: { changes }' Merci.'
orderChanges: 'Commande {orderId} livraison {landing} indisponible/s. Désolés pour le dérangement.'
en: Anglais en: Anglais
es: Espagnol es: Espagnol
fr: Français fr: Français
@ -224,12 +232,13 @@ pt:
Phone: Móvel Phone: Móvel
Subject: Assunto Subject: Assunto
Message: Mensagem Message: Mensagem
messageTooltip: Caracteres especiais como acentos contam como vários
templates: templates:
pendingPayment: 'Seu pedido está pendente de pagamento. pendingPayment: 'Seu pedido está pendente de pagamento.
Por favor, acesse o site e faça o pagamento com cartão. Muito obrigado.' Por favor, acesse o site e faça o pagamento com cartão. Muito obrigado.'
minAmount: 'É necessário um valor mínimo de 50 (sem IVA) em seu pedido minAmount: 'É necessário um valor mínimo de 50 (sem IVA) em seu pedido
{ orderId } do dia { shipped } para recebê-lo sem custos de envio adicionais.' { orderId } do dia { landing } para recebê-lo sem custos de envio adicionais.'
orderChanges: 'Pedido { orderId } dia { shipped }: { changes }' orderChanges: 'Pedido { orderId } com chegada dia { landing }: { changes }'
en: Inglês en: Inglês
es: Espanhol es: Espanhol
fr: Francês fr: Francês

View File

@ -47,7 +47,7 @@ const arrayData = useArrayData($props.dataKey || $props.module, {
skip: 0, skip: 0,
}); });
const { store } = arrayData; const { store } = arrayData;
const entity = computed(() => store.data); const entity = computed(() =>Array.isArray( store.data) ? store.data[0] : store.data);
const isLoading = ref(false); const isLoading = ref(false);
defineExpose({ defineExpose({
@ -67,7 +67,7 @@ async function getData() {
try { try {
const { data } = await arrayData.fetch({ append: false, updateRouter: false }); const { data } = await arrayData.fetch({ append: false, updateRouter: false });
state.set($props.dataKey, data); state.set($props.dataKey, data);
emit('onFetch', data); emit('onFetch', Array.isArray(data) ? data[0] : data);
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }

View File

@ -32,7 +32,7 @@ const arrayData = useArrayData(props.dataKey || route.meta.moduleName, {
skip: 0, skip: 0,
}); });
const { store } = arrayData; const { store } = arrayData;
const entity = computed(() => store.data); const entity = computed(() => Array.isArray(store.data) ? store.data[0] : store.data);
const isLoading = ref(false); const isLoading = ref(false);
defineExpose({ defineExpose({

View File

@ -0,0 +1,168 @@
<script setup>
import { computed } from 'vue';
import { useQuasar } from 'quasar';
import '@quasar/quasar-ui-qcalendar/src/QCalendarVariables.sass';
const $props = defineProps({
bordered: {
type: Boolean,
default: false,
},
transparentBackground: {
type: Boolean,
default: false,
},
viewCustomization: {
type: String,
default: '',
},
});
const $q = useQuasar();
// El objetivo de asignar las clases de personalización desde el wrapper es no tener conflictos entre vistas que usen el mismo componente
const viewCustomizationClasses = {
workerCalendar: 'worker-calendar-customizations',
};
const containerClasses = computed(() => {
const classes = ['main-container-background'];
if (viewCustomizationClasses[$props.viewCustomization])
classes.push(viewCustomizationClasses[$props.viewCustomization]);
if ($props.bordered) classes.push('--bordered');
if ($props.transparentBackground) classes.push('transparent-background');
else classes.push($q.dark.isActive ? '--dark' : '--light');
return classes;
});
</script>
<template>
<div :class="containerClasses">
<div class="nav-container row"><slot name="header" /></div>
<slot name="calendar" />
</div>
</template>
<style lang="scss">
@import '../../css/quasar.variables.scss';
:root {
// Cambia los colores del día actual del calendario por los de salix
--calendar-border-current-dark: #84d0e2 2px solid;
--calendar-border-current: #84d0e2 2px solid;
--calendar-current-color-dark: #84d0e2;
// Colores de fondo del calendario en dark mode
--calendar-outside-background-dark: #222;
--calendar-background-dark: #222;
}
// Clases para modificar el color de fecha seleccionada en componente QCalendarMonth
.q-dark div .q-calendar-mini .q-calendar-month__day.q-selected .q-calendar__button {
background-color: $primary !important;
color: white !important;
}
.q-calendar-mini .q-calendar-month__day.q-selected .q-calendar__button {
background-color: $primary !important;
color: white !important;
}
.q-calendar-month__head--weekday {
// Transforma los nombres de los días de la semana a mayúsculas
text-transform: capitalize;
}
.transparent-background {
--calendar-background-dark: transparent;
--calendar-background: transparent;
--calendar-outside-background-dark: transparent;
}
.q-calendar__button {
&:hover {
background-color: var(--vn-accent-color);
cursor: pointer;
}
}
.main-container-background {
--calendar-current-background-dark: transparent;
&.--dark {
background-color: var(--calendar-background-dark);
}
&.--light {
background-color: var(--calendar-background);
}
&.--bordered {
border: 1px solid #222;
}
}
.worker-calendar-customizations {
.q-calendar__button {
width: 32px;
height: 32px;
font-size: 13px;
&:hover {
background-color: var(--vn-accent-color);
cursor: pointer;
}
}
.q-calendar-month__week--days > div:nth-child(6),
.q-calendar-month__week--days > div:nth-child(7) {
// Cambia el color de los días sábado y domingo
color: #777777;
}
.q-calendar-month__week--wrapper {
margin-bottom: 4px;
}
.q-calendar-month__workweek {
height: 32px;
display: flex;
justify-content: center;
}
.q-calendar__button--bordered {
color: $info !important;
}
.q-calendar-month__day--content {
position: absolute;
top: 1;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
.q-outside .calendar-event {
display: none;
}
.q-calendar-month__workweek,
.q-calendar-month__head--workweek,
.q-calendar-month__head--weekday.q-calendar__center.q-calendar__ellipsis {
text-transform: capitalize;
color: #777;
font-weight: bold;
font-size: 0.8rem;
text-align: center;
}
}
.nav-container {
display: flex;
align-items: center;
&.--bordered {
border: 1px solid black;
}
}
</style>

View File

@ -1,11 +1,12 @@
<script setup> <script setup>
import VnAvatar from 'src/components/ui/VnAvatar.vue'; import VnAvatar from 'src/components/ui/VnAvatar.vue';
import { toDateHour } from 'src/filters'; import { toDateHourMin } from 'src/filters';
import { ref } from 'vue'; import { ref } from 'vue';
import axios from 'axios'; import axios from 'axios';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnPaginate from './VnPaginate.vue'; import VnPaginate from './VnPaginate.vue';
import VnUserLink from '../ui/VnUserLink.vue'; import VnUserLink from '../ui/VnUserLink.vue';
import { useState } from 'src/composables/useState';
const $props = defineProps({ const $props = defineProps({
url: { type: String, default: null }, url: { type: String, default: null },
@ -13,8 +14,10 @@ const $props = defineProps({
body: { type: Object, default: () => {} }, body: { type: Object, default: () => {} },
addNote: { type: Boolean, default: false }, addNote: { type: Boolean, default: false },
}); });
const { t } = useI18n(); const { t } = useI18n();
const noteModal = ref(false); const state = useState();
const currentUser = ref(state.getUser());
const newNote = ref(''); const newNote = ref('');
const vnPaginateRef = ref(); const vnPaginateRef = ref();
@ -22,98 +25,83 @@ async function insert() {
const body = $props.body; const body = $props.body;
Object.assign(body, { text: newNote.value }); Object.assign(body, { text: newNote.value });
await axios.post($props.url, body); await axios.post($props.url, body);
vnPaginateRef.value.fetch(); await vnPaginateRef.value.fetch();
newNote.value = ''; newNote.value = '';
} }
</script> </script>
<template> <template>
<div class="column items-center full-height full-width"> <QCard class="q-pa-xs q-mb-xl full-width" v-if="$props.addNote">
<VnPaginate <QCardSection horizontal>
:data-key="$props.url" <VnAvatar :worker-id="currentUser.id" size="md" />
:url="$props.url" <div class="full-width row justify-between q-pa-xs">
order="created DESC" <VnUserLink :name="t('New note')" :worker-id="currentUser.id" />
:limit="20" {{ t('globals.now') }}
:filter="$props.filter" </div>
auto-load </QCardSection>
ref="vnPaginateRef" <QCardSection class="q-pa-xs q-my-none q-py-none" horizontal>
> <QInput
<template #body="{ rows }"> v-model="newNote"
<div class="column items-center full-width"> class="full-width"
<QCard type="textarea"
class="q-pa-xs q-mb-sm full-width" :label="t('Add note here...')"
v-for="(note, index) in rows" filled
:key="index" size="lg"
> autogrow
<QCardSection horizontal> autofocus
<slot name="picture"> @keyup.ctrl.enter.stop="insert"
<VnAvatar clearable
:descriptor="false" >
:worker-id="note.workerFk" <template #append
size="md" ><QBtn
/> :title="t('Save (ctrl + Enter)')"
</slot> icon="save"
<div class="full-width row justify-between q-pa-xs"> color="primary"
<VnUserLink
:name="`${note.worker.user.nickname}`"
:worker-id="note.worker.id"
/>
<slot name="actions">
{{ toDateHour(note.created) }}
</slot>
</div>
</QCardSection>
<QCardSection class="q-pa-xs q-my-none q-py-none">
<slot name="text">
{{ note.text }}
</slot>
</QCardSection>
</QCard>
</div>
</template>
</VnPaginate>
<QPageSticky position="bottom-right" :offset="[25, 25]" v-if="addNote">
<QBtn color="primary" icon="add" size="lg" round @click="noteModal = true" />
</QPageSticky>
<QDialog v-model="noteModal" @hide="newNote = ''">
<QCard>
<QCardSection>
<QItem class="q-px-none">
<span class="text-primary text-h6 full-width">
<QIcon name="draft" class="q-mr-xs" />
{{ t('Add note') }}
</span>
<QBtn icon="close" flat round dense v-close-popup />
</QItem>
</QCardSection>
<QCardSection>
<QInput
autofocus
type="textarea"
:label="t('Add note here...')"
filled
size="lg"
autogrow
v-model="newNote"
></QInput>
</QCardSection>
<QCardActions class="justify-end q-mr-sm">
<QBtn
flat flat
:label="t('globals.close')"
color="primary"
v-close-popup
/>
<QBtn
:label="t('globals.save')"
color="primary"
v-close-popup
@click="insert" @click="insert"
/> />
</QCardActions> </template>
</QCard> </QInput>
</QDialog> </QCardSection>
</div> </QCard>
<VnPaginate
:data-key="$props.url"
:url="$props.url"
order="created DESC"
:limit="0"
:filter="$props.filter"
auto-load
ref="vnPaginateRef"
class="show"
v-bind="$attrs"
>
<template #body="{ rows }">
<TransitionGroup name="list" tag="div" class="column items-center full-width">
<QCard
class="q-pa-xs q-mb-sm full-width"
v-for="(note, index) in rows"
:key="note.id ?? index"
>
<QCardSection horizontal>
<VnAvatar
:descriptor="false"
:worker-id="note.workerFk"
size="md"
/>
<div class="full-width row justify-between q-pa-xs">
<VnUserLink
:name="`${note.worker.user.nickname}`"
:worker-id="note.worker.id"
/>
{{ toDateHourMin(note.created) }}
</div>
</QCardSection>
<QCardSection class="q-pa-xs q-my-none q-py-none">
{{ note.text }}
</QCardSection>
</QCard>
</TransitionGroup>
</template>
</VnPaginate>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.q-card { .q-card {
@ -128,9 +116,20 @@ async function insert() {
.q-dialog .q-card { .q-dialog .q-card {
width: 400px; width: 400px;
} }
.list-enter-active,
.list-leave-active {
transition: all 1s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
background-color: $primary;
}
</style> </style>
<i18n> <i18n>
es: es:
Add note here...: Añadir nota aquí... Add note here...: Añadir nota aquí...
Add note: Añadir nota New note: Nueva nota
Save (ctrl + Enter): Guardar (Ctrl + Intro)
</i18n> </i18n>

View File

@ -77,7 +77,6 @@ const arrayData = useArrayData(props.dataKey, {
userParams: props.userParams, userParams: props.userParams,
exprBuilder: props.exprBuilder, exprBuilder: props.exprBuilder,
}); });
const hasMoreData = ref();
const store = arrayData.store; const store = arrayData.store;
onMounted(() => { onMounted(() => {
@ -97,7 +96,7 @@ const addFilter = async (filter, params) => {
async function fetch() { async function fetch() {
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
if (!arrayData.hasMoreData.value) { if (!store.hasMoreData) {
isLoading.value = false; isLoading.value = false;
} }
emit('onFetch', store.data); emit('onFetch', store.data);
@ -110,8 +109,8 @@ async function paginate() {
isLoading.value = true; isLoading.value = true;
await arrayData.loadMore(); await arrayData.loadMore();
if (!arrayData.hasMoreData.value) { if (!store.hasMoreData) {
if (store.userParamsChanged) arrayData.hasMoreData.value = true; if (store.userParamsChanged) store.hasMoreData = true;
store.userParamsChanged = false; store.userParamsChanged = false;
endPagination(); endPagination();
return; return;
@ -127,15 +126,12 @@ async function paginate() {
} }
function endPagination() { function endPagination() {
hasMoreData.value = arrayData.hasMoreData.value;
isLoading.value = false; isLoading.value = false;
emit('onFetch', store.data); emit('onFetch', store.data);
emit('onPaginate'); emit('onPaginate');
} }
async function onLoad(index, done) { async function onLoad(index, done) {
if (!store.data) { if (!store.data) return done();
return done();
}
if (store.data.length === 0 || !props.url) return done(false); if (store.data.length === 0 || !props.url) return done(false);
@ -143,7 +139,7 @@ async function onLoad(index, done) {
await paginate(); await paginate();
let isDone = false; let isDone = false;
if (store.userParamsChanged) isDone = !arrayData.hasMoreData.value; if (store.userParamsChanged) isDone = !store.hasMoreData;
done(isDone); done(isDone);
} }
@ -187,8 +183,8 @@ defineExpose({ fetch, addFilter });
v-if="store.data" v-if="store.data"
@load="onLoad" @load="onLoad"
:offset="offset" :offset="offset"
:disable="disableInfiniteScroll || !arrayData.hasMoreData.value"
class="full-width" class="full-width"
:disable="disableInfiniteScroll || !store.hasMoreData"
v-bind="$attrs" v-bind="$attrs"
> >
<slot name="body" :rows="store.data"></slot> <slot name="body" :rows="store.data"></slot>
@ -196,7 +192,10 @@ defineExpose({ fetch, addFilter });
<QSpinner color="orange" size="md" /> <QSpinner color="orange" size="md" />
</div> </div>
</QInfiniteScroll> </QInfiniteScroll>
<div v-if="!isLoading && hasMoreData" class="w-full flex justify-center q-mt-md"> <div
v-if="!isLoading && store.hasMoreData"
class="w-full flex justify-center q-mt-md"
>
<QBtn color="primary" :label="t('Load more data')" @click="paginate()" /> <QBtn color="primary" :label="t('Load more data')" @click="paginate()" />
</div> </div>
</template> </template>

View File

@ -0,0 +1,6 @@
export const elementIsVisibleInViewport = (el) => {
const { top, left, bottom, right } = el.getBoundingClientRect();
const { innerHeight, innerWidth } = window;
return top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth;
};

View File

@ -9,12 +9,9 @@ const arrayDataStore = useArrayDataStore();
export function useArrayData(key, userOptions) { export function useArrayData(key, userOptions) {
if (!key) throw new Error('ArrayData: A key is required to use this composable'); if (!key) throw new Error('ArrayData: A key is required to use this composable');
if (!arrayDataStore.get(key)) { if (!arrayDataStore.get(key)) arrayDataStore.set(key);
arrayDataStore.set(key);
}
const store = arrayDataStore.get(key); const store = arrayDataStore.get(key);
const hasMoreData = ref(false);
const route = useRoute(); const route = useRoute();
let canceller = null; let canceller = null;
@ -22,6 +19,7 @@ export function useArrayData(key, userOptions) {
onMounted(() => { onMounted(() => {
setOptions(); setOptions();
store.skip = 0;
const query = route.query; const query = route.query;
if (query.params) { if (query.params) {
@ -29,9 +27,7 @@ export function useArrayData(key, userOptions) {
} }
}); });
if (key && userOptions) { if (key && userOptions) setOptions();
setOptions();
}
function setOptions() { function setOptions() {
const allowedOptions = [ const allowedOptions = [
@ -96,8 +92,7 @@ export function useArrayData(key, userOptions) {
}); });
const { limit } = filter; const { limit } = filter;
store.hasMoreData = limit && response.data.length >= limit;
hasMoreData.value = response.data.length >= limit;
if (append) { if (append) {
if (!store.data) store.data = []; if (!store.data) store.data = [];
@ -169,7 +164,7 @@ export function useArrayData(key, userOptions) {
} }
async function loadMore() { async function loadMore() {
if (!hasMoreData.value) return; if (!store.hasMoreData) return;
store.skip = store.limit * page.value; store.skip = store.limit * page.value;
page.value += 1; page.value += 1;
@ -211,7 +206,6 @@ export function useArrayData(key, userOptions) {
destroy, destroy,
loadMore, loadMore,
store, store,
hasMoreData,
totalRows, totalRows,
updateStateParams, updateStateParams,
isLoading, isLoading,

View File

@ -3,31 +3,29 @@ import { useRole } from './useRole';
import { useUserConfig } from './useUserConfig'; import { useUserConfig } from './useUserConfig';
import axios from 'axios'; import axios from 'axios';
import useNotify from './useNotify'; import useNotify from './useNotify';
const TOKEN_MULTIMEDIA = 'tokenMultimedia';
const TOKEN = 'token';
export function useSession() { export function useSession() {
const { notify } = useNotify(); const { notify } = useNotify();
function getToken() { function getToken() {
const localToken = localStorage.getItem('token'); const localToken = localStorage.getItem(TOKEN);
const sessionToken = sessionStorage.getItem('token'); const sessionToken = sessionStorage.getItem(TOKEN);
return localToken || sessionToken || ''; return localToken || sessionToken || '';
} }
function getTokenMultimedia() { function getTokenMultimedia() {
const localTokenMultimedia = localStorage.getItem('token'); // Temporal const localTokenMultimedia = localStorage.getItem(TOKEN_MULTIMEDIA);
const sessionTokenMultimedia = sessionStorage.getItem('token'); // Temporal const sessionTokenMultimedia = sessionStorage.getItem(TOKEN_MULTIMEDIA);
return localTokenMultimedia || sessionTokenMultimedia || ''; return localTokenMultimedia || sessionTokenMultimedia || '';
} }
function setToken(data) { function setToken(data) {
if (data.keepLogin) { const storage = data.keepLogin ? localStorage : sessionStorage;
localStorage.setItem('token', data.token); storage.setItem(TOKEN, data.token);
localStorage.setItem('tokenMultimedia', data.tokenMultimedia); storage.setItem(TOKEN_MULTIMEDIA, data.tokenMultimedia);
} else {
sessionStorage.setItem('token', data.token);
sessionStorage.setItem('tokenMultimedia', data.tokenMultimedia);
}
} }
async function destroyToken(url, storage, key) { async function destroyToken(url, storage, key) {
if (storage.getItem(key)) { if (storage.getItem(key)) {
@ -71,8 +69,8 @@ export function useSession() {
} }
function isLoggedIn() { function isLoggedIn() {
const localToken = localStorage.getItem('token'); const localToken = localStorage.getItem(TOKEN);
const sessionToken = sessionStorage.getItem('token'); const sessionToken = sessionStorage.getItem(TOKEN);
return !!(localToken || sessionToken); return !!(localToken || sessionToken);
} }

View File

@ -1,5 +1,6 @@
// app global css in SCSS form // app global css in SCSS form
@import './icons.scss'; @import './icons.scss';
@import '@quasar/quasar-ui-qcalendar/src/QCalendarMonth.sass';
body.body--light { body.body--light {
--font-color: black; --font-color: black;
@ -74,6 +75,10 @@ select:-webkit-autofill {
background-color: #666666; background-color: #666666;
} }
.color-vn-label {
color: var(--vn-label);
}
.color-vn-text { .color-vn-text {
color: var(--vn-text-color); color: var(--vn-text-color);
} }
@ -93,6 +98,10 @@ select:-webkit-autofill {
border-radius: 8px; border-radius: 8px;
} }
.card-width {
width: 770px;
}
.vn-card-list { .vn-card-list {
width: 100%; width: 100%;
max-width: 60em; max-width: 60em;

View File

@ -80,7 +80,7 @@
<glyph unicode="&#xe94b;" glyph-name="consignatarios" d="M409.6-64v349.867h204.8v-349.867h256v563.2h153.6l-512 460.8-512-460.8h153.6v-563.2h256z" /> <glyph unicode="&#xe94b;" glyph-name="consignatarios" d="M409.6-64v349.867h204.8v-349.867h256v563.2h153.6l-512 460.8-512-460.8h153.6v-563.2h256z" />
<glyph unicode="&#xe94c;" glyph-name="control" d="M418.133 315.733l-128-123.733 256-256 469.333 469.333-128 128-341.333-341.333zM546.133 311.467l34.133 34.133h-68.267zM230.4 128l-59.733 64 153.6 153.6h-68.267v102.4h426.667l204.8 204.8 85.333-85.333v187.733c0 55.467-46.933 102.4-102.4 102.4h-213.333c-21.333 59.733-76.8 102.4-145.067 102.4s-123.733-42.667-145.067-102.4h-213.333c-55.467 0-102.4-46.933-102.4-102.4v-716.8c0-55.467 46.933-102.4 102.4-102.4h273.067l-196.267 192zM512 857.6c29.867 0 51.2-21.333 51.2-51.2s-21.333-51.2-51.2-51.2-51.2 21.333-51.2 51.2c0 29.867 21.333 51.2 51.2 51.2zM256 652.8h512v-102.4h-512v102.4zM665.6-64h204.8c55.467 0 102.4 46.933 102.4 102.4v204.8l-307.2-307.2z" /> <glyph unicode="&#xe94c;" glyph-name="control" d="M418.133 315.733l-128-123.733 256-256 469.333 469.333-128 128-341.333-341.333zM546.133 311.467l34.133 34.133h-68.267zM230.4 128l-59.733 64 153.6 153.6h-68.267v102.4h426.667l204.8 204.8 85.333-85.333v187.733c0 55.467-46.933 102.4-102.4 102.4h-213.333c-21.333 59.733-76.8 102.4-145.067 102.4s-123.733-42.667-145.067-102.4h-213.333c-55.467 0-102.4-46.933-102.4-102.4v-716.8c0-55.467 46.933-102.4 102.4-102.4h273.067l-196.267 192zM512 857.6c29.867 0 51.2-21.333 51.2-51.2s-21.333-51.2-51.2-51.2-51.2 21.333-51.2 51.2c0 29.867 21.333 51.2 51.2 51.2zM256 652.8h512v-102.4h-512v102.4zM665.6-64h204.8c55.467 0 102.4 46.933 102.4 102.4v204.8l-307.2-307.2z" />
<glyph unicode="&#xe94d;" glyph-name="credit" d="M921.6 849.067h-819.2c-55.467 0-102.4-42.667-102.4-98.133v-601.6c0-55.467 46.933-102.4 102.4-102.4h819.2c55.467 0 102.4 42.667 102.4 102.4v601.6c0 55.467-46.933 98.133-102.4 98.133zM921.6 145.067h-819.2v302.933h819.2v-302.933zM921.6 648.533h-819.2v102.4h819.2v-102.4z" /> <glyph unicode="&#xe94d;" glyph-name="credit" d="M921.6 849.067h-819.2c-55.467 0-102.4-42.667-102.4-98.133v-601.6c0-55.467 46.933-102.4 102.4-102.4h819.2c55.467 0 102.4 42.667 102.4 102.4v601.6c0 55.467-46.933 98.133-102.4 98.133zM921.6 145.067h-819.2v302.933h819.2v-302.933zM921.6 648.533h-819.2v102.4h819.2v-102.4z" />
<glyph unicode="&#xe94e;" glyph-name="deaulter" d="M677.973-64c-30.72 35.84-61.867 70.827-91.307 107.52-40.96 51.2-80.64 103.253-121.173 154.88-16.64 21.333-21.76 20.48-30.72-4.693-13.227-36.693-25.6-73.387-40.107-109.653-5.12-12.8-13.227-26.88-24.32-34.56-51.627-34.987-104.107-69.12-157.867-100.693-10.667-6.4-30.72-5.547-41.813 0.853-8.107 4.693-12.373 23.893-11.093 35.84 0.853 8.96 11.093 19.627 19.627 25.6 39.253 26.453 78.933 51.627 119.040 76.8 18.347 11.52 30.293 26.027 35.84 47.787 12.373 48.213 27.307 95.573 39.253 143.36 8.533 33.707 26.88 58.88 56.32 77.227 40.533 25.173 80.64 52.053 120.747 78.507 6.4 4.267 10.24 11.52 15.36 17.493-7.253 2.56-14.933 7.253-22.187 6.827-75.52-6.4-151.467-13.227-226.987-20.48-2.133 0-4.693-0.853-6.827-0.853-22.613-1.707-39.253 10.24-40.96 29.867s12.373 33.707 35.413 35.84c45.227 4.267 90.88 8.96 136.107 12.8 65.707 5.547 131.84 10.667 197.547 15.36 26.027 1.707 53.76-21.76 67.413-55.467 9.813-23.893 5.12-46.080-18.347-65.28-49.92-40.107-100.693-78.933-151.040-118.187-23.040-17.92-23.893-23.467-6.4-46.507 58.453-78.080 116.48-156.587 174.933-234.667 27.307-36.693 25.173-50.773-12.373-75.52-5.12 0-9.813 0-14.080 0zM791.893 649.813c-43.093 1.28-76.373-31.573-77.227-75.52-0.853-44.373 29.44-76.8 72.107-77.653 45.227-1.28 77.653 29.44 78.080 73.813 0.427 45.227-29.44 78.080-72.96 79.36zM671.147 737.707c0-72.107-34.133-136.107-87.467-176.64l-235.52-21.76c-72.107 36.693-122.027 111.787-122.027 198.4 0 122.88 99.84 222.293 222.72 222.293 122.453 0 222.293-99.413 222.293-222.293zM592.213 680.533l-50.347 18.347c-2.133-8.533-5.12-16.213-9.813-22.613-5.12-6.4-10.24-11.947-16.213-17.067-5.973-4.267-12.373-8.107-19.2-11.093s-13.653-4.693-20.053-5.547c-17.92-2.987-33.707-0.427-48.64 6.827s-26.88 18.347-36.693 32.853l76.373 12.373 7.253 32.427-97.28-15.787c-1.28 5.547-2.987 11.093-3.84 16.64l-0.853 4.267 99.413 16.213 7.253 32.427-106.667-17.493c0.853 9.387 2.987 17.493 6.4 26.027 3.84 8.533 8.107 16.213 14.080 23.040 5.547 6.827 12.8 12.373 21.333 17.067s17.92 8.107 28.587 9.813c6.827 1.28 13.227 1.707 20.907 1.28s14.507-1.707 21.333-3.84c6.827-2.133 13.653-5.973 20.053-10.24 5.973-4.693 11.947-11.093 17.493-18.773l38.827 37.973c-13.227 17.92-30.293 31.147-52.053 39.253-21.76 8.533-46.080 10.667-73.387 6.4-19.627-2.987-36.267-9.387-51.2-17.92-14.507-8.533-26.88-19.2-37.547-32-10.667-12.373-18.773-26.027-23.893-40.96-5.547-14.507-8.96-29.867-9.813-45.653l-21.76-3.84-7.253-32.427 29.013 4.693 0.427-2.987c1.28-6.827 2.56-12.8 4.267-18.347l-23.467-3.84-8.107-32.427 43.52 7.253c6.827-13.653 15.787-26.027 26.027-36.693 10.24-11.52 22.187-20.48 35.413-27.733 13.227-7.68 27.307-12.8 42.667-15.787s31.573-3.413 47.36-0.853c12.373 2.133 24.32 5.12 35.84 10.667s22.613 11.52 32.853 19.2c10.24 8.107 18.347 16.64 26.027 26.453 6.827 9.387 12.373 20.48 15.36 32.427z" /> <glyph unicode="&#xe94e;" glyph-name="defaulter" d="M677.973-64c-30.72 35.84-61.867 70.827-91.307 107.52-40.96 51.2-80.64 103.253-121.173 154.88-16.64 21.333-21.76 20.48-30.72-4.693-13.227-36.693-25.6-73.387-40.107-109.653-5.12-12.8-13.227-26.88-24.32-34.56-51.627-34.987-104.107-69.12-157.867-100.693-10.667-6.4-30.72-5.547-41.813 0.853-8.107 4.693-12.373 23.893-11.093 35.84 0.853 8.96 11.093 19.627 19.627 25.6 39.253 26.453 78.933 51.627 119.040 76.8 18.347 11.52 30.293 26.027 35.84 47.787 12.373 48.213 27.307 95.573 39.253 143.36 8.533 33.707 26.88 58.88 56.32 77.227 40.533 25.173 80.64 52.053 120.747 78.507 6.4 4.267 10.24 11.52 15.36 17.493-7.253 2.56-14.933 7.253-22.187 6.827-75.52-6.4-151.467-13.227-226.987-20.48-2.133 0-4.693-0.853-6.827-0.853-22.613-1.707-39.253 10.24-40.96 29.867s12.373 33.707 35.413 35.84c45.227 4.267 90.88 8.96 136.107 12.8 65.707 5.547 131.84 10.667 197.547 15.36 26.027 1.707 53.76-21.76 67.413-55.467 9.813-23.893 5.12-46.080-18.347-65.28-49.92-40.107-100.693-78.933-151.040-118.187-23.040-17.92-23.893-23.467-6.4-46.507 58.453-78.080 116.48-156.587 174.933-234.667 27.307-36.693 25.173-50.773-12.373-75.52-5.12 0-9.813 0-14.080 0zM791.893 649.813c-43.093 1.28-76.373-31.573-77.227-75.52-0.853-44.373 29.44-76.8 72.107-77.653 45.227-1.28 77.653 29.44 78.080 73.813 0.427 45.227-29.44 78.080-72.96 79.36zM671.147 737.707c0-72.107-34.133-136.107-87.467-176.64l-235.52-21.76c-72.107 36.693-122.027 111.787-122.027 198.4 0 122.88 99.84 222.293 222.72 222.293 122.453 0 222.293-99.413 222.293-222.293zM592.213 680.533l-50.347 18.347c-2.133-8.533-5.12-16.213-9.813-22.613-5.12-6.4-10.24-11.947-16.213-17.067-5.973-4.267-12.373-8.107-19.2-11.093s-13.653-4.693-20.053-5.547c-17.92-2.987-33.707-0.427-48.64 6.827s-26.88 18.347-36.693 32.853l76.373 12.373 7.253 32.427-97.28-15.787c-1.28 5.547-2.987 11.093-3.84 16.64l-0.853 4.267 99.413 16.213 7.253 32.427-106.667-17.493c0.853 9.387 2.987 17.493 6.4 26.027 3.84 8.533 8.107 16.213 14.080 23.040 5.547 6.827 12.8 12.373 21.333 17.067s17.92 8.107 28.587 9.813c6.827 1.28 13.227 1.707 20.907 1.28s14.507-1.707 21.333-3.84c6.827-2.133 13.653-5.973 20.053-10.24 5.973-4.693 11.947-11.093 17.493-18.773l38.827 37.973c-13.227 17.92-30.293 31.147-52.053 39.253-21.76 8.533-46.080 10.667-73.387 6.4-19.627-2.987-36.267-9.387-51.2-17.92-14.507-8.533-26.88-19.2-37.547-32-10.667-12.373-18.773-26.027-23.893-40.96-5.547-14.507-8.96-29.867-9.813-45.653l-21.76-3.84-7.253-32.427 29.013 4.693 0.427-2.987c1.28-6.827 2.56-12.8 4.267-18.347l-23.467-3.84-8.107-32.427 43.52 7.253c6.827-13.653 15.787-26.027 26.027-36.693 10.24-11.52 22.187-20.48 35.413-27.733 13.227-7.68 27.307-12.8 42.667-15.787s31.573-3.413 47.36-0.853c12.373 2.133 24.32 5.12 35.84 10.667s22.613 11.52 32.853 19.2c10.24 8.107 18.347 16.64 26.027 26.453 6.827 9.387 12.373 20.48 15.36 32.427z" />
<glyph unicode="&#xe94f;" glyph-name="deletedTicket" d="M160.672 85.696h693.248v639.776c0 0-2.016 234.528-349.696 234.528s-343.552-234.528-343.552-234.528v-639.776zM291.328 652.704h170.976v152.256h102.336v-152.256h171.008v-102.336h-171.008v-356.96h-102.336v356.96h-170.976v102.336zM64 61.056v-123.456h899.008v123.456h-899.008z" /> <glyph unicode="&#xe94f;" glyph-name="deletedTicket" d="M160.672 85.696h693.248v639.776c0 0-2.016 234.528-349.696 234.528s-343.552-234.528-343.552-234.528v-639.776zM291.328 652.704h170.976v152.256h102.336v-152.256h171.008v-102.336h-171.008v-356.96h-102.336v356.96h-170.976v102.336zM64 61.056v-123.456h899.008v123.456h-899.008z" />
<glyph unicode="&#xe950;" glyph-name="deleteline" d="M354.133 192l-98.133 98.133 157.867 153.6-157.867 157.867 98.133 102.4 157.867-157.867 157.867 153.6 98.133-98.133-157.867-157.867 157.867-153.6-98.133-98.133-157.867 157.867-157.867-157.867zM780.8 507.733l-64-64 59.733-55.467h247.467v119.467h-243.2zM307.2 443.733l-64 64h-243.2v-119.467h251.733l55.467 55.467z" /> <glyph unicode="&#xe950;" glyph-name="deleteline" d="M354.133 192l-98.133 98.133 157.867 153.6-157.867 157.867 98.133 102.4 157.867-157.867 157.867 153.6 98.133-98.133-157.867-157.867 157.867-153.6-98.133-98.133-157.867 157.867-157.867-157.867zM780.8 507.733l-64-64 59.733-55.467h247.467v119.467h-243.2zM307.2 443.733l-64 64h-243.2v-119.467h251.733l55.467 55.467z" />
<glyph unicode="&#xe951;" glyph-name="delivery" d="M789.333 264.533c-55.467 0-102.4-46.933-102.4-102.4s46.933-102.4 102.4-102.4 102.4 46.933 102.4 102.4c0 59.733-46.933 102.4-102.4 102.4zM789.333 110.933c-29.867 0-51.2 21.333-51.2 51.2s21.333 51.2 51.2 51.2 51.2-21.333 51.2-51.2c0-25.6-25.6-51.2-51.2-51.2zM251.733 264.533c-55.467 0-102.4-46.933-102.4-102.4s46.933-102.4 102.4-102.4c55.467 0 102.4 46.933 102.4 102.4 0 59.733-46.933 102.4-102.4 102.4zM251.733 110.933c-29.867 0-51.2 21.333-51.2 51.2s21.333 51.2 51.2 51.2c29.867 0 51.2-21.333 51.2-51.2 0-25.6-25.6-51.2-51.2-51.2zM1006.933 537.6l-196.267 192c-12.8 12.8-29.867 17.067-46.933 17.067h-98.133v38.4c0 25.6-21.333 51.2-51.2 51.2h-563.2c-29.867 0-51.2-21.333-51.2-51.2v-554.667c0-29.867 25.6-51.2 51.2-51.2h68.267c8.533 64 64 115.2 132.267 115.2 64 0 123.733-51.2 132.267-115.2h268.8c8.533 64 64 115.2 132.267 115.2s128-51.2 136.533-115.2h51.2c29.867 0 51.2 25.6 51.2 51.2v260.267c0 17.067-8.533 34.133-17.067 46.933zM725.333 682.667c0 4.267 4.267 8.533 8.533 8.533h34.133c0 0 4.267 0 4.267-4.267l153.6-145.067c4.267 0 0-12.8-4.267-12.8h-187.733c-8.533 0-8.533 4.267-8.533 8.533v145.067zM311.467 597.333c0 46.933 29.867 85.333 59.733 93.867 4.267 0 4.267 0 8.533 0l98.133 12.8v-51.2c0-46.933-29.867-85.333-59.733-93.867-4.267 0-4.267 0-8.533 0l-98.133-17.067v55.467zM311.467 516.267l46.933 8.533c17.067 4.267 29.867-17.067 29.867-38.4l4.267-29.867-51.2-4.267c-17.067-4.267-29.867 12.8-29.867 38.4v25.6zM149.333 597.333v51.2l85.333 12.8c34.133 4.267 55.467-25.6 55.467-72.533v-51.2l-85.333-12.8c-34.133 0-59.733 29.867-55.467 72.533zM285.867 512v-38.4c0-34.133-21.333-64-42.667-68.267h-4.267l-72.533-8.533v38.4c0 34.133 21.333 64 42.667 68.267h4.267l72.533 8.533z" /> <glyph unicode="&#xe951;" glyph-name="delivery" d="M789.333 264.533c-55.467 0-102.4-46.933-102.4-102.4s46.933-102.4 102.4-102.4 102.4 46.933 102.4 102.4c0 59.733-46.933 102.4-102.4 102.4zM789.333 110.933c-29.867 0-51.2 21.333-51.2 51.2s21.333 51.2 51.2 51.2 51.2-21.333 51.2-51.2c0-25.6-25.6-51.2-51.2-51.2zM251.733 264.533c-55.467 0-102.4-46.933-102.4-102.4s46.933-102.4 102.4-102.4c55.467 0 102.4 46.933 102.4 102.4 0 59.733-46.933 102.4-102.4 102.4zM251.733 110.933c-29.867 0-51.2 21.333-51.2 51.2s21.333 51.2 51.2 51.2c29.867 0 51.2-21.333 51.2-51.2 0-25.6-25.6-51.2-51.2-51.2zM1006.933 537.6l-196.267 192c-12.8 12.8-29.867 17.067-46.933 17.067h-98.133v38.4c0 25.6-21.333 51.2-51.2 51.2h-563.2c-29.867 0-51.2-21.333-51.2-51.2v-554.667c0-29.867 25.6-51.2 51.2-51.2h68.267c8.533 64 64 115.2 132.267 115.2 64 0 123.733-51.2 132.267-115.2h268.8c8.533 64 64 115.2 132.267 115.2s128-51.2 136.533-115.2h51.2c29.867 0 51.2 25.6 51.2 51.2v260.267c0 17.067-8.533 34.133-17.067 46.933zM725.333 682.667c0 4.267 4.267 8.533 8.533 8.533h34.133c0 0 4.267 0 4.267-4.267l153.6-145.067c4.267 0 0-12.8-4.267-12.8h-187.733c-8.533 0-8.533 4.267-8.533 8.533v145.067zM311.467 597.333c0 46.933 29.867 85.333 59.733 93.867 4.267 0 4.267 0 8.533 0l98.133 12.8v-51.2c0-46.933-29.867-85.333-59.733-93.867-4.267 0-4.267 0-8.533 0l-98.133-17.067v55.467zM311.467 516.267l46.933 8.533c17.067 4.267 29.867-17.067 29.867-38.4l4.267-29.867-51.2-4.267c-17.067-4.267-29.867 12.8-29.867 38.4v25.6zM149.333 597.333v51.2l85.333 12.8c34.133 4.267 55.467-25.6 55.467-72.533v-51.2l-85.333-12.8c-34.133 0-59.733 29.867-55.467 72.533zM285.867 512v-38.4c0-34.133-21.333-64-42.667-68.267h-4.267l-72.533-8.533v38.4c0 34.133 21.333 64 42.667 68.267h4.267l72.533 8.533z" />

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 173 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,402 +1,418 @@
@font-face { @font-face {
font-family: 'icon'; font-family: 'icon';
src: url('fonts/icon.eot?2omjsr'); src: url('fonts/icon.eot?2omjsr');
src: url('fonts/icon.eot?2omjsr#iefix') format('embedded-opentype'), src: url('fonts/icon.eot?2omjsr#iefix') format('embedded-opentype'),
url('fonts/icon.ttf?2omjsr') format('truetype'), url('fonts/icon.ttf?2omjsr') format('truetype'),
url('fonts/icon.woff?2omjsr') format('woff'), url('fonts/icon.woff?2omjsr') format('woff'),
url('fonts/icon.svg?2omjsr#icon') format('svg'); url('fonts/icon.svg?2omjsr#icon') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: block; font-display: block;
} }
[class^="icon-"], [class*=" icon-"] { [class^='icon-'],
/* use !important to prevent issues with browser extensions that change fonts */ [class*=' icon-'] {
font-family: 'icon' !important; /* use !important to prevent issues with browser extensions that change fonts */
speak: never; font-family: 'icon' !important;
font-style: normal; speak: never;
font-weight: normal; font-style: normal;
font-variant: normal; font-weight: normal;
text-transform: none; font-variant: normal;
line-height: 1; text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */ /* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-100:before { .icon-100:before {
content: "\e926"; content: '\e926';
} }
.icon-Client_unpaid:before { .icon-Client_unpaid:before {
content: "\e925"; content: '\e925';
}
.icon-Client_unpaid:before {
content: '\e925';
} }
.icon-History:before { .icon-History:before {
content: "\e964"; content: '\e964';
} }
.icon-Person:before { .icon-Person:before {
content: "\e984"; content: '\e984';
} }
.icon-accessory:before { .icon-accessory:before {
content: "\e948"; content: '\e948';
} }
.icon-account:before { .icon-account:before {
content: "\e927"; content: '\e927';
} }
.icon-actions:before { .icon-actions:before {
content: "\e928"; content: '\e928';
} }
.icon-addperson:before { .icon-addperson:before {
content: "\e929"; content: '\e929';
} }
.icon-agency:before { .icon-agency:before {
content: "\e92a"; content: '\e92a';
}
.icon-agency:before {
content: '\e92a';
} }
.icon-agency-term:before { .icon-agency-term:before {
content: "\e92b"; content: '\e92b';
} }
.icon-albaran:before { .icon-albaran:before {
content: "\e92c"; content: '\e92c';
}
.icon-albaran:before {
content: '\e92c';
} }
.icon-anonymous:before { .icon-anonymous:before {
content: "\e92d"; content: '\e92d';
} }
.icon-apps:before { .icon-apps:before {
content: "\e92e"; content: '\e92e';
} }
.icon-artificial:before { .icon-artificial:before {
content: "\e92f"; content: '\e92f';
} }
.icon-attach:before { .icon-attach:before {
content: "\e930"; content: '\e930';
} }
.icon-barcode:before { .icon-barcode:before {
content: "\e932"; content: '\e932';
} }
.icon-basket:before { .icon-basket:before {
content: "\e933"; content: '\e933';
} }
.icon-basketadd:before { .icon-basketadd:before {
content: "\e934"; content: '\e934';
} }
.icon-bin:before { .icon-bin:before {
content: "\e935"; content: '\e935';
} }
.icon-botanical:before { .icon-botanical:before {
content: "\e936"; content: '\e936';
} }
.icon-bucket:before { .icon-bucket:before {
content: "\e937"; content: '\e937';
} }
.icon-buscaman:before { .icon-buscaman:before {
content: "\e938"; content: '\e938';
} }
.icon-buyrequest:before { .icon-buyrequest:before {
content: "\e939"; content: '\e939';
} }
.icon-calc_volum:before { .icon-calc_volum:before {
content: "\e93a"; content: '\e93a';
} }
.icon-calendar:before { .icon-calendar:before {
content: "\e940"; content: '\e940';
} }
.icon-catalog:before { .icon-catalog:before {
content: "\e941"; content: '\e941';
} }
.icon-claims:before { .icon-claims:before {
content: "\e942"; content: '\e942';
} }
.icon-client:before { .icon-client:before {
content: "\e943"; content: '\e943';
} }
.icon-clone:before { .icon-clone:before {
content: "\e945"; content: '\e945';
} }
.icon-columnadd:before { .icon-columnadd:before {
content: "\e946"; content: '\e946';
} }
.icon-columndelete:before { .icon-columndelete:before {
content: "\e947"; content: '\e947';
} }
.icon-components:before { .icon-components:before {
content: "\e949"; content: '\e949';
} }
.icon-consignatarios:before { .icon-consignatarios:before {
content: "\e94b"; content: '\e94b';
} }
.icon-control:before { .icon-control:before {
content: "\e94c"; content: '\e94c';
} }
.icon-credit:before { .icon-credit:before {
content: "\e94d"; content: '\e94d';
} }
.icon-deaulter:before { .icon-defaulter:before {
content: "\e94e"; content: '\e94e';
} }
.icon-deletedTicket:before { .icon-deletedTicket:before {
content: "\e94f"; content: '\e94f';
} }
.icon-deleteline:before { .icon-deleteline:before {
content: "\e950"; content: '\e950';
} }
.icon-delivery:before { .icon-delivery:before {
content: "\e951"; content: '\e951';
} }
.icon-deliveryprices:before { .icon-deliveryprices:before {
content: "\e952"; content: '\e952';
} }
.icon-details:before { .icon-details:before {
content: "\e954"; content: '\e954';
} }
.icon-dfiscales:before { .icon-dfiscales:before {
content: "\e955"; content: '\e955';
} }
.icon-disabled:before { .icon-disabled:before {
content: "\e965"; content: '\e965';
} }
.icon-doc:before { .icon-doc:before {
content: "\e956"; content: '\e956';
} }
.icon-entry:before { .icon-entry:before {
content: "\e958"; content: '\e958';
} }
.icon-exit:before { .icon-exit:before {
content: "\e959"; content: '\e959';
} }
.icon-eye:before { .icon-eye:before {
content: "\e95a"; content: '\e95a';
} }
.icon-fixedPrice:before { .icon-fixedPrice:before {
content: "\e95b"; content: '\e95b';
} }
.icon-flower:before { .icon-flower:before {
content: "\e95c"; content: '\e95c';
} }
.icon-frozen:before { .icon-frozen:before {
content: "\e95d"; content: '\e95d';
} }
.icon-fruit:before { .icon-fruit:before {
content: "\e95e"; content: '\e95e';
} }
.icon-funeral:before { .icon-funeral:before {
content: "\e95f"; content: '\e95f';
} }
.icon-grafana:before { .icon-grafana:before {
content: "\e931"; content: '\e931';
}
.icon-grafana:before {
content: '\e931';
} }
.icon-greenery:before { .icon-greenery:before {
content: "\e91e"; content: '\e91e';
} }
.icon-greuge:before { .icon-greuge:before {
content: "\e960"; content: '\e960';
} }
.icon-grid:before { .icon-grid:before {
content: "\e961"; content: '\e961';
} }
.icon-handmade:before { .icon-handmade:before {
content: "\e94a"; content: '\e94a';
} }
.icon-handmadeArtificial:before { .icon-handmadeArtificial:before {
content: "\e962"; content: '\e962';
} }
.icon-headercol:before { .icon-headercol:before {
content: "\e963"; content: '\e963';
} }
.icon-info:before { .icon-info:before {
content: "\e966"; content: '\e966';
} }
.icon-inventory:before { .icon-inventory:before {
content: "\e967"; content: '\e967';
} }
.icon-invoice:before { .icon-invoice:before {
content: "\e969"; content: '\e969';
} }
.icon-invoice-in:before { .icon-invoice-in:before {
content: "\e96a"; content: '\e96a';
} }
.icon-invoice-in-create:before { .icon-invoice-in-create:before {
content: "\e96b"; content: '\e96b';
} }
.icon-invoice-out:before { .icon-invoice-out:before {
content: "\e96c"; content: '\e96c';
} }
.icon-isTooLittle:before { .icon-isTooLittle:before {
content: "\e96e"; content: '\e96e';
} }
.icon-item:before { .icon-item:before {
content: "\e96f"; content: '\e96f';
} }
.icon-languaje:before { .icon-languaje:before {
content: "\e912"; content: '\e912';
} }
.icon-lines:before { .icon-lines:before {
content: "\e971"; content: '\e971';
} }
.icon-linesprepaired:before { .icon-linesprepaired:before {
content: "\e972"; content: '\e972';
} }
.icon-link-to-corrected:before { .icon-link-to-corrected:before {
content: "\e900"; content: '\e900';
} }
.icon-link-to-correcting:before { .icon-link-to-correcting:before {
content: "\e906"; content: '\e906';
} }
.icon-logout:before { .icon-logout:before {
content: "\e90a"; content: '\e90a';
} }
.icon-mana:before { .icon-mana:before {
content: "\e974"; content: '\e974';
} }
.icon-mandatory:before { .icon-mandatory:before {
content: "\e975"; content: '\e975';
} }
.icon-net:before { .icon-net:before {
content: "\e976"; content: '\e976';
} }
.icon-newalbaran:before { .icon-newalbaran:before {
content: "\e977"; content: '\e977';
} }
.icon-niche:before { .icon-niche:before {
content: "\e979"; content: '\e979';
} }
.icon-no036:before { .icon-no036:before {
content: "\e97a"; content: '\e97a';
} }
.icon-noPayMethod:before { .icon-noPayMethod:before {
content: "\e97b"; content: '\e97b';
} }
.icon-notes:before { .icon-notes:before {
content: "\e97c"; content: '\e97c';
} }
.icon-noweb:before { .icon-noweb:before {
content: "\e97e"; content: '\e97e';
} }
.icon-onlinepayment:before { .icon-onlinepayment:before {
content: "\e97f"; content: '\e97f';
} }
.icon-package:before { .icon-package:before {
content: "\e980"; content: '\e980';
} }
.icon-payment:before { .icon-payment:before {
content: "\e982"; content: '\e982';
} }
.icon-pbx:before { .icon-pbx:before {
content: "\e983"; content: '\e983';
} }
.icon-pets:before { .icon-pets:before {
content: "\e985"; content: '\e985';
} }
.icon-photo:before { .icon-photo:before {
content: "\e986"; content: '\e986';
} }
.icon-plant:before { .icon-plant:before {
content: "\e987"; content: '\e987';
} }
.icon-polizon:before { .icon-polizon:before {
content: "\e989"; content: '\e989';
} }
.icon-preserved:before { .icon-preserved:before {
content: "\e98a"; content: '\e98a';
} }
.icon-recovery:before { .icon-recovery:before {
content: "\e98b"; content: '\e98b';
} }
.icon-regentry:before { .icon-regentry:before {
content: "\e901"; content: '\e901';
} }
.icon-reserva:before { .icon-reserva:before {
content: "\e902"; content: '\e902';
} }
.icon-revision:before { .icon-revision:before {
content: "\e903"; content: '\e903';
} }
.icon-risk:before { .icon-risk:before {
content: "\e904"; content: '\e904';
} }
.icon-services:before { .icon-services:before {
content: "\e905"; content: '\e905';
} }
.icon-settings:before { .icon-settings:before {
content: "\e907"; content: '\e907';
} }
.icon-shipment:before { .icon-shipment:before {
content: "\e908"; content: '\e908';
} }
.icon-sign:before { .icon-sign:before {
content: "\e909"; content: '\e909';
} }
.icon-sms:before { .icon-sms:before {
content: "\e90b"; content: '\e90b';
} }
.icon-solclaim:before { .icon-solclaim:before {
content: "\e90c"; content: '\e90c';
} }
.icon-solunion:before { .icon-solunion:before {
content: "\e90d"; content: '\e90d';
} }
.icon-splitline:before { .icon-splitline:before {
content: "\e90e"; content: '\e90e';
} }
.icon-splur:before { .icon-splur:before {
content: "\e90f"; content: '\e90f';
} }
.icon-stowaway:before { .icon-stowaway:before {
content: "\e910"; content: '\e910';
} }
.icon-supplier:before { .icon-supplier:before {
content: "\e911"; content: '\e911';
} }
.icon-supplierfalse:before { .icon-supplierfalse:before {
content: "\e913"; content: '\e913';
} }
.icon-tags:before { .icon-tags:before {
content: "\e914"; content: '\e914';
} }
.icon-tax:before { .icon-tax:before {
content: "\e915"; content: '\e915';
} }
.icon-thermometer:before { .icon-thermometer:before {
content: "\e916"; content: '\e916';
} }
.icon-ticket:before { .icon-ticket:before {
content: "\e917"; content: '\e917';
} }
.icon-ticketAdd:before { .icon-ticketAdd:before {
content: "\e918"; content: '\e918';
} }
.icon-traceability:before { .icon-traceability:before {
content: "\e919"; content: '\e919';
} }
.icon-transaction:before { .icon-transaction:before {
content: "\e93b"; content: '\e93b';
}
.icon-transaction:before {
content: '\e93b';
} }
.icon-treatments:before { .icon-treatments:before {
content: "\e91c"; content: '\e91c';
} }
.icon-trolley:before { .icon-trolley:before {
content: "\e91a"; content: '\e91a';
} }
.icon-troncales:before { .icon-troncales:before {
content: "\e91b"; content: '\e91b';
} }
.icon-unavailable:before { .icon-unavailable:before {
content: "\e91d"; content: '\e91d';
} }
.icon-volume:before { .icon-volume:before {
content: "\e91f"; content: '\e91f';
} }
.icon-wand:before { .icon-wand:before {
content: "\e920"; content: '\e920';
} }
.icon-web:before { .icon-web:before {
content: "\e921"; content: '\e921';
} }
.icon-wiki:before { .icon-wiki:before {
content: "\e922"; content: '\e922';
} }
.icon-worker:before { .icon-worker:before {
content: "\e923"; content: '\e923';
} }
.icon-zone:before { .icon-zone:before {
content: "\e924"; content: '\e924';
} }

View File

@ -26,11 +26,11 @@ export function isValidDate(date) {
* // returns "02/12/2022" * // returns "02/12/2022"
* toDateFormat(new Date(2022, 11, 2)); * toDateFormat(new Date(2022, 11, 2));
*/ */
export function toDateFormat(date) { export function toDateFormat(date, locale = 'es-ES') {
if (!isValidDate(date)) { if (!isValidDate(date)) {
return ''; return '';
} }
return new Date(date).toLocaleDateString('es-ES', { return new Date(date).toLocaleDateString(locale, {
year: 'numeric', year: 'numeric',
month: '2-digit', month: '2-digit',
day: '2-digit', day: '2-digit',
@ -56,7 +56,7 @@ export function toTimeFormat(date, showSeconds = false) {
if (!isValidDate(date)) { if (!isValidDate(date)) {
return ''; return '';
} }
return new Date(date).toLocaleDateString('es-ES', { return new Date(date).toLocaleTimeString('es-ES', {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
second: showSeconds ? '2-digit' : undefined, second: showSeconds ? '2-digit' : undefined,
@ -91,3 +91,24 @@ export function toDateTimeFormat(date, showSeconds = false) {
second: showSeconds ? '2-digit' : undefined, second: showSeconds ? '2-digit' : undefined,
}); });
} }
/**
* Converts seconds to a formatted string representing hours and minutes (hh:mm).
* @param {number} seconds - The number of seconds to convert.
* @param {boolean} includeHSuffix - Optional parameter indicating whether to include "h." after the hour.
* @returns {string} A string representing the time in the format "hh:mm" with optional "h." suffix.
*/
export function secondsToHoursMinutes(seconds, includeHSuffix = true) {
if (!seconds) return includeHSuffix ? '00:00 h.' : '00:00';
const hours = Math.floor(seconds / 3600);
const remainingMinutes = seconds % 3600;
const minutes = Math.floor(remainingMinutes / 60);
const formattedHours = hours < 10 ? '0' + hours : hours;
const formattedMinutes = minutes < 10 ? '0' + minutes : minutes;
// Append "h." if includeHSuffix is true
const suffix = includeHSuffix ? ' h.' : '';
// Return formatted string
return formattedHours + ':' + formattedMinutes + suffix;
}

View File

@ -1,7 +1,8 @@
import toLowerCase from './toLowerCase'; import toLowerCase from './toLowerCase';
import toDate from './toDate'; import toDate from './toDate';
import toDateString from './toDateString'; import toDateString from './toDateString';
import toDateHour from './toDateHour'; import toDateHourMin from './toDateHourMin';
import toDateHourMinSec from './toDateHourMinSec';
import toRelativeDate from './toRelativeDate'; import toRelativeDate from './toRelativeDate';
import toCurrency from './toCurrency'; import toCurrency from './toCurrency';
import toPercentage from './toPercentage'; import toPercentage from './toPercentage';
@ -16,7 +17,8 @@ export {
toDate, toDate,
toHour, toHour,
toDateString, toDateString,
toDateHour, toDateHourMin,
toDateHourMinSec,
toRelativeDate, toRelativeDate,
toCurrency, toCurrency,
toPercentage, toPercentage,

View File

@ -0,0 +1,11 @@
export default function toDateHourMin(date) {
const dateHour = new Date(date).toLocaleDateString('es-ES', {
timeZone: 'Europe/Madrid',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
return dateHour;
}

View File

@ -1,4 +1,4 @@
export default function toDateHour(date) { export default function toDateHourMinSec(date) {
const dateHour = new Date(date).toLocaleDateString('es-ES', { const dateHour = new Date(date).toLocaleDateString('es-ES', {
timeZone: 'Europe/Madrid', timeZone: 'Europe/Madrid',
year: 'numeric', year: 'numeric',

View File

@ -1,17 +0,0 @@
const modules = import.meta.glob(`../pages/**/locale/**.yml`);
import translations from './index';
const LOCALE_EXTENSION = '.yml';
export async function locales() {
for await (const module of Object.keys(modules)) {
const splittedFile = module.split('/');
const lang = splittedFile.pop().split(LOCALE_EXTENSION)[0];
const moduleFiles = splittedFile.join('/') + '/' + lang + LOCALE_EXTENSION;
import(moduleFiles).then((t) => {
Object.assign(translations[lang], t.default);
});
}
return translations;
}
export default translations;

View File

@ -1,10 +1,23 @@
const files = import.meta.glob(`./locale/*.yml`); const files = import.meta.glob(`./locale/*.yml`);
const modules = import.meta.glob(`../pages/**/locale/*.yml`);
const translations = {}; const translations = {};
for (const file in files) { for (const file in files) {
const lang = file.split('/').at(2).split('.')[0]; const lang = file.split('/').at(2).split('.')[0];
import(file).then((t) => {
translations[lang] = t.default; files[file]()
.then((g) => {
translations[lang] = g.default;
})
.finally(() => {
const actualLang = lang + '.yml';
for (const module in modules) {
if (!module.endsWith(actualLang)) continue;
modules[module]().then((t) => {
Object.assign(translations[lang], t.default);
})
}
}); });
} }

View File

@ -67,6 +67,7 @@ globals:
reason: reason reason: reason
noResults: No results noResults: No results
system: System system: System
notificationSent: Notification sent
warehouse: Warehouse warehouse: Warehouse
company: Company company: Company
fieldRequired: Field required fieldRequired: Field required
@ -82,6 +83,7 @@ globals:
selectFile: Select a file selectFile: Select a file
copyClipboard: Copy on clipboard copyClipboard: Copy on clipboard
salesPerson: SalesPerson salesPerson: SalesPerson
send: Send
code: Code code: Code
pageTitles: pageTitles:
summary: Summary summary: Summary
@ -90,6 +92,7 @@ globals:
parkingList: Parkings list parkingList: Parkings list
created: Created created: Created
worker: Worker worker: Worker
now: Now
errors: errors:
statusUnauthorized: Access denied statusUnauthorized: Access denied
statusInternalServerError: An internal server error has ocurred statusInternalServerError: An internal server error has ocurred
@ -127,13 +130,12 @@ dashboard:
customer: customer:
pageTitles: pageTitles:
customers: Customers customers: Customers
create: Create
list: List list: List
webPayments: Web Payments webPayments: Web Payments
extendedList: Extended list extendedList: Extended list
notifications: Notifications notifications: Notifications
defaulter: Defaulter defaulter: Defaulter
createCustomer: Create customer customerCreate: New customer
summary: Summary summary: Summary
basicData: Basic data basicData: Basic data
fiscalData: Fiscal data fiscalData: Fiscal data
@ -151,6 +153,13 @@ customer:
creditContracts: Credit contracts creditContracts: Credit contracts
creditOpinion: Credit opinion creditOpinion: Credit opinion
others: Others others: Others
samples: Samples
consumption: Consumption
mandates: Mandates
contacts: Contacts
webPayment: Web payment
fileManagement: File management
unpaid: Unpaid
list: list:
phone: Phone phone: Phone
email: Email email: Email
@ -161,14 +170,18 @@ customer:
customerId: Claim ID customerId: Claim ID
salesPerson: Sales person salesPerson: Sales person
credit: Credit credit: Credit
risk: Risk
securedCredit: Secured credit securedCredit: Secured credit
payMethod: Pay method payMethod: Pay method
debt: Debt debt: Debt
isDisabled: Customer is disabled isFrozen: Customer frozen
isFrozen: Customer is frozen
hasDebt: Customer has debt hasDebt: Customer has debt
notChecked: Customer not checked isDisabled: Customer inactive
notChecked: Customer no checked
webAccountInactive: Web account inactive
noWebAccess: Web access is disabled noWebAccess: Web access is disabled
businessType: Business type
passwordRequirements: 'The password must have at least { length } length characters, {nAlpha} alphabetic characters, {nUpper} capital letters, {nDigits} digits and {nPunct} symbols (Ex: $%&.)\n'
businessTypeFk: Business type businessTypeFk: Business type
summary: summary:
basicData: Basic data basicData: Basic data
@ -228,17 +241,20 @@ customer:
recoverySince: Recovery since recoverySince: Recovery since
businessType: Business Type businessType: Business Type
city: City city: City
descriptorInfo: Invoices minus payments plus orders not yet
rating: Rating rating: Rating
recommendCredit: Recommended credit recommendCredit: Recommended credit
basicData: basicData:
socialName: Fiscal name socialName: Fiscal name
businessType: Business type businessType: Business type
contact: Contact contact: Contact
youCanSaveMultipleEmails: You can save multiple emails
email: Email email: Email
phone: Phone phone: Phone
mobile: Mobile mobile: Mobile
salesPerson: Sales person salesPerson: Sales person
contactChannel: Contact channel contactChannel: Contact channel
previousClient: Previous client
extendedList: extendedList:
tableVisibleColumns: tableVisibleColumns:
id: Identifier id: Identifier
@ -281,7 +297,7 @@ entry:
notes: Notes notes: Notes
dms: File management dms: File management
log: Log log: Log
create: Create entryCreate: New entry
latestBuys: Latest buys latestBuys: Latest buys
list: list:
newEntry: New entry newEntry: New entry
@ -396,7 +412,7 @@ ticket:
pageTitles: pageTitles:
tickets: Tickets tickets: Tickets
list: List list: List
createTicket: Create ticket ticketCreate: New ticket
summary: Summary summary: Summary
basicData: Basic Data basicData: Basic Data
boxing: Boxing boxing: Boxing
@ -468,11 +484,17 @@ ticket:
weight: Weight weight: Weight
goTo: Go to goTo: Go to
summaryAmount: Summary summaryAmount: Summary
create:
client: Client
address: Address
landed: Landed
warehouse: Warehouse
agency: Agency
claim: claim:
pageTitles: pageTitles:
claims: Claims claims: Claims
list: List list: List
createClaim: Create claim claimCreate: New claim
summary: Summary summary: Summary
basicData: Basic Data basicData: Basic Data
lines: Lines lines: Lines
@ -545,7 +567,7 @@ invoiceOut:
list: List list: List
negativeBases: Negative Bases negativeBases: Negative Bases
globalInvoicing: Global invoicing globalInvoicing: Global invoicing
createInvoiceOut: Create invoice out invoiceOutCreate: Create invoice out
summary: Summary summary: Summary
basicData: Basic Data basicData: Basic Data
list: list:
@ -618,7 +640,7 @@ shelving:
pageTitles: pageTitles:
shelving: Shelving shelving: Shelving
shelvingList: Shelving List shelvingList: Shelving List
create: Create shelvingCreate: New shelving
summary: Summary summary: Summary
basicData: Basic Data basicData: Basic Data
log: Logs log: Logs
@ -651,7 +673,7 @@ invoiceIn:
pageTitles: pageTitles:
invoiceIns: Invoices In invoiceIns: Invoices In
list: List list: List
createInvoiceIn: Create invoice in invoiceInCreate: Create invoice in
summary: Summary summary: Summary
basicData: Basic data basicData: Basic data
vat: VAT vat: VAT
@ -712,7 +734,7 @@ order:
pageTitles: pageTitles:
order: Orders order: Orders
orderList: List orderList: List
create: Create orderCreate: New order
summary: Summary summary: Summary
basicData: Basic Data basicData: Basic Data
catalog: Catalog catalog: Catalog
@ -789,6 +811,8 @@ worker:
dms: My documentation dms: My documentation
pbx: Private Branch Exchange pbx: Private Branch Exchange
log: Log log: Log
calendar: Calendar
timeControl: Time control
list: list:
name: Name name: Name
email: Email email: Email
@ -905,7 +929,7 @@ route:
routes: Routes routes: Routes
cmrsList: External CMRs list cmrsList: External CMRs list
RouteList: List RouteList: List
create: Create routeCreate: New route
basicData: Basic Data basicData: Basic Data
summary: Summary summary: Summary
RouteRoadmap: Roadmaps RouteRoadmap: Roadmaps
@ -932,7 +956,7 @@ supplier:
suppliers: Suppliers suppliers: Suppliers
supplier: Supplier supplier: Supplier
list: List list: List
create: Create supplierCreate: New supplier
summary: Summary summary: Summary
basicData: Basic data basicData: Basic data
fiscalData: Fiscal data fiscalData: Fiscal data
@ -1038,7 +1062,6 @@ travel:
pageTitles: pageTitles:
travel: Travels travel: Travels
list: List list: List
create: Create
summary: Summary summary: Summary
extraCommunity: Extra community extraCommunity: Extra community
travelCreate: New travel travelCreate: New travel
@ -1096,11 +1119,8 @@ item:
list: List list: List
diary: Diary diary: Diary
tags: Tags tags: Tags
create: Create wasteBreakdown: Waste breakdown
basicData: 'Basic data' itemCreate: New item
tax: 'Tax'
botanical: 'Botanical'
barcode: 'Barcodes'
descriptor: descriptor:
item: Item item: Item
buyer: Buyer buyer: Buyer
@ -1128,6 +1148,13 @@ item:
stemMultiplier: Multiplier stemMultiplier: Multiplier
producer: Producer producer: Producer
landed: Landed landed: Landed
create:
name: Name
tag: Tag
priority: Priority
type: Type
intrastat: Intrastat
origin: Origin
summary: summary:
basicData: 'Basic data' basicData: 'Basic data'
otherData: 'Other data' otherData: 'Other data'
@ -1184,3 +1211,24 @@ components:
VnLv: VnLv:
copyText: '{copyValue} has been copied to the clipboard' copyText: '{copyValue} has been copied to the clipboard'
iban_tooltip: 'IBAN: ES21 1234 5678 90 0123456789' iban_tooltip: 'IBAN: ES21 1234 5678 90 0123456789'
weekdays:
sun: Sunday
mon: Monday
tue: Tuesday
wed: Wednesday
thu: Thursday
fri: Friday
sat: Saturday
months:
jan: January
feb: February
mar: March
apr: April
may: May
jun: June
jul: July
aug: August
sep: September
oct: October
nov: November
dec: December

View File

@ -67,6 +67,7 @@ globals:
reason: motivo reason: motivo
noResults: Sin resultados noResults: Sin resultados
system: Sistema system: Sistema
notificationSent: Notificación enviada
warehouse: Almacén warehouse: Almacén
company: Empresa company: Empresa
fieldRequired: Campo requerido fieldRequired: Campo requerido
@ -82,6 +83,7 @@ globals:
selectFile: Seleccione un fichero selectFile: Seleccione un fichero
copyClipboard: Copiar en portapapeles copyClipboard: Copiar en portapapeles
salesPerson: Comercial salesPerson: Comercial
send: Enviar
code: Código code: Código
pageTitles: pageTitles:
summary: Resumen summary: Resumen
@ -90,6 +92,7 @@ globals:
parkingList: Listado de parkings parkingList: Listado de parkings
created: Fecha creación created: Fecha creación
worker: Trabajador worker: Trabajador
now: Ahora
errors: errors:
statusUnauthorized: Acceso denegado statusUnauthorized: Acceso denegado
statusInternalServerError: Ha ocurrido un error interno del servidor statusInternalServerError: Ha ocurrido un error interno del servidor
@ -125,7 +128,7 @@ dashboard:
customer: customer:
pageTitles: pageTitles:
customers: Clientes customers: Clientes
create: Crear customerCreate: Nuevo cliente
list: Listado list: Listado
webPayments: Pagos Web webPayments: Pagos Web
extendedList: Listado extendido extendedList: Listado extendido
@ -149,6 +152,13 @@ customer:
creditContracts: Contratos de crédito creditContracts: Contratos de crédito
creditOpinion: Opinión de crédito creditOpinion: Opinión de crédito
others: Otros others: Otros
samples: Plantillas
consumption: Consumo
mandates: Mandatos
contacts: Contactos
webPayment: Pago web
fileManagement: Gestión documental
unpaid: Impago
list: list:
phone: Teléfono phone: Teléfono
email: Email email: Email
@ -158,14 +168,18 @@ customer:
customerId: ID cliente customerId: ID cliente
salesPerson: Comercial salesPerson: Comercial
credit: Crédito credit: Crédito
risk: Riesgo
securedCredit: Crédito asegurado securedCredit: Crédito asegurado
payMethod: Método de pago payMethod: Método de pago
debt: Riesgo debt: Riesgo
isDisabled: El cliente está desactivado isFrozen: Cliente congelado
isFrozen: El cliente está congelado hasDebt: Cliente con riesgo
hasDebt: El cliente tiene riesgo isDisabled: Cliente inactivo
notChecked: El cliente no está comprobado notChecked: Cliente no comprobado
webAccountInactive: Sin acceso web
noWebAccess: El acceso web está desactivado noWebAccess: El acceso web está desactivado
businessType: Tipo de negocio
passwordRequirements: 'La contraseña debe tener al menos { length } caracteres de longitud, {nAlpha} caracteres alfabéticos, {nUpper} letras mayúsculas, {nDigits} dígitos y {nPunct} símbolos (Ej: $%&.)'
businessTypeFk: Tipo de negocio businessTypeFk: Tipo de negocio
summary: summary:
basicData: Datos básicos basicData: Datos básicos
@ -225,17 +239,20 @@ customer:
recoverySince: Recobro desde recoverySince: Recobro desde
businessType: Tipo de negocio businessType: Tipo de negocio
city: Población city: Población
descriptorInfo: Facturas menos recibos mas pedidos sin facturar
rating: Clasificación rating: Clasificación
recommendCredit: Crédito recomendado recommendCredit: Crédito recomendado
basicData: basicData:
socialName: Nombre fiscal socialName: Nombre fiscal
businessType: Tipo de negocio businessType: Tipo de negocio
contact: Contacto contact: Contacto
youCanSaveMultipleEmails: Puede guardar varios correos electrónicos encadenándolos mediante comas sin espacios{','} ejemplo{':'} user{'@'}dominio{'.'}com, user2{'@'}dominio{'.'}com siendo el primer correo electrónico el principal
email: Email email: Email
phone: Teléfono phone: Teléfono
mobile: Móvil mobile: Móvil
salesPerson: Comercial salesPerson: Comercial
contactChannel: Canal de contacto contactChannel: Canal de contacto
previousClient: Cliente anterior
extendedList: extendedList:
tableVisibleColumns: tableVisibleColumns:
id: Identificador id: Identificador
@ -278,7 +295,7 @@ entry:
notes: Notas notes: Notas
dms: Gestión documental dms: Gestión documental
log: Historial log: Historial
create: Crear entryCreate: Nueva entrada
latestBuys: Últimas compras latestBuys: Últimas compras
list: list:
newEntry: Nueva entrada newEntry: Nueva entrada
@ -393,7 +410,7 @@ ticket:
pageTitles: pageTitles:
tickets: Tickets tickets: Tickets
list: Listado list: Listado
createTicket: Crear ticket ticketCreate: Nuevo ticket
summary: Resumen summary: Resumen
basicData: Datos básicos basicData: Datos básicos
boxing: Encajado boxing: Encajado
@ -465,11 +482,17 @@ ticket:
weight: Peso weight: Peso
goTo: Ir a goTo: Ir a
summaryAmount: Resumen summaryAmount: Resumen
create:
client: Cliente
address: Dirección
landed: F. entrega
warehouse: Almacén
agency: Agencia
claim: claim:
pageTitles: pageTitles:
claims: Reclamaciones claims: Reclamaciones
list: Listado list: Listado
createClaim: Crear reclamación claimCreate: Crear reclamación
summary: Resumen summary: Resumen
basicData: Datos básicos basicData: Datos básicos
lines: Líneas lines: Líneas
@ -545,7 +568,7 @@ invoiceOut:
list: Listado list: Listado
negativeBases: Bases Negativas negativeBases: Bases Negativas
globalInvoicing: Facturación global globalInvoicing: Facturación global
createInvoiceOut: Crear fact. emitida invoiceOutCreate: Crear fact. emitida
summary: Resumen summary: Resumen
basicData: Datos básicos basicData: Datos básicos
list: list:
@ -618,7 +641,7 @@ order:
pageTitles: pageTitles:
order: Cesta order: Cesta
orderList: Listado orderList: Listado
create: Crear orderCreate: Nueva orden
summary: Resumen summary: Resumen
basicData: Datos básicos basicData: Datos básicos
catalog: Catálogo catalog: Catálogo
@ -668,7 +691,7 @@ shelving:
pageTitles: pageTitles:
shelving: Carros shelving: Carros
shelvingList: Listado de carros shelvingList: Listado de carros
create: Crear shelvingCreate: Nuevo carro
summary: Resumen summary: Resumen
basicData: Datos básicos basicData: Datos básicos
log: Historial log: Historial
@ -700,7 +723,7 @@ invoiceIn:
pageTitles: pageTitles:
invoiceIns: Fact. recibidas invoiceIns: Fact. recibidas
list: Listado list: Listado
createInvoiceIn: Crear fact. recibida invoiceInCreate: Crear fact. recibida
summary: Resumen summary: Resumen
basicData: Datos básicos basicData: Datos básicos
vat: IVA vat: IVA
@ -786,6 +809,8 @@ worker:
dms: Mi documentación dms: Mi documentación
pbx: Centralita pbx: Centralita
log: Historial log: Historial
calendar: Calendario
timeControl: Control de horario
list: list:
name: Nombre name: Nombre
email: Email email: Email
@ -902,7 +927,7 @@ route:
routes: Rutas routes: Rutas
cmrsList: Listado de CMRs externos cmrsList: Listado de CMRs externos
RouteList: Listado RouteList: Listado
create: Crear routeCreate: Nueva ruta
basicData: Datos básicos basicData: Datos básicos
summary: Resumen summary: Resumen
RouteRoadmap: Troncales RouteRoadmap: Troncales
@ -929,7 +954,7 @@ supplier:
suppliers: Proveedores suppliers: Proveedores
supplier: Proveedor supplier: Proveedor
list: Listado list: Listado
create: Crear supplierCreate: Nuevo proveedor
summary: Resumen summary: Resumen
basicData: Datos básicos basicData: Datos básicos
fiscalData: Datos fiscales fiscalData: Datos fiscales
@ -1093,7 +1118,8 @@ item:
list: Listado list: Listado
diary: Histórico diary: Histórico
tags: Etiquetas tags: Etiquetas
create: Crear wasteBreakdown: Deglose de mermas
itemCreate: Nuevo artículo
basicData: 'Datos básicos' basicData: 'Datos básicos'
tax: 'IVA' tax: 'IVA'
botanical: 'Botánico' botanical: 'Botánico'
@ -1125,6 +1151,13 @@ item:
stemMultiplier: Multiplicador stemMultiplier: Multiplicador
producer: Productor producer: Productor
landed: F. entrega landed: F. entrega
create:
name: Nombre
tag: Etiqueta
priority: Prioridad
type: Tipo
intrastat: Intrastat
origin: Origen
summary: summary:
basicData: 'Datos básicos' basicData: 'Datos básicos'
otherData: 'Otros datos' otherData: 'Otros datos'
@ -1181,3 +1214,24 @@ components:
VnLv: VnLv:
copyText: '{copyValue} se ha copiado al portapepeles' copyText: '{copyValue} se ha copiado al portapepeles'
iban_tooltip: 'IBAN: ES21 1234 5678 90 0123456789' iban_tooltip: 'IBAN: ES21 1234 5678 90 0123456789'
weekdays:
sun: Domingo
mon: Lunes
tue: Martes
wed: Miércoles
thu: Jueves
fri: Viernes
sat: Sábado
months:
jan: Enero
feb: Febrero
mar: Marzo
apr: Abril
may: Mayo
jun: Junio
jul: Julio
aug: Agosto
sep: Septiembre
oct: Octubre
nov: Noviembre
dec: Diciembre

View File

@ -121,11 +121,6 @@ async function fetchMana() {
mana.value = response.data; mana.value = response.data;
} }
async function updateQuantity({ id, quantity }) {
if (!id) return;
await axios.patch(`ClaimBeginnings/${id}`, { quantity });
}
async function updateDiscount({ saleFk, discount, canceller }) { async function updateDiscount({ saleFk, discount, canceller }) {
const body = { salesIds: [saleFk], newDiscount: discount }; const body = { salesIds: [saleFk], newDiscount: discount };
const claimId = claim.value.ticketFk; const claimId = claim.value.ticketFk;
@ -155,6 +150,10 @@ function showImportDialog() {
}) })
.onOk(() => claimLinesForm.value.reload()); .onOk(() => claimLinesForm.value.reload());
} }
function saveWhenHasChanges() {
claimLinesForm.value.getChanges().updates && claimLinesForm.value.onSubmit();
}
</script> </script>
<template> <template>
<Teleport to="#st-data" v-if="stateStore.isSubToolbarShown()"> <Teleport to="#st-data" v-if="stateStore.isSubToolbarShown()">
@ -181,161 +180,135 @@ function showImportDialog() {
@on-fetch="onFetchClaim" @on-fetch="onFetchClaim"
auto-load auto-load
/> />
<div class="column items-center"> <div class="q-pa-md">
<div class="list"> <CrudModel
<CrudModel data-key="ClaimLines"
data-key="ClaimLines" ref="claimLinesForm"
ref="claimLinesForm" :url="`Claims/${route.params.id}/lines`"
:url="`Claims/${route.params.id}/lines`" save-url="ClaimBeginnings/crud"
save-url="ClaimBeginnings/crud" :filter="linesFilter"
:filter="linesFilter" @on-fetch="onFetch"
@on-fetch="onFetch" @save-changes="onFetch"
@save-changes="onFetch" v-model:selected="selected"
v-model:selected="selected" :default-save="false"
:default-save="false" :default-reset="false"
:default-reset="false" auto-load
auto-load :limit="0"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<QTable <QTable
:columns="columns" :columns="columns"
:rows="rows" :rows="rows"
:dense="$q.screen.lt.md" :dense="$q.screen.lt.md"
row-key="id" row-key="id"
selection="multiple" selection="multiple"
v-model:selected="selected" v-model:selected="selected"
:grid="$q.screen.lt.md" :grid="$q.screen.lt.md"
> >
<template #body-cell-claimed="{ row, value }"> <template #body-cell-claimed="{ row }">
<QTd auto-width align="right" class="text-primary"> <QTd auto-width align="right" class="text-primary">
<span>{{ value }}</span> <QInput
v-model="row.quantity"
<QPopupEdit type="number"
v-model="row.quantity" dense
v-slot="scope" @keyup.enter="saveWhenHasChanges()"
:title="t('Claimed quantity')" @blur="saveWhenHasChanges()"
@update:model-value="updateQuantity(row)" />
buttons </QTd>
> </template>
<QInput <template #body-cell-description="{ row, value }">
v-model="scope.value" <QTd auto-width align="right" class="text-primary">
type="number" {{ value }}
dense <ItemDescriptorProxy
autofocus :id="row.sale.itemFk"
@keyup.enter="scope.set" ></ItemDescriptorProxy>
@focus="($event) => $event.target.select()" </QTd>
/> </template>
</QPopupEdit> <template #body-cell-discount="{ row, value, rowIndex }">
</QTd> <QTd auto-width align="right" class="text-primary">
</template> {{ value }}
<template #body-cell-description="{ row, value }"> <VnDiscount
<QTd auto-width align="right" class="text-primary"> :quantity="row.quantity"
{{ value }} :price="row.sale.price"
<ItemDescriptorProxy :discount="row.sale.discount"
:id="row.sale.itemFk" :mana="mana"
></ItemDescriptorProxy> :promise="updateDiscount"
</QTd> :data="{ saleFk: row.sale.id, rowIndex: rowIndex }"
</template> @on-update="onUpdateDiscount"
<template #body-cell-discount="{ row, value, rowIndex }"> />
<QTd auto-width align="right" class="text-primary"> </QTd>
{{ value }} </template>
<VnDiscount <!-- View for grid mode -->
:quantity="row.quantity" <template #item="props">
:price="row.sale.price" <div
:discount="row.sale.discount" class="q-mb-md col-12 grid-style-transition"
:mana="mana" :style="props.selected ? 'transform: scale(0.95);' : ''"
:promise="updateDiscount" >
:data="{ saleFk: row.sale.id, rowIndex: rowIndex }" <QCard>
@on-update="onUpdateDiscount" <QCardSection>
/> <QCheckbox v-model="props.selected" />
</QTd> </QCardSection>
</template> <QSeparator inset />
<!-- View for grid mode --> <QList dense>
<template #item="props"> <QItem
<div v-for="column of props.cols"
class="q-mb-md col-12 grid-style-transition" :key="column.name"
:style="props.selected ? 'transform: scale(0.95);' : ''" >
> <QItemSection>
<QCard> <QItemLabel caption>
<QCardSection> {{ column.label }}
<QCheckbox v-model="props.selected" /> </QItemLabel>
</QCardSection> </QItemSection>
<QSeparator inset /> <QItemSection side>
<QList dense> <template v-if="column.name === 'claimed'">
<QItem <QItemLabel class="text-primary">
v-for="column of props.cols" <QInput
:key="column.name" v-model="props.row.quantity"
> type="number"
<QItemSection> dense
<QItemLabel caption> autofocus
{{ column.label }} @keyup.enter="
saveWhenHasChanges()
"
@blur="saveWhenHasChanges()"
/>
</QItemLabel> </QItemLabel>
</QItemSection> </template>
<QItemSection side> <template
<template v-else-if="column.name === 'discount'"
v-if="column.name === 'claimed'" >
> <QItemLabel class="text-primary">
<QItemLabel class="text-primary"> {{ column.value }}
{{ column.value }} <VnDiscount
<QPopupEdit :quantity="props.row.quantity"
v-model="props.row.quantity" :price="props.row.sale.price"
v-slot="scope" :discount="
:title="t('Claimed quantity')" props.row.sale.discount
@update:model-value=" "
updateQuantity(props.row) :mana="mana"
" :promise="updateDiscount"
buttons :data="{
> saleFk: props.row.sale.id,
<QInput rowIndex: props.rowIndex,
v-model="scope.value" }"
type="number" @on-update="onUpdateDiscount"
dense />
autofocus </QItemLabel>
@keyup.enter="scope.set" </template>
@focus=" <template v-else>
($event) => <QItemLabel>
$event.target.select() {{ column.value }}
" </QItemLabel>
/> </template>
</QPopupEdit> </QItemSection>
</QItemLabel> </QItem>
</template> </QList>
<template </QCard>
v-else-if="column.name === 'discount'" </div>
> </template>
<QItemLabel class="text-primary"> </QTable>
{{ column.value }} </template>
<VnDiscount </CrudModel>
:quantity="props.row.quantity"
:price="props.row.sale.price"
:discount="
props.row.sale.discount
"
:mana="mana"
:promise="updateDiscount"
:data="{
saleFk: props.row.sale.id,
rowIndex: props.rowIndex,
}"
@on-update="onUpdateDiscount"
/>
</QItemLabel>
</template>
<template v-else>
<QItemLabel>
{{ column.value }}
</QItemLabel>
</template>
</QItemSection>
</QItem>
</QList>
</QCard>
</div>
</template>
</QTable>
</template>
</CrudModel>
</div>
</div> </div>
<QPageSticky position="bottom-right" :offset="[25, 25]"> <QPageSticky position="bottom-right" :offset="[25, 25]">

View File

@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import { toDate, toCurrency, toPercentage } from 'filters/index'; import { toDate, toCurrency, toPercentage } from 'filters/index';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import axios from 'axios'; import axios from 'axios';
defineEmits([...useDialogPluginComponent.emits]); defineEmits([...useDialogPluginComponent.emits]);
@ -118,7 +119,6 @@ function cancel() {
<QBtn icon="close" flat round dense v-close-popup /> <QBtn icon="close" flat round dense v-close-popup />
</QCardSection> </QCardSection>
<QTable <QTable
class="my-sticky-header-table"
:columns="columns" :columns="columns"
:rows="claimableSales" :rows="claimableSales"
row-key="saleFk" row-key="saleFk"
@ -126,7 +126,14 @@ function cancel() {
v-model:selected="selected" v-model:selected="selected"
square square
flat flat
/> >
<template #body-cell-description="{ row, value }">
<QTd auto-width align="right" class="link">
{{ value }}
<ItemDescriptorProxy :id="row.itemFk"></ItemDescriptorProxy>
</QTd>
</template>
</QTable>
<QSeparator /> <QSeparator />
<QCardActions align="right"> <QCardActions align="right">
<QBtn :label="t('globals.cancel')" color="primary" flat @click="cancel" /> <QBtn :label="t('globals.cancel')" color="primary" flat @click="cancel" />
@ -148,33 +155,6 @@ function cancel() {
} }
</style> </style>
<style lang="scss">
.my-sticky-header-table {
height: 400px;
thead tr th {
position: sticky;
z-index: 1;
}
thead tr:first-child th {
/* this is when the loading indicator appears */
top: 0;
}
&.q-table--loading thead tr:last-child th {
/* height of all previous header rows */
top: 48px;
}
// /* prevent scrolling behind sticky top row on focus */
tbody {
/* height of all previous header rows */
scroll-margin-top: 48px;
}
}
</style>
<i18n> <i18n>
es: es:
Available sales lines: Líneas de venta disponibles Available sales lines: Líneas de venta disponibles

View File

@ -16,7 +16,7 @@ const claimId = computed(() => $props.id || route.params.id);
const claimFilter = { const claimFilter = {
where: { claimFk: claimId.value }, where: { claimFk: claimId.value },
fields: ['created', 'workerFk', 'text'], fields: ['id', 'created', 'workerFk', 'text'],
include: { include: {
relation: 'worker', relation: 'worker',
scope: { scope: {
@ -38,10 +38,11 @@ const body = {
</script> </script>
<template> <template>
<VnNotes <VnNotes
style="overflow-y: auto"
:add-note="$props.addNote"
url="claimObservations" url="claimObservations"
:add-note="$props.addNote"
:filter="claimFilter" :filter="claimFilter"
:body="body" :body="body"
v-bind="$attrs"
style="overflow-y: auto"
/> />
</template> </template>

View File

@ -222,8 +222,8 @@ function openDialog(dmsId) {
</template> </template>
</VnLv> </VnLv>
<VnLv <VnLv
:label="t('claim.summary.pickup')" :label="t('claim.basicData.pickup')"
:value="t(`claim.summary.${claim.pickup}`)" :value="t(`claim.basicData.${claim.pickup}`)"
/> />
</QCard> </QCard>
<QCard class="vn-three"> <QCard class="vn-three">
@ -280,6 +280,48 @@ function openDialog(dmsId) {
</template> </template>
</QTable> </QTable>
</QCard> </QCard>
<QCard class="vn-two" v-if="claimDms.length > 0">
<VnTitle
:url="`#/claim/${entityId}/photos`"
:text="t('claim.summary.photos')"
/>
<div class="container">
<div
class="multimedia-container"
v-for="(media, index) of claimDms"
:key="index"
>
<div class="relative-position">
<QIcon
name="play_circle"
color="primary"
size="xl"
class="absolute-center zindex"
v-if="media.isVideo"
@click.stop="openDialog(media.dmsFk)"
>
<QTooltip>Video</QTooltip>
</QIcon>
<QCard class="multimedia relative-position">
<QImg
:src="media.url"
class="rounded-borders cursor-pointer fit"
@click="openDialog(media.dmsFk)"
v-if="!media.isVideo"
>
</QImg>
<video
:src="media.url"
class="rounded-borders cursor-pointer fit"
muted="muted"
v-if="media.isVideo"
@click="openDialog(media.dmsFk)"
/>
</QCard>
</div>
</div>
</div>
</QCard>
<QCard class="vn-two" v-if="developments.length > 0"> <QCard class="vn-two" v-if="developments.length > 0">
<VnTitle <VnTitle
:url="claimUrl + 'development'" :url="claimUrl + 'development'"
@ -302,49 +344,6 @@ function openDialog(dmsId) {
</template> </template>
</QTable> </QTable>
</QCard> </QCard>
<QCard class="vn-max" v-if="claimDms.length > 0">
<VnTitle
:url="`#/claim/${entityId}/photos`"
:text="t('claim.summary.photos')"
/>
<div class="container">
<div
class="multimedia-container"
v-for="(media, index) of claimDms"
:key="index"
>
<div class="relative-position">
<QIcon
name="play_circle"
color="primary"
size="xl"
class="absolute-center zindex"
v-if="media.isVideo"
@click.stop="openDialog(media.dmsFk)"
>
<QTooltip>Video</QTooltip>header
</QIcon>
<QCard class="multimedia relative-position">
<QImg
:src="media.url"
class="rounded-borders cursor-pointer fit"
@click="openDialog(media.dmsFk)"
v-if="!media.isVideo"
>
</QImg>
<video
:src="media.url"
class="rounded-borders cursor-pointer fit"
muted="muted"
v-if="media.isVideo"
@click="openDialog(media.dmsFk)"
/>
</QCard>
</div>
</div>
</div>
</QCard>
<QCard class="vn-max"> <QCard class="vn-max">
<VnTitle :url="claimUrl + 'action'" :text="t('claim.summary.actions')" /> <VnTitle :url="claimUrl + 'action'" :text="t('claim.summary.actions')" />
<div id="slider-container" class="q-px-xl q-py-md"> <div id="slider-container" class="q-px-xl q-py-md">

View File

@ -1,18 +1,21 @@
<script setup> <script setup>
import { ref } from 'vue'; import { onBeforeMount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const addresses = ref([]);
const client = ref(null);
const provincesLocation = ref([]); const provincesLocation = ref([]);
const consigneeFilter = { const addressFilter = {
fields: [ fields: [
'id', 'id',
'isDefaultAddress', 'isDefaultAddress',
@ -44,6 +47,42 @@ const consigneeFilter = {
], ],
}; };
onBeforeMount(() => {
const { id } = route.params;
getAddressesData(id);
getClientData(id);
});
watch(
() => route.params.id,
(newValue) => {
if (!newValue) return;
getAddressesData(newValue);
getClientData(newValue);
}
);
const getAddressesData = async (id) => {
try {
const { data } = await axios.get(`Clients/${id}/addresses`, {
params: { filter: JSON.stringify(addressFilter) },
});
addresses.value = data;
sortAddresses();
} catch (error) {
return error;
}
};
const getClientData = async (id) => {
try {
const { data } = await axios.get(`Clients/${id}`);
client.value = data;
} catch (error) {
return error;
}
};
const setProvince = (provinceFk) => { const setProvince = (provinceFk) => {
const result = provincesLocation.value.filter( const result = provincesLocation.value.filter(
(province) => province.id === provinceFk (province) => province.id === provinceFk
@ -51,16 +90,38 @@ const setProvince = (provinceFk) => {
return result[0]?.name || ''; return result[0]?.name || '';
}; };
const toCustomerConsigneeCreate = () => { const isDefaultAddress = (address) => {
router.push({ name: 'CustomerConsigneeCreate' }); return client?.value?.defaultAddressFk === address.id ? 1 : 0;
}; };
const toCustomerConsigneeEdit = (consigneeId) => { const setDefault = (address) => {
const url = `Clients/${route.params.id}`;
const payload = { defaultAddressFk: address.id };
axios.patch(url, payload).then((res) => {
if (res.data) {
client.value.defaultAddressFk = res.data.defaultAddressFk;
sortAddresses();
}
});
};
const sortAddresses = () => {
if (!client.value || !addresses.value) return;
addresses.value = addresses.value.sort((a, b) => {
return isDefaultAddress(b) - isDefaultAddress(a);
});
};
const toCustomerAddressCreate = () => {
router.push({ name: 'CustomerAddressCreate' });
};
const toCustomerAddressEdit = (addressId) => {
router.push({ router.push({
name: 'CustomerConsigneeEdit', name: 'CustomerAddressEdit',
params: { params: {
id: route.params.id, id: route.params.id,
consigneeId, addressId,
}, },
}); });
}; };
@ -73,26 +134,41 @@ const toCustomerConsigneeEdit = (consigneeId) => {
url="Provinces/location" url="Provinces/location"
/> />
<QCard class="q-pa-lg"> <div class="full-width flex justify-center">
<VnPaginate <QCard class="card-width q-pa-lg" v-if="addresses.length">
data-key="CustomerConsignees" <QCardSection>
:url="`Clients/${route.params.id}/addresses`" <div
order="id" v-for="(item, index) in addresses"
auto-load
:filter="consigneeFilter"
>
<template #body="{ rows }">
<QCard
v-for="(item, index) in rows"
:key="index" :key="index"
class="address-card"
:class="{ :class="{
'consignees-card': true, 'q-mb-md': index < addresses.length - 1,
'q-mb-md': index < rows.length - 1, 'item-disabled': !item.isActive,
}" }"
@click="toCustomerConsigneeEdit(item.id)" @click="toCustomerAddressEdit(item.id)"
> >
<div class="q-ml-xs q-mr-md flex items-center"> <div class="q-ml-xs q-mr-md flex items-center">
<QIcon name="star" size="md" color="primary" /> <QIcon
:style="{
'font-variation-settings': `'FILL' ${isDefaultAddress(
item
)}`,
}"
color="primary"
name="star"
size="md"
@click.stop="!isDefaultAddress(item) && setDefault(item)"
>
<QTooltip>
{{
t(
isDefaultAddress(item)
? 'Default address'
: 'Set as default'
)
}}
</QTooltip>
</QIcon>
</div> </div>
<div> <div>
<div class="text-weight-bold q-mb-sm"> <div class="text-weight-bold q-mb-sm">
@ -136,12 +212,13 @@ const toCustomerConsigneeEdit = (consigneeId) => {
<div>{{ observation.description }}</div> <div>{{ observation.description }}</div>
</div> </div>
</div> </div>
</QCard> </div>
</template> </QCardSection>
</VnPaginate> </QCard>
</QCard> </div>
<QPageSticky :offset="[18, 18]"> <QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerConsigneeCreate()" color="primary" fab icon="add" /> <QBtn @click.stop="toCustomerAddressCreate()" color="primary" fab icon="add" />
<QTooltip> <QTooltip>
{{ t('New consignee') }} {{ t('New consignee') }}
</QTooltip> </QTooltip>
@ -149,8 +226,8 @@ const toCustomerConsigneeEdit = (consigneeId) => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.consignees-card { .address-card {
border: 2px solid var(--vn-accent-color); border: 2px solid var(--vn-light-gray);
border-radius: 10px; border-radius: 10px;
padding: 10px; padding: 10px;
display: flex; display: flex;
@ -160,6 +237,10 @@ const toCustomerConsigneeEdit = (consigneeId) => {
background-color: var(--vn-accent-color); background-color: var(--vn-accent-color);
} }
} }
.item-disabled {
opacity: 0.6;
}
</style> </style>
<i18n> <i18n>
@ -167,4 +248,6 @@ es:
Is equalizated: Recargo de equivalencia Is equalizated: Recargo de equivalencia
Is logiflora allowed: Compra directa en Holanda Is logiflora allowed: Compra directa en Holanda
New consignee: Nuevo consignatario New consignee: Nuevo consignatario
Default address: Consignatario predeterminado
Set as default: Establecer como predeterminado
</i18n> </i18n>

View File

@ -1,145 +1,102 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, onBeforeMount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { date, QCheckbox, QBtn, useQuasar } from 'quasar'; import axios from 'axios';
import { QCheckbox, QBtn, useQuasar } from 'quasar';
import { toCurrency } from 'src/filters'; import { toCurrency, toDate, toDateHourMin } from 'src/filters';
import { useState } from 'src/composables/useState';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useValidator } from 'src/composables/useValidator';
import { usePrintService } from 'src/composables/usePrintService';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue'; import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import CustomerNewPayment from 'src/pages/Customer/components/CustomerNewPayment.vue'; import CustomerNewPayment from 'src/pages/Customer/components/CustomerNewPayment.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue';
const { sendEmail } = usePrintService();
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const { validate } = useValidator();
const quasar = useQuasar(); const quasar = useQuasar();
const route = useRoute();
const state = useState();
const stateStore = useStateStore(); const stateStore = useStateStore();
const user = state.getUser();
const clientRisks = ref(null); const clientRisks = ref(null);
const companiesOptions = ref([]);
const companyId = ref(442);
const rows = ref(null);
const workerId = ref(0);
const receiptsRef = ref(null);
const clientRisksRef = ref(null); const clientRisksRef = ref(null);
const companiesOptions = ref([]);
const companyId = ref(null);
const receiptsRef = ref(null);
const receiptsData = ref([]);
const filterCompanies = { order: ['code'] }; const filterCompanies = { order: ['code'] };
const params = { const userParams = {
clientId: `${route.params.id}`, clientId: route.params.id,
companyId: companyId.value, companyId: user.value.companyFk,
filter: { limit: 20 },
}; };
const filter = { const filter = {
include: { relation: 'company', scope: { fields: ['code'] } }, include: { relation: 'company', scope: { fields: ['code'] } },
where: { clientFk: `${route.params.id}`, companyFk: companyId.value }, where: { clientFk: route.params.id, companyFk: user.value.companyFk },
};
const tableColumnComponents = {
payed: {
component: 'span',
props: () => {},
event: () => {},
},
created: {
component: 'span',
props: () => {},
event: () => {},
},
userName: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: (prop) => {
workerId.value = prop.row.clientFk;
},
},
description: {
component: 'span',
props: () => {},
event: () => {},
},
bankFk: {
component: 'span',
props: () => {},
event: () => {},
},
debit: {
component: 'span',
props: () => {},
event: () => {},
},
credit: {
component: 'span',
props: () => {},
event: () => {},
},
balance: {
component: 'span',
props: () => {},
event: () => {},
},
isConciliate: {
component: QCheckbox,
props: (prop) => ({
disable: true,
'model-value': Boolean(prop.value),
}),
event: () => {},
},
}; };
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
field: 'payed', field: 'payed',
format: (value) => date.formatDate(value, 'DD/MM/YYYY'), format: (value) => toDate(value),
label: t('Date'), label: t('Date'),
name: 'payed', name: 'date',
}, },
{ {
align: 'left', align: 'left',
field: 'created', field: 'created',
format: (value) => date.formatDate(value, 'DD/MM/YYYY hh:mm'), format: (value) => toDateHourMin(value),
label: t('Creation date'), label: t('Creation date'),
name: 'created', name: 'creationDate',
}, },
{ {
align: 'left', align: 'left',
field: 'userName', field: 'userName',
label: t('Employee'), label: t('Employee'),
name: 'userName', name: 'employee',
}, },
{ {
align: 'left', align: 'left',
field: 'description', field: 'description',
label: t('Reference'), label: t('Reference'),
name: 'description', name: 'reference',
}, },
{ {
align: 'left', align: 'left',
field: 'bankFk', field: 'bankFk',
label: t('Bank'), label: t('Bank'),
name: 'bankFk', name: 'bank',
}, },
{ {
align: 'left', align: 'right',
field: 'debit', field: 'debit',
format: (value) => value && toCurrency(value),
label: t('Debit'), label: t('Debit'),
name: 'debit', name: 'debit',
}, },
{ {
align: 'left', align: 'right',
field: 'credit', field: 'credit',
format: (value) => toCurrency(value), format: (value) => value && toCurrency(value),
label: t('Havings'), label: t('Havings'),
name: 'credit', name: 'havings',
}, },
{ {
align: 'left', align: 'right',
field: (value) => value.debit - value.credit, field: 'balance',
format: (value) => toCurrency(value), format: (value) => value && toCurrency(value),
label: t('Balance'), label: t('Balance'),
name: 'balance', name: 'balance',
}, },
@ -147,16 +104,58 @@ const columns = computed(() => [
align: 'left', align: 'left',
field: 'isConciliate', field: 'isConciliate',
label: t('Conciliated'), label: t('Conciliated'),
name: 'isConciliate', name: 'conciliated',
},
{
align: 'left',
field: 'totalWithVat',
label: '',
name: 'actions',
}, },
]); ]);
const getData = () => { onBeforeMount(() => {
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
companyId.value = user.value.companyFk;
});
watch(
() => route.params.id,
(newValue) => {
if (!newValue) return;
userParams.clientId = newValue;
filter.where.clientFk = newValue;
getData();
}
);
const getData = () => {
receiptsRef.value?.fetch(); receiptsRef.value?.fetch();
clientRisksRef.value?.fetch(); clientRisksRef.value?.fetch();
}; };
const getCurrentBalance = () => {
const currentBalance = clientRisks.value.find((balance) => {
return balance.companyFk === companyId.value;
});
return currentBalance && currentBalance.amount;
};
const onFetch = (balances) => {
balances.forEach((balance, index) => {
if (index === 0) {
balance.balance = getCurrentBalance();
} else {
let previousBalance = balances[index - 1];
balance.balance =
previousBalance.balance -
(previousBalance.debit - previousBalance.credit);
}
});
receiptsData.value = balances;
};
const showNewPaymentDialog = () => { const showNewPaymentDialog = () => {
quasar.dialog({ quasar.dialog({
component: CustomerNewPayment, component: CustomerNewPayment,
@ -169,25 +168,29 @@ const showNewPaymentDialog = () => {
}; };
const updateCompanyId = (id) => { const updateCompanyId = (id) => {
if (id) companyId.value = id; if (id) {
companyId.value = id;
userParams.companyId = id;
filter.where.companyFk = id;
}
getData(); getData();
}; };
const saveFieldValue = async (row) => {
try {
const payload = { description: row.description };
await axios.patch(`Receipts/${row.id}`, payload);
} catch (err) {
return err;
}
};
const sendEmailAction = () => {
sendEmail(`Suppliers/${route.params.id}/campaign-metrics-email`);
};
</script> </script>
<template> <template>
<FetchData
:filter="filterCompanies"
@on-fetch="(data) => (companiesOptions = data)"
auto-load
url="Companies"
/>
<FetchData
:params="params"
@on-fetch="(data) => (rows = data)"
auto-load
ref="receiptsRef"
url="Receipts/filter"
/>
<FetchData <FetchData
:filter="filter" :filter="filter"
@on-fetch="(data) => (clientRisks = data)" @on-fetch="(data) => (clientRisks = data)"
@ -195,36 +198,121 @@ const updateCompanyId = (id) => {
ref="clientRisksRef" ref="clientRisksRef"
url="ClientRisks" url="ClientRisks"
/> />
<FetchData
:filter="filterCompanies"
@on-fetch="(data) => (companiesOptions = data)"
auto-load
url="Companies"
/>
<QTable <VnPaginate
:columns="columns" auto-load
:rows="rows" data-key="CustomerBalance"
class="full-width q-mt-md" url="Receipts/filter"
row-key="id" :user-params="userParams"
v-if="rows?.length" ref="receiptsRef"
@on-fetch="onFetch"
> >
<template #body-cell="props"> <template #body="{ rows }">
<QTd :props="props"> <QTable
<QTr :props="props" class="cursor-pointer"> :columns="columns"
<component :no-data-label="t('globals.noResults')"
:is="tableColumnComponents[props.col.name].component" :rows-per-page-options="[0]"
class="col-content" :rows="rows"
v-bind="tableColumnComponents[props.col.name].props(props)" class="full-width q-mt-md"
@click="tableColumnComponents[props.col.name].event(props)" row-key="id"
> >
<template v-if="props.col.name !== 'isConciliate'"> <template #body-cell-employee="{ row }">
{{ props.value }} <QTd auto-width @click.stop>
</template> <QBtn color="blue" flat no-caps>{{ row.userName }}</QBtn>
<WorkerDescriptorProxy :id="workerId" /> <WorkerDescriptorProxy :id="row.clientFk" />
</component> </QTd>
</QTr> </template>
</QTd>
</template>
</QTable>
<h5 class="flex justify-center label-color" v-else> <template #body-cell-reference="{ row }">
{{ t('globals.noResults') }} <QTd auto-width @click.stop v-if="row.isInvoice">
</h5> <QBtn color="blue" dense flat>
{{ t('bill', { ref: row.description }) }}
</QBtn>
<InvoiceOutDescriptorProxy :id="row.id" />
</QTd>
<QTd v-else>
<VnInput
@keyup.enter="saveFieldValue(row)"
autofocus
clearable
dense
v-model="row.description"
/>
</QTd>
</template>
<template #body-cell-conciliated="{ row }">
<QTd align="center">
<QCheckbox :model-value="row.isConciliate === 1" disable />
</QTd>
</template>
<template #body-cell-actions="{ row }">
<QTd align="center">
<QIcon
@click.stop="showDialog = true"
class="q-ml-md"
color="primary"
name="outgoing_mail"
size="sm"
style="font-variation-settings: 'FILL' 1"
v-if="row.isCompensation"
>
<QTooltip>
{{ t('Send compensation') }}
</QTooltip>
</QIcon>
<QDialog v-model="showDialog">
<QCard class="q-pa-sm">
<QCardSection>
<span
ref="closeButton"
class="flex justify-end color-vn-label"
v-close-popup
>
<QIcon name="close" size="sm" />
</span>
<div class="text-h6">
{{ t('Send compensation') }}
</div>
</QCardSection>
<QCardSection>
<div>
{{
t(
'Do you want to report compensation to the client by mail?'
)
}}
</div>
</QCardSection>
<QCardActions class="flex justify-end q-mb-sm">
<QBtn
:label="t('globals.cancel')"
color="primary"
flat
v-close-popup
/>
<QBtn
:label="t('globals.save')"
@click="sendEmailAction"
class="q-ml-sm"
color="primary"
/>
</QCardActions>
</QCard>
</QDialog>
</QTd>
</template>
</QTable>
</template>
</VnPaginate>
<QDrawer :width="256" show-if-above side="right" v-model="stateStore.rightDrawer"> <QDrawer :width="256" show-if-above side="right" v-model="stateStore.rightDrawer">
<div class="q-mt-xl q-px-md"> <div class="q-mt-xl q-px-md">
@ -236,10 +324,11 @@ const updateCompanyId = (id) => {
option-label="code" option-label="code"
option-value="id" option-value="id"
v-model="companyId" v-model="companyId"
:rules="validate('entry.companyFk')"
/> />
</div> </div>
<QCard class="q-ma-md q-pa-md q-mt-lg" v-if="rows?.length"> <QCard class="q-ma-md q-pa-md q-mt-lg" v-if="receiptsData?.length">
<QCardSection> <QCardSection>
<div class="flex justify-center text-subtitle1 text-bold"> <div class="flex justify-center text-subtitle1 text-bold">
{{ t('Total by company') }} {{ t('Total by company') }}
@ -265,6 +354,8 @@ const updateCompanyId = (id) => {
</template> </template>
<i18n> <i18n>
en:
bill: 'N/INV {ref}'
es: es:
Company: Empresa Company: Empresa
Total by company: Total por empresa Total by company: Total por empresa
@ -273,9 +364,12 @@ es:
Creation date: Fecha de creación Creation date: Fecha de creación
Employee: Empleado Employee: Empleado
Reference: Referencia Reference: Referencia
bill: 'N/FRA {ref}'
Bank: Caja Bank: Caja
Debit: Debe Debit: Debe
Havings: Haber Havings: Haber
Balance: Balance Balance: Balance
Conciliated: Conciliado Conciliated: Conciliado
Send compensation: Enviar compensación
Do you want to report compensation to the client by mail?: ¿Desea informar de la compensación al cliente por correo?
</i18n> </i18n>

View File

@ -60,90 +60,105 @@ const filterOptions = {
@on-fetch="(data) => (businessTypes = data)" @on-fetch="(data) => (businessTypes = data)"
auto-load auto-load
/> />
<fetch-data
:filter="filter"
@on-fetch="(data) => (clients = data)"
auto-load
url="Clients"
/>
<FormModel :url="`Clients/${route.params.id}`" model="customer" auto-load> <FormModel :url="`Clients/${route.params.id}`" auto-load model="customer">
<template #form="{ data, validate, filter }"> <template #form="{ data, validate, filter }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnInput <VnInput
v-model="data.socialName" :label="t('Comercial name')"
:label="t('customer.basicData.socialName')"
:rules="validate('client.socialName')" :rules="validate('client.socialName')"
autofocus autofocus
clearable
v-model="data.name"
/> />
</div> </div>
<div class="col"> <div class="col">
<QSelect <QSelect
v-model="data.businessTypeFk"
:options="businessTypes"
option-value="code"
option-label="description"
emit-value
:label="t('customer.basicData.businessType')"
map-options
:rules="validate('client.businessTypeFk')"
:input-debounce="0" :input-debounce="0"
:label="t('customer.basicData.businessType')"
:options="businessTypes"
:rules="validate('client.businessTypeFk')"
emit-value
map-options
option-label="description"
option-value="code"
v-model="data.businessTypeFk"
/> />
</div> </div>
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnInput <VnInput
v-model="data.contact"
:label="t('customer.basicData.contact')" :label="t('customer.basicData.contact')"
:rules="validate('client.contact')" :rules="validate('client.contact')"
clearable clearable
v-model="data.contact"
/> />
</div> </div>
<div class="col"> <div class="col">
<VnInput <VnInput
v-model="data.email"
type="email"
:label="t('customer.basicData.email')" :label="t('customer.basicData.email')"
:rules="validate('client.email')" :rules="validate('client.email')"
clearable clearable
/> type="email"
v-model="data.email"
>
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>{{
t('customer.basicData.youCanSaveMultipleEmails')
}}</QTooltip>
</QIcon>
</template>
</VnInput>
</div> </div>
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnInput <VnInput
v-model="data.phone"
:label="t('customer.basicData.phone')" :label="t('customer.basicData.phone')"
:rules="validate('client.phone')" :rules="validate('client.phone')"
clearable clearable
v-model="data.phone"
/> />
</div> </div>
<div class="col"> <div class="col">
<VnInput <VnInput
v-model="data.mobile"
:label="t('customer.basicData.mobile')" :label="t('customer.basicData.mobile')"
:rules="validate('client.mobile')" :rules="validate('client.mobile')"
clearable clearable
v-model="data.mobile"
/> />
</div> </div>
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<QSelect <QSelect
v-model="data.salesPersonFk"
:options="workers"
option-value="id"
option-label="name"
emit-value
:label="t('customer.basicData.salesPerson')"
map-options
use-input
@filter="(value, update) => filter(value, update, filterOptions)"
:rules="validate('client.salesPersonFk')"
:input-debounce="0" :input-debounce="0"
:label="t('customer.basicData.salesPerson')"
:options="workers"
:rules="validate('client.salesPersonFk')"
@filter="(value, update) => filter(value, update, filterOptions)"
emit-value
map-options
option-label="name"
option-value="id"
use-input
v-model="data.salesPersonFk"
> >
<template #prepend> <template #prepend>
<QAvatar color="orange"> <QAvatar color="orange">
<QImg <QImg
v-if="data.salesPersonFk"
:src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`" :src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`"
spinner-color="white" spinner-color="white"
v-if="data.salesPersonFk"
/> />
</QAvatar> </QAvatar>
</template> </template>
@ -151,18 +166,49 @@ const filterOptions = {
</div> </div>
<div class="col"> <div class="col">
<QSelect <QSelect
v-model="data.contactChannelFk"
:options="contactChannels"
option-value="id"
option-label="name"
emit-value
:label="t('customer.basicData.contactChannel')"
map-options
:rules="validate('client.contactChannelFk')"
:input-debounce="0" :input-debounce="0"
:label="t('customer.basicData.contactChannel')"
:options="contactChannels"
:rules="validate('client.contactChannelFk')"
emit-value
map-options
option-label="name"
option-value="id"
v-model="data.contactChannelFk"
/> />
</div> </div>
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QSelect
:input-debounce="0"
:label="t('customer.basicData.previousClient')"
:options="clients"
:rules="validate('client.transferorFk')"
emit-value
map-options
option-label="name"
option-value="id"
v-model="data.transferorFk"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{
t(
'In case of a company succession, specify the grantor company'
)
}}</QTooltip>
</QIcon>
</template>
</QSelect>
</div>
</VnRow>
</template> </template>
</FormModel> </FormModel>
</template> </template>
<i18n>
es:
In case of a company succession, specify the grantor company: En el caso de que haya habido una sucesión de empresa, indicar la empresa cedente
Comercial name: Nombre comercial
</i18n>

View File

@ -1,10 +1,8 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import axios from 'axios';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
@ -26,8 +24,9 @@ const filter = {
limit: 30, limit: 30,
}; };
const getBankEntities = () => { const getBankEntities = (data, formData) => {
bankEntitiesRef.value.fetch(); bankEntitiesRef.value.fetch();
formData.bankEntityFk = Number(data.id);
}; };
</script> </script>
@ -60,17 +59,13 @@ const getBankEntities = () => {
/> />
</div> </div>
<div class="col"> <div class="col">
<VnInput <VnInput :label="t('Due day')" clearable v-model="data.dueDay" />
:label="t('Due day')"
:rules="validate('client.socialName')"
v-model="data.dueDay"
/>
</div> </div>
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnInput :label="t('IBAN')" v-model="data.iban"> <VnInput :label="t('IBAN')" clearable v-model="data.iban">
<template #append> <template #append>
<QIcon name="info" class="cursor-info"> <QIcon name="info" class="cursor-info">
<QTooltip>{{ t('components.iban_tooltip') }}</QTooltip> <QTooltip>{{ t('components.iban_tooltip') }}</QTooltip>
@ -90,7 +85,9 @@ const getBankEntities = () => {
v-model="data.bankEntityFk" v-model="data.bankEntityFk"
> >
<template #form> <template #form>
<CreateBankEntityForm @on-data-saved="getBankEntities()" /> <CreateBankEntityForm
@on-data-saved="getBankEntities($event, data)"
/>
</template> </template>
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">

View File

@ -1,7 +1,6 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useRoute } from 'vue-router';
import CustomerDescriptor from './CustomerDescriptor.vue'; import CustomerDescriptor from './CustomerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue'; import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
@ -9,7 +8,6 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize'; import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore(); const stateStore = useStateStore();
const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
</script> </script>
<template> <template>

View File

@ -0,0 +1,16 @@
<script setup>
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>
<template>
<h5 class="flex justify-center color-vn-label">
{{ t('Enter a new search') }}
</h5>
</template>
<i18n>
es:
Enter a new search: Introduce una nueva búsqueda
</i18n>

View File

@ -0,0 +1,87 @@
<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 customerContactsRef = ref(null);
onMounted(() => {
if (customerContactsRef.value) customerContactsRef.value.reload();
});
</script>
<template>
<div class="full-width flex justify-center">
<QPage class="card-width q-pa-lg">
<CrudModel
:data-required="{ clientFk: route.params.id }"
:default-remove="false"
:filter="{
fields: ['id', 'name', 'phone', 'clientFk'],
where: { clientFk: route.params.id },
}"
data-key="CustomerContacts"
model="CustomerContacts"
ref="customerContactsRef"
url="ClientContacts"
>
<template #body="{ rows }">
<QCard class="q-pl-lg q-py-md">
<VnRow
v-for="(row, index) in rows"
:key="index"
class="row q-gutter-md q-mb-md"
>
<div class="col">
<VnInput :label="t('Name')" v-model="row.name" />
</div>
<div class="col">
<VnInput :label="t('Phone')" v-model="row.phone" />
</div>
<div class="col-1 row justify-center items-center">
<QIcon
@click="customerContactsRef.remove([row])"
class="cursor-pointer"
color="primary"
name="delete"
size="sm"
>
<QTooltip>
{{ t('Remove contact') }}
</QTooltip>
</QIcon>
</div>
</VnRow>
<VnRow>
<QIcon
@click="customerContactsRef.insert()"
class="cursor-pointer"
color="primary"
name="add"
size="sm"
>
<QTooltip>
{{ t('Add contact') }}
</QTooltip>
</QIcon>
</VnRow>
</QCard>
</template>
</CrudModel>
</QPage>
</div>
</template>
<i18n>
es:
Name: Nombre
Phone: Teléfono
Remove contact: Quitar contacto
Add contact: Añadir contacto
</i18n>

View File

@ -1,3 +1,233 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import ModalCloseContract from 'src/pages/Customer/components/ModalCloseContract.vue';
import { toDate } from 'src/filters';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const quasar = useQuasar();
const vnPaginateRef = ref(null);
const showQPageSticky = ref(true);
const filter = {
order: 'finished ASC, started DESC',
include: [
{
relation: 'insurances',
scope: {
fields: ['id', 'credit', 'created', 'grade'],
order: 'created DESC',
limit: 2,
},
},
],
where: { client: `${route.params.id}` },
};
const fetch = (data) => {
data.forEach((element) => {
if (!element.finished) {
showQPageSticky.value = false;
return;
}
});
};
const toCustomerCreditContractsCreate = () => {
router.push({ name: 'CustomerCreditContractsCreate' });
};
const openDialog = (item) => {
quasar.dialog({
component: ModalCloseContract,
componentProps: {
id: item.id,
promise: updateData,
},
});
};
const openViewCredit = (credit) => {
router.push({
name: 'CustomerCreditContractsInsurance',
params: {
creditId: credit.id,
},
});
};
const updateData = () => {
vnPaginateRef.value?.fetch();
};
</script>
<template> <template>
<div class="flex justify-center">Customer credit contracts</div> <div class="full-width flex justify-center">
<QCard class="card-width q-pa-lg">
<VnPaginate
:filter="filter"
@on-fetch="fetch"
auto-load
data-key="CustomerCreditContracts"
order="id DESC"
ref="vnPaginateRef"
url="CreditClassifications"
>
<template #body="{ rows }">
<div v-if="rows.length">
<QCard
v-for="(item, index) in rows"
:key="index"
:class="{
'customer-card': true,
'q-mb-md': index < rows.length - 1,
'is-active': !item.finished,
}"
>
<QCardSection
class="full-width flex justify-between q-py-none"
>
<div class="width-state flex">
<div
class="flex items-center cursor-pointer q-mr-md"
v-if="!item.finished"
>
<QIcon
@click.stop="openDialog(item)"
color="primary"
name="lock"
size="md"
style="font-variation-settings: 'FILL' 1"
>
<QTooltip>{{ t('Close contract') }}</QTooltip>
</QIcon>
</div>
<div>
<div class="flex q-mb-xs">
<div class="q-mr-sm color-vn-label">
{{ t('Since') }}:
</div>
<div class="text-weight-bold">
{{ toDate(item.started) }}
</div>
</div>
<div class="flex">
<div class="q-mr-sm color-vn-label">
{{ t('To') }}:
</div>
<div class="text-weight-bold">
{{ toDate(item.finished) }}
</div>
</div>
</div>
</div>
<QSeparator vertical />
<div class="width-data flex">
<div
class="full-width flex justify-between items-center"
v-if="item?.insurances.length"
>
<div class="flex">
<div class="color-vn-label q-mr-xs">
{{ t('Credit') }}:
</div>
<div class="text-weight-bold">
{{ item.insurances[0].credit }}
</div>
</div>
<div class="flex">
<div class="color-vn-label q-mr-xs">
{{ t('Grade') }}:
</div>
<div class="text-weight-bold">
{{ item.insurances[0].grade || '-' }}
</div>
</div>
<div class="flex">
<div class="color-vn-label q-mr-xs">
{{ t('Date') }}:
</div>
<div class="text-weight-bold">
{{ toDate(item.insurances[0].created) }}
</div>
</div>
<div class="flex items-center cursor-pointer">
<QIcon
@click.stop="openViewCredit(item)"
color="primary"
name="preview"
size="md"
>
<QTooltip>{{
t('View credits')
}}</QTooltip>
</QIcon>
</div>
</div>
</div>
</QCardSection>
</QCard>
</div>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</template>
</VnPaginate>
</QCard>
</div>
<QPageSticky :offset="[18, 18]" v-if="showQPageSticky">
<QBtn
@click.stop="toCustomerCreditContractsCreate()"
color="primary"
fab
icon="add"
/>
<QTooltip>
{{ t('New contract') }}
</QTooltip>
</QPageSticky>
</template> </template>
<style lang="scss" scoped>
.customer-card {
border: 2px solid var(--vn-light-gray);
border-radius: 10px;
padding: 10px;
display: flex;
justify-content: space-between;
}
.is-active {
background-color: var(--vn-light-gray);
}
.width-state {
width: 30%;
}
.width-data {
width: 65%;
}
</style>
<i18n>
es:
Close contract: Cerrar contrato
Since: Desde
To: Hasta
Credit: Crédito
Grade: Grade
Date: Fecha
View credits: Ver créditos
Created: Fecha creación
New contract: Nuevo contrato
</i18n>

View File

@ -1,3 +0,0 @@
<template>
<div class="flex justify-center">Credit management</div>
</template>

View File

@ -1,3 +1,142 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { QBtn } from 'quasar';
import { toCurrency, toDateHourMin } from 'src/filters';
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 WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
const { t } = useI18n();
const route = useRoute();
const clientInformasRef = ref(null);
const rows = ref([]);
const filter = {
include: [
{
relation: 'worker',
scope: {
fields: ['id'],
include: { relation: 'user', scope: { fields: ['nickname'] } },
},
},
],
where: { clientFk: route.params.id },
order: ['created DESC'],
limit: 20,
};
const columns = computed(() => [
{
align: 'left',
field: 'created',
format: (value) => toDateHourMin(value),
label: t('Since'),
name: 'since',
},
{
align: 'left',
field: (row) => row.worker.user.nickname,
label: t('Employee'),
name: 'employee',
},
{
align: 'right',
field: 'rating',
label: t('Rating'),
name: 'rating',
},
{
align: 'right',
field: 'recommendedCredit',
format: (value) => toCurrency(value),
label: t('Recommended credit'),
name: 'recommendedCredit',
},
]);
watch(
() => route.params.id,
(newValue) => {
if (!newValue) return;
filter.where.clientFk = newValue;
clientInformasRef.value?.fetch();
}
);
</script>
<template> <template>
<div class="flex justify-center">Customer credit opinion</div> <FetchData
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
ref="clientInformasRef"
url="ClientInformas"
/>
<FormModel
:form-initial-data="{}"
:observe-form-changes="false"
:url-create="`Clients/${route.params.id}/setRating`"
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Rating')"
clearable
type="number"
v-model.number="data.rating"
/>
</div>
<div class="col">
<VnInput
:label="t('Recommended credit')"
clearable
type="number"
v-model.number="data.recommendedCredit"
/>
</div>
</VnRow>
</template>
</FormModel>
<div class="full-width flex justify-center" v-if="rows.length">
<QTable
:columns="columns"
:pagination="{ rowsPerPage: 0 }"
:rows="rows"
hide-bottom
row-key="id"
v-model:selected="selected"
class="card-width q-px-lg"
>
<template #body-cell-employee="{ row }">
<QTd auto-width @click.stop>
<QBtn color="blue" flat no-caps>{{ row.worker.user.nickname }}</QBtn>
<WorkerDescriptorProxy :id="row.clientFk" />
</QTd>
</template>
</QTable>
</div>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</template> </template>
<i18n>
es:
Rating: Clasificación
Recommended credit: Crédito recomendado
Since: Desde
Employee: Empleado
</i18n>

View File

@ -1,47 +1,35 @@
<script setup> <script setup>
import { ref, computed, onBeforeMount } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { date, QBtn } from 'quasar'; import { QBtn } from 'quasar';
import { toCurrency } from 'src/filters'; import { toCurrency, toDateHourMin } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import FetchData from 'components/FetchData.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const stateStore = useStateStore();
const arrayData = ref(null); const rows = ref([]);
const workerId = ref(0);
const rows = computed(() => arrayData.value.store.data);
onBeforeMount(async () => { const filter = {
const filter = { include: [
include: [ {
{ relation: 'worker',
relation: 'worker', scope: {
scope: { fields: ['id'],
fields: ['id'], include: { relation: 'user', scope: { fields: ['name'] } },
include: { relation: 'user', scope: { fields: ['name'] } },
},
}, },
], },
where: { clientFk: `${route.params.id}` }, ],
order: ['created DESC'], where: { clientFk: route.params.id },
limit: 20, order: ['created DESC'],
}; limit: 20,
arrayData.value = useArrayData('CustomerCreditsCard', { };
url: 'ClientCredits',
filter,
});
await arrayData.value.fetch({ append: false });
stateStore.rightDrawer = true;
});
const tableColumnComponents = { const tableColumnComponents = {
created: { created: {
@ -51,10 +39,8 @@ const tableColumnComponents = {
}, },
employee: { employee: {
component: QBtn, component: QBtn,
props: () => ({ flat: true, color: 'blue' }), props: () => ({ flat: true, color: 'blue', noCaps: true }),
event: (prop) => { event: () => {},
selectWorkerId(prop.row.clientFk);
},
}, },
amount: { amount: {
component: 'span', component: 'span',
@ -69,7 +55,7 @@ const columns = computed(() => [
field: 'created', field: 'created',
label: t('Since'), label: t('Since'),
name: 'created', name: 'created',
format: (value) => date.formatDate(value, 'DD/MM/YYYY hh:mm:ss'), format: (value) => toDateHourMin(value),
}, },
{ {
align: 'left', align: 'left',
@ -86,35 +72,58 @@ const columns = computed(() => [
}, },
]); ]);
const selectWorkerId = (id) => {
workerId.value = id;
};
const toCustomerCreditCreate = () => { const toCustomerCreditCreate = () => {
router.push({ name: 'CustomerCreditCreate' }); router.push({ name: 'CustomerCreditCreate' });
}; };
</script> </script>
<template> <template>
<QPage class="column items-center q-pa-md"> <FetchData
<QTable :columns="columns" :rows="rows" class="full-width q-mt-md" row-key="id"> :filter="filter"
<template #body-cell="props"> @on-fetch="(data) => (rows = data)"
<QTd :props="props"> auto-load
<QTr :props="props" class="cursor-pointer"> url="ClientCredits"
<component />
:is="tableColumnComponents[props.col.name].component"
@click="tableColumnComponents[props.col.name].event(props)" <div class="full-width flex justify-center">
class="rounded-borders q-pa-sm" <QCard class="card-width q-pa-lg">
v-bind="tableColumnComponents[props.col.name].props(props)" <QTable
> :columns="columns"
{{ props.value }} :pagination="{ rowsPerPage: 12 }"
<WorkerDescriptorProxy :id="workerId" /> :rows="rows"
</component> class="full-width q-mt-md"
</QTr> row-key="id"
</QTd> v-if="rows?.length"
</template> >
</QTable> <template #body-cell="props">
</QPage> <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="props.row.workerFk"
v-if="props.col.name === 'employee'"
/>
</component>
</QTr>
</QTd>
</template>
</QTable>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</QCard>
</div>
<QPageSticky :offset="[18, 18]"> <QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerCreditCreate()" color="primary" fab icon="add" /> <QBtn @click.stop="toCustomerCreditCreate()" color="primary" fab icon="add" />

View File

@ -2,11 +2,15 @@
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toCurrency } from 'src/filters';
import { toCurrency, toDate } from 'src/filters';
import useCardDescription from 'src/composables/useCardDescription';
import CardDescriptor from 'components/ui/CardDescriptor.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import CustomerDescriptorMenu from './CustomerDescriptorMenu.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -19,8 +23,10 @@ const $props = defineProps({
default: null, default: null,
}, },
}); });
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
@ -39,6 +45,23 @@ const setData = (entity) => (data.value = useCardDescription(entity.name, entity
:summary="$props.summary" :summary="$props.summary"
data-key="customerData" data-key="customerData"
> >
<template #header-extra-action>
<QBtn
round
flat
size="sm"
icon="vn:Person"
color="white"
:to="{ name: 'CustomerList' }"
>
<QTooltip>
{{ t('Go to module index') }}
</QTooltip>
</QBtn>
</template>
<template #menu="{ entity }">
<CustomerDescriptorMenu :customer="entity" />
</template>
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="t('customer.card.payMethod')" :value="entity.payMethod.name" /> <VnLv :label="t('customer.card.payMethod')" :value="entity.payMethod.name" />
@ -63,23 +86,26 @@ const setData = (entity) => (data.value = useCardDescription(entity.name, entity
/> />
</template> </template>
<template #icons="{ entity }"> <template #icons="{ entity }">
<QCardActions> <QCardActions class="q-gutter-x-md">
<QIcon <QIcon
v-if="entity.isActive == false" v-if="!entity.isActive"
name="vn:disabled" name="vn:disabled"
size="xs" size="xs"
color="primary" color="primary"
> >
<QTooltip>{{ t('customer.card.isDisabled') }}</QTooltip> <QTooltip>{{ t('customer.card.isDisabled') }}</QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon v-if="entity.isFreezed" name="vn:frozen" size="xs" color="primary">
v-if="entity.isFreezed == true"
name="vn:frozen"
size="xs"
color="primary"
>
<QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip> <QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip>
</QIcon> </QIcon>
<QIcon
v-if="!entity.account.active"
color="primary"
name="vn:noweb"
size="xs"
>
<QTooltip>{{ t('customer.card.webAccountInactive') }}</QTooltip>
</QIcon>
<QIcon <QIcon
v-if="entity.debt > entity.credit" v-if="entity.debt > entity.credit"
name="vn:risk" name="vn:risk"
@ -89,17 +115,41 @@ const setData = (entity) => (data.value = useCardDescription(entity.name, entity
<QTooltip>{{ t('customer.card.hasDebt') }}</QTooltip> <QTooltip>{{ t('customer.card.hasDebt') }}</QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon
v-if="entity.isTaxDataChecked == false" v-if="!entity.isTaxDataChecked"
name="vn:no036" name="vn:no036"
size="xs" size="xs"
color="primary" color="primary"
> >
<QTooltip>{{ t('customer.card.notChecked') }}</QTooltip> <QTooltip>{{ t('customer.card.notChecked') }}</QTooltip>
</QIcon> </QIcon>
<QBtn
v-if="entity.unpaid"
flat
size="sm"
icon="vn:Client_unpaid"
color="primary"
:to="{ name: 'CustomerUnpaid' }"
>
<QTooltip>
{{ t('Unpaid') }}
<br />
{{
t('unpaidDated', {
dated: toDate(entity.unpaid.dated),
})
}}
<br />
{{
t('unpaidAmount', {
amount: toCurrency(entity.unpaid.amount),
})
}}
</QTooltip>
</QBtn>
</QCardActions> </QCardActions>
</template> </template>
<template #actions="{ entity }"> <template #actions="{ entity }">
<QCardActions> <QCardActions class="flex justify-center">
<QBtn <QBtn
:to="{ :to="{
name: 'TicketList', name: 'TicketList',
@ -109,7 +159,7 @@ const setData = (entity) => (data.value = useCardDescription(entity.name, entity
icon="vn:ticket" icon="vn:ticket"
color="primary" color="primary"
> >
<QTooltip>{{ t('ticketList') }}</QTooltip> <QTooltip>{{ t('Customer ticket list') }}</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
:to="{ :to="{
@ -120,21 +170,40 @@ const setData = (entity) => (data.value = useCardDescription(entity.name, entity
icon="vn:invoice-out" icon="vn:invoice-out"
color="primary" color="primary"
> >
<QTooltip>{{ t('invoiceOutList') }}</QTooltip> <QTooltip>{{ t('Customer invoice out list') }}</QTooltip>
</QBtn>
<QBtn
:to="{
name: 'OrderCreate',
query: { clientFk: entity.id },
}"
size="md"
icon="vn:basketadd"
color="primary"
>
<QTooltip>{{ t('New order') }}</QTooltip>
</QBtn>
<QBtn size="md" icon="face" color="primary">
<!-- TODO:: Redirigir a la vista de usuario cuando exista -->
<QTooltip>{{ t('Go to user') }}</QTooltip>
</QBtn> </QBtn>
</QCardActions> </QCardActions>
</template> </template>
</CardDescriptor> </CardDescriptor>
</template> </template>
<i18n> <i18n>
{ en:
"en": { unpaidDated: 'Date {dated}'
"ticketList": "Customer ticket list", unpaidAmount: 'Amount {amount}'
"invoiceOutList": "Customer invoice out list" es:
}, Go to module index: Ir al índice del módulo
"es": { Customer ticket list: Listado de tickets del cliente
"ticketList": "Listado de tickets del cliente", Customer invoice out list: Listado de facturas del cliente
"invoiceOutList": "Listado de facturas del cliente" New order: Nuevo pedido
} Go to user: Ir al usuario
} Customer unpaid: Cliente impago
Unpaid: Impagado
unpaidDated: 'Fecha {dated}'
unpaidAmount: 'Importe {amount}'
</i18n> </i18n>

View File

@ -0,0 +1,68 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import axios from 'axios';
import { useQuasar } from 'quasar';
import useNotify from 'src/composables/useNotify';
import VnSmsDialog from 'src/components/common/VnSmsDialog.vue';
const $props = defineProps({
customer: {
type: Object,
required: true,
},
});
const { notify } = useNotify();
const { t } = useI18n();
const quasar = useQuasar();
const route = useRoute();
const showSmsDialog = () => {
quasar.dialog({
component: VnSmsDialog,
componentProps: {
phone: $props.customer.phone || $props.customer.mobile,
promise: sendSms,
},
});
};
const sendSms = async (payload) => {
payload.destinationFk = route.params.id;
try {
await axios.post(`Clients/${route.params.id}/sendSms`, payload);
notify('globals.notificationSent', 'positive');
} catch (error) {
notify(error.message, 'positive');
}
};
</script>
<template>
<QItem v-ripple clickable>
<QItemSection>
<RouterLink
:to="{
name: 'TicketCreate',
query: { clientFk: customer.id },
}"
class="color-vn-text"
>
{{ t('Simple ticket') }}
</RouterLink>
</QItemSection>
</QItem>
<QItem v-ripple clickable>
<QItemSection @click="showSmsDialog()">{{ t('Send SMS') }}</QItemSection>
</QItem>
</template>
<i18n>
es:
Simple ticket: Ticket simple
Send SMS: Enviar SMS
</i18n>

View File

@ -0,0 +1,259 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { QBadge, QBtn, QCheckbox } from 'quasar';
import { downloadFile } from 'src/composables/downloadFile';
import { toDateTimeFormat } from 'src/filters/date';
import FetchData from 'components/FetchData.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerFileManagementActions from 'src/pages/Customer/components/CustomerFileManagementActions.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const ClientDmsRef = ref(null);
const rows = ref([]);
const filter = {
include: {
relation: 'dms',
scope: {
fields: [
'dmsTypeFk',
'reference',
'hardCopyNumber',
'workerFk',
'description',
'hasFile',
'file',
'created',
],
include: [
{ relation: 'dmsType', scope: { fields: ['name'] } },
{
relation: 'worker',
scope: {
fields: ['id'],
include: { relation: 'user', scope: { fields: ['name'] } },
},
},
],
},
},
where: { clientFk: route.params.id },
order: ['dmsFk DESC'],
limit: 20,
};
const tableColumnComponents = {
id: {
component: 'span',
props: () => {},
event: () => {},
},
type: {
component: 'span',
props: () => {},
event: () => {},
},
order: {
component: QBadge,
props: () => {},
event: () => {},
},
reference: {
component: 'span',
props: () => {},
event: () => {},
},
description: {
component: 'span',
props: () => {},
event: () => {},
},
original: {
component: QCheckbox,
props: (prop) => ({
disable: true,
'model-value': Boolean(prop.value),
}),
event: () => {},
},
file: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: ({ row }) => downloadFile(row.dmsFk),
},
employee: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: () => {},
},
created: {
component: 'span',
props: () => {},
event: () => {},
},
actions: {
component: CustomerFileManagementActions,
props: (prop) => ({
id: prop.row.dmsFk,
promise: setData,
}),
event: () => {},
},
};
const columns = computed(() => [
{
align: 'left',
field: ({ dms }) => dms.id,
label: t('Id'),
name: 'id',
},
{
align: 'left',
field: ({ dms }) => dms.dmsType.name,
label: t('Type'),
name: 'type',
},
{
align: 'left',
field: ({ dms }) => dms.hardCopyNumber,
label: t('Order'),
name: 'order',
},
{
align: 'left',
field: ({ dms }) => dms.reference,
label: t('Reference'),
name: 'reference',
},
{
align: 'left',
field: ({ dms }) => dms.description,
label: t('Description'),
name: 'description',
},
{
align: 'left',
field: ({ dms }) => dms.hasFile,
label: t('Original'),
name: 'original',
},
{
align: 'left',
field: ({ dms }) => dms.file,
label: t('File'),
name: 'file',
},
{
align: 'left',
field: ({ dms }) => dms.worker.user.name,
label: t('Employee'),
name: 'employee',
},
{
align: 'left',
field: (value) => value.dms.created,
label: t('Created'),
name: 'created',
format: (value) => toDateTimeFormat(value),
},
{
align: 'right',
field: 'actions',
label: '',
name: 'actions',
},
]);
const setData = () => {
ClientDmsRef.value.fetch();
};
const toCustomerFileManagementCreate = () => {
router.push({ name: 'CustomerFileManagementCreate' });
};
</script>
<template>
<FetchData
ref="ClientDmsRef"
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
url="ClientDms"
/>
<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"
v-if="rows?.length"
>
<template #body-cell="props">
<QTd :props="props">
<QTr :props="props" class="cursor-pointer">
<component
:is="
props.col.name === 'order' && !props.value
? 'span'
: tableColumnComponents[props.col.name].component
"
@click="tableColumnComponents[props.col.name].event(props)"
class="col-content"
v-bind="tableColumnComponents[props.col.name].props(props)"
>
<template v-if="props.col.name !== 'original'">
{{ props.value }}
</template>
<WorkerDescriptorProxy
:id="props.row.dms.workerFk"
v-if="props.col.name === 'employee'"
/>
</component>
</QTr>
</QTd>
</template>
</QTable>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</QPage>
<QPageSticky :offset="[18, 18]">
<QBtn
@click.stop="toCustomerFileManagementCreate()"
color="primary"
fab
icon="add"
/>
<QTooltip>
{{ t('Upload file') }}
</QTooltip>
</QPageSticky>
</template>
<i18n>
es:
Id: Id
Type: Tipo
Order: Orden
Reference: Referencia
Description: Descripción
Original: Original
File: Fichero
Employee: Empleado
Created: Fecha creación
Upload file: Subir fichero
</i18n>

View File

@ -46,17 +46,24 @@ function handleLocation(data, location) {
:label="t('Social name')" :label="t('Social name')"
:required="true" :required="true"
:rules="validate('client.socialName')" :rules="validate('client.socialName')"
clearable
v-model="data.socialName" v-model="data.socialName"
/> >
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>{{ t('onlyLetters') }}</QTooltip>
</QIcon>
</template>
</VnInput>
</div> </div>
<div class="col"> <div class="col">
<VnInput :label="t('Tax number')" v-model="data.fi" /> <VnInput :label="t('Tax number')" clearable v-model="data.fi" />
</div> </div>
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnInput :label="t('Street')" v-model="data.street" /> <VnInput :label="t('Street')" clearable v-model="data.street" />
</div> </div>
</VnRow> </VnRow>
@ -76,10 +83,21 @@ function handleLocation(data, location) {
:label="t('Sage transaction type')" :label="t('Sage transaction type')"
:options="typesTransactions" :options="typesTransactions"
hide-selected hide-selected
option-label="vat" option-label="transaction"
option-value="id" option-value="id"
v-model="data.sageTransactionTypeFk" v-model="data.sageTransactionTypeFk"
/> >
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt.name }}</QItemLabel>
<QItemLabel caption>
{{ `${scope.opt.id}: ${scope.opt.transaction}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div> </div>
</VnRow> </VnRow>
@ -111,6 +129,11 @@ function handleLocation(data, location) {
</div> </div>
<div class="col"> <div class="col">
<QCheckbox :label="t('Vies')" v-model="data.isVies" /> <QCheckbox :label="t('Vies')" v-model="data.isVies" />
<QIcon name="info" class="cursor-info q-ml-sm" size="sm">
<QTooltip>
{{ t('whenActivatingIt') }}
</QTooltip>
</QIcon>
</div> </div>
</VnRow> </VnRow>
@ -135,6 +158,11 @@ function handleLocation(data, location) {
:label="t('Is equalizated')" :label="t('Is equalizated')"
v-model="data.isEqualizated" v-model="data.isEqualizated"
/> />
<QIcon class="cursor-info q-ml-sm" name="info" size="sm">
<QTooltip>
{{ t('inOrderToInvoice') }}
</QTooltip>
</QIcon>
</div> </div>
<div class="col"> <div class="col">
<QCheckbox <QCheckbox
@ -183,4 +211,11 @@ es:
Verified data: Datos comprobados Verified data: Datos comprobados
Incoterms authorization: Autorización incoterms Incoterms authorization: Autorización incoterms
Electronic invoice: Factura electrónica Electronic invoice: Factura electrónica
onlyLetters: Sólo se pueden usar letras, números y espacios
whenActivatingIt: Al activarlo, no informar el código del país en el campo nif
inOrderToInvoice: Para facturar no se consulta este campo, sino el RE de consignatario. Al modificar este campo si no esta marcada la casilla Facturar por consignatario, se propagará automaticamente el cambio a todos lo consignatarios, en caso contrario preguntará al usuario si quiere o no propagar
en:
onlyLetters: Only letters, numbers and spaces can be used
whenActivatingIt: When activating it, do not enter the country code in the ID field
inOrderToInvoice: In order to invoice, this field is not contulted, but the consignee's ET. When modifiying this field if the invoice by address option is not checked, the change will be automatically propagated to all addresses, otherwise the user will be asked if he wants to propagate it or not
</i18n> </i18n>

View File

@ -1,59 +1,44 @@
<script setup> <script setup>
import { ref, computed, onBeforeMount } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { date, QBtn } from 'quasar'; import { QBtn } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import { toDateTimeFormat } from 'src/filters/date';
import FetchData from 'components/FetchData.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const stateStore = useStateStore();
const arrayData = ref(null); const rows = ref([]);
const totalAmount = ref(0); const totalAmount = ref(0);
const workerId = ref(0);
const rows = computed(() => arrayData.value.store.data);
onBeforeMount(async () => { const filter = {
const filter = { include: [
include: [ {
{ relation: 'greugeType',
relation: 'greugeType', scope: {
scope: { fields: ['id', 'name'],
fields: ['id', 'name'],
},
}, },
{
relation: 'user',
scope: {
fields: ['id', 'name'],
},
},
],
order: 'shipped DESC, amount',
where: {
clientFk: `${route.params.id}`,
}, },
limit: 20, {
}; relation: 'user',
scope: {
arrayData.value = useArrayData('CustomerGreugesCard', { fields: ['id', 'name'],
url: 'greuges', },
filter, },
}); ],
await arrayData.value.fetch({ append: false }); order: 'shipped DESC, amount',
totalAmount.value = arrayData.value.store.data.reduce((accumulator, currentValue) => { where: {
return accumulator + currentValue.amount; clientFk: `${route.params.id}`,
}, 0); },
stateStore.rightDrawer = true; limit: 20,
}); };
const tableColumnComponents = { const tableColumnComponents = {
date: { date: {
@ -63,10 +48,8 @@ const tableColumnComponents = {
}, },
createdBy: { createdBy: {
component: QBtn, component: QBtn,
props: () => ({ flat: true, color: 'blue' }), props: () => ({ flat: true, color: 'blue', noCaps: true }),
event: (prop) => { event: () => {},
selectWorkerId(prop.row.clientFk);
},
}, },
comment: { comment: {
component: 'span', component: 'span',
@ -91,7 +74,7 @@ const columns = computed(() => [
field: 'shipped', field: 'shipped',
label: t('Date'), label: t('Date'),
name: 'date', name: 'date',
format: (value) => date.formatDate(value, 'DD/MM/YYYY hh:mm:ss'), format: (value) => toDateTimeFormat(value),
}, },
{ {
align: 'left', align: 'left',
@ -120,8 +103,11 @@ const columns = computed(() => [
}, },
]); ]);
const selectWorkerId = (id) => { const setRows = (data) => {
workerId.value = id; rows.value = data;
totalAmount.value = data.reduce((accumulator, currentValue) => {
return accumulator + currentValue.amount;
}, 0);
}; };
const toCustomerGreugeCreate = () => { const toCustomerGreugeCreate = () => {
@ -130,44 +116,52 @@ const toCustomerGreugeCreate = () => {
</script> </script>
<template> <template>
<QPage class="column items-center q-pa-md"> <FetchData :filter="filter" @on-fetch="setRows" auto-load url="greuges" />
<QCard class="full-width" v-if="totalAmount">
<h6 class="flex justify-end q-my-lg q-pr-lg">
<span class="label-color q-mr-md">{{ t('Total') }}:</span>
{{ toCurrency(totalAmount) }}
</h6>
</QCard>
<QTable <div class="full-width flex justify-center">
:columns="columns" <QPage class="card-width q-pa-lg">
:rows="rows" <QCard class="full-width q-pa-sm" v-if="totalAmount">
class="full-width q-mt-md" <h6 class="flex justify-end q-my-lg q-pr-lg">
row-key="id" <span class="color-vn-label q-mr-md">{{ t('Total') }}:</span>
v-if="rows?.length" {{ toCurrency(totalAmount) }}
> </h6>
<template #body-cell="props"> </QCard>
<QTd :props="props">
<QTr :props="props" class="cursor-pointer">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="tableColumnComponents[props.col.name].props(props)"
@click="tableColumnComponents[props.col.name].event(props)"
>
{{ props.value }}
<WorkerDescriptorProxy :id="workerId" />
</component>
</QTr>
</QTd>
</template>
</QTable>
<QCard class="full-width" v-else> <QCard class="q-pa-sm q-mt-md">
<h5 class="flex justify-center label-color"> <QTable
{{ t('globals.noResults') }} :columns="columns"
</h5> :no-data-label="t('globals.noResults')"
</QCard> :pagination="{ rowsPerPage: 12 }"
</QPage> :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"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
{{ props.value }}
<WorkerDescriptorProxy
:id="props.row.userFk"
v-if="props.col.name === 'createdBy'"
/>
</component>
</QTr>
</QTd>
</template>
</QTable>
</QCard>
</QPage>
</div>
<QPageSticky :offset="[18, 18]"> <QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerGreugeCreate()" color="primary" fab icon="add" /> <QBtn @click.stop="toCustomerGreugeCreate()" color="primary" fab icon="add" />

View File

@ -124,13 +124,13 @@ const setInq = (value, status) => {
:url="urlClientLogsModels" :url="urlClientLogsModels"
/> />
<h5 class="flex justify-center label-color"> <h5 class="flex justify-center color-vn-label">
{{ t('globals.noResults') }} {{ t('globals.noResults') }}
</h5> </h5>
<QDrawer :width="256" show-if-above side="right" v-model="stateStore.rightDrawer"> <QDrawer :width="256" show-if-above side="right" v-model="stateStore.rightDrawer">
<div class="q-mt-sm q-px-md"> <div class="q-mt-sm q-px-md">
<VnInput :label="t('Search')"> <VnInput :label="t('Search')" clearable>
<template #append> <template #append>
<QIcon name="info" class="cursor-pointer"> <QIcon name="info" class="cursor-pointer">
<QTooltip> <QTooltip>
@ -187,7 +187,7 @@ const setInq = (value, status) => {
option-label="name" option-label="name"
option-value="id" option-value="id"
/> />
<VnInput :label="t('Changes')" class="q-mt-sm"> <VnInput :label="t('Changes')" clearable class="q-mt-sm">
<template #append> <template #append>
<QIcon name="info" class="cursor-pointer"> <QIcon name="info" class="cursor-pointer">
<QTooltip> <QTooltip>
@ -227,7 +227,7 @@ const setInq = (value, status) => {
</div> </div>
<VnInputDate :label="t('Date')" class="q-mt-sm" /> <VnInputDate :label="t('Date')" class="q-mt-sm" />
<VnInput :label="t('To')" class="q-mt-md" /> <VnInput :label="t('To')" clearable class="q-mt-md" />
</div> </div>
</QDrawer> </QDrawer>

View File

@ -0,0 +1,135 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { toDateTimeFormat } from 'src/filters/date';
import FetchData from 'components/FetchData.vue';
const { t } = useI18n();
const route = useRoute();
const rows = ref([]);
const filter = {
include: [
{ relation: 'mandateType', scope: { fields: ['id', 'name'] } },
{ relation: 'company', scope: { fields: ['id', 'code'] } },
],
where: { clientFk: route.params.id },
order: ['created DESC'],
limit: 20,
};
const tableColumnComponents = {
id: {
component: 'span',
props: () => {},
event: () => {},
},
company: {
component: 'span',
props: () => {},
event: () => {},
},
type: {
component: 'span',
props: () => {},
event: () => {},
},
registerDate: {
component: 'span',
props: () => {},
event: () => {},
},
endDate: {
component: 'span',
props: () => {},
event: () => {},
},
};
const columns = computed(() => [
{
align: 'left',
field: 'id',
label: t('Id'),
name: 'id',
},
{
align: 'left',
field: (row) => row.company.code,
label: t('Company'),
name: 'company',
},
{
align: 'left',
field: (row) => row.mandateType.name,
label: t('Type'),
name: 'type',
},
{
align: 'left',
field: 'created',
label: t('Register date'),
name: 'registerDate',
format: (value) => toDateTimeFormat(value),
},
{
align: 'left',
field: 'finished',
label: t('End date'),
name: 'endDate',
format: (value) => (value ? toDateTimeFormat(value) : '-'),
},
]);
</script>
<template>
<FetchData
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
url="Mandates"
/>
<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"
v-if="rows?.length"
>
<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 }}
</component>
</QTr>
</QTd>
</template>
</QTable>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</QPage>
</template>
<i18n>
es:
Id: Id
Company: Empresa
Type: Tipo
Register date: Fecha alta
End date: Fecha baja
</i18n>

View File

@ -2,7 +2,7 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { date } from 'quasar'; import { toDateTimeFormat } from 'src/filters/date';
import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnPaginate from 'src/components/ui/VnPaginate.vue';
@ -23,62 +23,48 @@ const toCustomerNoteCreate = () => {
</script> </script>
<template> <template>
<QCard class="q-pa-lg"> <div class="full-width flex justify-center">
<VnPaginate <QCard class="card-width q-pa-lg">
data-key="CustomerNotes" <VnPaginate
:url="'clientObservations'" data-key="CustomerNotes"
auto-load url="clientObservations"
:filter="noteFilter" auto-load
> :filter="noteFilter"
<template #body="{ rows }"> >
<div v-if="rows.length"> <template #body="{ rows }">
<QCard <div v-if="rows.length">
v-for="(item, index) in rows" <QCard
:key="index" v-for="(item, index) in rows"
:class="{ :key="index"
'q-pa-md': true, class="q-pa-md q-rounded custom-border"
'q-rounded': true, :class="{ 'q-mb-md': index < rows.length - 1 }"
'custom-border': true, >
'q-mb-md': index < rows.length - 1, <div class="flex justify-between">
}" <p class="color-vn-label">
> {{ item.worker.user.nickname }}
<div class="flex justify-between"> </p>
<p class="label-color">{{ item.worker.user.nickname }}</p> <p class="color-vn-label">
<p class="label-color"> {{ toDateTimeFormat(item?.created) }}
{{ </p>
date.formatDate(item?.created, 'DD-MM-YYYY HH:mm:ss') </div>
}} <h6 class="q-mt-xs q-mb-none">{{ item.text }}</h6>
</p> </QCard>
</div> </div>
<h6 class="q-mt-xs q-mb-none">{{ item.text }}</h6>
</QCard>
</div>
<div v-else> <div v-else>
<h5 class="flex justify-center label-color"> <h5 class="flex justify-center color-vn-label">
{{ t('globals.noResults') }} {{ t('globals.noResults') }}
</h5> </h5>
</div> </div>
</template>
<QPageSticky :offset="[18, 18]"> </VnPaginate>
<QBtn </QCard>
@click.stop="toCustomerNoteCreate()" </div>
color="primary"
fab
icon="add"
/>
<QTooltip>
{{ t('New consignee') }}
</QTooltip>
</QPageSticky>
</template>
</VnPaginate>
</QCard>
<QPageSticky :offset="[18, 18]"> <QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerNoteCreate()" color="primary" fab icon="add" /> <QBtn @click.stop="toCustomerNoteCreate()" color="primary" fab icon="add" />
<QTooltip> <QTooltip>
{{ t('New consignee') }} {{ t('New note') }}
</QTooltip> </QTooltip>
</QPageSticky> </QPageSticky>
</template> </template>
@ -89,8 +75,9 @@ const toCustomerNoteCreate = () => {
border-radius: 10px; border-radius: 10px;
padding: 10px; padding: 10px;
} }
.label-color {
color: var(--vn-label-color);
}
</style> </style>
<i18n>
es:
New note: Nueva nota
</i18n>

View File

@ -1,3 +0,0 @@
<template>
<div class="flex justify-center">Others</div>
</template>

View File

@ -1,39 +1,22 @@
<script setup> <script setup>
import { ref, computed, onBeforeMount } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { date, QBtn } from 'quasar'; import { toCurrency, toDate } from 'src/filters';
import { useArrayData } from 'composables/useArrayData'; import FetchData from 'components/FetchData.vue';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency } from 'src/filters';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const stateStore = useStateStore(); const rows = ref([]);
const arrayData = ref(null); const filter = {
const workerId = ref(0); where: { clientFk: route.params.id },
const rows = computed(() => arrayData.value.store.data); order: ['started DESC'],
limit: 20,
onBeforeMount(async () => { };
const filter = {
where: { clientFk: `${route.params.id}` },
order: ['started DESC'],
limit: 20,
};
arrayData.value = useArrayData('CustomerRecoveriesCard', {
url: 'Recoveries',
filter,
});
await arrayData.value.fetch({ append: false });
stateStore.rightDrawer = true;
});
const tableColumnComponents = { const tableColumnComponents = {
since: { since: {
@ -64,14 +47,14 @@ const columns = computed(() => [
field: 'started', field: 'started',
label: t('Since'), label: t('Since'),
name: 'since', name: 'since',
format: (value) => date.formatDate(value, 'DD/MM/YYYY'), format: (value) => toDate(value),
}, },
{ {
align: 'left', align: 'left',
field: 'finished', field: 'finished',
label: t('To'), label: t('To'),
name: 'to', name: 'to',
format: (value) => date.formatDate(value, 'DD/MM/YYYY'), format: (value) => toDate(value),
}, },
{ {
align: 'left', align: 'left',
@ -94,37 +77,44 @@ const toCustomerRecoverieCreate = () => {
</script> </script>
<template> <template>
<QPage class="column items-center q-pa-md"> <FetchData
<QTable :filter="filter"
:columns="columns" @on-fetch="(data) => (rows = data)"
:rows="rows" auto-load
class="full-width q-mt-md" url="Recoveries"
row-key="id" />
v-if="rows?.length"
>
<template #body-cell="props">
<QTd :props="props">
<QTr :props="props" class="cursor-pointer">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="tableColumnComponents[props.col.name].props(props)"
@click="tableColumnComponents[props.col.name].event(props)"
>
{{ props.value }}
<WorkerDescriptorProxy :id="workerId" />
</component>
</QTr>
</QTd>
</template>
</QTable>
<QCard class="full-width" v-else> <div class="full-width flex justify-center">
<h5 class="flex justify-center label-color"> <QPage class="card-width q-pa-lg">
{{ t('globals.noResults') }} <QTable
</h5> :columns="columns"
</QCard> :no-data-label="t('globals.noResults')"
</QPage> :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"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
{{ props.value }}
</component>
</QTr>
</QTd>
</template>
</QTable>
</QPage>
</div>
<QPageSticky :offset="[18, 18]"> <QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerRecoverieCreate()" color="primary" fab icon="add" /> <QBtn @click.stop="toCustomerRecoverieCreate()" color="primary" fab icon="add" />

View File

@ -0,0 +1,144 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { QBtn } from 'quasar';
import FetchData from 'components/FetchData.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import { toDateTimeFormat } from 'src/filters/date';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const rows = ref([]);
const filter = {
include: [
{ relation: 'type', scope: { fields: ['code', 'description'] } },
{ relation: 'user', scope: { fields: ['id', 'name'] } },
{ relation: 'company', scope: { fields: ['code'] } },
],
where: { clientFk: route.params.id },
order: ['created DESC'],
limit: 20,
};
const tableColumnComponents = {
sent: {
component: 'span',
props: () => {},
event: () => {},
},
description: {
component: 'span',
props: () => {},
event: () => {},
},
worker: {
component: QBtn,
props: () => ({ flat: true, color: 'blue', noCaps: true }),
event: () => {},
},
company: {
component: 'span',
props: () => {},
event: () => {},
},
};
const columns = computed(() => [
{
align: 'left',
field: 'created',
label: t('Sent'),
name: 'sent',
format: (value) => toDateTimeFormat(value),
},
{
align: 'left',
field: (value) => value.type.description,
label: t('Description'),
name: 'description',
},
{
align: 'left',
field: (value) => value.user.name,
label: t('Worker'),
name: 'worker',
},
{
align: 'left',
field: (value) => value.company?.code,
label: t('Company'),
name: 'company',
},
]);
const toCustomerSamplesCreate = () => {
router.push({ name: 'CustomerSamplesCreate' });
};
</script>
<template>
<FetchData
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
url="ClientSamples"
/>
<div class="full-width flex justify-center">
<QPage class="card-width q-pa-lg">
<QTable
:columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
:no-data-label="t('globals.noResults')"
>
<template #body-cell="props">
<QTd :props="props">
<QTr :props="props" class="cursor-pointer">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
{{ props.value }}
<WorkerDescriptorProxy
:id="props.row.userFk"
v-if="props.col.name === 'worker'"
/>
</component>
</QTr>
</QTd>
</template>
</QTable>
</QPage>
</div>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerSamplesCreate()" color="primary" fab icon="add" />
<QTooltip>
{{ t('Send sample') }}
</QTooltip>
</QPageSticky>
</template>
<i18n>
es:
Sent: Enviado
Description: Descripción
Worker: Trabajador
Company: Empresa
Send sample: Enviar plantilla
</i18n>

View File

@ -7,6 +7,7 @@ import CardSummary from 'components/ui/CardSummary.vue';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import CustomerSummaryTable from 'src/pages/Customer/components/CustomerSummaryTable.vue';
import VnTitle from 'src/components/common/VnTitle.vue'; import VnTitle from 'src/components/common/VnTitle.vue';
const route = useRoute(); const route = useRoute();
@ -305,9 +306,16 @@ const creditWarning = computed(() => {
:value="entity.recommendedCredit" :value="entity.recommendedCredit"
/> />
</QCard> </QCard>
<QCard>
<div class="header">
{{ t('Latest tickets') }}
</div>
<CustomerSummaryTable />
</QCard>
</template> </template>
</CardSummary> </CardSummary>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@media (min-width: $breakpoint-md) { @media (min-width: $breakpoint-md) {
.summary .vn-one { .summary .vn-one {
@ -315,9 +323,11 @@ const creditWarning = computed(() => {
} }
} }
</style> </style>
<i18n> <i18n>
en: en:
valueInfo: Value from {min} to {max}. The higher the better value valueInfo: Value from {min} to {max}. The higher the better value
es: es:
valueInfo: Valor de {min} a {max}. Cuanto más alto, mejor valor valueInfo: Valor de {min} a {max}. Cuanto más alto, mejor valor
Latest tickets: Últimos tickets
</i18n> </i18n>

View File

@ -0,0 +1,167 @@
<script setup>
import { computed, onBeforeMount, ref, watch, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'src/components/common/VnInput.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify';
import { useStateStore } from 'stores/useStateStore';
const { t } = useI18n();
const route = useRoute();
const { notify } = useNotify();
const stateStore = useStateStore();
const amountInputRef = ref(null);
const initialDated = Date.vnNew();
const unpaidClient = ref(false);
const isLoading = ref(false);
const amount = ref(null);
const dated = ref(initialDated);
const initialData = ref({
dated: initialDated,
});
const hasChanged = computed(() => {
return (
initialData.value.dated !== dated.value ||
initialData.value.amount !== amount.value
);
});
onBeforeMount(() => {
getData(route.params.id);
});
watch(
() => route.params.id,
(newValue) => {
if (!newValue) return;
getData(newValue);
}
);
const getData = async (id) => {
const filter = { where: { clientFk: id } };
try {
const { data } = await axios.get('ClientUnpaids', {
params: { filter: JSON.stringify(filter) },
});
if (data.length) {
setValues(data[0]);
} else {
defaultValues();
}
} catch (error) {
defaultValues();
}
};
const setValues = (data) => {
unpaidClient.value = true;
amount.value = data.amount;
dated.value = data.dated;
initialData.value = data;
};
const defaultValues = () => {
unpaidClient.value = false;
initialData.value.amount = null;
setInitialData();
};
const setInitialData = () => {
amount.value = initialData.value.amount;
dated.value = initialData.value.dated;
};
const onSubmit = async () => {
isLoading.value = true;
const payload = {
amount: amount.value,
clientFk: route.params.id,
dated: dated.value,
};
try {
await axios.patch('ClientUnpaids', payload);
notify('globals.dataSaved', 'positive');
unpaidClient.value = true;
} catch (error) {
notify('errors.create', 'negative');
} finally {
isLoading.value = false;
}
};
watch(
() => unpaidClient.value,
async (val) => {
await nextTick();
if (val) amountInputRef.value.focus();
}
);
</script>
<template>
<Teleport v-if="stateStore?.isSubToolbarShown()" to="#st-actions">
<QBtnGroup push class="q-gutter-x-sm">
<QBtn
:disabled="!hasChanged"
:label="t('globals.reset')"
:loading="isLoading"
@click="setInitialData"
color="primary"
flat
icon="restart_alt"
type="reset"
/>
<QBtn
:disabled="!hasChanged"
:label="t('globals.save')"
:loading="isLoading"
@click="onSubmit"
color="primary"
icon="save"
/>
</QBtnGroup>
</Teleport>
<div class="full-width flex justify-center">
<QCard class="card-width q-pa-lg">
<QForm>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox :label="t('Unpaid client')" v-model="unpaidClient" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md" v-show="unpaidClient">
<div class="col">
<VnInputDate :label="t('Date')" v-model="dated" />
</div>
<div class="col">
<VnInput
ref="amountInputRef"
:label="t('Amount')"
clearable
type="number"
v-model="amount"
/>
</div>
</VnRow>
</QForm>
</QCard>
</div>
</template>
<i18n>
es:
Unpaid client: Cliente impagado
Date: Fecha
Amount: Importe
</i18n>

View File

@ -1,48 +1,158 @@
<script setup> <script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import FormModel from 'components/FormModel.vue'; import axios from 'axios';
import { useQuasar } from 'quasar';
import { useValidator } from 'src/composables/useValidator';
import useNotify from 'src/composables/useNotify';
import { useStateStore } from 'stores/useStateStore';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue';
const { notify } = useNotify();
const { t } = useI18n(); const { t } = useI18n();
const { validate } = useValidator();
const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
const stateStore = useStateStore();
const active = ref(false);
const canChangePassword = ref(0);
const email = ref(null);
const isLoading = ref(false);
const name = ref(null);
const usersPreviewRef = ref(null);
const user = ref([]);
const userPasswords = ref(0);
const dataChanges = computed(() => {
return (
user.value.active !== active.value ||
user.value.email !== email.value ||
user.value.name !== name.value
);
});
const filter = { where: { id: `${route.params.id}` } }; const filter = { where: { id: `${route.params.id}` } };
const showChangePasswordDialog = () => {
quasar.dialog({
component: CustomerChangePassword,
componentProps: {
id: route.params.id,
userPasswords: userPasswords.value,
promise: usersPreviewRef.value.fetch(),
},
});
};
const setInitialData = () => {
if (user.value.length) {
active.value = user.value[0].active;
email.value = user.value[0].email;
name.value = user.value[0].name;
}
};
const onSubmit = async () => {
isLoading.value = true;
const payload = {
active: active.value,
email: email.value,
name: name.value,
};
try {
await axios.patch(`Clients/${route.params.id}/updateUser`, payload);
notify('globals.dataSaved', 'positive');
if (usersPreviewRef.value) usersPreviewRef.value.fetch();
} catch (error) {
notify('errors.create', 'negative');
} finally {
isLoading.value = false;
}
};
</script> </script>
<template> <template>
<FormModel <FetchData
:filter="filter" :filter="filter"
:observe-form-changes="false" @on-fetch="
:url-update="`Clients/${route.params.id}/updateUser`" (data) => {
:url="'VnUsers/preview'" user = data;
model="client" setInitialData();
> }
<template #form="{ data }"> "
<div auto-load
v-for="(item, index) in data" ref="usersPreviewRef"
:key="index" url="VnUsers/preview"
:class="{ />
'q-mb-md': index < data.length - 1, <FetchData
}" :url="`Clients/${route.params.id}/hasCustomerRole`"
> @on-fetch="(data) => (canChangePassword = data)"
<VnRow class="row q-gutter-md q-mb-md"> auto-load
<div class="col"> />
<QCheckbox <FetchData
:label="t('Enable web access')" @on-fetch="(data) => (userPasswords = data[0])"
v-model="item.active" auto-load
/> url="UserPasswords"
</div> />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<div class="col"> <QBtnGroup push class="q-gutter-x-sm">
<VnInput :label="t('User')" v-model="item.name" /> <QBtn
</div> :disabled="isLoading"
<div class="col"> :label="t('globals.reset')"
<VnInput :label="t('Recovery email')" v-model="item.email"> :loading="isLoading"
@click="setInitialData"
color="primary"
flat
icon="restart_alt"
type="reset"
/>
<QBtn
:disabled="isLoading"
:label="t('Change password')"
:loading="isLoading"
@click.stop="showChangePasswordDialog()"
color="primary"
flat
icon="edit"
v-if="canChangePassword"
/>
<QBtn
:disabled="isLoading || !dataChanges"
:label="t('globals.save')"
:loading="isLoading"
@click="onSubmit"
color="primary"
icon="save"
/>
</QBtnGroup>
</Teleport>
<div class="full-width flex justify-center">
<QCard class="card-width q-pa-lg">
<QCardSection>
<QForm>
<QCheckbox :label="t('Enable web access')" v-model="active" />
<div class="q-px-sm">
<VnInput :label="t('User')" clearable v-model="name" />
<VnInput
:label="t('Recovery email')"
:rules="validate('client.email')"
clearable
type="email"
v-model="email"
class="q-mt-sm"
>
<template #append> <template #append>
<QIcon name="info" class="cursor-pointer"> <QIcon name="info" class="cursor-pointer">
<QTooltip>{{ <QTooltip>{{
@ -54,10 +164,10 @@ const filter = { where: { id: `${route.params.id}` } };
</template> </template>
</VnInput> </VnInput>
</div> </div>
</VnRow> </QForm>
</div> </QCardSection>
</template> </QCard>
</FormModel> </div>
</template> </template>
<i18n> <i18n>
@ -66,4 +176,5 @@ es:
User: Usuario User: Usuario
Recovery email: Correo de recuperacion Recovery email: Correo de recuperacion
This email is used for user to regain access their account: Este correo electrónico se usa para que el usuario recupere el acceso a su cuenta This email is used for user to regain access their account: Este correo electrónico se usa para que el usuario recupere el acceso a su cuenta
Change password: Cambiar contraseña
</i18n> </i18n>

View File

@ -0,0 +1,167 @@
<script setup>
import { computed, onBeforeMount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import axios from 'axios';
import { toCurrency, toDateHourMinSec } from 'src/filters';
import FetchData from 'components/FetchData.vue';
import CustomerCloseIconTooltip from '../components/CustomerCloseIconTooltip.vue';
import CustomerCheckIconTooltip from '../components/CustomerCheckIconTooltip.vue';
const { t } = useI18n();
const route = useRoute();
const rows = ref([]);
const filter = {
include: [
{ relation: 'mandateType', scope: { fields: ['id', 'name'] } },
{ relation: 'company', scope: { fields: ['id', 'code'] } },
],
where: { clientFk: null },
order: ['created DESC'],
limit: 20,
};
const tableColumnComponents = {
state: {
component: CustomerCloseIconTooltip,
props: ({ row }) => ({ transaction: row }),
event: () => {},
},
id: {
component: 'span',
props: () => {},
event: () => {},
},
date: {
component: 'span',
props: () => {},
event: () => {},
},
amount: {
component: 'span',
props: () => {},
event: () => {},
},
validate: {
component: CustomerCheckIconTooltip,
props: ({ row }) => ({
transaction: row,
promise: refreshData,
}),
event: () => {},
},
};
const columns = computed(() => [
{
align: 'left',
field: '',
label: t('State'),
name: 'state',
},
{
align: 'left',
field: 'id',
label: t('Id'),
name: 'id',
},
{
align: 'left',
field: 'created',
label: t('Date'),
name: 'date',
format: (value) => toDateHourMinSec(value),
},
{
align: 'left',
field: 'amount',
label: t('Amount'),
name: 'amount',
format: (value) => toCurrency(value),
},
{
align: 'left',
field: '',
name: 'validate',
},
]);
onBeforeMount(() => {
getData(route.params.id);
});
watch(
() => route.params.id,
(newValue) => {
if (!newValue) return;
getData(newValue);
}
);
const getData = async (id) => {
filter.where.clientFk = id;
try {
const { data } = await axios.get('clients/transactions', {
params: { filter: JSON.stringify(filter) },
});
rows.value = data;
} catch (error) {
return error;
}
};
const refreshData = () => {
getData(route.params.id);
};
</script>
<template>
<div class="full-width flex justify-center">
<QPage class="card-width q-pa-lg">
<QTable
:columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
v-if="rows?.length"
>
<template #body-cell="props">
<QTd :props="props">
<QTr :props="props">
<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 }}
</component>
</QTr>
</QTd>
</template>
</QTable>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</QPage>
</div>
</template>
<i18n>
es:
State: Estado
Id: Id
Date: Fecha
Amount: Importe
</i18n>

View File

@ -11,20 +11,8 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
const { t } = useI18n(); const { t } = useI18n();
const newClientForm = reactive({ const initialData = reactive({
active: true, active: true,
name: null,
salesPersonFk: null,
businessTypeFk: null,
fi: null,
socialName: null,
street: null,
postcode: null,
city: null,
provinceFk: null,
countryFk: null,
userName: null,
email: null,
isEqualizated: false, isEqualizated: false,
}); });
@ -55,7 +43,7 @@ function handleLocation(data, location) {
<QPage> <QPage>
<VnSubToolbar /> <VnSubToolbar />
<FormModel <FormModel
:form-initial-data="newClientForm" :form-initial-data="initialData"
model="client" model="client"
url-create="Clients/createWithUser" url-create="Clients/createWithUser"
> >
@ -94,7 +82,7 @@ function handleLocation(data, location) {
<div class="col"> <div class="col">
<QInput <QInput
:label="t('Business name')" :label="t('Business name')"
:rules="validate('Client.socialName')" :rules="validate('client.socialName')"
v-model="data.socialName" v-model="data.socialName"
/> />
</div> </div>
@ -103,7 +91,7 @@ function handleLocation(data, location) {
<div class="col"> <div class="col">
<QInput <QInput
:label="t('Street')" :label="t('Street')"
:rules="validate('Client.street')" :rules="validate('client.street')"
v-model="data.street" v-model="data.street"
/> />
</div> </div>
@ -128,12 +116,26 @@ function handleLocation(data, location) {
<QInput v-model="data.userName" :label="t('Web user')" /> <QInput v-model="data.userName" :label="t('Web user')" />
</div> </div>
<div class="col"> <div class="col">
<QInput v-model="data.email" :label="t('Email')" /> <QInput
:label="t('Email')"
:rules="validate('client.email')"
clearable
type="email"
v-model="data.email"
>
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip max-width="400px">{{
t('customer.basicData.youCanSaveMultipleEmails')
}}</QTooltip>
</QIcon>
</template>
</QInput>
</div> </div>
</VnRow> </VnRow>
<QCheckbox <QCheckbox
:label="t('Is equalizated')" :label="t('Is equalizated')"
v-model="newClientForm.isEqualizated" v-model="initialData.isEqualizated"
/> />
</template> </template>
</FormModel> </FormModel>

View File

@ -1,56 +1,52 @@
<script setup> <script setup>
import { ref, computed, onBeforeMount } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { QBtn, QCheckbox, useQuasar } 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 { useStateStore } from 'stores/useStateStore';
import FetchData from 'components/FetchData.vue';
import CustomerNotificationsFilter from './CustomerDefaulterFilter.vue'; 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 VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnInput from 'src/components/common/VnInput.vue';
import CustomerDefaulterAddObservation from './CustomerDefaulterAddObservation.vue'; import CustomerDefaulterAddObservation from './CustomerDefaulterAddObservation.vue';
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore();
const quasar = useQuasar(); const quasar = useQuasar();
const arrayData = ref(null);
const balanceDueTotal = ref(0); const balanceDueTotal = ref(0);
const customerId = ref(0);
const selected = ref([]); const selected = ref([]);
const workerId = ref(0); const rows = ref([]);
const rows = computed(() => arrayData.value.store.data);
const tableColumnComponents = { const tableColumnComponents = {
client: { client: {
component: QBtn, component: QBtn,
props: () => ({ flat: true, color: 'blue' }), props: () => ({ flat: true, color: 'blue', noCaps: true }),
event: ({ row }) => selectCustomerId(row.clientFk), event: () => {},
}, },
isWorker: { isWorker: {
component: QCheckbox, component: QCheckbox,
props: ({ row }) => ({ props: (prop) => ({
disable: true, disable: true,
'model-value': Boolean(row.selected), 'model-value': Boolean(prop.value),
}), }),
event: () => {},
}, },
salesperson: { salesPerson: {
component: QBtn, component: QBtn,
props: () => ({ flat: true, color: 'blue' }), props: () => ({ flat: true, color: 'blue', noCaps: true }),
event: ({ row }) => selectWorkerId(row.salesPersonFk), event: () => {},
}, },
country: { country: {
component: 'span', component: 'span',
props: () => {}, props: () => {},
event: () => {}, event: () => {},
}, },
paymentMethod: { payMethod: {
component: 'span', component: 'span',
props: () => {}, props: () => {},
event: () => {}, event: () => {},
@ -62,8 +58,8 @@ const tableColumnComponents = {
}, },
author: { author: {
component: QBtn, component: QBtn,
props: () => ({ flat: true, color: 'blue' }), props: () => ({ flat: true, color: 'blue', noCaps: true }),
event: ({ row }) => selectWorkerId(row.workerFk), event: () => {},
}, },
lastObservation: { lastObservation: {
component: 'span', component: 'span',
@ -93,6 +89,7 @@ const columns = computed(() => [
field: 'clientName', field: 'clientName',
label: t('Client'), label: t('Client'),
name: 'client', name: 'client',
sortable: true,
}, },
{ {
align: 'left', align: 'left',
@ -104,85 +101,77 @@ const columns = computed(() => [
align: 'left', align: 'left',
field: 'salesPersonName', field: 'salesPersonName',
label: t('Salesperson'), label: t('Salesperson'),
name: 'salesperson', name: 'salesPerson',
sortable: true,
}, },
{ {
align: 'left', align: 'left',
field: 'country', field: 'country',
label: t('Country'), label: t('Country'),
name: 'country', name: 'country',
sortable: true,
}, },
{ {
align: 'left', align: 'left',
field: 'payMethod', field: 'payMethod',
label: t('P. Method'), label: t('P. Method'),
name: 'paymentMethod', name: 'payMethod',
sortable: true,
tooltip: t('Pay method'),
}, },
{ {
align: 'left', align: 'left',
field: ({ amount }) => toCurrency(amount), field: ({ amount }) => toCurrency(amount),
label: t('Balance D.'), label: t('Balance D.'),
name: 'balance', name: 'balance',
sortable: true,
tooltip: t('Balance due'),
}, },
{ {
align: 'left', align: 'left',
field: 'workerName', field: 'workerName',
label: t('Author'), label: t('Author'),
name: 'author', name: 'author',
sortable: true,
tooltip: t('Worker who made the last observation'),
}, },
{ {
align: 'left', align: 'left',
field: 'observation', field: 'observation',
label: t('Last observation'), label: t('Last observation'),
name: 'lastObservation', name: 'lastObservation',
sortable: true,
}, },
{ {
align: 'left', align: 'left',
field: ({ created }) => toDate(created), field: ({ created }) => toDate(created),
label: t('L. O. Date'), label: t('L. O. Date'),
name: 'date', name: 'date',
sortable: true,
tooltip: t('Last observation date'),
}, },
{ {
align: 'left', align: 'left',
field: ({ creditInsurance }) => toCurrency(creditInsurance), field: ({ creditInsurance }) => toCurrency(creditInsurance),
label: t('Credit I.'), label: t('Credit I.'),
name: 'credit', name: 'credit',
sortable: true,
tooltip: t('Credit insurance'),
}, },
{ {
align: 'left', align: 'left',
field: ({ defaulterSinced }) => toDate(defaulterSinced), field: ({ defaulterSinced }) => toDate(defaulterSinced),
label: t('From'), label: t('From'),
name: 'from', name: 'from',
sortable: true,
}, },
]); ]);
onBeforeMount(() => { const setRows = (data) => {
getArrayData(); rows.value = data;
}); balanceDueTotal.value = data.reduce((accumulator, currentValue) => {
return accumulator + (currentValue['amount'] || 0);
const getArrayData = async () => { }, 0);
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) => {
workerId.value = 0;
customerId.value = id;
};
const selectWorkerId = (id) => {
customerId.value = 0;
workerId.value = id;
}; };
const viewAddObservation = (rowsSelected) => { const viewAddObservation = (rowsSelected) => {
@ -196,12 +185,21 @@ const viewAddObservation = (rowsSelected) => {
}; };
const refreshData = () => { const refreshData = () => {
getArrayData(); setRows();
};
const onFetch = (data) => {
for (const element of data) {
element.isWorker = element.businessTypeFk === 'worker';
}
rows.value = data;
}; };
</script> </script>
<template> <template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <FetchData :filter="filter" @on-fetch="onFetch" auto-load url="Defaulters/filter" />
<QDrawer side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8"> <QScrollArea class="fit text-grey-8">
<CustomerNotificationsFilter data-key="CustomerDefaulter" /> <CustomerNotificationsFilter data-key="CustomerDefaulter" />
</QScrollArea> </QScrollArea>
@ -230,6 +228,18 @@ const refreshData = () => {
selection="multiple" selection="multiple"
v-model:selected="selected" v-model:selected="selected"
> >
<template #header="props">
<QTr :props="props" class="bg">
<QTh>
<QCheckbox v-model="props.selected" />
</QTh>
<QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
<QTooltip v-if="col.tooltip">{{ col.tooltip }}</QTooltip>
</QTh>
</QTr>
</template>
<template #body-cell="props"> <template #body-cell="props">
<QTd :props="props"> <QTd :props="props">
<QTr :props="props" class="cursor-pointer"> <QTr :props="props" class="cursor-pointer">
@ -239,10 +249,29 @@ const refreshData = () => {
v-bind="tableColumnComponents[props.col.name].props(props)" v-bind="tableColumnComponents[props.col.name].props(props)"
@click="tableColumnComponents[props.col.name].event(props)" @click="tableColumnComponents[props.col.name].event(props)"
> >
{{ props.value }} <template v-if="props.col.name !== 'isWorker'">
<div v-if="props.col.name === 'lastObservation'">
<VnInput
type="textarea"
v-model="props.value"
autogrow
/>
</div>
<div v-else>{{ props.value }}</div>
</template>
<WorkerDescriptorProxy v-if="workerId" :id="workerId" /> <WorkerDescriptorProxy
<CustomerDescriptorProxy v-else :id="customerId" /> :id="props.row.salesPersonFk"
v-if="props.col.name === 'salesPerson'"
/>
<WorkerDescriptorProxy
:id="props.row.workerFk"
v-if="props.col.name === 'author'"
/>
<CustomerDescriptorProxy
:id="props.row.clientFk"
v-if="props.col.name === 'client'"
/>
</component> </component>
</QTr> </QTr>
</QTd> </QTd>
@ -265,10 +294,15 @@ es:
Salesperson: Comercial Salesperson: Comercial
Country: País Country: País
P. Method: F. Pago P. Method: F. Pago
Pay method: Forma de pago
Balance D.: Saldo V. Balance D.: Saldo V.
Balance due: Saldo vencido
Author: Autor Author: Autor
Worker who made the last observation: Trabajador que ha realizado la última observación
Last observation: Última observación Last observation: Última observación
L. O. Date: Fecha Ú. O. L. O. Date: Fecha Ú. O.
Last observation date: Fecha última observación
Credit I.: Crédito A. Credit I.: Crédito A.
Credit insurance: Crédito asegurado
From: Desde From: Desde
</i18n> </i18n>

View File

@ -3,7 +3,9 @@ import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios'; import axios from 'axios';
import { useQuasar } from 'quasar'; import { useDialogPluginComponent } from 'quasar';
import useNotify from 'src/composables/useNotify';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
@ -18,8 +20,9 @@ const $props = defineProps({
}, },
}); });
const { dialogRef } = useDialogPluginComponent();
const { notify } = useNotify();
const { t } = useI18n(); const { t } = useI18n();
const quasar = useQuasar();
const newObservation = ref(null); const newObservation = ref(null);
@ -38,15 +41,9 @@ const onSubmit = async () => {
await $props.promise(); await $props.promise();
quasar.notify({ notify('globals.dataSaved', 'positive');
message: t('globals.dataSaved'),
type: 'positive',
});
} catch (error) { } catch (error) {
quasar.notify({ notify(error.message, 'negative');
message: t(`${error.message}`),
type: 'negative',
});
} }
}; };
</script> </script>

View File

@ -119,6 +119,7 @@ const authors = ref();
<QItemSection> <QItemSection>
<VnInput <VnInput
:label="t('P. Method')" :label="t('P. Method')"
clearable
is-outlined is-outlined
v-model="params.paymentMethod" v-model="params.paymentMethod"
/> />
@ -129,6 +130,7 @@ const authors = ref();
<QItemSection> <QItemSection>
<VnInput <VnInput
:label="t('Balance D.')" :label="t('Balance D.')"
clearable
is-outlined is-outlined
v-model="params.balance" v-model="params.balance"
/> />
@ -160,7 +162,12 @@ const authors = ref();
<QItem class="q-mb-sm"> <QItem class="q-mb-sm">
<QItemSection> <QItemSection>
<VnInput :label="t('L. O. Date')" is-outlined v-model="params.date" /> <VnInput
:label="t('L. O. Date')"
clearable
is-outlined
v-model="params.date"
/>
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -168,6 +175,7 @@ const authors = ref();
<QItemSection> <QItemSection>
<VnInput <VnInput
:label="t('Credit I.')" :label="t('Credit I.')"
clearable
is-outlined is-outlined
v-model="params.credit" v-model="params.credit"
/> />

View File

@ -1,32 +1,17 @@
<script setup> <script setup>
import { ref, computed, onBeforeMount } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { QBtn } from 'quasar'; import { QBtn } from 'quasar';
import { useArrayData } from 'composables/useArrayData'; import FetchData from 'components/FetchData.vue';
import { useStateStore } from 'stores/useStateStore';
import CustomerNotificationsFilter from './CustomerNotificationsFilter.vue'; import CustomerNotificationsFilter from './CustomerNotificationsFilter.vue';
import CustomerDescriptorProxy from '../Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from '../Card/CustomerDescriptorProxy.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore();
const arrayData = ref(null);
onBeforeMount(async () => {
arrayData.value = useArrayData('CustomerNotifications', {
url: 'Clients',
limit: 0,
});
await arrayData.value.fetch({ append: false });
stateStore.rightDrawer = true;
});
const rows = computed(() => arrayData.value.store.data);
const rows = ref([]);
const selected = ref([]); const selected = ref([]);
const selectedCustomerId = ref(0); const selectedCustomerId = ref(0);
@ -97,7 +82,14 @@ const selectCustomerId = (id) => {
</script> </script>
<template> <template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <FetchData
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
url="Clients"
/>
<QDrawer side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8"> <QScrollArea class="fit text-grey-8">
<CustomerNotificationsFilter data-key="CustomerNotifications" /> <CustomerNotificationsFilter data-key="CustomerNotifications" />
</QScrollArea> </QScrollArea>

View File

@ -40,6 +40,7 @@ const clients = ref();
<QItemSection> <QItemSection>
<VnInput <VnInput
:label="t('Identifier')" :label="t('Identifier')"
clearable
is-outlined is-outlined
v-model="params.identifier" v-model="params.identifier"
/> />
@ -96,13 +97,24 @@ const clients = ref();
<QItem class="q-mb-sm"> <QItem class="q-mb-sm">
<QItemSection> <QItemSection>
<VnInput :label="t('Phone')" is-outlined v-model="params.phone" /> <VnInput
:label="t('Phone')"
clearable
is-outlined
v-model="params.phone"
/>
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem class="q-mb-sm"> <QItem class="q-mb-sm">
<QItemSection> <QItemSection>
<VnInput :label="t('Email')" is-outlined v-model="params.email" /> <VnInput
:label="t('Email')"
clearable
is-outlined
type="email"
v-model="params.email"
/>
</QItemSection> </QItemSection>
</QItem> </QItem>
<QSeparator /> <QSeparator />

View File

@ -41,9 +41,9 @@ const refreshData = () => {
getCustomsAgents(); getCustomsAgents();
}; };
const toCustomerConsignees = () => { const toCustomerAddress = () => {
router.push({ router.push({
name: 'CustomerConsignees', name: 'CustomerAddress',
params: { params: {
id: route.params.id, id: route.params.id,
}, },
@ -70,84 +70,78 @@ function handleLocation(data, location) {
:form-initial-data="formInitialData" :form-initial-data="formInitialData"
:observe-form-changes="false" :observe-form-changes="false"
:url-create="urlCreate" :url-create="urlCreate"
@on-data-saved="toCustomerConsignees()" @on-data-saved="toCustomerAddress()"
model="client" model="client"
> >
<template #moreActions>
<QBtn
:label="t('globals.cancel')"
@click="toCustomerAddress"
color="primary"
flat
icon="close"
/>
</template>
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md"> <QCheckbox :label="t('Default')" v-model="data.isDefaultAddress" />
<div class="col">
<QCheckbox :label="t('Default')" v-model="data.isDefaultAddress" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <VnInput :label="t('Consignee')" clearable v-model="data.nickname" />
<VnInput :label="t('Consignee')" v-model="data.nickname" />
</div> <VnInput :label="t('Street address')" clearable v-model="data.street" />
<div class="col">
<VnInput :label="t('Street address')" v-model="data.street" />
</div>
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnLocation
<div class="col"> :rules="validate('Worker.postcode')"
<VnLocation :roles-allowed-to-create="['deliveryAssistant']"
:rules="validate('Worker.postcode')" :options="postcodesOptions"
:roles-allowed-to-create="['deliveryAssistant']" v-model="data.location"
:options="postcodesOptions" @update:model-value="(location) => handleLocation(data, location)"
v-model="data.location" />
@update:model-value="(location) => handleLocation(data, location)"
></VnLocation> <div class="row justify-between q-gutter-md q-mb-md">
</div> <VnSelectFilter
</VnRow> :label="t('Agency')"
:options="agencyModes"
:rules="validate('route.agencyFk')"
hide-selected
option-label="name"
option-value="id"
v-model="data.agencyModeFk"
class="col"
/>
<VnInput class="col" :label="t('Phone')" clearable v-model="data.phone" />
<VnInput
class="col"
:label="t('Mobile')"
clearable
v-model="data.mobile"
/>
</div>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <VnSelectFilter
<VnSelectFilter :label="t('Incoterms')"
:label="t('Agency')" :options="incoterms"
:options="agencyModes" hide-selected
hide-selected option-label="name"
option-label="name" option-value="code"
option-value="id" v-model="data.incotermsFk"
v-model="data.agencyModeFk" />
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnSelectDialog
<div class="col"> :label="t('Customs agent')"
<VnInput :label="t('Phone')" v-model="data.phone" /> :options="customsAgents"
</div> hide-selected
<div class="col"> option-label="fiscalName"
<VnInput :label="t('Mobile')" v-model="data.mobile" /> option-value="id"
</div> v-model="data.customsAgentFk"
</VnRow> >
<template #form>
<VnRow class="row q-gutter-md q-mb-md"> <CustomerNewCustomsAgent @on-data-saved="refreshData()" />
<div class="col"> </template>
<VnSelectFilter </VnSelectDialog>
:label="t('Incoterms')"
:options="incoterms"
hide-selected
option-label="name"
option-value="code"
v-model="data.incotermsFk"
/>
</div>
<div class="col">
<VnSelectDialog
:label="t('Customs agent')"
:options="customsAgents"
hide-selected
option-label="fiscalName"
option-value="id"
v-model="data.customsAgentFk"
>
<template #form>
<CustomerNewCustomsAgent @on-data-saved="refreshData()" />
</template>
</VnSelectDialog>
</div>
</VnRow> </VnRow>
</template> </template>
</FormModel> </FormModel>

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { onBeforeMount, ref } from 'vue'; import { onBeforeMount, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import VnLocation from 'src/components/common/VnLocation.vue'; import VnLocation from 'src/components/common/VnLocation.vue';
@ -16,6 +16,7 @@ import CustomsNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCus
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter();
const urlUpdate = ref(''); const urlUpdate = ref('');
const postcodesOptions = ref([]); const postcodesOptions = ref([]);
@ -24,9 +25,10 @@ const incoterms = ref([]);
const customsAgents = ref([]); const customsAgents = ref([]);
const observationTypes = ref([]); const observationTypes = ref([]);
const notes = ref([]); const notes = ref([]);
const deletes = ref([]);
onBeforeMount(() => { onBeforeMount(() => {
urlUpdate.value = `Clients/${route.params.id}/updateAddress/${route.params.consigneeId}`; urlUpdate.value = `Clients/${route.params.id}/updateAddress/${route.params.addressId}`;
}); });
const getData = async (observations) => { const getData = async (observations) => {
@ -35,7 +37,7 @@ const getData = async (observations) => {
if (observationTypes.value.length) { if (observationTypes.value.length) {
const filter = { const filter = {
fields: ['id', 'addressFk', 'observationTypeFk', 'description'], fields: ['id', 'addressFk', 'observationTypeFk', 'description'],
where: { addressFk: `${route.params.consigneeId}` }, where: { addressFk: `${route.params.addressId}` },
}; };
const { data } = await axios.get('AddressObservations', { const { data } = await axios.get('AddressObservations', {
params: { filter: JSON.stringify(filter) }, params: { filter: JSON.stringify(filter) },
@ -52,8 +54,9 @@ const getData = async (observations) => {
$isNew: false, $isNew: false,
$oldData: null, $oldData: null,
$orgIndex: null, $orgIndex: null,
addressFk: `${route.params.consigneeId}`, addressFk: `${route.params.addressId}`,
description: observation.description, description: observation.description,
id: observation.id,
observationTypeFk: type.id, observationTypeFk: type.id,
} }
: null; : null;
@ -68,21 +71,39 @@ const addNote = () => {
$isNew: true, $isNew: true,
$oldData: null, $oldData: null,
$orgIndex: null, $orgIndex: null,
addressFk: `${route.params.consigneeId}`, addressFk: `${route.params.addressId}`,
description: '', description: '',
observationTypeFk: '', observationTypeFk: '',
}); });
}; };
const deleteNote = (index) => { const deleteNote = (id, index) => {
deletes.value.push(id);
notes.value.splice(index, 1); notes.value.splice(index, 1);
}; };
const onDataSaved = () => { const onDataSaved = async () => {
const payload = { let payload = {};
creates: notes.value, const creates = notes.value.filter((note) => note.$isNew);
}; if (creates.length) {
axios.post('AddressObservations/crud', payload); payload.creates = creates;
}
if (deletes.value.length) {
payload.deletes = deletes.value;
}
await axios.post('AddressObservations/crud', payload);
notes.value = [];
deletes.value = [];
toCustomerAddress();
};
const toCustomerAddress = () => {
router.push({
name: 'CustomerAddress',
params: {
id: route.params.id,
},
});
}; };
function handleLocation(data, location) { function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {}; const { town, code, provinceFk, countryFk } = location ?? {};
@ -110,11 +131,21 @@ function handleLocation(data, location) {
<FormModel <FormModel
:observe-form-changes="false" :observe-form-changes="false"
:url-update="urlUpdate" :url-update="urlUpdate"
:url="`Addresses/${route.params.consigneeId}`" :url="`Addresses/${route.params.addressId}`"
@on-data-saved="onDataSaved()" @on-data-saved="onDataSaved()"
auto-load auto-load
model="client" model="client"
> >
<template #moreActions>
<QBtn
:label="t('globals.cancel')"
@click="toCustomerAddress"
color="primary"
flat
icon="close"
/>
</template>
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
@ -136,10 +167,10 @@ function handleLocation(data, location) {
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnInput :label="t('Consignee')" v-model="data.nickname" /> <VnInput :label="t('Consignee')" clearable v-model="data.nickname" />
</div> </div>
<div class="col"> <div class="col">
<VnInput :label="t('Street address')" v-model="data.street" /> <VnInput :label="t('Street')" clearable v-model="data.street" />
</div> </div>
</VnRow> </VnRow>
@ -153,27 +184,22 @@ function handleLocation(data, location) {
@update:model-value="(location) => handleLocation(data, location)" @update:model-value="(location) => handleLocation(data, location)"
></VnLocation> ></VnLocation>
</div> </div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnSelectFilter <VnSelectFilter
:label="t('Agency')" :label="t('Agency')"
:options="agencyModes" :options="agencyModes"
:rules="validate('route.agencyFk')"
hide-selected hide-selected
option-label="name" option-label="name"
option-value="id" option-value="id"
v-model="data.agencyModeFk" v-model="data.agencyModeFk"
/> />
</div> </div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnInput :label="t('Phone')" v-model="data.phone" /> <VnInput :label="t('Phone')" clearable v-model="data.phone" />
</div> </div>
<div class="col"> <div class="col">
<VnInput :label="t('Mobile')" v-model="data.mobile" /> <VnInput :label="t('Mobile')" clearable v-model="data.mobile" />
</div> </div>
</VnRow> </VnRow>
@ -221,18 +247,23 @@ function handleLocation(data, location) {
/> />
</div> </div>
<div class="col"> <div class="col">
<VnInput :label="t('Description')" v-model="note.description" /> <VnInput
:label="t('Description')"
:rules="validate('route.description')"
clearable
v-model="note.description"
/>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<QIcon <QIcon
@click.stop="deleteNote(index)" @click.stop="deleteNote(note.id, index)"
class="cursor-pointer" class="cursor-pointer"
color="primary" color="primary"
name="delete" name="delete"
size="sm" size="sm"
> >
<QTooltip> <QTooltip>
{{ t('Remove') }} {{ t('Remove note') }}
</QTooltip> </QTooltip>
</QIcon> </QIcon>
</div> </div>
@ -265,7 +296,7 @@ es:
Is equalizated: Recargo de equivalencia Is equalizated: Recargo de equivalencia
Is Loginflora allowed: Compra directa en Holanda Is Loginflora allowed: Compra directa en Holanda
Consignee: Consignatario Consignee: Consignatario
Street address: Dirección postal Street: Dirección fiscal
Postcode: Código postal Postcode: Código postal
City: Población City: Población
Province: Provincia Province: Provincia

View File

@ -0,0 +1,140 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { useDialogPluginComponent } from 'quasar';
import useNotify from 'src/composables/useNotify';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
const { dialogRef } = useDialogPluginComponent();
const { notify } = useNotify();
const { t } = useI18n();
const $props = defineProps({
id: {
type: String,
required: true,
},
userPasswords: {
type: Object,
required: true,
},
promise: {
type: Function,
required: true,
},
});
const closeButton = ref(null);
const isLoading = ref(false);
const newPassword = ref('');
const requestPassword = ref('');
const onSubmit = async () => {
isLoading.value = true;
if (newPassword.value !== requestPassword.value) {
notify(t("Passwords don't match"), 'negative');
isLoading.value = false;
return;
}
const payload = {
newPassword: newPassword.value,
};
try {
await axios.patch(`Clients/${$props.id}/setPassword`, payload);
await $props.promise();
} catch (error) {
notify('errors.create', 'negative');
} finally {
isLoading.value = false;
if (closeButton.value) closeButton.value.click();
}
};
</script>
<template>
<QDialog ref="dialogRef">
<QCard class="q-pa-lg">
<QCardSection>
<QForm @submit.prevent="onSubmit">
<span
ref="closeButton"
class="row justify-end close-icon"
v-close-popup
>
<QIcon name="close" size="sm" />
</span>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('New password')"
clearable
v-model="newPassword"
type="password"
>
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>
{{
t('customer.card.passwordRequirements', {
length: $props.userPasswords.length,
nAlpha: $props.userPasswords.nAlpha,
nDigits: $props.userPasswords.nDigits,
nPunct: $props.userPasswords.nPunct,
nUpper: $props.userPasswords.nUpper,
})
}}
</QTooltip>
</QIcon>
</template>
</VnInput>
</div>
<div class="col">
<VnInput
:label="t('Request password')"
clearable
v-model="requestPassword"
type="password"
/>
</div>
</VnRow>
<div class="q-mt-lg row justify-end">
<QBtn
:disabled="isLoading"
:label="t('globals.cancel')"
:loading="isLoading"
class="q-ml-sm"
color="primary"
flat
type="reset"
v-close-popup
/>
<QBtn
:disabled="isLoading"
:label="t('Change password')"
:loading="isLoading"
color="primary"
type="submit"
/>
</div>
</QForm>
</QCardSection>
</QCard>
</QDialog>
</template>
<i18n>
es:
New password: Nueva contraseña
Request password: Repetir contraseña
Change password: Cambiar contraseña
Passwords don't match: Las contraseñas no coinciden
</i18n>

View File

@ -0,0 +1,47 @@
<script setup>
import { useI18n } from 'vue-i18n';
import axios from 'axios';
const { t } = useI18n();
const $props = defineProps({
transaction: {
type: Object,
required: true,
},
promise: {
type: Function,
required: true,
},
});
const setClientsConfirmTransaction = async () => {
try {
const payload = { id: $props.transaction.id };
await axios.post('Clients/confirmTransaction', payload);
$props.promise();
} catch (error) {
return error;
}
};
</script>
<template>
<div v-if="!$props?.transaction?.isConfirmed">
<QIcon
@click.stop="setClientsConfirmTransaction"
color="primary"
name="done_all"
size="sm"
class="cursor-pointer"
>
<QTooltip>{{ t('Confirm transaction') }}</QTooltip>
</QIcon>
</div>
</template>
<i18n>
es:
Confirm transaction: Confirmar transacción
</i18n>

View File

@ -0,0 +1,48 @@
<script setup>
import { onBeforeMount, ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const tooltip = ref('');
const $props = defineProps({
transaction: {
type: Object,
required: true,
},
});
onBeforeMount(() => {
const errorMessage = $props.transaction.errorMessage
? $props.transaction.errorMessage
: '';
const responseMessage = $props.transaction.responseMessage
? $props.transaction.responseMessage
: '';
tooltip.value = `${errorMessage} ${responseMessage}`;
});
</script>
<template>
<div
v-if="
($props.transaction.errorMessage || $props.transaction.responseMessage) &&
!$props.transaction.isConfirmed
"
>
<QIcon color="negative" name="close" size="sm">
<QTooltip>{{ tooltip }}</QTooltip>
</QIcon>
</div>
<div v-if="$props.transaction.isConfirmed">
<QIcon color="positive" name="check" size="sm">
<QTooltip>{{ t('Confirmed') }}</QTooltip>
</QIcon>
</div>
</template>
<i18n>
es:
Confirmed: Confirmada
</i18n>

View File

@ -0,0 +1,72 @@
<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';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const initialData = reactive({
clientFK: Number(route.params.id),
});
const toCustomerCreditContracts = () => {
router.push({ name: 'CustomerCreditContracts' });
};
</script>
<template>
<FormModel
:form-initial-data="initialData"
:observe-form-changes="false"
url-create="creditClassifications/createWithInsurance"
@on-data-saved="toCustomerCreditContracts()"
>
<template #moreActions>
<QBtn
:label="t('globals.cancel')"
@click="toCustomerCreditContracts"
color="primary"
flat
icon="close"
/>
</template>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Credit')"
clearable
type="number"
v-model.number="data.credit"
/>
</div>
<div class="col">
<VnInput
:label="t('Grade')"
clearable
type="number"
v-model.number="data.grade"
/>
</div>
<div class="col">
<VnInputDate :label="t('Since')" v-model="data.started" />
</div>
</VnRow>
</template>
</FormModel>
</template>
<i18n>
es:
Credit: Crédito
Grade: Grade
Since: Desde
</i18n>

View File

@ -0,0 +1,107 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { toCurrency, toDateHourMinSec } from 'src/filters';
import FetchData from 'components/FetchData.vue';
const { t } = useI18n();
const route = useRoute();
const rows = ref([]);
const filter = {
where: {
creditClassificationFk: `${route.params.creditId}`,
},
limit: 20,
};
const tableColumnComponents = {
created: {
component: 'span',
props: () => {},
event: () => {},
},
grade: {
component: 'span',
props: () => {},
event: () => {},
},
credit: {
component: 'span',
props: () => {},
event: () => {},
},
};
const columns = computed(() => [
{
align: 'left',
field: 'created',
format: (value) => toDateHourMinSec(value),
label: t('Created'),
name: 'created',
},
{
align: 'left',
field: 'grade',
label: t('Grade'),
name: 'grade',
},
{
align: 'left',
field: 'credit',
format: (value) => toCurrency(value),
label: t('Credit'),
name: 'credit',
},
]);
</script>
<template>
<FetchData
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
url="CreditInsurances"
/>
<QPage class="column items-center q-pa-md" v-if="rows.length">
<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"
class="col-content"
v-bind="tableColumnComponents[props.col.name].props(props)"
@click="tableColumnComponents[props.col.name].event(props)"
>
{{ props.value }}
</component>
</QTr>
</QTd>
</template>
</QTable>
</QPage>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</template>
<i18n>
es:
Created: Fecha creación
Grade: Grade
Credit: Crédito
</i18n>

View File

@ -1,18 +1,21 @@
<script setup> <script setup>
import { reactive } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const initialData = reactive({ const initialData = ref({});
credit: null,
}); const setClient = (data) => {
console.log(data.credit);
initialData.value.credit = data.credit;
};
const toCustomerCredits = () => { const toCustomerCredits = () => {
router.push({ router.push({
@ -25,22 +28,36 @@ const toCustomerCredits = () => {
</script> </script>
<template> <template>
<FetchData
:filter="filter"
@on-fetch="setClient"
auto-load
:url="`Clients/${route.params.id}/getCard`"
/>
<FormModel <FormModel
:form-initial-data="initialData" :form-initial-data="initialData"
:observe-form-changes="false" :observe-form-changes="false"
:url-update="`/Clients/${route.params.id}`" :url-update="`/Clients/${route.params.id}`"
@on-data-saved="toCustomerCredits()" @on-data-saved="toCustomerCredits()"
> >
<template #moreActions>
<QBtn
:label="t('globals.cancel')"
@click="toCustomerCredits"
color="primary"
flat
icon="close"
/>
</template>
<template #form="{ data }"> <template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md"> <QInput
<div class="col"> :label="t('Credit')"
<QInput clearable
:label="t('Credit')" type="number"
type="number" v-model.number="data.credit"
v-model.number="data.credit" />
/>
</div>
</VnRow>
</template> </template>
</FormModel> </FormModel>
</template> </template>

View File

@ -0,0 +1,96 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { downloadFile } from 'src/composables/downloadFile';
import CustomerFileManagementDelete from 'src/pages/Customer/components/CustomerFileManagementDelete.vue';
const { t } = useI18n();
const quasar = useQuasar();
const route = useRoute();
const router = useRouter();
const $props = defineProps({
id: {
type: Number,
required: true,
},
promise: {
type: Function,
required: true,
},
});
const setDownloadFile = () => downloadFile($props.id);
const toCustomerFileManagementEdit = () => {
router.push({
name: 'CustomerFileManagementEdit',
params: {
id: route.params.id,
dmsId: $props.id,
},
});
};
const showCustomerFileManagementDeleteDialog = () => {
quasar.dialog({
component: CustomerFileManagementDelete,
componentProps: {
id: $props.id,
promise: setData,
},
});
};
const setData = () => {
$props.promise();
};
</script>
<template>
<div>
<QIcon
@click.stop="setDownloadFile"
color="primary"
name="cloud_download"
size="sm"
>
<QTooltip>
{{ t('actionFile', { action: t('globals.download') }) }}
</QTooltip>
</QIcon>
<QIcon
@click.stop="toCustomerFileManagementEdit"
class="q-ml-md"
color="primary"
name="edit"
size="sm"
>
<QTooltip>
{{ t('actionFile', { action: t('globals.edit') }) }}
</QTooltip>
</QIcon>
<QIcon
@click.stop="showCustomerFileManagementDeleteDialog"
class="q-ml-md"
color="primary"
name="delete"
size="sm"
>
<QTooltip>
{{ t('actionFile', { action: t('globals.remove') }) }}
</QTooltip>
</QIcon>
</div>
</template>
<i18n>
en:
actionFile: '{action} file'
es:
actionFile: '{action} fichero'
</i18n>

View File

@ -0,0 +1,260 @@
<script setup>
import { onBeforeMount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import { useState } from 'src/composables/useState';
import { useValidator } from 'src/composables/useValidator';
import useNotify from 'src/composables/useNotify';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnInput from 'src/components/common/VnInput.vue';
const { notify } = useNotify();
const { t } = useI18n();
const { validate } = useValidator();
const route = useRoute();
const router = useRouter();
const state = useState();
const user = state.getUser();
const filterFindOne = { where: { code: 'paymentsLaw' } };
const filterCompanies = { order: ['code'] };
const filterWarehouses = { order: ['name'] };
const inputFileRef = ref();
const client = ref({});
const findOne = ref([]);
const allowedContentTypes = ref([]);
const optionsCompanies = ref([]);
const optionsWarehouses = ref([]);
const optionsDmsTypes = ref([]);
const isLoading = ref(false);
const dms = ref({
hasFile: false,
});
onBeforeMount(() => {
const { companyFk, warehouseFk } = user.value;
dms.value.reference = route.params.id;
dms.value.companyId = companyFk;
dms.value.warehouseId = warehouseFk;
});
watch([client, findOne], ([newClient, newFindOne]) => {
dms.value.description = t('clientFileDescription', {
dmsTypeName: newFindOne.name?.toUpperCase(),
clientName: newClient.name?.toUpperCase(),
clientId: newClient.id,
});
dms.value.dmsTypeId = newFindOne.id;
});
const saveData = async () => {
try {
const formData = new FormData();
const files = dms.value.files;
if (files && files.length > 0) {
for (let file of files) {
formData.append(file.name, file);
}
dms.value.hasFileAttached = true;
const url = `clients/${route.params.id}/uploadFile`;
await axios.post(url, formData, {
params: dms.value,
});
notify('globals.dataSaved', 'positive');
toCustomerFileManagement();
}
} catch (error) {
notify(error.message, 'negative');
}
};
const toCustomerFileManagement = () => {
router.push({ name: 'CustomerFileManagement' });
};
</script>
<template>
<fetch-data
@on-fetch="(data) => (client = data)"
auto-load
:url="`Clients/${route.params.id}/getCard`"
/>
<fetch-data
:filter="filterFindOne"
@on-fetch="(data) => (findOne = data)"
auto-load
url="DmsTypes/findOne"
/>
<fetch-data
@on-fetch="(data) => (allowedContentTypes = data)"
auto-load
url="DmsContainers/allowedContentTypes"
/>
<fetch-data
:filter="filterCompanies"
@on-fetch="(data) => (optionsCompanies = data)"
auto-load
url="Companies"
/>
<fetch-data
:filter="filterWarehouses"
@on-fetch="(data) => (optionsWarehouses = data)"
auto-load
url="Warehouses"
/>
<fetch-data
:filter="filterWarehouses"
@on-fetch="(data) => (optionsDmsTypes = data)"
auto-load
url="DmsTypes"
/>
<Teleport to="#st-actions">
<QBtnGroup push class="q-gutter-x-sm">
<QBtn
:disabled="isLoading"
:label="t('globals.cancel')"
:loading="isLoading"
@click="toCustomerFileManagement"
color="primary"
flat
icon="close"
/>
<QBtn
:disabled="isLoading"
:label="t('globals.save')"
:loading="isLoading"
@click.stop="saveData"
color="primary"
icon="save"
/>
</QBtnGroup>
</Teleport>
<QCard class="q-pa-lg">
<QCardSection>
<QForm>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Reference')"
clearable
v-model="dms.reference"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('Company')"
:options="optionsCompanies"
:rules="validate('entry.companyFk')"
option-label="code"
option-value="id"
v-model="dms.companyId"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Warehouse')"
:options="optionsWarehouses"
option-label="name"
option-value="id"
v-model="dms.warehouseId"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('Type')"
:options="optionsDmsTypes"
option-label="name"
option-value="id"
v-model="dms.dmsTypeId"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Description')"
:rules="validate('route.description')"
clearable
type="textarea"
v-model="dms.description"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QFile
ref="inputFileRef"
class="required"
:label="t('File')"
v-model="dms.files"
multiple
:accept="allowedContentTypes.join(',')"
clearable
clear-icon="close"
>
<template #append>
<QBtn
icon="vn:attach"
flat
round
padding="xs"
@click="inputFileRef.pickFiles()"
>
<QTooltip>
{{ t('Select a file') }}
</QTooltip>
</QBtn>
<QBtn icon="info" flat round padding="xs">
<QTooltip max-width="30rem">
{{
`${t(
'Allowed content types'
)}: ${allowedContentTypes.join(', ')}`
}}
</QTooltip>
</QBtn>
</template>
</QFile>
</div>
</VnRow>
<QCheckbox
:label="t('Generate identifier for original file')"
v-model="dms.hasFile"
/>
</QForm>
</QCardSection>
</QCard>
</template>
<i18n>
en:
clientFileDescription: '{dmsTypeName} FROM CLIENT {clientName} ID {clientId}'
es:
Reference: Referencia
Company: Empresa
Warehouse: Almacén
Type: Tipo
Description: Descripción
clientFileDescription: '{dmsTypeName} DEL CLIENTE {clientName} ID {clientId}'
File: Fichero
Select a file: Selecciona un fichero
Allowed content types: Tipos de archivo permitidos
Generate identifier for original file: Generar identificador para archivo original
</i18n>

View File

@ -0,0 +1,82 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useDialogPluginComponent } from 'quasar';
import axios from 'axios';
import useNotify from 'src/composables/useNotify';
const $props = defineProps({
id: {
type: Number,
required: true,
},
promise: {
type: Function,
required: true,
},
});
const { dialogRef } = useDialogPluginComponent();
const { notify } = useNotify();
const { t } = useI18n();
const closeButton = ref(null);
const isLoading = ref(false);
const deleteDms = async () => {
isLoading.value = true;
try {
await axios.post(`ClientDms/${$props.id}/removeFile`);
if ($props.promise) await $props.promise();
notify('globals.dataDeleted', 'positive');
} catch (error) {
notify(error.message, 'negative');
} finally {
closeButton.value.click();
isLoading.value = false;
}
};
</script>
<template>
<QDialog ref="dialogRef">
<QCard class="q-pa-md q-mb-md">
<span ref="closeButton" class="row justify-end close-icon" v-close-popup>
<QIcon name="close" size="sm" />
</span>
<QCardSection>
<div class="mt-1 text-h6">{{ t('This file will be deleted') }}</div>
<div>{{ t('Are you sure you want to continue?') }}</div>
</QCardSection>
<QCardActions class="flex justify-end">
<QBtn
:disabled="isLoading"
:label="t('globals.cancel')"
:loading="isLoading"
class="q-mr-xl"
color="primary"
flat
v-close-popup
/>
<QBtn
:disabled="isLoading"
:label="t('globals.save')"
:loading="isLoading"
@click.stop="deleteDms"
color="primary"
/>
</QCardActions>
</QCard>
</QDialog>
</template>
<i18n>
es:
This file will be deleted: Este fichero va a ser borrado
Are you sure you want to continue?: ¿Seguro que quieres continuar?
</i18n>

View File

@ -0,0 +1,237 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import { useValidator } from 'src/composables/useValidator';
import useNotify from 'src/composables/useNotify';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnInput from 'src/components/common/VnInput.vue';
const { notify } = useNotify();
const { t } = useI18n();
const { validate } = useValidator();
const route = useRoute();
const router = useRouter();
const filterCompanies = { order: ['code'] };
const filterWarehouses = { order: ['name'] };
const inputFileRef = ref();
const allowedContentTypes = ref([]);
const optionsCompanies = ref([]);
const optionsWarehouses = ref([]);
const optionsDmsTypes = ref([]);
const isLoading = ref(false);
const dms = ref({
hasFile: true,
});
const setCurrentDms = (data) => {
dms.value.reference = data.reference;
dms.value.companyId = data.companyFk;
dms.value.warehouseId = data.warehouseFk;
dms.value.dmsTypeId = data.dmsTypeFk;
dms.value.description = data.description;
};
const saveData = async () => {
try {
const formData = new FormData();
const files = dms.value.files;
if (files && files.length > 0) {
for (let file of files) {
formData.append(file.name, file);
}
dms.value.hasFileAttached = true;
const url = `dms/${route.params.dmsId}/updateFile`;
await axios.post(url, formData, {
params: dms.value,
});
notify('globals.dataSaved', 'positive');
toCustomerFileManagement();
}
} catch (error) {
notify(error.message, 'negative');
}
};
const toCustomerFileManagement = () => {
router.push({ name: 'CustomerFileManagement' });
};
</script>
<template>
<fetch-data :url="`Dms/${route.params.dmsId}`" @on-fetch="setCurrentDms" auto-load />
<fetch-data
@on-fetch="(data) => (allowedContentTypes = data)"
auto-load
url="DmsContainers/allowedContentTypes"
/>
<fetch-data
:filter="filterCompanies"
@on-fetch="(data) => (optionsCompanies = data)"
auto-load
url="Companies"
/>
<fetch-data
:filter="filterWarehouses"
@on-fetch="(data) => (optionsWarehouses = data)"
auto-load
url="Warehouses"
/>
<fetch-data
:filter="filterWarehouses"
@on-fetch="(data) => (optionsDmsTypes = data)"
auto-load
url="DmsTypes"
/>
<Teleport to="#st-actions">
<QBtnGroup push class="q-gutter-x-sm">
<QBtn
:disabled="isLoading"
:label="t('globals.cancel')"
:loading="isLoading"
@click="toCustomerFileManagement"
color="primary"
flat
icon="close"
/>
<QBtn
:disabled="isLoading"
:label="t('globals.save')"
:loading="isLoading"
@click.stop="saveData"
color="primary"
icon="save"
/>
</QBtnGroup>
</Teleport>
<QCard class="q-pa-lg">
<QCardSection>
<QForm>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Reference')"
clearable
v-model="dms.reference"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('Company')"
:options="optionsCompanies"
:rules="validate('entry.companyFk')"
option-label="code"
option-value="id"
v-model="dms.companyId"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Warehouse')"
:options="optionsWarehouses"
option-label="name"
option-value="id"
v-model="dms.warehouseId"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('Type')"
:options="optionsDmsTypes"
option-label="name"
option-value="id"
v-model="dms.dmsTypeId"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Description')"
:rules="validate('route.description')"
clearable
type="textarea"
v-model="dms.description"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QFile
ref="inputFileRef"
class="required"
:label="t('File')"
v-model="dms.files"
multiple
:accept="allowedContentTypes.join(',')"
clearable
clear-icon="close"
>
<template #append>
<QBtn
icon="vn:attach"
flat
round
padding="xs"
@click="inputFileRef.pickFiles()"
>
<QTooltip>
{{ t('Select a file') }}
</QTooltip>
</QBtn>
<QBtn icon="info" flat round padding="xs">
<QTooltip max-width="30rem">
{{
`${t(
'Allowed content types'
)}: ${allowedContentTypes.join(', ')}`
}}
</QTooltip>
</QBtn>
</template>
</QFile>
</div>
</VnRow>
<QCheckbox
:label="t('Generate identifier for original file')"
v-model="dms.hasFile"
disable
/>
</QForm>
</QCardSection>
</QCard>
</template>
<i18n>
en:
clientFileDescription: '{dmsTypeName} FROM CLIENT {clientName} ID {clientId}'
es:
Reference: Referencia
Company: Empresa
Warehouse: Almacén
Type: Tipo
Description: Descripción
clientFileDescription: '{dmsTypeName} DEL CLIENTE {clientName} ID {clientId}'
File: Fichero
Select a file: Selecciona un fichero
Allowed content types: Tipos de archivo permitidos
Generate identifier for original file: Generar identificador para archivo original
</i18n>

View File

@ -15,9 +15,6 @@ const route = useRoute();
const router = useRouter(); const router = useRouter();
const initialData = reactive({ const initialData = reactive({
amount: null,
description: null,
greugeTypeFk: null,
shipped: '2001-01-01T11:00:00.000Z', shipped: '2001-01-01T11:00:00.000Z',
}); });
@ -47,10 +44,25 @@ const toCustomerGreuges = () => {
model="client" model="client"
url-create="Greuges" url-create="Greuges"
> >
<template #moreActions>
<QBtn
:label="t('globals.cancel')"
@click="toCustomerGreuges"
color="primary"
flat
icon="close"
/>
</template>
<template #form="{ data }"> <template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnInput :label="t('Amount')" type="number" v-model="data.amount" /> <VnInput
:label="t('Amount')"
clearable
type="number"
v-model="data.amount"
/>
</div> </div>
<div class="col"> <div class="col">
<VnInputDate :label="t('Date')" v-model="data.shipped" /> <VnInputDate :label="t('Date')" v-model="data.shipped" />
@ -59,7 +71,7 @@ const toCustomerGreuges = () => {
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnInput :label="t('Comment')" v-model="data.description" /> <VnInput :label="t('Comment')" clearable v-model="data.description" />
</div> </div>
<div class="col"> <div class="col">
<VnSelectFilter <VnSelectFilter

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { reactive, ref } from 'vue'; import { reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
@ -10,13 +10,6 @@ const emit = defineEmits(['onDataSaved']);
const { t } = useI18n(); const { t } = useI18n();
const initialData = reactive({
nif: null,
fiscalName: null,
street: null,
phone: null,
});
const onDataSaved = (dataSaved) => { const onDataSaved = (dataSaved) => {
emit('onDataSaved', dataSaved); emit('onDataSaved', dataSaved);
}; };
@ -25,30 +18,35 @@ const onDataSaved = (dataSaved) => {
<template> <template>
<FormModelPopup <FormModelPopup
:title="t('New customs agent')" :title="t('New customs agent')"
url-create="CustomsAgents"
model="customer"
:form-initial-data="initialData"
@on-data-saved="onDataSaved($event)" @on-data-saved="onDataSaved($event)"
model="customer"
url-create="CustomsAgents"
> >
<template #form-inputs="{ data }"> <template #form-inputs="{ data }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnInput :label="t('NIF')" :required="true" v-model="data.nif" /> <VnInput
:label="t('NIF')"
:required="true"
clearable
v-model="data.nif"
/>
</div> </div>
<div class="col"> <div class="col">
<VnInput <VnInput
:label="t('Fiscal name')" :label="t('Fiscal name')"
:required="true" :required="true"
clearable
v-model="data.fiscalName" v-model="data.fiscalName"
/> />
</div> </div>
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnInput :label="t('Street')" v-model="data.street" /> <VnInput :label="t('Street')" clearable v-model="data.street" />
</div> </div>
<div class="col"> <div class="col">
<VnInput :label="t('Phone')" v-model="data.phone" /> <VnInput :label="t('Phone')" clearable v-model="data.phone" />
</div> </div>
</VnRow> </VnRow>
</template> </template>

View File

@ -3,6 +3,8 @@ import { onBeforeMount, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useDialogPluginComponent } from 'quasar';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
@ -12,6 +14,7 @@ import VnInput from 'src/components/common/VnInput.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const { dialogRef } = useDialogPluginComponent();
const $props = defineProps({ const $props = defineProps({
companyId: { companyId: {
@ -49,19 +52,15 @@ const filterBanks = {
const filterClientFindOne = { const filterClientFindOne = {
fields: ['email'], fields: ['email'],
where: { where: {
id: `${route.params.id}`, id: route.params.id,
}, },
}; };
const initialData = reactive({ const initialData = reactive({
amountPaid: $props.totalCredit, amountPaid: $props.totalCredit,
bankFk: null, clientFk: route.params.id,
clientFk: `${route.params.id}`,
companyFk: $props.companyId, companyFk: $props.companyId,
compensationAccount: null,
description: null,
email: clientFindOne.value.email, email: clientFindOne.value.email,
payed: null,
}); });
onBeforeMount(() => { onBeforeMount(() => {
@ -128,14 +127,12 @@ const onDataSaved = async () => {
:url-create="urlCreate" :url-create="urlCreate"
@on-data-saved="onDataSaved()" @on-data-saved="onDataSaved()"
> >
<template #form="{ data }"> <template #form="{ data, validate }">
<span ref="closeButton" class="row justify-end close-icon" v-close-popup> <span ref="closeButton" class="row justify-end close-icon" v-close-popup>
<QIcon name="close" size="sm" /> <QIcon name="close" size="sm" />
</span> </span>
<h5 class="flex justify-center q-mt-xs"> <h5 class="q-mt-none">{{ t('New payment') }}</h5>
{{ t('New payment') }}
</h5>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
@ -150,6 +147,7 @@ const onDataSaved = async () => {
:label="t('Company')" :label="t('Company')"
:options="companyOptions" :options="companyOptions"
:required="true" :required="true"
:rules="validate('entry.companyFk')"
hide-selected hide-selected
option-label="code" option-label="code"
option-value="id" option-value="id"
@ -186,6 +184,7 @@ const onDataSaved = async () => {
:label="t('Amount')" :label="t('Amount')"
:required="true" :required="true"
@update:model-value="calculateFromAmount($event)" @update:model-value="calculateFromAmount($event)"
clearable
type="number" type="number"
v-model.number="data.amountPaid" v-model.number="data.amountPaid"
/> />
@ -200,6 +199,7 @@ const onDataSaved = async () => {
<div class="col" v-if="data.bankFk === 3 || data.bankFk === 3117"> <div class="col" v-if="data.bankFk === 3 || data.bankFk === 3117">
<VnInput <VnInput
:label="t('Compensation account')" :label="t('Compensation account')"
clearable
v-model="data.compensationAccount" v-model="data.compensationAccount"
/> />
</div> </div>
@ -207,6 +207,7 @@ const onDataSaved = async () => {
<VnInput <VnInput
:label="t('Reference')" :label="t('Reference')"
:required="true" :required="true"
clearable
v-model="data.description" v-model="data.description"
/> />
</div> </div>
@ -220,6 +221,7 @@ const onDataSaved = async () => {
<VnInput <VnInput
:label="t('Delivered amount')" :label="t('Delivered amount')"
@update:model-value="calculateFromDeliveredAmount($event)" @update:model-value="calculateFromDeliveredAmount($event)"
clearable
type="number" type="number"
v-model="deliveredAmount" v-model="deliveredAmount"
/> />
@ -227,6 +229,7 @@ const onDataSaved = async () => {
<div class="col"> <div class="col">
<VnInput <VnInput
:label="t('Amount to return')" :label="t('Amount to return')"
clearable
disable disable
type="number" type="number"
v-model="amountToReturn" v-model="amountToReturn"

View File

@ -10,9 +10,7 @@ const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const initialData = reactive({ const initialData = reactive({});
text: null,
});
onMounted(() => { onMounted(() => {
initialData.clientFk = `${route.params.id}`; initialData.clientFk = `${route.params.id}`;
@ -35,6 +33,16 @@ const toCustomerNotes = () => {
@on-data-saved="toCustomerNotes()" @on-data-saved="toCustomerNotes()"
url-create="ClientObservations" url-create="ClientObservations"
> >
<template #moreActions>
<QBtn
:label="t('globals.cancel')"
@click="toCustomerNotes"
color="primary"
flat
icon="close"
/>
</template>
<template #form="{ data }"> <template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">

View File

@ -14,9 +14,6 @@ const router = useRouter();
const initialData = reactive({ const initialData = reactive({
started: '2001-01-01T11:00:00.000Z', started: '2001-01-01T11:00:00.000Z',
finished: null,
amount: null,
period: null,
}); });
onMounted(() => { onMounted(() => {
@ -41,6 +38,16 @@ const toCustomerRecoveries = () => {
model="client" model="client"
url-create="Recoveries" url-create="Recoveries"
> >
<template #moreActions>
<QBtn
:label="t('globals.cancel')"
@click="toCustomerRecoveries"
color="primary"
flat
icon="close"
/>
</template>
<template #form="{ data }"> <template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
@ -53,10 +60,20 @@ const toCustomerRecoveries = () => {
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnInput :label="t('Amount')" type="number" v-model="data.amount" /> <VnInput
:label="t('Amount')"
clearable
type="number"
v-model="data.amount"
/>
</div> </div>
<div class="col"> <div class="col">
<VnInput :label="t('Period')" type="number" v-model="data.period" /> <VnInput
:label="t('Period')"
clearable
type="number"
v-model="data.period"
/>
</div> </div>
</VnRow> </VnRow>
</template> </template>

View File

@ -0,0 +1,385 @@
<script setup>
import { onBeforeMount, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import { useQuasar } from 'quasar';
import { useState } from 'src/composables/useState';
import { useValidator } from 'src/composables/useValidator';
import useNotify from 'src/composables/useNotify';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import CustomerSamplesPreview from 'src/pages/Customer/components/CustomerSamplesPreview.vue';
import { useStateStore } from 'stores/useStateStore';
const { notify } = useNotify();
const { t } = useI18n();
const { validate } = useValidator();
const quasar = useQuasar();
const route = useRoute();
const router = useRouter();
const state = useState();
const user = state.getUser();
const stateStore = useStateStore();
const client = ref({});
const hasChanged = ref(false);
const isLoading = ref(false);
const optionsClientsAddressess = ref([]);
const optionsCompanies = ref([]);
const optionsEmailUsers = ref([]);
const optionsSamplesVisible = ref([]);
const sampleType = ref({ hasPreview: false });
const filterEmailUsers = { where: { userFk: user.value.id } };
const filterClientsAddresses = {
include: [
{ relation: 'province', scope: { fields: ['name'] } },
{ relation: 'agencyMode', scope: { fields: ['name'] } },
],
};
const filterCompanies = { order: ['code'] };
const filterSamplesVisible = {
fields: [
'id',
'code',
'description',
'model',
'hasCompany',
'hasAddress',
'hasPreview',
'datepickerEnabled',
],
order: ['description'],
};
const initialData = reactive({});
onBeforeMount(async () => {
const { data } = await axios.get(`Clients/1/getCard`);
client.value = data;
initialData.clientFk = route.params?.id;
initialData.recipient = data.email;
initialData.recipientId = data.id;
});
const setEmailUser = (data) => {
optionsEmailUsers.value = data;
initialData.replyTo = data[0]?.email;
};
const setClientsAddresses = (data) => {
initialData.addressId = data[0].id;
optionsClientsAddressess.value = data;
};
const setSampleType = (sampleId) => {
hasChanged.value = true;
const { companyFk } = user.value;
sampleType.value = optionsSamplesVisible.value.find((option) => {
return option.id === sampleId;
});
initialData.companyFk = companyFk;
initialData.companyId = companyFk;
};
const setInitialData = () => {
hasChanged.value = false;
initialData.addressId = optionsClientsAddressess.value[0].id;
initialData.companyFk = null;
initialData.from = null;
initialData.recipient = client.value.email;
initialData.recipientId = client.value.id;
initialData.replyTo = optionsEmailUsers.value[0]?.email;
initialData.typeFk = '';
sampleType.value = {};
};
const validateMessage = () => {
if (!initialData.recipient) return 'Email cannot be blank';
if (!sampleType.value) return 'Choose a sample';
if (sampleType.value.hasCompany && !initialData.companyFk) return 'Choose a company';
if (sampleType.value.datepickerEnabled && !initialData.from) return 'Choose a date';
return '';
};
const setParams = (params) => {
if (sampleType.value.hasCompany) params.companyId = initialData.companyFk;
if (sampleType.value.datepickerEnabled) params.from = initialData.from;
if (initialData.addressId) params.addressId = initialData.addressId;
};
const getPreview = async () => {
try {
const params = {
recipientId: route.params.id,
};
const validationMessage = validateMessage();
if (validationMessage) return notify(t(validationMessage), 'negative');
setParams(params);
const path = `${sampleType.value.model}/${route.params.id}/${sampleType.value.code}-html`;
const { data } = await axios.get(path, { params });
if (!data) return;
quasar.dialog({
component: CustomerSamplesPreview,
componentProps: {
htmlContent: data,
},
});
} catch (err) {
notify('Errors getting preview', 'negative');
}
};
const onSubmit = async () => {
isLoading.value = true;
try {
const { data } = await axios.patch('ClientSamples', initialData);
notify('globals.dataSaved', 'positive');
onDataSaved(data);
} catch (error) {
notify('errors.create', 'negative');
} finally {
isLoading.value = false;
}
};
const onDataSaved = async ({
addressId,
companyFk,
companyId,
from,
recipient,
replyTo,
}) => {
await axios.post(`Clients/${route.params.id}/incoterms-authorization-email`, {
addressId,
companyFk,
companyId,
from,
recipient,
replyTo,
});
toCustomerSamples();
};
const updateModelValue = () => {
hasChanged.value = true;
};
const toCustomerSamples = () => {
router.push({ name: 'CustomerSamples' });
};
</script>
<template>
<fetch-data
:filter="filterEmailUsers"
@on-fetch="setEmailUser"
auto-load
url="EmailUsers"
/>
<fetch-data
:filter="filterClientsAddresses"
:url="`Clients/${route.params.id}/addresses`"
@on-fetch="setClientsAddresses"
auto-load
/>
<fetch-data
:filter="filterCompanies"
@on-fetch="(data) => (optionsCompanies = data)"
auto-load
url="Companies"
/>
<fetch-data
:filter="filterSamplesVisible"
@on-fetch="(data) => (optionsSamplesVisible = data)"
auto-load
url="Samples/visible"
/>
<Teleport v-if="stateStore?.isSubToolbarShown()" to="#st-actions">
<QBtnGroup push class="q-gutter-x-sm">
<QBtn
:label="t('globals.cancel')"
@click="toCustomerSamples"
color="primary"
flat
icon="close"
/>
<QBtn
:disabled="isLoading || !sampleType?.hasPreview"
:label="t('Preview')"
:loading="isLoading"
@click.stop="getPreview()"
color="primary"
flat
icon="preview"
/>
<QBtn
:disabled="!hasChanged"
:label="t('globals.reset')"
:loading="isLoading"
@click="setInitialData"
color="primary"
flat
icon="restart_alt"
type="reset"
/>
<QBtn
:disabled="!hasChanged"
:label="t('globals.save')"
:loading="isLoading"
@click="onSubmit"
color="primary"
icon="save"
/>
</QBtnGroup>
</Teleport>
<div class="full-width flex justify-center">
<QCard class="card-width q-pa-lg">
<QForm>
<div class="col">
<VnSelectFilter
:label="t('Sample')"
:options="optionsSamplesVisible"
@update:model-value="setSampleType"
hide-selected
option-label="description"
option-value="id"
v-model="initialData.typeFk"
/>
</div>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Recipient')"
@update:model-value="updateModelValue"
clearable
required="true"
v-model="initialData.recipient"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{
t('Its only used when sample is sent')
}}</QTooltip>
</QIcon>
</template>
</VnInput>
</div>
<div class="col">
<VnInput
:label="t('Reply to')"
@update:model-value="updateModelValue"
clearable
required="true"
v-model="initialData.replyTo"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{
t('To who should the recipient replay?')
}}</QTooltip>
</QIcon>
</template>
</VnInput>
</div>
</VnRow>
<VnRow
class="row q-gutter-md q-mb-md"
v-if="sampleType?.hasCompany || sampleType?.datepickerEnabled"
>
<div class="col">
<VnSelectFilter
:label="t('Company')"
:options="optionsCompanies"
:rules="validate('entry.companyFk')"
hide-selected
option-label="code"
option-value="id"
required="true"
v-model="initialData.companyFk"
v-if="sampleType.hasCompany"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('Address')"
:options="optionsClientsAddressess"
hide-selected
option-label="nickname"
option-value="id"
required="true"
v-model="initialData.addressId"
v-if="sampleType.id === 20"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{
`${scope.opt.nickname}, ${scope.opt.street}, ${scope.opt.city}, ${scope.opt.province.name} - ${scope.opt.agencyMode.name}`
}}
</QItemLabel>
</QItemSection>
</QItem>
</template>
<template #append>
<QIcon name="edit" class="cursor-pointer">
<QTooltip>{{ t('Edit address') }}</QTooltip>
</QIcon>
</template>
</VnSelectFilter>
</div>
</VnRow>
<VnRow
class="row q-gutter-md q-mb-md"
v-if="sampleType?.datepickerEnabled"
>
<div class="col">
<VnInputDate
:label="t('Since')"
required="true"
v-model="initialData.from"
/>
</div>
</VnRow>
</QForm>
</QCard>
</div>
</template>
<i18n>
es:
Sample: Plantilla
Recipient: Destinatario
Reply to: Responder a
Company: Empresa
Address: Dirección
Since: Desde
Its only used when sample is sent: Se utiliza únicamente cuando se envía la plantilla
To who should the recipient replay?: ¿A quien debería responder el destinatario?
Edit address: Editar dirección
Preview: Vista previa
Email cannot be blank: Debes introducir un email
Choose a sample: Selecciona una plantilla
Choose a company: Selecciona una empresa
Choose a date: Selecciona una fecha
</i18n>

View File

@ -0,0 +1,34 @@
<script setup>
import { ref } from 'vue';
import { useDialogPluginComponent } from 'quasar';
const { dialogRef } = useDialogPluginComponent();
const closeButton = ref(null);
const $props = defineProps({
htmlContent: {
type: String,
required: true,
},
});
</script>
<template>
<QDialog ref="dialogRef" full-width>
<QCard class="dialog-width q-pa-lg">
<QCardSection>
<span ref="closeButton" class="row justify-end close-icon" v-close-popup>
<QIcon name="close" size="sm" />
</span>
<div v-html="$props.htmlContent" />
</QCardSection>
</QCard>
</QDialog>
</template>
<style lang="scss" scoped>
.dialog-width {
max-width: 900px !important;
}
</style>

View File

@ -0,0 +1,247 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { QBtn, date } from 'quasar';
import { toCurrency } from 'src/filters';
import FetchData from 'components/FetchData.vue';
import CustomerSummaryTableActions from './CustomerSummaryTableActions.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import RouteDescriptorProxy from 'src/pages/Route/Card/RouteDescriptorProxy.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const rows = ref([]);
const filter = {
include: [
{
relation: 'ticketState',
scope: {
fields: ['stateFk', 'code', 'alertLevel'],
include: { relation: 'state' },
},
},
{ relation: 'invoiceOut', scope: { fields: ['id'] } },
{ relation: 'agencyMode', scope: { fields: ['name'] } },
],
where: { clientFk: route.params.id },
order: ['shipped DESC', 'id'],
limit: 10,
};
const tableColumnComponents = {
id: {
component: 'span',
props: () => {},
event: () => {},
},
nickname: {
component: QBtn,
props: () => ({ flat: true, color: 'blue', noCaps: true }),
event: () => {},
},
agency: {
component: 'span',
props: () => {},
event: () => {},
},
route: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: () => {},
},
packages: {
component: 'span',
props: () => {},
event: () => {},
},
date: {
component: 'span',
props: () => {},
event: () => {},
},
state: {
component: 'span',
props: () => {},
event: () => {},
},
total: {
component: 'span',
props: () => {},
event: () => {},
},
actions: {
component: CustomerSummaryTableActions,
props: (prop) => ({
id: prop.row.id,
}),
event: () => {},
},
};
const columns = computed(() => [
{
align: 'left',
field: 'id',
label: t('Id'),
name: 'id',
},
{
align: 'left',
field: 'nickname',
label: t('Nickname'),
name: 'nickname',
},
{
align: 'left',
field: (row) => row?.agencyMode?.name,
label: t('Agency'),
name: 'agency',
},
{
align: 'left',
field: 'routeFk',
label: t('Route'),
name: 'route',
},
{
align: 'left',
field: 'packages',
label: t('Packages'),
name: 'packages',
},
{
align: 'left',
field: (row) => date.formatDate(row?.shipped, 'DD/MM/YYYY'),
label: t('Date'),
name: 'date',
},
{
align: 'left',
field: (row) => row?.ticketState?.state?.name,
label: t('State'),
name: 'state',
},
{
align: 'left',
field: 'totalWithVat',
label: t('Total'),
name: 'total',
},
{
align: 'left',
field: 'totalWithVat',
label: '',
name: 'actions',
},
]);
const setStateColor = (ticket) => {
const { ticketState } = ticket;
if (!ticketState) return;
const codeColorMap = { OK: 'success', FREE: 'notice' };
const alertLevelColorMap = { 0: 'alert', 1: 'warning' };
if (codeColorMap[ticketState.code]) return codeColorMap[ticketState.code];
return alertLevelColorMap[ticketState.alertLevel];
};
const setTotalPriceColor = (ticket) => {
const total = parseInt(ticket.totalWithVat);
if (total > 0 && total < 50) return 'warning';
};
const navigateToticketSummary = (id) => {
router.push({
name: 'TicketSummary',
params: { id },
});
};
</script>
<template>
<FetchData
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
url="Tickets"
/>
<QTable
:columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
v-if="rows?.length"
>
<template #body-cell="props">
<QTd :props="props" @click="navigateToticketSummary(props.row.id)">
<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)"
>
<template
v-if="
props.col.name === 'id' ||
props.col.name === 'nickname' ||
props.col.name === 'agency' ||
props.col.name === 'route' ||
props.col.name === 'packages'
"
>
{{ props.value }}
</template>
<template v-if="props.col.name === 'date'">
<QBadge class="q-pa-sm" color="warning">
{{ props.value }}
</QBadge>
</template>
<template v-if="props.col.name === 'state'">
<QBadge :color="setStateColor(props.row)" class="q-pa-sm">
{{ props.value }}
</QBadge>
</template>
<template v-if="props.col.name === 'total'">
<QBadge
:color="setTotalPriceColor(props.row)"
class="q-pa-sm"
v-if="setTotalPriceColor(props.row)"
>
{{ toCurrency(props.value) }}
</QBadge>
<div v-else>{{ toCurrency(props.value) }}</div>
</template>
<CustomerDescriptorProxy
:id="props.row.clientFk"
v-if="props.col.name === 'nickname'"
/>
<RouteDescriptorProxy
:id="props.row.routeFk"
v-if="props.col.name === 'route'"
/>
</component>
</QTr>
</QTd>
</template>
</QTable>
</template>
<i18n>
es:
Id: Id
Nickname: Alias
Agency: Agencia
Route: Ruta
Packages: Bultos
Date: Fecha
State: Estado
Total: Total
</i18n>

View File

@ -0,0 +1,44 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import TicketSummary from 'src/pages/Ticket/Card/TicketSummary.vue';
const { t } = useI18n();
defineProps({
id: {
type: Number,
required: true,
},
});
const { viewSummary } = useSummaryDialog();
</script>
<template>
<div>
<QIcon color="primary" name="vn:lines" size="sm">
<QTooltip>
{{ t('Go to lines') }}
</QTooltip>
</QIcon>
<QIcon
@click.stop="viewSummary(id, TicketSummary)"
class="q-ml-md"
color="primary"
name="preview"
size="sm"
>
<QTooltip>
{{ t('Preview') }}
</QTooltip>
</QIcon>
</div>
</template>
<i18n>
es:
Go to lines: Ir a lineas
Preview: Vista previa
</i18n>

View File

@ -0,0 +1,73 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { useDialogPluginComponent } from 'quasar';
const { t } = useI18n();
const { dialogRef } = useDialogPluginComponent();
const closeButton = ref(null);
const $props = defineProps({
id: {
type: Number,
required: true,
},
promise: {
type: Function,
required: true,
},
});
const isLoading = ref(false);
const saveData = async () => {
const timestamp = new Date().getTime();
const payload = {
finished: timestamp,
};
await axios.patch(`CreditClassifications/${$props.id}`, payload);
$props.promise();
closeButton.value.click();
};
</script>
<template>
<QDialog ref="dialogRef">
<QCard class="q-pa-sm">
<span
ref="closeButton"
class="flex justify-end cursor-pointer q-mb-sm"
v-close-popup
>
<QIcon name="close" size="sm" />
</span>
<QCardSection>
<div class="text-h6 q-mb-sm">
{{ t('Are you sure you want to close this contract?') }}
</div>
<div class="q-mb-sm">{{ t('Close contract') }}</div>
</QCardSection>
<QCardActions align="right">
<QBtn :label="t('globals.cancel')" color="primary" flat v-close-popup />
<QBtn
:label="t('globals.confirm')"
color="primary"
:loading="isLoading"
@click="saveData"
unelevated
/>
</QCardActions>
</QCard>
</QDialog>
</template>
<i18n>
es:
Are you sure you want to close this contract?: ¿Seguro que quieres cerrar este contrato?
Close contract: Cerrar contrato
</i18n>

View File

@ -2,6 +2,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRole } from 'src/composables/useRole';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
@ -15,6 +16,8 @@ import { toDate } from 'src/filters';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { hasAny } = useRole();
const isAdministrative = () => hasAny(['administrative']);
const suppliersOptions = ref([]); const suppliersOptions = ref([]);
const travelsOptions = ref([]); const travelsOptions = ref([]);
@ -206,6 +209,7 @@ const onFilterTravelSelected = (formData, id) => {
</div> </div>
<div class="col"> <div class="col">
<QCheckbox <QCheckbox
v-if="isAdministrative()"
v-model="data.isBooked" v-model="data.isBooked"
:label="t('entry.basicData.booked')" :label="t('entry.basicData.booked')"
/> />

View File

@ -268,16 +268,11 @@ const importBuys = () => {
const toggleGroupingMode = async (buy, mode) => { const toggleGroupingMode = async (buy, mode) => {
try { try {
const grouping = 1; const groupingMode = mode === 'grouping' ? mode : 'packing';
const packing = 2; const newGroupingMode = buy.groupingMode === groupingMode ? null : groupingMode;
const groupingMode = mode === 'grouping' ? grouping : packing;
const newGroupingMode = buy.groupingMode === groupingMode ? 0 : groupingMode;
const params = { const params = {
groupingMode: newGroupingMode, groupingMode: newGroupingMode,
}; };
await axios.patch(`Buys/${buy.id}`, params); await axios.patch(`Buys/${buy.id}`, params);
buy.groupingMode = newGroupingMode; buy.groupingMode = newGroupingMode;
} catch (err) { } catch (err) {
@ -287,9 +282,9 @@ const toggleGroupingMode = async (buy, mode) => {
const lockIconType = (groupingMode, mode) => { const lockIconType = (groupingMode, mode) => {
if (mode === 'packing') { if (mode === 'packing') {
return groupingMode === 2 ? 'lock' : 'lock_open'; return groupingMode === 'packing' ? 'lock' : 'lock_open';
} else { } else {
return groupingMode === 1 ? 'lock' : 'lock_open'; return groupingMode === 'grouping' ? 'lock' : 'lock_open';
} }
}; };
</script> </script>

View File

@ -1,8 +1,8 @@
entryList: entryList:
list: list:
inventoryEntry: 'Inventory entry' inventoryEntry: Inventory entry
virtualEntry: 'Virtual entry' virtualEntry: Virtual entry
entryFilter: entryFilter:
filter: filter:
search: 'General search' search: General search
reference: 'Reference' reference: Reference

View File

@ -1,8 +1,8 @@
entryList: entryList:
list: list:
inventoryEntry: 'Es inventario' inventoryEntry: Es inventario
virtualEntry: 'Es una redada' virtualEntry: Es una redada
entryFilter: entryFilter:
filter: filter:
search: 'Búsqueda general' search: Búsqueda general
reference: 'Referencia' reference: Referencia

View File

@ -257,7 +257,7 @@ const requiredFieldRule = (val) => val || t('globals.requiredField');
const isAdministrative = () => hasAny(['administrative']); const isAdministrative = () => hasAny(['administrative']);
const isAgricultural = () => const isAgricultural = () =>
invoiceIn.value.supplier.sageWithholdingFk == config.value[0].sageWithholdingFk; invoiceIn.value?.supplier?.sageWithholdingFk === config.value[0]?.sageWithholdingFk;
function showPdfInvoice() { function showPdfInvoice() {
if (isAgricultural()) openReport(`InvoiceIns/${entityId.value}/invoice-in-pdf`); if (isAgricultural()) openReport(`InvoiceIns/${entityId.value}/invoice-in-pdf`);

View File

@ -1 +1,170 @@
<template>Item create view</template> <script setup>
import { reactive, ref, onBeforeMount } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } 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';
import VnInput from 'src/components/common/VnInput.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import axios from 'axios';
const { t } = useI18n();
const router = useRouter();
const newItemForm = reactive({});
const originsOptions = ref([]);
const tagsOptions = ref([]);
const validPriorities = ref([]);
const itemTypesOptions = ref([]);
const intrastatsOptions = ref([]);
const fetchDefaultPriorityTag = async () => {
const filter = {
fields: ['defaultPriority', 'defaultTag', 'validPriorities'],
limit: 1,
};
const { data } = await axios.get(`ItemConfigs`, { filter });
if (!data) return;
const dataRow = data[0];
validPriorities.value = [...dataRow.validPriorities];
newItemForm.priority = dataRow.defaultPriority;
newItemForm.tag = dataRow.defaultTag;
};
const redirectToItemBasicData = (_, { id }) => {
router.push({ name: 'ItemBasicData', params: { id } });
};
onBeforeMount(async () => {
await fetchDefaultPriorityTag();
});
</script>
<template>
<FetchData
url="Origins"
@on-fetch="(data) => (originsOptions = data)"
:filter="{ order: 'name' }"
auto-load
/>
<FetchData
url="Tags"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
@on-fetch="(data) => (tagsOptions = data)"
auto-load
/>
<FetchData
url="ItemTypes"
:filter="{
fields: ['id', 'code', 'categoryFk', 'name'],
include: 'category',
order: 'name ASC',
}"
@on-fetch="(data) => (itemTypesOptions = data)"
auto-load
/>
<FetchData
url="Intrastats"
:filter="{ fields: ['id', 'description'], order: 'description ASC' }"
@on-fetch="(data) => (intrastatsOptions = data)"
auto-load
/>
<QPage>
<VnSubToolbar />
<FormModel
url-create="Items/new"
model="item"
:form-initial-data="newItemForm"
observe-form-changes
@on-data-saved="redirectToItemBasicData"
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
v-model="data.provisionalName"
:label="t('item.create.name')"
/>
<VnSelectFilter
:label="t('item.create.tag')"
v-model="data.tag"
:options="tagsOptions"
option-value="id"
option-label="name"
hide-selected
/>
<VnSelectFilter
:label="t('item.create.priority')"
v-model="data.priority"
:options="validPriorities"
option-value="priority"
option-label="priority"
hide-selected
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
:label="t('item.create.type')"
v-model="data.typeFk"
:options="itemTypesOptions"
option-label="name"
option-value="id"
hide-selected
>
<template #option="scope">
<QItem class="column" v-bind="scope.itemProps">
<QItemLabel class="row">
<span style="width: 3em">
{{ scope.opt?.code }}
</span>
<span>
{{ scope.opt?.name }}
</span>
</QItemLabel>
<QItemLabel v-if="scope.opt?.category" caption>
{{ scope.opt?.category?.name }}
</QItemLabel>
</QItem>
</template>
</VnSelectFilter>
<VnSelectFilter
:label="t('item.create.intrastat')"
v-model="data.intrastatFk"
:options="intrastatsOptions"
option-label="description"
option-value="id"
hide-selected
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.description }}
</QItemLabel>
<QItemLabel caption>
#{{ scope.opt?.id }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
:label="t('item.create.origin')"
v-model="data.originFk"
:options="originsOptions"
option-value="id"
option-label="name"
hide-selected
/>
</VnRow>
</template>
</FormModel>
</QPage>
</template>

Some files were not shown because too many files have changed in this diff Show More