Compare commits

..

3 Commits

Author SHA1 Message Date
Jon Elias c0f19b3131 Merge branch 'master' into Hotfix-CustomerCredits
gitea/salix-front/pipeline/pr-master This commit looks good Details
2025-05-08 10:09:21 +00:00
Jon Elias 0691d5e966 Merge branch 'master' into Hotfix-CustomerCredits
gitea/salix-front/pipeline/pr-master This commit looks good Details
2025-05-07 11:09:49 +00:00
Jon Elias 9ee69274ae fix: make 0 become valid in VnInputNumber when using required from validator
gitea/salix-front/pipeline/pr-master This commit is unstable Details
2025-05-07 12:28:05 +02:00
328 changed files with 2335 additions and 9717 deletions

View File

@ -26,7 +26,7 @@ if (branchName) {
const splitedMsg = msg.split(':'); const splitedMsg = msg.split(':');
if (splitedMsg.length > 1) { if (splitedMsg.length > 1) {
const finalMsg = `${splitedMsg[0]}: ${referenceTag}${splitedMsg.slice(1).join(':')}`; const finalMsg = splitedMsg[0] + ': ' + referenceTag + splitedMsg.slice(1).join(':');
writeFileSync(msgPath, finalMsg); writeFileSync(msgPath, finalMsg);
} }
} }

View File

@ -1,9 +1,6 @@
import { defineConfig } from 'cypress'; import { defineConfig } from 'cypress';
let urlHost; let urlHost, reporter, reporterOptions, timeouts;
let reporter;
let reporterOptions;
let timeouts;
if (process.env.CI) { if (process.env.CI) {
urlHost = 'front'; urlHost = 'front';
@ -47,7 +44,6 @@ export default defineConfig({
supportFile: 'test/cypress/support/index.js', supportFile: 'test/cypress/support/index.js',
videosFolder: 'test/cypress/videos', videosFolder: 'test/cypress/videos',
downloadsFolder: 'test/cypress/downloads', downloadsFolder: 'test/cypress/downloads',
tmpUploadFolder: 'test/cypress/storage/tmp/dms',
video: false, video: false,
specPattern: 'test/cypress/integration/**/*.spec.js', specPattern: 'test/cypress/integration/**/*.spec.js',
experimentalRunAllSpecs: true, experimentalRunAllSpecs: true,
@ -64,6 +60,5 @@ export default defineConfig({
...timeouts, ...timeouts,
includeShadowDom: true, includeShadowDom: true,
waitForAnimations: true, waitForAnimations: true,
testIsolation: false,
}, },
}); });

View File

@ -1,4 +1,4 @@
<!doctype html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title><%= productName %></title> <title><%= productName %></title>
@ -12,12 +12,7 @@
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>"
/> />
<link <link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png" />
rel="icon"
type="image/png"
sizes="128x128"
href="icons/favicon-128x128.png"
/>
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png" /> <link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png" />
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png" />

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "25.18.0", "version": "25.16.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",

View File

@ -1,3 +1,4 @@
/* eslint-disable */
// https://github.com/michael-ciniawsky/postcss-load-config // https://github.com/michael-ciniawsky/postcss-load-config
import autoprefixer from 'autoprefixer'; import autoprefixer from 'autoprefixer';

View File

@ -13,7 +13,7 @@ import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
import path from 'path'; import path from 'path';
const target = `http://${process.env.CI ? 'back' : 'localhost'}:3000`; const target = `http://${process.env.CI ? 'back' : 'localhost'}:3000`;
export default configure((/* ctx */) => { export default configure(function (/* ctx */) {
return { return {
eslint: { eslint: {
// fix: true, // fix: true,

View File

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

View File

@ -1,5 +1,5 @@
{ {
"unit-vitest": { "unit-vitest": {
"runnerCommand": "vitest run" "runnerCommand": "vitest run"
} }
} }

View File

@ -2,7 +2,6 @@
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { useQuasar, Dark } from 'quasar'; import { useQuasar, Dark } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnScroll from './components/common/VnScroll.vue';
const quasar = useQuasar(); const quasar = useQuasar();
const { availableLocales, locale, fallbackLocale } = useI18n(); const { availableLocales, locale, fallbackLocale } = useI18n();
@ -39,7 +38,6 @@ quasar.iconMapFn = (iconName) => {
<template> <template>
<RouterView /> <RouterView />
<VnScroll />
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@ -67,7 +67,7 @@ describe('Axios boot', () => {
}; };
const result = onResponseError(error); const result = onResponseError(error);
await expect(result).rejects.toEqual(expect.objectContaining(error)); expect(result).rejects.toEqual(expect.objectContaining(error));
}); });
it('should call to the Notify plugin with a message from the response property', async () => { it('should call to the Notify plugin with a message from the response property', async () => {
@ -83,7 +83,7 @@ describe('Axios boot', () => {
}; };
const result = onResponseError(error); const result = onResponseError(error);
await expect(result).rejects.toEqual(expect.objectContaining(error)); expect(result).rejects.toEqual(expect.objectContaining(error));
}); });
}); });
}); });

View File

@ -11,8 +11,8 @@ export default function (component, key, value) {
}; };
break; break;
case 'undefined': case 'undefined':
throw new Error(`unknown prop: ${key}`); throw new Error('unknown prop: ' + key);
default: default:
throw new Error(`unhandled type: ${typeof prop}`); throw new Error('unhandled type: ' + typeof prop);
} }
} }

View File

@ -9,7 +9,7 @@ export default {
const keyBindingMap = routes const keyBindingMap = routes
.filter((route) => route.meta.keyBinding) .filter((route) => route.meta.keyBinding)
.reduce((map, route) => { .reduce((map, route) => {
map[`Key${route.meta.keyBinding.toUpperCase()}`] = route.path; map['Key' + route.meta.keyBinding.toUpperCase()] = route.path;
return map; return map;
}, {}); }, {});

View File

@ -1,3 +1,4 @@
/* eslint-disable eslint/export */
export * from './defaults/qTable'; export * from './defaults/qTable';
export * from './defaults/qInput'; export * from './defaults/qInput';
export * from './defaults/qSelect'; export * from './defaults/qSelect';

View File

@ -7,7 +7,7 @@ import { QLayout } from 'quasar';
import mainShortcutMixin from './mainShortcutMixin'; import mainShortcutMixin from './mainShortcutMixin';
import { useCau } from 'src/composables/useCau'; import { useCau } from 'src/composables/useCau';
export default boot(({ app, router }) => { export default boot(({ app }) => {
QForm.mixins = [qFormMixin]; QForm.mixins = [qFormMixin];
QLayout.mixins = [mainShortcutMixin]; QLayout.mixins = [mainShortcutMixin];
@ -22,19 +22,14 @@ export default boot(({ app, router }) => {
} }
switch (response?.status) { switch (response?.status) {
case 401:
if (!router.currentRoute.value.name.toLowerCase().includes('login')) {
message = 'errors.sessionExpired';
} else message = 'login.loginError';
break;
case 403:
if (!message || message.toLowerCase() === 'access denied')
message = 'errors.accessDenied';
case 422: case 422:
if (error.name == 'ValidationError') if (error.name == 'ValidationError')
message += ` "${responseError.details.context}.${Object.keys( message +=
responseError.details.codes, ' "' +
).join(',')}"`; responseError.details.context +
'.' +
Object.keys(responseError.details.codes).join(',') +
'"';
break; break;
case 500: case 500:
message = 'errors.statusInternalServerError'; message = 'errors.statusInternalServerError';

View File

@ -20,6 +20,7 @@ const postcodeFormData = reactive({
provinceFk: null, provinceFk: null,
townFk: null, townFk: null,
}); });
const townFilter = ref({});
const countriesRef = ref(false); const countriesRef = ref(false);
const provincesOptions = ref([]); const provincesOptions = ref([]);
@ -32,11 +33,11 @@ function onDataSaved(formData) {
newPostcode.town = town.value.name; newPostcode.town = town.value.name;
newPostcode.townFk = town.value.id; newPostcode.townFk = town.value.id;
const provinceObject = provincesOptions.value.find( const provinceObject = provincesOptions.value.find(
({ id }) => id === formData.provinceFk, ({ id }) => id === formData.provinceFk
); );
newPostcode.province = provinceObject?.name; newPostcode.province = provinceObject?.name;
const countryObject = countriesRef.value.opts.find( const countryObject = countriesRef.value.opts.find(
({ id }) => id === formData.countryFk, ({ id }) => id === formData.countryFk
); );
newPostcode.country = countryObject?.name; newPostcode.country = countryObject?.name;
emit('onDataSaved', newPostcode); emit('onDataSaved', newPostcode);
@ -66,11 +67,21 @@ function setTown(newTown, data) {
} }
async function onCityCreated(newTown, formData) { async function onCityCreated(newTown, formData) {
newTown.province = provincesOptions.value.find( newTown.province = provincesOptions.value.find(
(province) => province.id === newTown.provinceFk, (province) => province.id === newTown.provinceFk
); );
formData.townFk = newTown; formData.townFk = newTown;
setTown(newTown, formData); setTown(newTown, formData);
} }
async function filterTowns(name) {
if (name !== '') {
townFilter.value.where = {
name: {
like: `%${name}%`,
},
};
}
}
</script> </script>
<template> <template>
@ -96,6 +107,7 @@ async function onCityCreated(newTown, formData) {
<VnSelectDialog <VnSelectDialog
:label="t('City')" :label="t('City')"
@update:model-value="(value) => setTown(value, data)" @update:model-value="(value) => setTown(value, data)"
@filter="filterTowns"
:tooltip="t('Create city')" :tooltip="t('Create city')"
v-model="data.townFk" v-model="data.townFk"
url="Towns/location" url="Towns/location"

View File

@ -25,7 +25,7 @@ const autonomiesRef = ref([]);
const onDataSaved = (dataSaved, requestResponse) => { const onDataSaved = (dataSaved, requestResponse) => {
requestResponse.autonomy = autonomiesRef.value.opts.find( requestResponse.autonomy = autonomiesRef.value.opts.find(
(autonomy) => autonomy.id == requestResponse.autonomyFk, (autonomy) => autonomy.id == requestResponse.autonomyFk
); );
emit('onDataSaved', dataSaved, requestResponse); emit('onDataSaved', dataSaved, requestResponse);
}; };

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { computed, ref, useAttrs, watch, nextTick } from 'vue'; import { computed, ref, useAttrs, watch } from 'vue';
import { useRouter, onBeforeRouteLeave } from 'vue-router'; import { useRouter, onBeforeRouteLeave } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
@ -42,15 +42,7 @@ const $props = defineProps({
}, },
dataRequired: { dataRequired: {
type: Object, type: Object,
default: () => ({}), default: () => {},
},
dataDefault: {
type: Object,
default: () => ({}),
},
insertOnLoad: {
type: Boolean,
default: false,
}, },
defaultSave: { defaultSave: {
type: Boolean, type: Boolean,
@ -73,7 +65,7 @@ const $props = defineProps({
default: null, default: null,
}, },
beforeSaveFn: { beforeSaveFn: {
type: [String, Function], type: Function,
default: null, default: null,
}, },
goTo: { goTo: {
@ -95,7 +87,6 @@ const formData = ref();
const saveButtonRef = ref(null); const saveButtonRef = ref(null);
const watchChanges = ref(); const watchChanges = ref();
const formUrl = computed(() => $props.url); const formUrl = computed(() => $props.url);
const rowsContainer = ref(null);
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']); const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
@ -131,11 +122,9 @@ async function fetch(data) {
const rows = keyData ? data[keyData] : data; const rows = keyData ? data[keyData] : data;
resetData(rows); resetData(rows);
emit('onFetch', rows); emit('onFetch', rows);
$props.insertOnLoad && await insert();
return rows; return rows;
} }
function resetData(data) { function resetData(data) {
if (!data) return; if (!data) return;
if (data && Array.isArray(data)) { if (data && Array.isArray(data)) {
@ -146,16 +135,9 @@ function resetData(data) {
formData.value = JSON.parse(JSON.stringify(data)); formData.value = JSON.parse(JSON.stringify(data));
if (watchChanges.value) watchChanges.value(); //destroy watcher if (watchChanges.value) watchChanges.value(); //destroy watcher
watchChanges.value = watch(formData, (nVal) => { watchChanges.value = watch(formData, () => (hasChanges.value = true), { deep: true });
hasChanges.value = false;
const filteredNewData = nVal.filter(row => !isRowEmpty(row) || row[$props.primaryKey]);
const filteredOriginal = originalData.value.filter(row => row[$props.primaryKey]);
const changes = getDifferences(filteredOriginal, filteredNewData);
hasChanges.value = !isEmpty(changes);
}, { deep: true });
} }
async function reset() { async function reset() {
await fetch(originalData.value); await fetch(originalData.value);
hasChanges.value = false; hasChanges.value = false;
@ -183,9 +165,7 @@ async function onSubmit() {
}); });
} }
isLoading.value = true; isLoading.value = true;
await saveChanges($props.saveFn ? formData.value : null); await saveChanges($props.saveFn ? formData.value : null);
} }
async function onSubmitAndGo() { async function onSubmitAndGo() {
@ -194,10 +174,6 @@ async function onSubmitAndGo() {
} }
async function saveChanges(data) { async function saveChanges(data) {
formData.value = formData.value.filter(row =>
row[$props.primaryKey] || !isRowEmpty(row)
);
if ($props.saveFn) { if ($props.saveFn) {
$props.saveFn(data, getChanges); $props.saveFn(data, getChanges);
isLoading.value = false; isLoading.value = false;
@ -227,32 +203,14 @@ async function saveChanges(data) {
}); });
} }
async function insert(pushData = { ...$props.dataRequired, ...$props.dataDefault }) { async function insert(pushData = $props.dataRequired) {
formData.value = formData.value.filter(row => !isRowEmpty(row)); const $index = formData.value.length
? formData.value[formData.value.length - 1].$index + 1
const lastRow = formData.value.at(-1); : 0;
const isLastRowEmpty = lastRow ? isRowEmpty(lastRow) : false; formData.value.push(Object.assign({ $index }, pushData));
hasChanges.value = true;
if (formData.value.length && isLastRowEmpty) return;
const $index = formData.value.length ? formData.value.at(-1).$index + 1 : 0;
const nRow = Object.assign({ $index }, pushData);
formData.value.push(nRow);
const hasChange = Object.keys(nRow).some(key => !isChange(nRow, key));
if (hasChange) hasChanges.value = true;
} }
function isRowEmpty(row) {
return Object.keys(row).every(key => isChange(row, key));
}
function isChange(row,key){
return !row[key] || key == '$index' || Object.hasOwn($props.dataRequired || {}, key);
}
async function remove(data) { async function remove(data) {
if (!data.length) if (!data.length)
return quasar.notify({ return quasar.notify({
@ -269,8 +227,10 @@ async function remove(data) {
newData = newData.filter( newData = newData.filter(
(form) => !preRemove.some((index) => index == form.$index), (form) => !preRemove.some((index) => index == form.$index),
); );
formData.value = newData; const changes = getChanges();
hasChanges.value = JSON.stringify(removeIndexField(formData.value)) !== JSON.stringify(removeIndexField(originalData.value)); if (!changes.creates?.length && !changes.updates?.length)
hasChanges.value = false;
fetch(newData);
} }
if (ids.length) { if (ids.length) {
quasar quasar
@ -288,8 +248,9 @@ async function remove(data) {
newData = newData.filter((form) => !ids.some((id) => id == form[pk])); newData = newData.filter((form) => !ids.some((id) => id == form[pk]));
fetch(newData); fetch(newData);
}); });
} else {
reset();
} }
emit('update:selected', []); emit('update:selected', []);
} }
@ -300,7 +261,7 @@ function getChanges() {
const pk = $props.primaryKey; const pk = $props.primaryKey;
for (const [i, row] of formData.value.entries()) { for (const [i, row] of formData.value.entries()) {
if (!row[pk]) { if (!row[pk]) {
creates.push(Object.assign(row, { ...$props.dataRequired })); creates.push(row);
} else if (originalData.value[i]) { } else if (originalData.value[i]) {
const data = getDifferences(originalData.value[i], row); const data = getDifferences(originalData.value[i], row);
if (!isEmpty(data)) { if (!isEmpty(data)) {
@ -326,33 +287,6 @@ function isEmpty(obj) {
return !Object.keys(obj).length; return !Object.keys(obj).length;
} }
function removeIndexField(data) {
if (Array.isArray(data)) {
return data.map(({ $index, ...rest }) => rest);
} else if (typeof data === 'object' && data !== null) {
const { $index, ...rest } = data;
return rest;
}
}
async function handleTab(event) {
event.preventDefault();
const { shiftKey, target } = event;
const focusableSelector = `tbody tr td:not(:first-child) :is(a, button, input, textarea, select, details):not([disabled])`;
const focusableElements = rowsContainer.value?.querySelectorAll(focusableSelector);
const currentIndex = Array.prototype.indexOf.call(focusableElements, target);
const index = shiftKey ? currentIndex - 1 : currentIndex + 1;
const isLast = target === focusableElements[focusableElements.length - 1];
const isFirst = currentIndex === 0;
if ((shiftKey && !isFirst) || (!shiftKey && !isLast))
focusableElements[index]?.focus();
else if (isLast) {
await insert();
await nextTick();
}
}
async function reload(params) { async function reload(params) {
const data = await vnPaginateRef.value.fetch(params); const data = await vnPaginateRef.value.fetch(params);
fetch(data); fetch(data);
@ -378,14 +312,12 @@ watch(formUrl, async () => {
v-bind="$attrs" v-bind="$attrs"
> >
<template #body v-if="formData"> <template #body v-if="formData">
<div ref="rowsContainer" @keydown.tab="handleTab"> <slot
<slot name="body"
name="body" :rows="formData"
:rows="formData" :validate="validate"
:validate="validate" :filter="filter"
:filter="filter" ></slot>
></slot>
</div>
</template> </template>
</VnPaginate> </VnPaginate>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubToolbar"> <Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubToolbar">
@ -415,16 +347,8 @@ watch(formUrl, async () => {
<QBtnDropdown <QBtnDropdown
v-if="$props.goTo && $props.defaultSave" v-if="$props.goTo && $props.defaultSave"
@click="onSubmitAndGo" @click="onSubmitAndGo"
:label=" :label="tMobile('globals.saveAndContinue')"
tMobile('globals.saveAndContinue') + :title="t('globals.saveAndContinue')"
' ' +
t('globals.' + $props.goTo.split('/').pop())
"
:title="
t('globals.saveAndContinue') +
' ' +
t('globals.' + $props.goTo.split('/').pop())
"
:disable="!hasChanges" :disable="!hasChanges"
color="primary" color="primary"
icon="save" icon="save"

View File

@ -1,154 +0,0 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { date } from 'quasar';
import QCalendarMonthWrapper from 'src/components/ui/QCalendarMonthWrapper.vue';
import { QCalendarMonth } from '@quasar/quasar-ui-qcalendar/src/index.js';
import '@quasar/quasar-ui-qcalendar/src/QCalendarVariables.scss';
import { useWeekdayStore } from 'src/stores/useWeekdayStore';
import useWeekdaysOrder from 'src/composables/getWeekdays';
const formatDate = (dateToFormat, format = 'YYYY-MM-DD') =>
date.formatDate(dateToFormat, format);
const props = defineProps({
year: {
type: Number,
required: true,
},
month: {
type: Number,
required: true,
},
monthDate: {
type: Object,
default: null,
},
daysMap: {
type: Object,
default: null,
},
});
const emit = defineEmits(['onDateSelected']);
const { locale } = useI18n();
const weekdayStore = useWeekdayStore();
const weekDays = useWeekdaysOrder();
const calendarRef = ref(null);
const today = ref(formatDate(Date.vnNew()));
const todayTimestamp = computed(() => {
const date = Date.vnNew();
date.setHours(0, 0, 0, 0);
return date.getTime();
});
const _monthDate = computed(() => formatDate(props.monthDate));
const calendarHeaderTitle = computed(() => {
return `${weekdayStore.getLocaleMonths[props.month - 1].locale} ${props.year}`;
});
const isToday = (timestamp) => {
const { year, month, day } = timestamp;
return todayTimestamp.value === new Date(year, month - 1, day).getTime();
};
const getEventByTimestamp = ({ year, month, day }) => {
const stamp = new Date(year, month - 1, day).getTime();
return props.daysMap?.[stamp] || null;
};
const handleDateClick = (timestamp) => {
const event = getEventByTimestamp(timestamp);
const { year, month, day } = timestamp;
const date = new Date(year, month - 1, day);
emit('onDateSelected', {
date,
isNewMode: !event,
event: event?.[0] || null,
});
};
const getEventAttrs = (timestamp) => {
return {
class: '--event',
label: timestamp.day,
};
};
defineExpose({ getEventByTimestamp, handleDateClick });
</script>
<template>
<QCalendarMonthWrapper
style="height: 290px; width: 290px"
transparent-background
view-customization="workerCalendar"
>
<template #header>
<span class="full-width text-center text-body1 q-py-sm">{{
calendarHeaderTitle
}}</span>
</template>
<template #calendar>
<QCalendarMonth
ref="calendarRef"
:model-value="_monthDate"
show-work-weeks
no-outside-days
no-active-date
:weekdays="weekDays"
short-weekday-label
:locale="locale"
:now="today"
@click-date="handleDateClick($event.scope.timestamp)"
mini-mode
>
<template #day="{ scope: { timestamp } }">
<slot
name="day"
:timestamp="timestamp"
:getEventAttrs="getEventAttrs"
>
<QBtn
v-if="getEventByTimestamp(timestamp)"
v-bind="{ ...getEventAttrs(timestamp) }"
@click="handleDateClick(timestamp)"
rounded
dense
flat
class="calendar-event"
:class="{ '--today': isToday(timestamp) }"
/>
</slot>
</template>
</QCalendarMonth>
</template>
</QCalendarMonthWrapper>
</template>
<style lang="scss">
.calendar-event {
display: flex;
justify-content: center;
width: 32px;
height: 32px;
font-size: 13px;
line-height: 1.715em;
cursor: pointer;
color: white;
&.--today {
border: 2px solid $info;
}
&.--event {
background-color: $positive;
color: black;
}
&:hover {
opacity: 0.8;
}
}
</style>

View File

@ -1,126 +0,0 @@
<script setup>
import { computed, onMounted, ref, onUnmounted, nextTick } from 'vue';
import { useStateStore } from 'stores/useStateStore';
import { useWeekdayStore } from 'src/stores/useWeekdayStore';
import { useArrayData } from 'src/composables/useArrayData';
const props = defineProps({
dataKey: {
type: String,
required: true,
},
calendarComponent: {
type: Object,
required: true,
},
additionalProps: {
type: Object,
default: () => ({}),
},
});
const stateStore = useStateStore();
const weekdayStore = useWeekdayStore();
const nMonths = ref(4);
const _date = ref(Date.vnNew());
const firstDay = ref(Date.vnNew());
const lastDay = ref(Date.vnNew());
const months = ref([]);
const arrayData = useArrayData(props.dataKey);
onMounted(async () => {
const initialDate = Date.vnNew();
initialDate.setDate(1);
initialDate.setHours(0, 0, 0, 0);
date.value = initialDate;
await nextTick();
stateStore.rightDrawer = true;
});
onUnmounted(() => arrayData.destroy());
const emit = defineEmits([
'update:firstDay',
'update:lastDay',
'update:events',
'onDateSelected',
]);
const date = computed({
get: () => _date.value,
set: (value) => {
if (!(value instanceof Date)) return;
_date.value = value;
const stamp = value.getTime();
firstDay.value = new Date(stamp);
firstDay.value.setDate(1);
lastDay.value = new Date(stamp);
lastDay.value.setMonth(lastDay.value.getMonth() + nMonths.value);
lastDay.value.setDate(0);
months.value = [];
for (let i = 0; i < nMonths.value; i++) {
const monthDate = new Date(stamp);
monthDate.setMonth(value.getMonth() + i);
months.value.push(monthDate);
}
emit('update:firstDay', firstDay.value);
emit('update:lastDay', lastDay.value);
emit('refresh-events');
},
});
const headerTitle = computed(() => {
if (!months.value?.length) return '';
const getMonthName = (date) =>
`${weekdayStore.getLocaleMonths[date.getMonth()].locale} ${date.getFullYear()}`;
return `${getMonthName(months.value[0])} - ${getMonthName(months.value[months.value.length - 1])}`;
});
const step = (direction) => {
const newDate = new Date(date.value);
newDate.setMonth(newDate.getMonth() + nMonths.value * direction);
date.value = newDate;
};
defineExpose({
firstDay,
lastDay,
});
</script>
<template>
<QCard style="height: max-content">
<div class="calendars-header">
<QBtn
icon="arrow_left"
size="sm"
flat
class="full-height"
@click="step(-1)"
/>
<span>{{ headerTitle }}</span>
<QBtn
icon="arrow_right"
size="sm"
flat
class="full-height"
@click="step(1)"
/>
</div>
<div class="calendars-container">
<component
:is="calendarComponent"
v-for="(month, index) in months"
:key="index"
:month="month.getMonth() + 1"
:year="month.getFullYear()"
:month-date="month"
v-bind="additionalProps"
@on-date-selected="(data) => emit('onDateSelected', data)"
/>
</div>
</QCard>
</template>

View File

@ -156,9 +156,6 @@ const selectTravel = ({ id }) => {
option-label="name" option-label="name"
option-value="id" option-value="id"
v-model="travelFilterParams.warehouseOutFk" v-model="travelFilterParams.warehouseOutFk"
:where="{
isOrigin: true,
}"
/> />
<VnSelect <VnSelect
:label="t('globals.warehouseIn')" :label="t('globals.warehouseIn')"
@ -167,9 +164,6 @@ const selectTravel = ({ id }) => {
option-label="name" option-label="name"
option-value="id" option-value="id"
v-model="travelFilterParams.warehouseInFk" v-model="travelFilterParams.warehouseInFk"
:where="{
isDestiny: true,
}"
/> />
<VnInputDate <VnInputDate
:label="t('globals.shipped')" :label="t('globals.shipped')"

View File

@ -22,6 +22,7 @@ const { validate, validations } = useValidator();
const { notify } = useNotify(); const { notify } = useNotify();
const route = useRoute(); const route = useRoute();
const myForm = ref(null); const myForm = ref(null);
const attrs = useAttrs();
const $props = defineProps({ const $props = defineProps({
url: { url: {
type: String, type: String,
@ -98,12 +99,8 @@ const $props = defineProps({
type: Function, type: Function,
default: () => {}, default: () => {},
}, },
preventSubmit: {
type: Boolean,
default: false,
},
}); });
const emit = defineEmits(['onFetch', 'onDataSaved', 'submit']); const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed( const modelValue = computed(
() => $props.model ?? `formModel_${route?.meta?.title ?? route.name}`, () => $props.model ?? `formModel_${route?.meta?.title ?? route.name}`,
).value; ).value;
@ -304,7 +301,7 @@ function onBeforeSave(formData, originalData) {
); );
} }
async function onKeyup(evt) { async function onKeyup(evt) {
if (evt.key === 'Enter' && !$props.preventSubmit) { if (evt.key === 'Enter' && !('prevent-submit' in attrs)) {
const input = evt.target; const input = evt.target;
if (input.type == 'textarea' && evt.shiftKey) { if (input.type == 'textarea' && evt.shiftKey) {
let { selectionStart, selectionEnd } = input; let { selectionStart, selectionEnd } = input;
@ -333,7 +330,6 @@ defineExpose({
<template> <template>
<div class="column items-center full-width"> <div class="column items-center full-width">
<QForm <QForm
v-on="$attrs"
ref="myForm" ref="myForm"
v-if="formData" v-if="formData"
@submit.prevent="save" @submit.prevent="save"
@ -380,16 +376,8 @@ defineExpose({
data-cy="saveAndContinueDefaultBtn" data-cy="saveAndContinueDefaultBtn"
v-if="$props.goTo" v-if="$props.goTo"
@click="saveAndGo" @click="saveAndGo"
:label=" :label="tMobile('globals.saveAndContinue')"
tMobile('globals.saveAndContinue') + :title="t('globals.saveAndContinue')"
' ' +
t('globals.' + $props.goTo.split('/').pop())
"
:title="
t('globals.saveAndContinue') +
' ' +
t('globals.' + $props.goTo.split('/').pop())
"
:disable="!hasChanges" :disable="!hasChanges"
color="primary" color="primary"
icon="save" icon="save"
@ -418,7 +406,6 @@ defineExpose({
</QBtnDropdown> </QBtnDropdown>
<QBtn <QBtn
v-else v-else
data-cy="saveDefaultBtn"
:label="tMobile('globals.save')" :label="tMobile('globals.save')"
color="primary" color="primary"
icon="save" icon="save"

View File

@ -181,7 +181,7 @@ const searchModule = () => {
<template> <template>
<QList padding class="column-max-width"> <QList padding class="column-max-width">
<template v-if="$props.source === 'main'"> <template v-if="$props.source === 'main'">
<template v-if="route?.matched[1]?.name === 'Dashboard'"> <template v-if="$route?.matched[1]?.name === 'Dashboard'">
<QItem class="q-pb-md"> <QItem class="q-pb-md">
<VnInput <VnInput
v-model="search" v-model="search"
@ -262,7 +262,7 @@ const searchModule = () => {
</template> </template>
<template v-for="item in items" :key="item.name"> <template v-for="item in items" :key="item.name">
<template v-if="item.name === route?.matched[1]?.name"> <template v-if="item.name === $route?.matched[1]?.name">
<QItem class="header"> <QItem class="header">
<QItemSection avatar v-if="item.icon"> <QItemSection avatar v-if="item.icon">
<QIcon :name="item.icon" /> <QIcon :name="item.icon" />

View File

@ -69,7 +69,7 @@ const refresh = () => window.location.reload();
'no-visible': !stateQuery.isLoading().value, 'no-visible': !stateQuery.isLoading().value,
}" }"
size="sm" size="sm"
data-cy="navBar-spinner" data-cy="loading-spinner"
/> />
<QSpace /> <QSpace />
<div id="searchbar" class="searchbar"></div> <div id="searchbar" class="searchbar"></div>

View File

@ -26,7 +26,7 @@ async function redirect() {
if (route?.params?.id) if (route?.params?.id)
return (window.location.href = await getUrl( return (window.location.href = await getUrl(
`${section}/${route.params.id}/summary`, `${section}/${route.params.id}/summary`
)); ));
return (window.location.href = await getUrl(section + '/index')); return (window.location.href = await getUrl(section + '/index'));
} }

View File

@ -55,7 +55,7 @@ const refund = async () => {
(data) => ( (data) => (
(rectificativeTypeOptions = data), (rectificativeTypeOptions = data),
(invoiceParams.cplusRectificationTypeFk = data.filter( (invoiceParams.cplusRectificationTypeFk = data.filter(
(type) => type.description == 'I Por diferencias', (type) => type.description == 'I Por diferencias'
)[0].id) )[0].id)
) )
" "
@ -68,7 +68,7 @@ const refund = async () => {
(data) => ( (data) => (
(siiTypeInvoiceOutsOptions = data), (siiTypeInvoiceOutsOptions = data),
(invoiceParams.siiTypeInvoiceOutFk = data.filter( (invoiceParams.siiTypeInvoiceOutFk = data.filter(
(type) => type.code == 'R4', (type) => type.code == 'R4'
)[0].id) )[0].id)
) )
" "

View File

@ -40,9 +40,6 @@ const onDataSaved = (data) => {
url="Warehouses" url="Warehouses"
@on-fetch="(data) => (warehousesOptions = data)" @on-fetch="(data) => (warehousesOptions = data)"
auto-load auto-load
:where="{
isInventory: true,
}"
/> />
<FormModelPopup <FormModelPopup
url-create="Items/regularize" url-create="Items/regularize"

View File

@ -52,7 +52,7 @@ watch(
} else filter.value.where = {}; } else filter.value.where = {};
await provincesFetchDataRef.value.fetch({}); await provincesFetchDataRef.value.fetch({});
emit('onProvinceFetched', provincesOptions.value); emit('onProvinceFetched', provincesOptions.value);
}, }
); );
</script> </script>

View File

@ -180,11 +180,7 @@ const col = computed(() => {
) )
newColumn.component = 'checkbox'; newColumn.component = 'checkbox';
if ($props.default && !newColumn.component) newColumn.component = $props.default; if ($props.default && !newColumn.component) newColumn.component = $props.default;
if (typeof newColumn.component !== 'string') {
newColumn.attrs = { ...newColumn.component.attrs, autofocus: $props.autofocus };
newColumn.event = { ...newColumn.component.event, ...$props?.eventHandlers };
}
return newColumn; return newColumn;
}); });

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { markRaw, computed, onBeforeMount } from 'vue'; import { markRaw, computed } from 'vue';
import { QToggle } from 'quasar'; import { QCheckbox, QToggle } from 'quasar';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'components/common/VnInput.vue'; import VnInput from 'components/common/VnInput.vue';
@ -150,16 +150,6 @@ const showFilter = computed(
const onTabPressed = async () => { const onTabPressed = async () => {
if (model.value) enterEvent['keyup.enter'](); if (model.value) enterEvent['keyup.enter']();
}; };
onBeforeMount(() => {
const columnFilter = $props.column?.columnFilter;
const component = columnFilter?.component;
const defaultComponent = components[component];
const events = { update: updateEvent, enter: enterEvent };
if (!columnFilter || defaultComponent) return;
$props.column.columnFilter.event = events[columnFilter.event];
});
</script> </script>
<template> <template>
<div v-if="showFilter" class="full-width" style="overflow: hidden"> <div v-if="showFilter" class="full-width" style="overflow: hidden">

View File

@ -16,7 +16,7 @@ const $props = defineProps({
required: true, required: true,
}, },
searchUrl: { searchUrl: {
type: [String, Boolean], type: String,
default: 'table', default: 'table',
}, },
vertical: { vertical: {

View File

@ -33,9 +33,6 @@ import VnTableOrder from 'src/components/VnTable/VnOrder.vue';
import VnTableFilter from './VnTableFilter.vue'; import VnTableFilter from './VnTableFilter.vue';
import { getColAlign } from 'src/composables/getColAlign'; import { getColAlign } from 'src/composables/getColAlign';
import RightMenu from '../common/RightMenu.vue'; import RightMenu from '../common/RightMenu.vue';
import VnScroll from '../common/VnScroll.vue';
import VnCheckboxMenu from '../common/VnCheckboxMenu.vue';
import VnCheckbox from '../common/VnCheckbox.vue';
const arrayData = useArrayData(useAttrs()['data-key']); const arrayData = useArrayData(useAttrs()['data-key']);
const $props = defineProps({ const $props = defineProps({
@ -68,7 +65,7 @@ const $props = defineProps({
default: null, default: null,
}, },
create: { create: {
type: [Boolean, Object], type: Object,
default: null, default: null,
}, },
createAsDialog: { createAsDialog: {
@ -115,10 +112,6 @@ const $props = defineProps({
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
multiCheck: {
type: Object,
default: () => ({}),
},
crudModel: { crudModel: {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
@ -163,7 +156,6 @@ const CARD_MODE = 'card';
const TABLE_MODE = 'table'; const TABLE_MODE = 'table';
const mode = ref(CARD_MODE); const mode = ref(CARD_MODE);
const selected = ref([]); const selected = ref([]);
const selectAll = ref(false);
const hasParams = ref(false); const hasParams = ref(false);
const CrudModelRef = ref({}); const CrudModelRef = ref({});
const showForm = ref(false); const showForm = ref(false);
@ -176,7 +168,6 @@ const params = ref(useFilterParams($attrs['data-key']).params);
const orders = ref(useFilterParams($attrs['data-key']).orders); const orders = ref(useFilterParams($attrs['data-key']).orders);
const app = inject('app'); const app = inject('app');
const tableHeight = useTableHeight(); const tableHeight = useTableHeight();
const vnScrollRef = ref(null);
const editingRow = ref(null); const editingRow = ref(null);
const editingField = ref(null); const editingField = ref(null);
@ -198,17 +189,6 @@ const tableModes = [
}, },
]; ];
const onVirtualScroll = ({ to }) => {
handleScroll();
const virtualScrollContainer = tableRef.value?.$el?.querySelector('.q-table__middle');
if (virtualScrollContainer) {
virtualScrollContainer.dispatchEvent(new CustomEvent('scroll'));
if (vnScrollRef.value) {
vnScrollRef.value.updateScrollContainer(virtualScrollContainer);
}
}
};
onBeforeMount(() => { onBeforeMount(() => {
const urlParams = route.query[$props.searchUrl]; const urlParams = route.query[$props.searchUrl];
hasParams.value = urlParams && Object.keys(urlParams).length !== 0; hasParams.value = urlParams && Object.keys(urlParams).length !== 0;
@ -333,7 +313,6 @@ function stopEventPropagation(event) {
function reload(params) { function reload(params) {
selected.value = []; selected.value = [];
selectAll.value = false;
CrudModelRef.value.reload(params); CrudModelRef.value.reload(params);
} }
@ -348,13 +327,16 @@ function handleOnDataSaved(_) {
if (_.onDataSaved) _.onDataSaved({ CrudModelRef: CrudModelRef.value }); if (_.onDataSaved) _.onDataSaved({ CrudModelRef: CrudModelRef.value });
else $props.create.onDataSaved(_); else $props.create.onDataSaved(_);
} }
function handleScroll() { function handleScroll() {
if ($props.crudModel.disableInfiniteScroll) return; if ($props.crudModel.disableInfiniteScroll) return;
const tMiddle = tableRef.value.$el.querySelector('.q-table__middle'); const tMiddle = tableRef.value.$el.querySelector('.q-table__middle');
const { scrollHeight, scrollTop, clientHeight } = tMiddle; const { scrollHeight, scrollTop, clientHeight } = tMiddle;
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) <= 40; const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) <= 40;
if (isAtBottom) CrudModelRef.value.vnPaginateRef.paginate(); if (isAtBottom) CrudModelRef.value.vnPaginateRef.paginate();
} }
function handleSelection({ evt, added, rows: selectedRows }, rows) { function handleSelection({ evt, added, rows: selectedRows }, rows) {
if (evt?.shiftKey && added) { if (evt?.shiftKey && added) {
const rowIndex = selectedRows[0].$index; const rowIndex = selectedRows[0].$index;
@ -646,17 +628,6 @@ const rowCtrlClickFunction = computed(() => {
}; };
return () => {}; return () => {};
}); });
const handleHeaderSelection = (evt, data) => {
if (evt === 'updateSelected' && selectAll.value) {
selected.value = tableRef.value.rows;
} else if (evt === 'selectAll') {
selected.value = data;
} else {
selected.value = [];
}
emit('update:selected', selected.value);
};
</script> </script>
<template> <template>
<RightMenu v-if="$props.rightSearch" :overlay="overlay"> <RightMenu v-if="$props.rightSearch" :overlay="overlay">
@ -682,15 +653,7 @@ const handleHeaderSelection = (evt, data) => {
:class="$attrs['class'] ?? 'q-px-md'" :class="$attrs['class'] ?? 'q-px-md'"
:limit="$attrs['limit'] ?? 100" :limit="$attrs['limit'] ?? 100"
ref="CrudModelRef" ref="CrudModelRef"
@on-fetch=" @on-fetch="(...args) => emit('onFetch', ...args)"
(...args) => {
if ($props.multiCheck.expand) {
selectAll = false;
selected = [];
}
emit('onFetch', ...args);
}
"
:search-url="searchUrl" :search-url="searchUrl"
:disable-infinite-scroll="isTableMode" :disable-infinite-scroll="isTableMode"
:before-save-fn="removeTextValue" :before-save-fn="removeTextValue"
@ -720,33 +683,13 @@ const handleHeaderSelection = (evt, data) => {
flat flat
:style="isTableMode && `max-height: ${$props.tableHeight || tableHeight}`" :style="isTableMode && `max-height: ${$props.tableHeight || tableHeight}`"
:virtual-scroll="isTableMode" :virtual-scroll="isTableMode"
@virtual-scroll="onVirtualScroll" @virtual-scroll="handleScroll"
@row-click="(event, row) => handleRowClick(event, row)" @row-click="(event, row) => handleRowClick(event, row)"
@update:selected="emit('update:selected', $event)" @update:selected="emit('update:selected', $event)"
@selection="(details) => handleSelection(details, rows)" @selection="(details) => handleSelection(details, rows)"
:hide-selected-banner="true" :hide-selected-banner="true"
:data-cy :data-cy
> >
<template #header-selection>
<div class="flex items-center no-wrap" style="display: flex">
<VnCheckbox
v-model="selectAll"
@click="handleHeaderSelection('updateSelected', $event)"
/>
<VnCheckboxMenu
v-if="selectAll && $props.multiCheck.expand"
:searchUrl="searchUrl"
v-model="selectAll"
:url="$attrs['url']"
@update:selected="
handleHeaderSelection('updateSelected', $event)
"
@select:all="handleHeaderSelection('selectAll', $event)"
/>
</div>
</template>
<template #top-left v-if="!$props.withoutHeader"> <template #top-left v-if="!$props.withoutHeader">
<slot name="top-left"> </slot> <slot name="top-left"> </slot>
</template> </template>
@ -798,7 +741,6 @@ const handleHeaderSelection = (evt, data) => {
withFilters withFilters
" "
:column="col" :column="col"
:data-cy="`column-filter-${col.name}`"
:show-title="true" :show-title="true"
:data-key="$attrs['data-key']" :data-key="$attrs['data-key']"
v-model="params[columnName(col)]" v-model="params[columnName(col)]"
@ -1145,11 +1087,6 @@ const handleHeaderSelection = (evt, data) => {
</template> </template>
</FormModelPopup> </FormModelPopup>
</QDialog> </QDialog>
<VnScroll
ref="vnScrollRef"
v-if="isTableMode"
:scroll-target="tableRef?.$el?.querySelector('.q-table__middle')"
/>
</template> </template>
<i18n> <i18n>
en: en:

View File

@ -30,7 +30,6 @@ function columnName(col) {
v-bind="$attrs" v-bind="$attrs"
:search-button="true" :search-button="true"
:disable-submit-event="true" :disable-submit-event="true"
:data-key="$attrs['data-key']"
:search-url :search-url
> >
<template #body="{ params, orders, searchFn }"> <template #body="{ params, orders, searchFn }">

View File

@ -58,7 +58,7 @@ async function getConfig(url, filter) {
const response = await axios.get(url, { const response = await axios.get(url, {
params: { filter: filter }, params: { filter: filter },
}); });
return response?.data && response?.data?.length > 0 ? response.data[0] : null; return response.data && response.data.length > 0 ? response.data[0] : null;
} }
async function fetchViewConfigData() { async function fetchViewConfigData() {

View File

@ -11,9 +11,6 @@ describe('VnTable', () => {
propsData: { propsData: {
columns: [], columns: [],
}, },
attrs: {
'data-key': 'test',
},
}); });
vm = wrapper.vm; vm = wrapper.vm;

View File

@ -6,7 +6,7 @@ export default function (initialFooter, data) {
}); });
return acc; return acc;
}, },
{ ...initialFooter }, { ...initialFooter }
); );
return footer; return footer;
} }

View File

@ -11,7 +11,13 @@ describe('CrudModel', () => {
beforeAll(() => { beforeAll(() => {
wrapper = createWrapper(CrudModel, { wrapper = createWrapper(CrudModel, {
global: { global: {
stubs: ['vnPaginate', 'vue-i18n'], stubs: [
'vnPaginate',
'useState',
'arrayData',
'useStateStore',
'vue-i18n',
],
mocks: { mocks: {
validate: vi.fn(), validate: vi.fn(),
}, },
@ -23,7 +29,7 @@ describe('CrudModel', () => {
dataKey: 'crudModelKey', dataKey: 'crudModelKey',
model: 'crudModel', model: 'crudModel',
url: 'crudModelUrl', url: 'crudModelUrl',
saveFn: vi.fn(), saveFn: '',
}, },
}); });
wrapper = wrapper.wrapper; wrapper = wrapper.wrapper;
@ -193,11 +199,11 @@ describe('CrudModel', () => {
}); });
it('should set originalData and formatData with data and generate watchChanges', async () => { it('should set originalData and formatData with data and generate watchChanges', async () => {
data = [{ data = {
name: 'Tony', name: 'Tony',
lastName: 'Stark', lastName: 'Stark',
age: 42, age: 42,
}]; };
vm.resetData(data); vm.resetData(data);
@ -225,7 +231,7 @@ describe('CrudModel', () => {
expect(vm.isLoading).toBe(false); expect(vm.isLoading).toBe(false);
expect(vm.hasChanges).toBe(false); expect(vm.hasChanges).toBe(false);
await wrapper.setProps({ saveFn: null }); await wrapper.setProps({ saveFn: '' });
}); });
it("should use default url if there's not saveFn", async () => { it("should use default url if there's not saveFn", async () => {
@ -241,7 +247,7 @@ describe('CrudModel', () => {
await vm.saveChanges(data); await vm.saveChanges(data);
expect(postMock).toHaveBeenCalledWith(`${vm.url}/crud`, data); expect(postMock).toHaveBeenCalledWith(vm.url + '/crud', data);
expect(vm.isLoading).toBe(false); expect(vm.isLoading).toBe(false);
expect(vm.hasChanges).toBe(false); expect(vm.hasChanges).toBe(false);
expect(vm.originalData).toEqual(JSON.parse(JSON.stringify(vm.formData))); expect(vm.originalData).toEqual(JSON.parse(JSON.stringify(vm.formData)));

View File

@ -142,14 +142,14 @@ describe('getRoutes', () => {
const fn = (props) => getRoutes(props, getMethodA, getMethodB); const fn = (props) => getRoutes(props, getMethodA, getMethodB);
it('should call getMethodB when source is card', () => { it('should call getMethodB when source is card', () => {
const props = { source: 'methodB' }; let props = { source: 'methodB' };
fn(props); fn(props);
expect(getMethodB).toHaveBeenCalled(); expect(getMethodB).toHaveBeenCalled();
expect(getMethodA).not.toHaveBeenCalled(); expect(getMethodA).not.toHaveBeenCalled();
}); });
it('should call getMethodA when source is main', () => { it('should call getMethodA when source is main', () => {
const props = { source: 'methodA' }; let props = { source: 'methodA' };
fn(props); fn(props);
expect(getMethodA).toHaveBeenCalled(); expect(getMethodA).toHaveBeenCalled();
@ -157,7 +157,7 @@ describe('getRoutes', () => {
}); });
it('should call getMethodA when source is not exists or undefined', () => { it('should call getMethodA when source is not exists or undefined', () => {
const props = { source: 'methodC' }; let props = { source: 'methodC' };
expect(() => fn(props)).toThrowError('Method not defined'); expect(() => fn(props)).toThrowError('Method not defined');
expect(getMethodA).not.toHaveBeenCalled(); expect(getMethodA).not.toHaveBeenCalled();
@ -170,7 +170,7 @@ describe('LeftMenu as card', () => {
vm = mount('card').vm; vm = mount('card').vm;
}); });
it('should get routes for card source', () => { it('should get routes for card source', async () => {
vm.getRoutes(); vm.getRoutes();
}); });
}); });
@ -251,6 +251,7 @@ describe('LeftMenu as main', () => {
}); });
it('should get routes for main source', () => { it('should get routes for main source', () => {
vm.props.source = 'main';
vm.getRoutes(); vm.getRoutes();
expect(navigation.getModules).toHaveBeenCalled(); expect(navigation.getModules).toHaveBeenCalled();
}); });

View File

@ -1,93 +0,0 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnInput from 'src/components/common/VnInput.vue';
import FetchData from '../FetchData.vue';
import VnSelectDialog from './VnSelectDialog.vue';
import CreateBankEntityForm from '../CreateBankEntityForm.vue';
const $props = defineProps({
iban: {
type: String,
default: null,
},
bankEntityFk: {
type: Number,
default: null,
},
disableElement: {
type: Boolean,
default: false,
},
});
const filter = {
fields: ['id', 'bic', 'name'],
order: 'bic ASC',
};
const { t } = useI18n();
const emit = defineEmits(['updateBic']);
const iban = ref($props.iban);
const bankEntityFk = ref($props.bankEntityFk);
const bankEntities = ref([]);
const autofillBic = async (bic) => {
if (!bic) return;
const bankEntityId = parseInt(bic.substr(4, 4));
const ibanCountry = bic.substr(0, 2);
if (ibanCountry != 'ES') return;
const existBank = bankEntities.value.find((b) => b.id === bankEntityId);
bankEntityFk.value = existBank ? bankEntityId : null;
emit('updateBic', { iban: iban.value, bankEntityFk: bankEntityFk.value });
};
const getBankEntities = (data) => {
bankEntityFk.value = data.id;
};
</script>
<template>
<FetchData
url="BankEntities"
:filter="filter"
auto-load
@on-fetch="(data) => (bankEntities = data)"
/>
<VnInput
:label="t('IBAN')"
clearable
v-model="iban"
@update:model-value="autofillBic($event)"
:disable="disableElement"
>
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>{{ t('components.iban_tooltip') }}</QTooltip>
</QIcon>
</template>
</VnInput>
<VnSelectDialog
:label="t('Swift / BIC')"
:acls="[{ model: 'BankEntity', props: '*', accessType: 'WRITE' }]"
:options="bankEntities"
hide-selected
option-label="bic"
option-value="id"
v-model="bankEntityFk"
@update:model-value="$emit('updateBic', { iban, bankEntityFk })"
:disable="disableElement"
>
<template #form>
<CreateBankEntityForm @on-data-saved="getBankEntities($event)" />
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection v-if="scope.opt">
<QItemLabel>{{ scope.opt.bic }} </QItemLabel>
<QItemLabel caption> {{ scope.opt.name }}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectDialog>
</template>

View File

@ -15,7 +15,7 @@ let root = ref(null);
watchEffect(() => { watchEffect(() => {
matched.value = currentRoute.value.matched.filter( matched.value = currentRoute.value.matched.filter(
(matched) => !!matched?.meta?.title || !!matched?.meta?.icon, (matched) => !!matched?.meta?.title || !!matched?.meta?.icon
); );
breadcrumbs.value.length = 0; breadcrumbs.value.length = 0;
if (!matched.value[0]) return; if (!matched.value[0]) return;

View File

@ -1,104 +0,0 @@
<script setup>
import { ref } from 'vue';
import VnCheckbox from './VnCheckbox.vue';
import axios from 'axios';
import { toRaw } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
const route = useRoute();
const { t } = useI18n();
const model = defineModel({ type: [Boolean] });
const props = defineProps({
url: {
type: String,
required: true,
},
searchUrl: {
type: [String, Boolean],
default: 'table',
},
});
const menuRef = ref(null);
const errorMessage = ref(null);
const rows = ref(0);
const onClick = async () => {
errorMessage.value = null;
const { filter } = JSON.parse(route.query[props.searchUrl]);
filter.limit = 0;
const params = {
params: { filter: JSON.stringify(filter) },
};
try {
const { data } = await axios.get(props.url, params);
rows.value = data;
} catch (error) {
const response = error.response;
if (response.data.error.name === 'UserError') {
errorMessage.value = t('tooManyResults');
} else {
errorMessage.value = response.data.error.message;
}
}
};
defineEmits(['update:selected', 'select:all']);
</script>
<template>
<QIcon
style="margin-left: -10px"
data-cy="btnMultiCheck"
name="expand_more"
@click="onClick"
class="cursor-pointer"
color="primary"
size="xs"
>
<QMenu
fit
anchor="bottom start"
self="top left"
ref="menuRef"
data-cy="menuMultiCheck"
>
<QList separator>
<QItem
data-cy="selectAll"
v-ripple
clickable
@click="
$refs.menuRef.hide();
$emit('select:all', toRaw(rows));
"
>
<QItemSection>
<QItemLabel>
<span v-text="t('Select all')" />
</QItemLabel>
<QItemLabel overline caption>
<span
v-if="errorMessage"
class="text-negative"
v-text="errorMessage"
/>
<span
v-else
v-text="t('records', { rows: rows?.length ?? 0 })"
/>
</QItemLabel>
</QItemSection>
</QItem>
<slot name="more-options"></slot>
</QList>
</QMenu>
</QIcon>
</template>
<i18n lang="yml">
en:
tooManyResults: Too many results. Please narrow down your search.
records: '{rows} records'
es:
Select all: Seleccionar todo
tooManyResults: Demasiados registros. Restringe la búsqueda.
records: '{rows} registros'
</i18n>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
const model = defineModel({ type: [String, Number], default: '' }); const model = defineModel({ type: [String, Number], required: true });
</script> </script>
<template> <template>
<QDate v-model="model" :today-btn="true" :options="$attrs.options" /> <QDate v-model="model" :today-btn="true" :options="$attrs.options" />

View File

@ -4,7 +4,6 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
@ -13,7 +12,6 @@ import FormModelPopup from 'components/FormModelPopup.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { notify } = useNotify();
const emit = defineEmits(['onDataSaved']); const emit = defineEmits(['onDataSaved']);
const $props = defineProps({ const $props = defineProps({
@ -63,11 +61,8 @@ function onFileChange(files) {
function mapperDms(data) { function mapperDms(data) {
const formData = new FormData(); const formData = new FormData();
let files = data.files; const { files } = data;
if (files) { if (files) formData.append(files?.name, files);
files = Array.isArray(files) ? files : [files];
files.forEach((file) => formData.append(file?.name, file));
}
const dms = { const dms = {
hasFile: !!data.hasFile, hasFile: !!data.hasFile,
@ -88,16 +83,11 @@ function getUrl() {
} }
async function save() { async function save() {
try { const body = mapperDms(dms.value);
const body = mapperDms(dms.value); const response = await axios.post(getUrl(), body[0], body[1]);
const response = await axios.post(getUrl(), body[0], body[1]); emit('onDataSaved', body[1].params, response);
emit('onDataSaved', body[1].params, response); delete dms.value.files;
notify(t('globals.dataSaved'), 'positive'); return response;
delete dms.value.files;
return response;
} catch (e) {
throw e;
}
} }
function defaultData() { function defaultData() {
@ -218,7 +208,7 @@ function addDefaultData(data) {
} }
</style> </style>
<i18n> <i18n>
en: en:
contentTypesInfo: Allowed file types {allowedContentTypes} contentTypesInfo: Allowed file types {allowedContentTypes}
EntryDmsDescription: Reference {reference} EntryDmsDescription: Reference {reference}
WorkersDescription: Working of employee id {reference} WorkersDescription: Working of employee id {reference}

View File

@ -14,12 +14,10 @@ import VnDms from 'src/components/common/VnDms.vue';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import useNotify from 'src/composables/useNotify.js';
const route = useRoute(); const route = useRoute();
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const { notify } = useNotify();
const rows = ref([]); const rows = ref([]);
const dmsRef = ref(); const dmsRef = ref();
const formDialog = ref({}); const formDialog = ref({});
@ -92,6 +90,7 @@ const dmsFilter = {
], ],
}, },
}, },
where: { [$props.filter]: route.params.id },
}; };
const columns = computed(() => [ const columns = computed(() => [
@ -256,16 +255,9 @@ function deleteDms(dmsFk) {
}, },
}) })
.onOk(async () => { .onOk(async () => {
try { await axios.post(`${$props.deleteModel ?? $props.model}/${dmsFk}/removeFile`);
await axios.post( const index = rows.value.findIndex((row) => row.id == dmsFk);
`${$props.deleteModel ?? $props.model}/${dmsFk}/removeFile`, rows.value.splice(index, 1);
);
const index = rows.value.findIndex((row) => row.id == dmsFk);
rows.value.splice(index, 1);
notify(t('globals.dataDeleted'), 'positive');
} catch (e) {
throw e;
}
}); });
} }
@ -303,9 +295,7 @@ defineExpose({
:data-key="$props.model" :data-key="$props.model"
:url="$props.model" :url="$props.model"
:user-filter="dmsFilter" :user-filter="dmsFilter"
search-url="dmsFilter"
:order="['dmsFk DESC']" :order="['dmsFk DESC']"
:filter="{ where: { [$props.filter]: route.params.id } }"
auto-load auto-load
@on-fetch="setData" @on-fetch="setData"
> >

View File

@ -0,0 +1,44 @@
<script setup>
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import VnInput from 'src/components/common/VnInput.vue';
const { t } = useI18n();
const model = defineModel({ type: [Number, String] });
const emit = defineEmits(['updateBic']);
const getIbanCountry = (bank) => {
return bank.substr(0, 2);
};
const autofillBic = async (iban) => {
if (!iban) return;
const bankEntityId = parseInt(iban.substr(4, 4));
const ibanCountry = getIbanCountry(iban);
if (ibanCountry != 'ES') return;
const filter = { where: { id: bankEntityId } };
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get(`BankEntities`, { params });
emit('updateBic', data[0]?.id);
};
</script>
<template>
<VnInput
:label="t('IBAN')"
clearable
v-model="model"
@update:model-value="autofillBic($event)"
>
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>{{ t('components.iban_tooltip') }}</QTooltip>
</QIcon>
</template>
</VnInput>
</template>

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { nextTick, watch, computed, ref, useAttrs } from 'vue'; import { onMounted, watch, computed, ref, useAttrs } from 'vue';
import { date, getCssVar } from 'quasar'; import { date } from 'quasar';
import VnDate from './VnDate.vue'; import VnDate from './VnDate.vue';
import { useRequired } from 'src/composables/useRequired'; import { useRequired } from 'src/composables/useRequired';
@ -20,18 +20,61 @@ const $props = defineProps({
}); });
const vnInputDateRef = ref(null); const vnInputDateRef = ref(null);
const errColor = getCssVar('negative');
const textColor = ref('');
const dateFormat = 'DD/MM/YYYY'; const dateFormat = 'DD/MM/YYYY';
const isPopupOpen = ref(); const isPopupOpen = ref();
const hover = ref(); const hover = ref();
const mask = ref();
const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])]; const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])];
const formattedDate = computed({
get() {
if (!model.value) return model.value;
return date.formatDate(new Date(model.value), dateFormat);
},
set(value) {
if (value == model.value) return;
let newDate;
if (value) {
// parse input
if (value.includes('/') && value.length >= 10) {
if (value.at(2) == '/') value = value.split('/').reverse().join('/');
value = date.formatDate(
new Date(value).toISOString(),
'YYYY-MM-DDTHH:mm:ss.SSSZ',
);
}
const [year, month, day] = value.split('-').map((e) => parseInt(e));
newDate = new Date(year, month - 1, day);
if (model.value) {
const orgDate =
model.value instanceof Date ? model.value : new Date(model.value);
newDate.setHours(
orgDate.getHours(),
orgDate.getMinutes(),
orgDate.getSeconds(),
orgDate.getMilliseconds(),
);
}
}
if (!isNaN(newDate)) model.value = newDate.toISOString();
},
});
const popupDate = computed(() => const popupDate = computed(() =>
model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value, model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value,
); );
onMounted(() => {
// fix quasar bug
mask.value = '##/##/####';
});
watch(
() => model.value,
(val) => (formattedDate.value = val),
{ immediate: true },
);
const styleAttrs = computed(() => { const styleAttrs = computed(() => {
return $props.isOutlined return $props.isOutlined
@ -43,138 +86,28 @@ const styleAttrs = computed(() => {
: {}; : {};
}); });
const inputValue = ref('');
const validateAndCleanInput = (value) => {
inputValue.value = value.replace(/[^0-9./-]/g, '');
};
const manageDate = (date) => { const manageDate = (date) => {
inputValue.value = date.split('/').reverse().join('/'); formattedDate.value = date;
isPopupOpen.value = false; isPopupOpen.value = false;
}; };
watch(
() => model.value,
(nVal) => {
if (nVal) inputValue.value = date.formatDate(new Date(model.value), dateFormat);
else inputValue.value = '';
},
{ immediate: true },
);
const formatDate = () => {
let value = inputValue.value;
if (!value || value === model.value) {
textColor.value = '';
return;
}
const regex =
/^([0]?[1-9]|[12][0-9]|3[01])([./-])([0]?[1-9]|1[0-2])([./-](\d{1,4}))?$/;
if (!regex.test(value)) {
textColor.value = errColor;
return;
}
value = value.replace(/[.-]/g, '/');
const parts = value.split('/');
if (parts.length < 2) {
textColor.value = errColor;
return;
}
let [day, month, year] = parts;
if (day.length === 1) day = '0' + day;
if (month.length === 1) month = '0' + month;
const currentYear = Date.vnNew().getFullYear();
if (!year) year = currentYear;
const millennium = currentYear.toString().slice(0, 1);
switch (year.length) {
case 1:
year = `${millennium}00${year}`;
break;
case 2:
year = `${millennium}0${year}`;
break;
case 3:
year = `${millennium}${year}`;
break;
case 4:
break;
}
let isoCandidate = `${year}/${month}/${day}`;
isoCandidate = date.formatDate(
new Date(isoCandidate).toISOString(),
'YYYY-MM-DDTHH:mm:ss.SSSZ',
);
const [isoYear, isoMonth, isoDay] = isoCandidate.split('-').map((e) => parseInt(e));
const parsedDate = new Date(isoYear, isoMonth - 1, isoDay);
const isValidDate =
parsedDate instanceof Date &&
!isNaN(parsedDate) &&
parsedDate.getFullYear() === parseInt(year) &&
parsedDate.getMonth() === parseInt(month) - 1 &&
parsedDate.getDate() === parseInt(day);
if (!isValidDate) {
textColor.value = errColor;
return;
}
if (model.value) {
const original =
model.value instanceof Date ? model.value : new Date(model.value);
parsedDate.setHours(
original.getHours(),
original.getMinutes(),
original.getSeconds(),
original.getMilliseconds(),
);
}
model.value = parsedDate.toISOString();
textColor.value = '';
};
const handleEnter = (event) => {
formatDate();
nextTick(() => {
const newEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
bubbles: true,
cancelable: true,
});
vnInputDateRef.value?.$el?.dispatchEvent(newEvent);
});
};
</script> </script>
<template> <template>
<div @mouseover="hover = true" @mouseleave="hover = false"> <div @mouseover="hover = true" @mouseleave="hover = false">
<QInput <QInput
ref="vnInputDateRef" ref="vnInputDateRef"
v-model="inputValue" v-model="formattedDate"
class="vn-input-date" class="vn-input-date"
:mask="mask"
placeholder="dd/mm/aaaa" placeholder="dd/mm/aaaa"
v-bind="{ ...$attrs, ...styleAttrs }" v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: isRequired }" :class="{ required: isRequired }"
:rules="mixinRules" :rules="mixinRules"
:clearable="false" :clearable="false"
:input-style="{ color: textColor }"
@click="isPopupOpen = !isPopupOpen" @click="isPopupOpen = !isPopupOpen"
@keydown="isPopupOpen = false" @keydown="isPopupOpen = false"
@blur="formatDate"
@keydown.enter.prevent="handleEnter"
hide-bottom-space hide-bottom-space
:data-cy="($attrs['data-cy'] ?? $attrs.label) + '_inputDate'" :data-cy="($attrs['data-cy'] ?? $attrs.label) + '_inputDate'"
@update:model-value="validateAndCleanInput"
> >
<template #append> <template #append>
<QIcon <QIcon
@ -183,12 +116,11 @@ const handleEnter = (event) => {
v-if=" v-if="
($attrs.clearable == undefined || $attrs.clearable) && ($attrs.clearable == undefined || $attrs.clearable) &&
hover && hover &&
inputValue && model &&
!$attrs.disable !$attrs.disable
" "
@click=" @click="
vnInputDateRef.focus(); vnInputDateRef.focus();
inputValue = null;
model = null; model = null;
isPopupOpen = false; isPopupOpen = false;
" "

View File

@ -5,7 +5,7 @@ import VnDate from './VnDate.vue';
import VnTime from './VnTime.vue'; import VnTime from './VnTime.vue';
const $attrs = useAttrs(); const $attrs = useAttrs();
const model = defineModel({ type: [Date, String] }); const model = defineModel({ type: [Date] });
const $props = defineProps({ const $props = defineProps({
isOutlined: { isOutlined: {
@ -29,7 +29,7 @@ const styleAttrs = computed(() => {
const mask = 'DD-MM-YYYY HH:mm'; const mask = 'DD-MM-YYYY HH:mm';
const selectedDate = computed({ const selectedDate = computed({
get() { get() {
if (!model.value) return JSON.stringify(new Date(model.value)); if (!model.value) return new Date(model.value);
return date.formatDate(new Date(model.value), mask); return date.formatDate(new Date(model.value), mask);
}, },
set(value) { set(value) {

View File

@ -21,7 +21,7 @@ watch(
(newValue) => { (newValue) => {
if (!modelValue.value) return; if (!modelValue.value) return;
modelValue.value = formatLocation(newValue) ?? null; modelValue.value = formatLocation(newValue) ?? null;
}, }
); );
const mixinRules = [requiredFieldRule]; const mixinRules = [requiredFieldRule];
@ -45,7 +45,7 @@ const formatLocation = (obj, properties = locationProperties) => {
}); });
const filteredParts = parts.filter( const filteredParts = parts.filter(
(part) => part !== null && part !== undefined && part !== '', (part) => part !== null && part !== undefined && part !== ''
); );
return filteredParts.join(', '); return filteredParts.join(', ');

View File

@ -1,98 +0,0 @@
<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
const props = defineProps({
scrollTarget: { type: [String, Object], default: 'window' },
});
const scrollPosition = ref(0);
const showButton = ref(false);
let scrollContainer = null;
const onScroll = () => {
if (!scrollContainer) return;
scrollPosition.value =
typeof props.scrollTarget === 'object'
? scrollContainer.scrollTop
: window.scrollY;
};
watch(scrollPosition, (newValue) => {
showButton.value = newValue > 0;
});
const scrollToTop = () => {
if (scrollContainer) {
scrollContainer.scrollTo({ top: 0, behavior: 'smooth' });
}
};
const updateScrollContainer = (container) => {
if (container) {
if (scrollContainer) {
scrollContainer.removeEventListener('scroll', onScroll);
}
scrollContainer = container;
scrollContainer.addEventListener('scroll', onScroll);
onScroll();
}
};
defineExpose({
updateScrollContainer,
});
const initScrollContainer = async () => {
await nextTick();
if (typeof props.scrollTarget === 'object') {
scrollContainer = props.scrollTarget;
} else {
scrollContainer = window;
}
if (!scrollContainer) return;
scrollContainer.addEventListener('scroll', onScroll);
};
onMounted(() => {
initScrollContainer();
});
onUnmounted(() => {
if (scrollContainer) {
scrollContainer.removeEventListener('scroll', onScroll);
scrollContainer = null;
}
});
</script>
<template>
<QIcon
v-if="showButton"
color="primary"
name="keyboard_arrow_up"
class="scroll-to-top"
@click="scrollToTop"
>
<QTooltip>{{ $t('globals.scrollToTop') }}</QTooltip>
</QIcon>
</template>
<style scoped>
.scroll-to-top {
position: fixed;
top: 70px;
font-size: 65px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
transition: transform 0.2s ease-in-out;
}
.scroll-to-top:hover {
transform: translateX(-50%) scale(1.2);
cursor: pointer;
filter: brightness(0.8);
}
</style>

View File

@ -54,10 +54,6 @@ const $props = defineProps({
type: [Array], type: [Array],
default: () => [], default: () => [],
}, },
filterFn: {
type: Function,
default: null,
},
exprBuilder: { exprBuilder: {
type: Function, type: Function,
default: null, default: null,
@ -66,12 +62,16 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
defaultFilter: {
type: Boolean,
default: true,
},
fields: { fields: {
type: Array, type: Array,
default: null, default: null,
}, },
include: { include: {
type: [Object, Array, String], type: [Object, Array],
default: null, default: null,
}, },
where: { where: {
@ -79,7 +79,7 @@ const $props = defineProps({
default: null, default: null,
}, },
sortBy: { sortBy: {
type: [String, Array], type: String,
default: null, default: null,
}, },
limit: { limit: {
@ -166,8 +166,6 @@ const computedSortBy = computed(() => {
return $props.sortBy || $props.optionLabel + ' ASC'; return $props.sortBy || $props.optionLabel + ' ASC';
}); });
const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val);
watch(options, (newValue) => { watch(options, (newValue) => {
setOptions(newValue); setOptions(newValue);
}); });
@ -257,41 +255,43 @@ async function fetchFilter(val) {
} }
async function filterHandler(val, update) { async function filterHandler(val, update) {
if (isLoading.value) return update();
if (!val && lastVal.value === val) {
lastVal.value = val;
return update();
}
lastVal.value = val;
let newOptions; let newOptions;
if ($props.filterFn) update($props.filterFn(val)); if (!$props.defaultFilter) return update();
else if (!val && lastVal.value === val) update(); if (
else { $props.url &&
const makeRequest = ($props.limit || (!$props.limit && Object.keys(myOptions.value).length === 0))
($props.url && $props.limit) || ) {
(!$props.limit && Object.keys(myOptions.value).length === 0); newOptions = await fetchFilter(val);
newOptions = makeRequest } else newOptions = filter(val, myOptionsOriginal.value);
? await fetchFilter(val) update(
: filter(val, myOptionsOriginal.value); () => {
if ($props.noOne && noOneText.toLowerCase().includes(val.toLowerCase()))
newOptions.unshift(noOneOpt.value);
update( myOptions.value = newOptions;
() => { },
if ($props.noOne && noOneText.toLowerCase().includes(val.toLowerCase())) (ref) => {
newOptions.unshift(noOneOpt.value); if (val !== '' && ref.options.length > 0) {
ref.setOptionIndex(-1);
myOptions.value = newOptions; ref.moveOptionSelection(1, true);
}, }
(ref) => { },
if (val !== '' && ref.options.length > 0) { );
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
},
);
}
lastVal.value = val;
} }
function nullishToTrue(value) { function nullishToTrue(value) {
return value ?? true; return value ?? true;
} }
const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val);
async function onScroll({ to, direction, from, index }) { async function onScroll({ to, direction, from, index }) {
const lastIndex = myOptions.value.length - 1; const lastIndex = myOptions.value.length - 1;
@ -379,7 +379,7 @@ function getCaption(opt) {
> >
<template #append> <template #append>
<QIcon <QIcon
v-show="isClearable && value != null && value !== ''" v-show="isClearable && value"
name="close" name="close"
@click=" @click="
() => { () => {
@ -394,7 +394,7 @@ function getCaption(opt) {
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName"> <template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
<div v-if="slotName == 'append'"> <div v-if="slotName == 'append'">
<QIcon <QIcon
v-show="isClearable && value != null && value !== ''" v-show="isClearable && value"
name="close" name="close"
@click.stop=" @click.stop="
() => { () => {
@ -419,7 +419,7 @@ function getCaption(opt) {
<QItemLabel> <QItemLabel>
{{ opt[optionLabel] }} {{ opt[optionLabel] }}
</QItemLabel> </QItemLabel>
<QItemLabel caption v-if="getCaption(opt) !== false"> <QItemLabel caption v-if="getCaption(opt)">
{{ `#${getCaption(opt)}` }} {{ `#${getCaption(opt)}` }}
</QItemLabel> </QItemLabel>
</QItemSection> </QItemSection>

View File

@ -1,49 +0,0 @@
<script setup>
import { ref, useTemplateRef } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard';
import VnSelectDialog from './VnSelectDialog.vue';
import CreateNewExpenseForm from '../CreateNewExpenseForm.vue';
import FetchData from '../FetchData.vue';
const model = defineModel({ type: [String, Number, Object] });
const { t } = useI18n();
const expenses = ref([]);
const selectDialogRef = useTemplateRef('selectDialogRef');
async function autocompleteExpense(evt) {
const val = evt.target.value;
if (!val || isNaN(val)) return;
const lookup = expenses.value.find(({ id }) => id == useAccountShortToStandard(val));
if (selectDialogRef.value)
selectDialogRef.value.vnSelectDialogRef.vnSelectRef.toggleOption(lookup);
}
</script>
<template>
<VnSelectDialog
v-bind="$attrs"
ref="selectDialogRef"
v-model="model"
:options="expenses"
option-value="id"
:option-label="(x) => `${x.id}: ${x.name}`"
:filter-options="['id', 'name']"
:tooltip="t('Create a new expense')"
:acls="[{ model: 'Expense', props: '*', accessType: 'WRITE' }]"
@keydown.tab.prevent="autocompleteExpense"
>
<template #form>
<CreateNewExpenseForm @on-data-saved="$refs.expensesRef.fetch()" />
</template>
</VnSelectDialog>
<FetchData
ref="expensesRef"
url="Expenses"
auto-load
@on-fetch="(data) => (expenses = data)"
/>
</template>
<i18n>
es:
Create a new expense: Crear nuevo gasto
</i18n>

View File

@ -1,50 +0,0 @@
import { createWrapper } from 'app/test/vitest/helper';
import VnBankDetailsForm from 'components/common/VnBankDetailsForm.vue';
import { vi, afterEach, expect, it, beforeEach, describe } from 'vitest';
describe('VnBankDetail Component', () => {
let vm;
let wrapper;
const bankEntities = [
{ id: 2100, bic: 'CAIXESBBXXX', name: 'CaixaBank' },
{ id: 1234, bic: 'TESTBIC', name: 'Test Bank' },
];
const correctIban = 'ES6621000418401234567891';
beforeAll(() => {
wrapper = createWrapper(VnBankDetailsForm, {
$props: {
iban: null,
bankEntityFk: null,
disableElement: false,
},
});
vm = wrapper.vm;
wrapper = wrapper.wrapper;
});
afterEach(() => {
vi.clearAllMocks();
});
it('should update bankEntityFk when IBAN exists in bankEntities', async () => {
vm.bankEntities = bankEntities;
await vm.autofillBic(correctIban);
expect(vm.bankEntityFk).toBe(2100);
});
it('should set bankEntityFk to null when IBAN bank code is not found', async () => {
vm.bankEntities = bankEntities;
await vm.autofillBic('ES1234567891324567891234');
expect(vm.bankEntityFk).toBe(null);
});
it('should not update bankEntityFk if IBAN country is not ES', async () => {
vm.bankEntities = bankEntities;
await vm.autofillBic('FR1420041010050500013M02606');
expect(vm.bankEntityFk).toBe(null);
});
});

View File

@ -35,7 +35,7 @@ describe('VnSmsDialog', () => {
expect.objectContaining({ expect.objectContaining({
message: 'You must enter a new password', message: 'You must enter a new password',
type: 'negative', type: 'negative',
}), })
); );
}); });
@ -47,7 +47,7 @@ describe('VnSmsDialog', () => {
expect.objectContaining({ expect.objectContaining({
message: `Passwords don't match`, message: `Passwords don't match`,
type: 'negative', type: 'negative',
}), })
); );
}); });

View File

@ -4,7 +4,7 @@ import VnDiscount from 'components/common/vnDiscount.vue';
describe('VnDiscount', () => { describe('VnDiscount', () => {
let vm; let vm;
beforeAll(() => { beforeAll(() => {
vm = createWrapper(VnDiscount, { vm = createWrapper(VnDiscount, {
props: { props: {
@ -12,9 +12,7 @@ describe('VnDiscount', () => {
price: 100, price: 100,
quantity: 2, quantity: 2,
discount: 10, discount: 10,
mana: 10, }
promise: vi.fn(),
},
}).vm; }).vm;
}); });
@ -23,8 +21,8 @@ describe('VnDiscount', () => {
}); });
describe('total', () => { describe('total', () => {
it('should calculate total correctly', () => { it('should calculate total correctly', () => {
expect(vm.total).toBe(180); expect(vm.total).toBe(180);
}); });
}); });
}); });

View File

@ -41,12 +41,10 @@ describe('VnDms', () => {
companyFk: 2, companyFk: 2,
dmsTypeFk: 3, dmsTypeFk: 3,
description: 'This is a test description', description: 'This is a test description',
files: [ files: {
{ name: 'example.txt',
name: 'example.txt', content: new Blob(['file content'], { type: 'text/plain' }),
content: new Blob(['file content'], { type: 'text/plain' }), },
},
],
}; };
const expectedBody = { const expectedBody = {
@ -85,7 +83,7 @@ describe('VnDms', () => {
it('should map DMS data correctly and add file to FormData', () => { it('should map DMS data correctly and add file to FormData', () => {
const [formData, params] = vm.mapperDms(data); const [formData, params] = vm.mapperDms(data);
expect([formData.get('example.txt')]).toStrictEqual(data.files); expect(formData.get('example.txt')).toBe(data.files);
expect(expectedBody).toEqual(params.params); expect(expectedBody).toEqual(params.params);
}); });

View File

@ -2,6 +2,7 @@ import { createWrapper } from 'app/test/vitest/helper';
import { vi, describe, expect, it } from 'vitest'; import { vi, describe, expect, it } from 'vitest';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
describe('VnInput', () => { describe('VnInput', () => {
let vm; let vm;
let wrapper; let wrapper;
@ -10,28 +11,26 @@ describe('VnInput', () => {
function generateWrapper(value, isOutlined, emptyToNull, insertable) { function generateWrapper(value, isOutlined, emptyToNull, insertable) {
wrapper = createWrapper(VnInput, { wrapper = createWrapper(VnInput, {
props: { props: {
modelValue: value, modelValue: value,
isOutlined, isOutlined, emptyToNull, insertable,
emptyToNull, maxlength: 101
insertable,
maxlength: 101,
}, },
attrs: { attrs: {
label: 'test', label: 'test',
required: true, required: true,
maxlength: 101, maxlength: 101,
maxLength: 10, maxLength: 10,
'max-length': 20, 'max-length':20
}, },
}); });
wrapper = wrapper.wrapper; wrapper = wrapper.wrapper;
vm = wrapper.vm; vm = wrapper.vm;
input = wrapper.find('[data-cy="test_input"]'); input = wrapper.find('[data-cy="test_input"]');
} };
describe('value', () => { describe('value', () => {
it('should emit update:modelValue when value changes', async () => { it('should emit update:modelValue when value changes', async () => {
generateWrapper('12345', false, false, true); generateWrapper('12345', false, false, true)
await input.setValue('123'); await input.setValue('123');
expect(wrapper.emitted('update:modelValue')).toBeTruthy(); expect(wrapper.emitted('update:modelValue')).toBeTruthy();
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['123']); expect(wrapper.emitted('update:modelValue')[0]).toEqual(['123']);
@ -47,36 +46,37 @@ describe('VnInput', () => {
describe('styleAttrs', () => { describe('styleAttrs', () => {
it('should return empty styleAttrs when isOutlined is false', async () => { it('should return empty styleAttrs when isOutlined is false', async () => {
generateWrapper('123', false, false, false); generateWrapper('123', false, false, false);
expect(vm.styleAttrs).toEqual({}); expect(vm.styleAttrs).toEqual({});
}); });
it('should set styleAttrs when isOutlined is true', async () => { it('should set styleAttrs when isOutlined is true', async () => {
generateWrapper('123', true, false, false); generateWrapper('123', true, false, false);
expect(vm.styleAttrs.outlined).toBe(true); expect(vm.styleAttrs.outlined).toBe(true);
}); });
}); });
describe('handleKeydown', () => { describe('handleKeydown', () => {
it('should do nothing when "Backspace" key is pressed', async () => { it('should do nothing when "Backspace" key is pressed', async () => {
generateWrapper('12345', false, false, true); generateWrapper('12345', false, false, true);
await input.trigger('keydown', { key: 'Backspace' }); await input.trigger('keydown', { key: 'Backspace' });
expect(wrapper.emitted('update:modelValue')).toBeUndefined(); expect(wrapper.emitted('update:modelValue')).toBeUndefined();
const spyhandler = vi.spyOn(vm, 'handleInsertMode'); const spyhandler = vi.spyOn(vm, 'handleInsertMode');
expect(spyhandler).not.toHaveBeenCalled(); expect(spyhandler).not.toHaveBeenCalled();
}); });
/* /*
TODO: #8399 REDMINE TODO: #8399 REDMINE
*/ */
it.skip('handleKeydown respects insertable behavior', async () => { it.skip('handleKeydown respects insertable behavior', async () => {
const expectedValue = '12345'; const expectedValue = '12345';
generateWrapper('1234', false, false, true); generateWrapper('1234', false, false, true);
vm.focus(); vm.focus()
await input.trigger('keydown', { key: '5' }); await input.trigger('keydown', { key: '5' });
await vm.$nextTick(); await vm.$nextTick();
expect(wrapper.emitted('update:modelValue')).toBeTruthy(); expect(wrapper.emitted('update:modelValue')).toBeTruthy();
expect(wrapper.emitted('update:modelValue')[0]).toEqual([expectedValue]); expect(wrapper.emitted('update:modelValue')[0]).toEqual([expectedValue ]);
expect(vm.value).toBe(expectedValue); expect(vm.value).toBe( expectedValue);
}); });
}); });
@ -86,6 +86,6 @@ describe('VnInput', () => {
const focusSpy = vi.spyOn(input.element, 'focus'); const focusSpy = vi.spyOn(input.element, 'focus');
vm.focus(); vm.focus();
expect(focusSpy).toHaveBeenCalled(); expect(focusSpy).toHaveBeenCalled();
}); });
}); });
}); });

View File

@ -5,71 +5,52 @@ import VnInputDate from 'components/common/VnInputDate.vue';
let vm; let vm;
let wrapper; let wrapper;
function generateWrapper(outlined = false, required = false) { function generateWrapper(date, outlined, required) {
wrapper = createWrapper(VnInputDate, { wrapper = createWrapper(VnInputDate, {
props: { props: {
modelValue: '2000-12-31T23:00:00.000Z', modelValue: date,
'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }),
}, },
attrs: { attrs: {
isOutlined: outlined, isOutlined: outlined,
required: required, required: required
}, },
}); });
wrapper = wrapper.wrapper; wrapper = wrapper.wrapper;
vm = wrapper.vm; vm = wrapper.vm;
} };
describe('VnInputDate', () => { describe('VnInputDate', () => {
describe('formattedDate', () => {
it('validateAndCleanInput should remove non-numeric characters', async () => { describe('formattedDate', () => {
generateWrapper(); it('formats a valid date correctly', async () => {
vm.validateAndCleanInput('10a/1s2/2dd0a23'); generateWrapper('2023-12-25', false, false);
await vm.$nextTick(); await vm.$nextTick();
expect(vm.inputValue).toBe('10/12/2023'); expect(vm.formattedDate).toBe('25/12/2023');
}); });
it('manageDate should reverse the date', async () => { it('updates the model value when a new date is set', async () => {
generateWrapper();
vm.manageDate('10/12/2023');
await vm.$nextTick();
expect(vm.inputValue).toBe('2023/12/10');
});
it('formatDate should format the date correctly when a valid date is entered with full year', async () => {
const input = wrapper.find('input'); const input = wrapper.find('input');
await input.setValue('25.12/2002'); await input.setValue('31/12/2023');
await vm.$nextTick(); expect(wrapper.emitted()['update:modelValue']).toBeTruthy();
await vm.formatDate(); expect(wrapper.emitted()['update:modelValue'][0][0]).toBe('2023-12-31T00:00:00.000Z');
expect(vm.model).toBe('2002-12-24T23:00:00.000Z');
}); });
it('should format the date correctly when a valid date is entered with short year', async () => { it('should not update the model value when an invalid date is set', async () => {
const input = wrapper.find('input'); const input = wrapper.find('input');
await input.setValue('31.12-23'); await input.setValue('invalid-date');
await vm.$nextTick(); expect(wrapper.emitted()['update:modelValue'][0][0]).toBe('2023-12-31T00:00:00.000Z');
await vm.formatDate(); });
expect(vm.model).toBe('2023-12-30T23:00:00.000Z');
});
it('should format the date correctly when a valid date is entered without year', async () => {
const input = wrapper.find('input');
await input.setValue('12.03');
await vm.$nextTick();
await vm.formatDate();
expect(vm.model).toBe('2001-03-11T23:00:00.000Z');
});
}); });
describe('styleAttrs', () => { describe('styleAttrs', () => {
it('should return empty styleAttrs when isOutlined is false', async () => { it('should return empty styleAttrs when isOutlined is false', async () => {
generateWrapper(); generateWrapper('2023-12-25', false, false);
await vm.$nextTick(); await vm.$nextTick();
expect(vm.styleAttrs).toEqual({}); expect(vm.styleAttrs).toEqual({});
}); });
it('should set styleAttrs when isOutlined is true', async () => { it('should set styleAttrs when isOutlined is true', async () => {
generateWrapper(true, false); generateWrapper('2023-12-25', true, false);
await vm.$nextTick(); await vm.$nextTick();
expect(vm.styleAttrs.outlined).toBe(true); expect(vm.styleAttrs.outlined).toBe(true);
}); });
@ -77,15 +58,15 @@ describe('VnInputDate', () => {
describe('required', () => { describe('required', () => {
it('should not applies required class when isRequired is false', async () => { it('should not applies required class when isRequired is false', async () => {
generateWrapper(); generateWrapper('2023-12-25', false, false);
await vm.$nextTick(); await vm.$nextTick();
expect(wrapper.find('.vn-input-date').classes()).not.toContain('required'); expect(wrapper.find('.vn-input-date').classes()).not.toContain('required');
}); });
it('should applies required class when isRequired is true', async () => { it('should applies required class when isRequired is true', async () => {
generateWrapper(false, true); generateWrapper('2023-12-25', false, true);
await vm.$nextTick(); await vm.$nextTick();
expect(wrapper.find('.vn-input-date').classes()).toContain('required'); expect(wrapper.find('.vn-input-date').classes()).toContain('required');
}); });
}); });
}); });

View File

@ -33,7 +33,7 @@ describe('VnInputDateTime', () => {
it('handles null date value', async () => { it('handles null date value', async () => {
generateWrapper(null, false, true); generateWrapper(null, false, true);
await vm.$nextTick(); await vm.$nextTick();
expect(vm.selectedDate).not.toBe(null); expect(vm.selectedDate).toBeInstanceOf(Date);
}); });
it('updates the model value when a new datetime is set', async () => { it('updates the model value when a new datetime is set', async () => {

View File

@ -60,4 +60,4 @@ describe('VnInputTime', () => {
expect(vm.model).toBe(previousModel); expect(vm.model).toBe(previousModel);
}); });
}); });
}); });

View File

@ -6,8 +6,8 @@ function buildComponent(data) {
return createWrapper(VnLocation, { return createWrapper(VnLocation, {
global: { global: {
props: { props: {
location: data, location: data
}, }
}, },
}).vm; }).vm;
} }
@ -24,7 +24,7 @@ describe('formatLocation', () => {
postcode: '46680', postcode: '46680',
city: 'Algemesi', city: 'Algemesi',
province: { name: 'Valencia' }, province: { name: 'Valencia' },
country: { name: 'Spain' }, country: { name: 'Spain' }
}; };
}); });
@ -47,12 +47,7 @@ describe('formatLocation', () => {
}); });
it('should return the country', () => { it('should return the country', () => {
const location = { const location = { ...locationBase, postcode: undefined, city: undefined, province: undefined };
...locationBase,
postcode: undefined,
city: undefined,
province: undefined,
};
const vm = buildComponent(location); const vm = buildComponent(location);
expect(vm.formatLocation(location)).toEqual('Spain'); expect(vm.formatLocation(location)).toEqual('Spain');
}); });
@ -66,7 +61,7 @@ describe('showLabel', () => {
code: '46680', code: '46680',
town: 'Algemesi', town: 'Algemesi',
province: 'Valencia', province: 'Valencia',
country: 'Spain', country: 'Spain'
}; };
}); });
@ -89,13 +84,8 @@ describe('showLabel', () => {
}); });
it('should show the label with country', () => { it('should show the label with country', () => {
const location = { const location = { ...locationBase, code: undefined, town: undefined, province: undefined };
...locationBase,
code: undefined,
town: undefined,
province: undefined,
};
const vm = buildComponent(location); const vm = buildComponent(location);
expect(vm.showLabel(location)).toEqual('Spain'); expect(vm.showLabel(location)).toEqual('Spain');
}); });
}); });

View File

@ -90,10 +90,8 @@ describe('VnLog', () => {
vm = createWrapper(VnLog, { vm = createWrapper(VnLog, {
global: { global: {
stubs: ['FetchData', 'vue-i18n', 'VnUserLink'], stubs: ['VnUserLink'],
mocks: { mocks: {},
fetch: vi.fn(),
},
}, },
propsData: { propsData: {
model: 'Claim', model: 'Claim',

View File

@ -26,7 +26,7 @@ describe('VnNotes', () => {
) { ) {
vi.spyOn(axios, 'get').mockResolvedValue({ data: [] }); vi.spyOn(axios, 'get').mockResolvedValue({ data: [] });
wrapper = createWrapper(VnNotes, { wrapper = createWrapper(VnNotes, {
propsData: { ...defaultOptions, ...options }, propsData: options,
}); });
wrapper = wrapper.wrapper; wrapper = wrapper.wrapper;
vm = wrapper.vm; vm = wrapper.vm;

View File

@ -2,14 +2,13 @@ import { createWrapper } from 'app/test/vitest/helper';
import VnSmsDialog from 'components/common/VnSmsDialog.vue'; import VnSmsDialog from 'components/common/VnSmsDialog.vue';
import { vi, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { vi, afterEach, beforeAll, describe, expect, it } from 'vitest';
describe('VnSmsDialog', () => { describe('VnSmsDialog', () => {
let vm; let vm;
const orderId = 1; const orderId = 1;
const shipped = new Date(); const shipped = new Date();
const phone = '012345678'; const phone = '012345678';
const promise = (response) => { const promise = (response) => {return response;};
return response;
};
const template = 'minAmount'; const template = 'minAmount';
const locale = 'en'; const locale = 'en';
@ -18,13 +17,13 @@ describe('VnSmsDialog', () => {
propsData: { propsData: {
data: { data: {
orderId, orderId,
shipped, shipped
}, },
template, template,
locale, locale,
phone, phone,
promise, promise
}, }
}).vm; }).vm;
}); });
@ -36,9 +35,7 @@ describe('VnSmsDialog', () => {
it('should update the message value with the correct template and parameters', () => { it('should update the message value with the correct template and parameters', () => {
vm.updateMessage(); vm.updateMessage();
expect(vm.message).toEqual( expect(vm.message).toEqual(`A minimum amount of 50€ (VAT excluded) is required for your order ${orderId} of ${shipped} to receive it without additional shipping costs.`);
`A minimum amount of 50€ (VAT excluded) is required for your order ${orderId} of ${shipped} to receive it without additional shipping costs.`,
);
}); });
}); });
@ -50,7 +47,7 @@ describe('VnSmsDialog', () => {
orderId, orderId,
shipped, shipped,
destination: phone, destination: phone,
message: vm.message, message: vm.message
}; };
await vm.send(); await vm.send();

View File

@ -1,14 +1,14 @@
<template> <template>
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" class="col" square /> <QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" class="col" square /> <QSkeleton type="QInput" class="col" square />
</div> </div>
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" class="col" square /> <QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" class="col" square /> <QSkeleton type="QInput" class="col" square />
</div> </div>
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" class="col" square /> <QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" class="col" square /> <QSkeleton type="QInput" class="col" square />
</div> </div>
</template> </template>

View File

@ -17,7 +17,7 @@ const token = getTokenMultimedia();
const { t } = useI18n(); const { t } = useI18n();
const src = computed( const src = computed(
() => `/api/Images/user/160x160/${$props.workerId}/download?access_token=${token}`, () => `/api/Images/user/160x160/${$props.workerId}/download?access_token=${token}`
); );
const title = computed(() => $props.title?.toUpperCase() || t('globals.system')); const title = computed(() => $props.title?.toUpperCase() || t('globals.system'));
const showLetter = ref(false); const showLetter = ref(false);

View File

@ -89,26 +89,24 @@ function cancel() {
<slot name="customHTML"></slot> <slot name="customHTML"></slot>
</QCardSection> </QCardSection>
<QCardActions align="right"> <QCardActions align="right">
<slot name="actions" :actions="{ confirm, cancel }"> <QBtn
<QBtn :label="t('globals.cancel')"
:label="t('globals.cancel')" color="primary"
color="primary" :disable="isLoading"
:disable="isLoading" flat
flat @click="cancel()"
@click="cancel()" data-cy="VnConfirm_cancel"
data-cy="VnConfirm_cancel" />
/> <QBtn
<QBtn :label="t('globals.confirm')"
:label="t('globals.confirm')" :title="t('globals.confirm')"
:title="t('globals.confirm')" color="primary"
color="primary" :loading="isLoading"
:loading="isLoading" @click="confirm()"
@click="confirm()" unelevated
unelevated autofocus
autofocus data-cy="VnConfirm_confirm"
data-cy="VnConfirm_confirm" />
/>
</slot>
</QCardActions> </QCardActions>
</QCard> </QCard>
</QDialog> </QDialog>

View File

@ -212,7 +212,6 @@ const getLocale = (label) => {
color="primary" color="primary"
style="position: fixed; z-index: 1; right: 0; bottom: 0" style="position: fixed; z-index: 1; right: 0; bottom: 0"
icon="search" icon="search"
data-cy="vnFilterPanel_search"
@click="search()" @click="search()"
> >
<QTooltip bottom anchor="bottom right"> <QTooltip bottom anchor="bottom right">
@ -230,7 +229,6 @@ const getLocale = (label) => {
<QItemSection top side> <QItemSection top side>
<QBtn <QBtn
@click="clearFilters" @click="clearFilters"
data-cy="clearFilters"
color="primary" color="primary"
dense dense
flat flat
@ -294,7 +292,6 @@ const getLocale = (label) => {
</QList> </QList>
</QForm> </QForm>
<QInnerLoading <QInnerLoading
data-cy="filterPanel-spinner"
:label="t('globals.pleaseWait')" :label="t('globals.pleaseWait')"
:showing="isLoading" :showing="isLoading"
color="primary" color="primary"

View File

@ -13,7 +13,7 @@ const src = computed({
get() { get() {
return new URL( return new URL(
`../../assets/${$props.logo}${Dark.isActive ? '_dark' : ''}.svg`, `../../assets/${$props.logo}${Dark.isActive ? '_dark' : ''}.svg`,
import.meta.url, import.meta.url
).href; ).href;
}, },
}); });

View File

@ -1,11 +1,10 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { ref, reactive, useAttrs, computed, onMounted, nextTick } from 'vue'; import { ref, reactive, useAttrs, computed } from 'vue';
import { onBeforeRouteLeave, useRouter, useRoute } from 'vue-router'; import { onBeforeRouteLeave } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { tMobile } from 'src/composables/tMobile';
import { toDateHourMin } from 'src/filters'; import { toDateHourMin } from 'src/filters';
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
@ -34,47 +33,20 @@ const $props = defineProps({
addNote: { type: Boolean, default: false }, addNote: { type: Boolean, default: false },
selectType: { type: Boolean, default: false }, selectType: { type: Boolean, default: false },
justInput: { type: Boolean, default: false }, justInput: { type: Boolean, default: false },
goTo: { type: String, default: '' },
useUserRelation: { type: Boolean, default: true },
}); });
const { t } = useI18n(); const { t } = useI18n();
const quasar = useQuasar(); const quasar = useQuasar();
const stateStore = useStateStore();
const router = useRouter();
const route = useRoute();
const componentIsRendered = ref(false);
const newNote = reactive({ text: null, observationTypeFk: null }); const newNote = reactive({ text: null, observationTypeFk: null });
const observationTypes = ref([]); const observationTypes = ref([]);
const vnPaginateRef = ref(); const vnPaginateRef = ref();
const defaultObservationType = computed( const defaultObservationType = computed(() =>
() => observationTypes.value.find((ot) => ot.code === 'salesPerson')?.id, observationTypes.value.find(ot => ot.code === 'salesPerson')?.id
); );
let savedNote = false;
let originalText; let originalText;
onBeforeRouteLeave((to, from, next) => {
if (
(newNote.text && !$props.justInput) ||
(newNote.text !== originalText && $props.justInput)
)
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('globals.unsavedPopup.title'),
message: t('globals.unsavedPopup.subtitle'),
promise: () => next(),
},
});
else next();
});
onMounted(() => {
nextTick(() => (componentIsRendered.value = true));
});
function handleClick(e) { function handleClick(e) {
if (e.shiftKey && e.key === 'Enter') return; if (e.shiftKey && e.key === 'Enter') return;
if ($props.justInput) confirmAndUpdate(); if ($props.justInput) confirmAndUpdate();
@ -96,7 +68,6 @@ async function insert() {
}; };
await axios.post($props.url, newBody); await axios.post($props.url, newBody);
await vnPaginateRef.value.fetch(); await vnPaginateRef.value.fetch();
savedNote = true;
} }
function confirmAndUpdate() { function confirmAndUpdate() {
@ -129,6 +100,22 @@ async function update() {
); );
} }
onBeforeRouteLeave((to, from, next) => {
if (
(newNote.text && !$props.justInput) ||
(newNote.text !== originalText && $props.justInput)
)
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('globals.unsavedPopup.title'),
message: t('globals.unsavedPopup.subtitle'),
promise: () => next(),
},
});
else next();
});
function fetchData([data]) { function fetchData([data]) {
newNote.text = data?.notes; newNote.text = data?.notes;
originalText = data?.notes; originalText = data?.notes;
@ -137,71 +124,13 @@ function fetchData([data]) {
const handleObservationTypes = (data) => { const handleObservationTypes = (data) => {
observationTypes.value = data; observationTypes.value = data;
if (defaultObservationType.value) { if(defaultObservationType.value) {
newNote.observationTypeFk = defaultObservationType.value; newNote.observationTypeFk = defaultObservationType.value;
} }
}; };
async function saveAndGo() {
savedNote = false;
await insert();
router.push({ path: $props.goTo });
}
function getUserFilter() {
const newUserFilter = $props.userFilter ?? {};
const userInclude = {
relation: 'user',
scope: {
fields: ['id', 'nickname', 'name'],
},
};
if (newUserFilter.include) {
if (Array.isArray(newUserFilter.include)) {
newUserFilter.include.push(userInclude);
} else {
newUserFilter.include = [userInclude, newUserFilter.include];
}
} else {
newUserFilter.include = userInclude;
}
if ($props.useUserRelation) {
return {
...newUserFilter,
...$props.userFilter,
};
}
return $props.filter;
}
</script> </script>
<template> <template>
<Teleport
to="#st-actions"
v-if="
stateStore?.isSubToolbarShown() &&
componentIsRendered &&
$props.goTo &&
!route.path.includes('summary')
"
>
<QBtn
:label="
tMobile('globals.saveAndContinue') +
' ' +
t('globals.' + $props.goTo.split('/').pop())
"
:title="
t('globals.saveAndContinue') +
' ' +
t('globals.' + $props.goTo.split('/').pop())
"
color="primary"
icon="save"
@click="saveAndGo"
data-cy="saveContinueNoteButton"
/>
</Teleport>
<FetchData <FetchData
v-if="selectType" v-if="selectType"
url="ObservationTypes" url="ObservationTypes"
@ -247,7 +176,7 @@ function getUserFilter() {
:required="'required' in originalAttrs" :required="'required' in originalAttrs"
clearable clearable
> >
<template #append v-if="!$props.goTo"> <template #append>
<QBtn <QBtn
:title="t('Save (Enter)')" :title="t('Save (Enter)')"
icon="save" icon="save"
@ -269,14 +198,16 @@ function getUserFilter() {
:url="$props.url" :url="$props.url"
order="created DESC" order="created DESC"
:limit="0" :limit="0"
:user-filter="getUserFilter()" :user-filter="userFilter"
:filter="filter" :filter="filter"
auto-load auto-load
ref="vnPaginateRef" ref="vnPaginateRef"
class="show" class="show"
v-bind="$attrs" v-bind="$attrs"
:search-url="false" :search-url="false"
@on-fetch="newNote.text = ''" @on-fetch="
newNote.text = '';
"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<TransitionGroup name="list" tag="div" class="column items-center full-width"> <TransitionGroup name="list" tag="div" class="column items-center full-width">
@ -288,15 +219,15 @@ function getUserFilter() {
<QCardSection horizontal> <QCardSection horizontal>
<VnAvatar <VnAvatar
:descriptor="false" :descriptor="false"
:worker-id="note.user?.id" :worker-id="note.workerFk"
size="md" size="md"
:title="note.user?.nickname" :title="note.worker?.user.nickname"
/> />
<div class="full-width row justify-between q-pa-xs"> <div class="full-width row justify-between q-pa-xs">
<div> <div>
<VnUserLink <VnUserLink
:name="`${note.user?.name}`" :name="`${note.worker.user.name}`"
:worker-id="note.user?.id" :worker-id="note.worker.id"
/> />
<QBadge <QBadge
class="q-ml-xs" class="q-ml-xs"

View File

@ -2,9 +2,7 @@
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useAttrs } from 'vue';
const attrs = useAttrs();
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
@ -69,7 +67,7 @@ const props = defineProps({
default: null, default: null,
}, },
searchUrl: { searchUrl: {
type: [String, Boolean], type: String,
default: null, default: null,
}, },
disableInfiniteScroll: { disableInfiniteScroll: {
@ -77,7 +75,7 @@ const props = defineProps({
default: false, default: false,
}, },
mapKey: { mapKey: {
type: [String, Boolean], type: String,
default: '', default: '',
}, },
keyData: { keyData: {
@ -146,14 +144,14 @@ const addFilter = async (filter, params) => {
}; };
async function fetch(params) { async function fetch(params) {
arrayData.setOptions(params); useArrayData(props.dataKey, params);
arrayData.resetPagination(); arrayData.resetPagination();
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
return emitStoreData(); return emitStoreData();
} }
async function update(params) { async function update(params) {
arrayData.setOptions(params); useArrayData(props.dataKey, params);
const { limit, skip } = store; const { limit, skip } = store;
store.limit = limit + skip; store.limit = limit + skip;
store.skip = 0; store.skip = 0;

View File

@ -46,7 +46,7 @@ const props = defineProps({
default: null, default: null,
}, },
order: { order: {
type: [String, Array], type: String,
default: '', default: '',
}, },
limit: { limit: {

View File

@ -87,7 +87,7 @@ function formatNumber(number) {
<QItemLabel caption>{{ <QItemLabel caption>{{
date.formatDate( date.formatDate(
row.sms.created, row.sms.created,
'YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD HH:mm:ss'
) )
}}</QItemLabel> }}</QItemLabel>
<QItemLabel class="row center"> <QItemLabel class="row center">

View File

@ -37,7 +37,8 @@ onBeforeUnmount(() => stateStore.toggleSubToolbar() && hasSubToolbar);
class="justify-end sticky" class="justify-end sticky"
> >
<slot name="st-data"> <slot name="st-data">
<div id="st-data" :class="{ 'full-width': !actionsChildCount() }"></div> <div id="st-data" :class="{ 'full-width': !actionsChildCount() }">
</div>
</slot> </slot>
<QSpace /> <QSpace />
<slot name="st-actions"> <slot name="st-actions">

View File

@ -23,15 +23,10 @@ describe('CardSummary', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createWrapper(CardSummary, { wrapper = createWrapper(CardSummary, {
global: {
mocks: {
validate: vi.fn(),
},
},
propsData: { propsData: {
dataKey: 'cardSummaryKey', dataKey: 'cardSummaryKey',
url: 'cardSummaryUrl', url: 'cardSummaryUrl',
filter: { key: 'cardFilter' }, filter: 'cardFilter',
}, },
}); });
vm = wrapper.vm; vm = wrapper.vm;
@ -55,7 +50,7 @@ describe('CardSummary', () => {
it('should set correct props to the store', () => { it('should set correct props to the store', () => {
expect(vm.store.url).toEqual('cardSummaryUrl'); expect(vm.store.url).toEqual('cardSummaryUrl');
expect(vm.store.filter).toEqual({ key: 'cardFilter' }); expect(vm.store.filter).toEqual('cardFilter');
}); });
it('should respond to prop changes and refetch data', async () => { it('should respond to prop changes and refetch data', async () => {

View File

@ -54,7 +54,7 @@ describe('tags computed property', () => {
const expectedStyle = { const expectedStyle = {
'grid-template-columns': 'repeat(2, 1fr)', 'grid-template-columns': 'repeat(2, 1fr)',
'max-width': '8rem', 'max-width': '8rem',
}; };
expect(vm.columnStyle).toEqual(expectedStyle); expect(vm.columnStyle).toEqual(expectedStyle);

View File

@ -9,29 +9,30 @@ const isEmployeeMock = vi.fn();
function generateWrapper(storage = 'images') { function generateWrapper(storage = 'images') {
wrapper = createWrapper(VnImg, { wrapper = createWrapper(VnImg, {
props: { props: {
id: 123, id: 123,
zoomResolution: '400x400', zoomResolution: '400x400',
storage, storage,
}, }
}); });
wrapper = wrapper.wrapper; wrapper = wrapper.wrapper;
vm = wrapper.vm; vm = wrapper.vm;
vm.timeStamp = 'timestamp'; vm.timeStamp = 'timestamp';
} };
vi.mock('src/composables/useSession', () => ({ vi.mock('src/composables/useSession', () => ({
useSession: () => ({ useSession: () => ({
getTokenMultimedia: () => 'token', getTokenMultimedia: () => 'token',
}), }),
})); }));
vi.mock('src/composables/useRole', () => ({ vi.mock('src/composables/useRole', () => ({
useRole: () => ({ useRole: () => ({
isEmployee: isEmployeeMock, isEmployee: isEmployeeMock,
}), }),
})); }));
describe('VnImg', () => {
describe('VnImg', () => {
beforeEach(() => { beforeEach(() => {
isEmployeeMock.mockReset(); isEmployeeMock.mockReset();
}); });
@ -46,13 +47,13 @@ describe('VnImg', () => {
generateWrapper('dms'); generateWrapper('dms');
await vm.$nextTick(); await vm.$nextTick();
const url = vm.getUrl(); const url = vm.getUrl();
expect(url).toBe('/api/dms/123/downloadFile?access_token=token'); expect(url).toBe('/api/dms/123/downloadFile?access_token=token');
}); });
it('should return /no-user.png when role is not employee and storage is not dms', async () => { it('should return /no-user.png when role is not employee and storage is not dms', async () => {
isEmployeeMock.mockReturnValue(false); isEmployeeMock.mockReturnValue(false);
generateWrapper(); generateWrapper();
await vm.$nextTick(); await vm.$nextTick();
const url = vm.getUrl(); const url = vm.getUrl();
expect(url).toBe('/no-user.png'); expect(url).toBe('/no-user.png');
}); });
@ -62,9 +63,7 @@ describe('VnImg', () => {
generateWrapper(); generateWrapper();
await vm.$nextTick(); await vm.$nextTick();
const url = vm.getUrl(); const url = vm.getUrl();
expect(url).toBe( expect(url).toBe('/api/images/catalog/200x200/123/download?access_token=token&timestamp');
'/api/images/catalog/200x200/123/download?access_token=token&timestamp',
);
}); });
it('should return /api/{storage}/{collection}/{curResolution}/{id}/download?access_token={token}&{timeStamp} when zoom is true and role is employee and storage is not dms', async () => { it('should return /api/{storage}/{collection}/{curResolution}/{id}/download?access_token={token}&{timeStamp} when zoom is true and role is employee and storage is not dms', async () => {
@ -72,9 +71,7 @@ describe('VnImg', () => {
generateWrapper(); generateWrapper();
await vm.$nextTick(); await vm.$nextTick();
const url = vm.getUrl(true); const url = vm.getUrl(true);
expect(url).toBe( expect(url).toBe('/api/images/catalog/400x400/123/download?access_token=token&timestamp');
'/api/images/catalog/400x400/123/download?access_token=token&timestamp',
);
}); });
}); });
@ -85,8 +82,8 @@ describe('VnImg', () => {
wrapper.vm.reload(); wrapper.vm.reload();
const newTimestamp = wrapper.vm.timeStamp; const newTimestamp = wrapper.vm.timeStamp;
expect(initialTimestamp).not.toEqual(newTimestamp); expect(initialTimestamp).not.toEqual(newTimestamp);
}); });
}); });
}); });

View File

@ -7,7 +7,7 @@ describe('VnSearchbar', () => {
let wrapper; let wrapper;
let applyFilterSpy; let applyFilterSpy;
const searchText = 'Bolas de madera'; const searchText = 'Bolas de madera';
const userParams = { staticKey: 'staticValue' }; const userParams = {staticKey: 'staticValue'};
beforeEach(async () => { beforeEach(async () => {
wrapper = createWrapper(VnSearchbar, { wrapper = createWrapper(VnSearchbar, {
@ -23,9 +23,8 @@ describe('VnSearchbar', () => {
vm.searchText = searchText; vm.searchText = searchText;
vm.arrayData.store.userParams = userParams; vm.arrayData.store.userParams = userParams;
applyFilterSpy = vi applyFilterSpy = vi.spyOn(vm.arrayData, 'applyFilter').mockImplementation(() => {});
.spyOn(vm.arrayData, 'applyFilter')
.mockImplementation(() => {});
}); });
afterEach(() => { afterEach(() => {
@ -33,9 +32,7 @@ describe('VnSearchbar', () => {
}); });
it('search resets pagination and applies filter', async () => { it('search resets pagination and applies filter', async () => {
const resetPaginationSpy = vi const resetPaginationSpy = vi.spyOn(vm.arrayData, 'resetPagination').mockImplementation(() => {});
.spyOn(vm.arrayData, 'resetPagination')
.mockImplementation(() => {});
await vm.search(); await vm.search();
expect(resetPaginationSpy).toHaveBeenCalled(); expect(resetPaginationSpy).toHaveBeenCalled();
@ -51,7 +48,7 @@ describe('VnSearchbar', () => {
expect(applyFilterSpy).toHaveBeenCalledWith({ expect(applyFilterSpy).toHaveBeenCalledWith({
params: { staticKey: 'staticValue', search: searchText }, params: { staticKey: 'staticValue', search: searchText },
filter: { skip: 0 }, filter: {skip: 0},
}); });
}); });
@ -71,4 +68,4 @@ describe('VnSearchbar', () => {
}); });
expect(vm.to.query.searchParam).toBe(expectedQuery); expect(vm.to.query.searchParam).toBe(expectedQuery);
}); });
}); });

View File

@ -1,4 +1,5 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import axios from 'axios';
import { createWrapper } from 'app/test/vitest/helper'; import { createWrapper } from 'app/test/vitest/helper';
import VnSms from 'src/components/ui/VnSms.vue'; import VnSms from 'src/components/ui/VnSms.vue';
@ -11,9 +12,6 @@ describe('VnSms', () => {
stubs: ['VnPaginate'], stubs: ['VnPaginate'],
mocks: {}, mocks: {},
}, },
propsData: {
url: 'SmsUrl',
},
}).vm; }).vm;
}); });

View File

@ -53,26 +53,26 @@ describe('useAcl', () => {
expect( expect(
acl.hasAny([ acl.hasAny([
{ model: 'Worker', props: 'updateAttributes', accessType: 'WRITE' }, { model: 'Worker', props: 'updateAttributes', accessType: 'WRITE' },
]), ])
).toBeFalsy(); ).toBeFalsy();
}); });
it('should return false if no roles matched', async () => { it('should return false if no roles matched', async () => {
expect( expect(
acl.hasAny([{ model: 'Worker', props: 'holidays', accessType: 'READ' }]), acl.hasAny([{ model: 'Worker', props: 'holidays', accessType: 'READ' }])
).toBeTruthy(); ).toBeTruthy();
}); });
describe('*', () => { describe('*', () => {
it('should return true if an acl matched', async () => { it('should return true if an acl matched', async () => {
expect( expect(
acl.hasAny([{ model: 'Address', props: '*', accessType: 'WRITE' }]), acl.hasAny([{ model: 'Address', props: '*', accessType: 'WRITE' }])
).toBeTruthy(); ).toBeTruthy();
}); });
it('should return false if no acls matched', async () => { it('should return false if no acls matched', async () => {
expect( expect(
acl.hasAny([{ model: 'Worker', props: '*', accessType: 'READ' }]), acl.hasAny([{ model: 'Worker', props: '*', accessType: 'READ' }])
).toBeFalsy(); ).toBeFalsy();
}); });
}); });
@ -80,15 +80,13 @@ describe('useAcl', () => {
describe('$authenticated', () => { describe('$authenticated', () => {
it('should return false if no acls matched', async () => { it('should return false if no acls matched', async () => {
expect( expect(
acl.hasAny([{ model: 'Url', props: 'getByUser', accessType: '*' }]), acl.hasAny([{ model: 'Url', props: 'getByUser', accessType: '*' }])
).toBeFalsy(); ).toBeFalsy();
}); });
it('should return true if an acl matched', async () => { it('should return true if an acl matched', async () => {
expect( expect(
acl.hasAny([ acl.hasAny([{ model: 'Url', props: 'getByUser', accessType: 'READ' }])
{ model: 'Url', props: 'getByUser', accessType: 'READ' },
]),
).toBeTruthy(); ).toBeTruthy();
}); });
}); });
@ -98,7 +96,7 @@ describe('useAcl', () => {
expect( expect(
acl.hasAny([ acl.hasAny([
{ model: 'TpvTransaction', props: 'start', accessType: 'READ' }, { model: 'TpvTransaction', props: 'start', accessType: 'READ' },
]), ])
).toBeFalsy(); ).toBeFalsy();
}); });
@ -106,7 +104,7 @@ describe('useAcl', () => {
expect( expect(
acl.hasAny([ acl.hasAny([
{ model: 'TpvTransaction', props: 'start', accessType: 'WRITE' }, { model: 'TpvTransaction', props: 'start', accessType: 'WRITE' },
]), ])
).toBeTruthy(); ).toBeTruthy();
}); });
}); });

View File

@ -4,8 +4,6 @@ import { useArrayData } from 'composables/useArrayData';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import * as vueRouter from 'vue-router'; import * as vueRouter from 'vue-router';
import { setActivePinia, createPinia } from 'pinia'; import { setActivePinia, createPinia } from 'pinia';
import { defineComponent, h } from 'vue';
import { mount } from '@vue/test-utils';
describe('useArrayData', () => { describe('useArrayData', () => {
const filter = '{"limit":20,"skip":0}'; const filter = '{"limit":20,"skip":0}';
@ -45,7 +43,7 @@ describe('useArrayData', () => {
it('should fetch and replace url with new params', async () => { it('should fetch and replace url with new params', async () => {
vi.spyOn(axios, 'get').mockResolvedValueOnce({ data: [] }); vi.spyOn(axios, 'get').mockResolvedValueOnce({ data: [] });
const arrayData = mountArrayData('ArrayData', { const arrayData = useArrayData('ArrayData', {
url: 'mockUrl', url: 'mockUrl',
searchUrl: 'params', searchUrl: 'params',
}); });
@ -74,7 +72,7 @@ describe('useArrayData', () => {
data: [{ id: 1 }], data: [{ id: 1 }],
}); });
const arrayData = mountArrayData('ArrayData', { const arrayData = useArrayData('ArrayData', {
url: 'mockUrl', url: 'mockUrl',
navigate: {}, navigate: {},
}); });
@ -96,7 +94,7 @@ describe('useArrayData', () => {
], ],
}); });
const arrayData = mountArrayData('ArrayData', { const arrayData = useArrayData('ArrayData', {
url: 'mockUrl', url: 'mockUrl',
oneRecord: true, oneRecord: true,
}); });
@ -109,17 +107,3 @@ describe('useArrayData', () => {
}); });
}); });
}); });
function mountArrayData(...args) {
let arrayData;
const TestComponent = defineComponent({
setup() {
arrayData = useArrayData(...args);
return () => h('div');
},
});
const asd = mount(TestComponent);
return arrayData;
}

View File

@ -64,84 +64,88 @@ describe('session', () => {
}); });
}); });
describe('login', () => { describe(
const expectedUser = { 'login',
id: 999, () => {
name: `T'Challa`, const expectedUser = {
nickname: 'Black Panther', id: 999,
lang: 'en', name: `T'Challa`,
userConfig: { nickname: 'Black Panther',
darkMode: false, lang: 'en',
}, userConfig: {
worker: { department: { departmentFk: 155 } }, darkMode: false,
};
const rolesData = [
{
role: {
name: 'salesPerson',
}, },
}, worker: { department: { departmentFk: 155 } },
{ };
role: { const rolesData = [
name: 'admin', {
role: {
name: 'salesPerson',
},
}, },
}, {
]; role: {
beforeEach(() => { name: 'admin',
vi.spyOn(axios, 'get').mockImplementation((url) => { },
if (url === 'VnUsers/acls') return Promise.resolve({ data: [] }); },
return Promise.resolve({ ];
data: { roles: rolesData, user: expectedUser }, beforeEach(() => {
vi.spyOn(axios, 'get').mockImplementation((url) => {
if (url === 'VnUsers/acls') return Promise.resolve({ data: [] });
return Promise.resolve({
data: { roles: rolesData, user: expectedUser },
});
}); });
}); });
});
it('should fetch the user roles and then set token in the sessionStorage', async () => { it('should fetch the user roles and then set token in the sessionStorage', async () => {
const expectedRoles = ['salesPerson', 'admin']; const expectedRoles = ['salesPerson', 'admin'];
const expectedToken = 'mySessionToken'; const expectedToken = 'mySessionToken';
const expectedTokenMultimedia = 'mySessionTokenMultimedia'; const expectedTokenMultimedia = 'mySessionTokenMultimedia';
const keepLogin = false; const keepLogin = false;
await session.login({ await session.login({
token: expectedToken, token: expectedToken,
tokenMultimedia: expectedTokenMultimedia, tokenMultimedia: expectedTokenMultimedia,
keepLogin, keepLogin,
});
const roles = state.getRoles();
const localToken = localStorage.getItem('token');
const sessionToken = sessionStorage.getItem('token');
expect(roles.value).toEqual(expectedRoles);
expect(localToken).toBeNull();
expect(sessionToken).toEqual(expectedToken);
await session.destroy(); // this clears token and user for any other test
}); });
const roles = state.getRoles(); it('should fetch the user roles and then set token in the localStorage', async () => {
const localToken = localStorage.getItem('token'); const expectedRoles = ['salesPerson', 'admin'];
const sessionToken = sessionStorage.getItem('token'); const expectedToken = 'myLocalToken';
const expectedTokenMultimedia = 'myLocalTokenMultimedia';
const keepLogin = true;
expect(roles.value).toEqual(expectedRoles); await session.login({
expect(localToken).toBeNull(); token: expectedToken,
expect(sessionToken).toEqual(expectedToken); tokenMultimedia: expectedTokenMultimedia,
keepLogin,
});
await session.destroy(); // this clears token and user for any other test const roles = state.getRoles();
}); const localToken = localStorage.getItem('token');
const sessionToken = sessionStorage.getItem('token');
it('should fetch the user roles and then set token in the localStorage', async () => { expect(roles.value).toEqual(expectedRoles);
const expectedRoles = ['salesPerson', 'admin']; expect(localToken).toEqual(expectedToken);
const expectedToken = 'myLocalToken'; expect(sessionToken).toBeNull();
const expectedTokenMultimedia = 'myLocalTokenMultimedia';
const keepLogin = true;
await session.login({ await session.destroy(); // this clears token and user for any other test
token: expectedToken,
tokenMultimedia: expectedTokenMultimedia,
keepLogin,
}); });
},
const roles = state.getRoles(); {},
const localToken = localStorage.getItem('token'); );
const sessionToken = sessionStorage.getItem('token');
expect(roles.value).toEqual(expectedRoles);
expect(localToken).toEqual(expectedToken);
expect(sessionToken).toBeNull();
await session.destroy(); // this clears token and user for any other test
});
});
describe('RenewToken', () => { describe('RenewToken', () => {
const expectedToken = 'myToken'; const expectedToken = 'myToken';

View File

@ -18,7 +18,7 @@ export async function downloadFile(id, model = 'dms', urlPath = '/downloadFile',
export async function downloadDocuware(url, params) { export async function downloadDocuware(url, params) {
const appUrl = await getAppUrl(); const appUrl = await getAppUrl();
const response = await axios.get(`${appUrl}/api/${url}`, { const response = await axios.get(`${appUrl}/api/` + url, {
responseType: 'blob', responseType: 'blob',
params, params,
}); });

View File

@ -20,5 +20,5 @@ export function getColAlign(col) {
if (/^is[A-Z]/.test(col.name) || /^has[A-Z]/.test(col.name)) align = 'center'; if (/^is[A-Z]/.test(col.name) || /^has[A-Z]/.test(col.name)) align = 'center';
return `text-${align ?? 'center'}`; return 'text-' + (align ?? 'center');
} }

View File

@ -1,10 +1,10 @@
export function getDateQBadgeColor(date) { export function getDateQBadgeColor(date) {
const today = Date.vnNew(); let today = Date.vnNew();
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
const timeTicket = new Date(date); let timeTicket = new Date(date);
timeTicket.setHours(0, 0, 0, 0); timeTicket.setHours(0, 0, 0, 0);
const comparation = today - timeTicket; let comparation = today - timeTicket;
if (comparation == 0) return 'warning'; if (comparation == 0) return 'warning';
if (comparation < 0) return 'success'; if (comparation < 0) return 'success';

View File

@ -8,4 +8,4 @@ export function getValueFromPath(root, path) {
else current = current[key]; else current = current[key];
} }
return current; return current;
} }

View File

@ -1,9 +0,0 @@
import { ref } from 'vue';
import moment from 'moment';
export default function useWeekdaysOrder() {
const firstDay = moment().weekday(1).day();
const weekdays = [...Array(7).keys()].map((i) => (i + firstDay) % 7);
return ref(weekdays);
}

View File

@ -7,7 +7,7 @@ export async function beforeSave(data, getChanges, modelOrigin) {
const patchPromises = []; const patchPromises = [];
for (const change of changes) { for (const change of changes) {
const patchData = {}; let patchData = {};
if ('hasMinPrice' in change.data) { if ('hasMinPrice' in change.data) {
patchData.hasMinPrice = change.data?.hasMinPrice; patchData.hasMinPrice = change.data?.hasMinPrice;

View File

@ -19,7 +19,7 @@ export function useArrayData(key, userOptions) {
let canceller = null; let canceller = null;
onMounted(() => { onMounted(() => {
setOptions(userOptions ?? {}); setOptions();
reset(['skip']); reset(['skip']);
const query = route.query; const query = route.query;
@ -39,10 +39,9 @@ export function useArrayData(key, userOptions) {
setCurrentFilter(); setCurrentFilter();
}); });
if (userOptions) setOptions(userOptions); if (key && userOptions) setOptions();
function setOptions(params) { function setOptions() {
if (!params) return;
const allowedOptions = [ const allowedOptions = [
'url', 'url',
'filter', 'filter',
@ -58,14 +57,14 @@ export function useArrayData(key, userOptions) {
'mapKey', 'mapKey',
'oneRecord', 'oneRecord',
]; ];
if (typeof params === 'object') { if (typeof userOptions === 'object') {
for (const option in params) { for (const option in userOptions) {
const isEmpty = params[option] == null || params[option] === ''; const isEmpty = userOptions[option] == null || userOptions[option] === '';
if (isEmpty || !allowedOptions.includes(option)) continue; if (isEmpty || !allowedOptions.includes(option)) continue;
if (Object.hasOwn(store, option)) { if (Object.hasOwn(store, option)) {
const defaultOpts = params[option]; const defaultOpts = userOptions[option];
store[option] = params.keepOpts?.includes(option) store[option] = userOptions.keepOpts?.includes(option)
? Object.assign(defaultOpts, store[option]) ? Object.assign(defaultOpts, store[option])
: defaultOpts; : defaultOpts;
if (option === 'userParams') store.defaultParams = store[option]; if (option === 'userParams') store.defaultParams = store[option];
@ -368,6 +367,5 @@ export function useArrayData(key, userOptions) {
deleteOption, deleteOption,
reset, reset,
resetPagination, resetPagination,
setOptions,
}; };
} }

View File

@ -1,6 +1,7 @@
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
export default function () { export default function() {
const quasar = useQuasar(); const quasar = useQuasar();
return quasar.screen.gt.xs ? 'q-pa-md' : 'q-pa-xs'; return quasar.screen.gt.xs ? 'q-pa-md': 'q-pa-xs';
} }

View File

@ -6,7 +6,7 @@ export function djb2a(string) {
} }
export function useColor(value) { export function useColor(value) {
return `#${colors[djb2a(value || '') % colors.length]}`; return '#' + colors[djb2a(value || '') % colors.length];
} }
const colors = [ const colors = [

View File

@ -15,16 +15,18 @@ export function usePrintService() {
message: t('globals.notificationSent'), message: t('globals.notificationSent'),
type: 'positive', type: 'positive',
icon: 'check', icon: 'check',
}), })
); );
} }
function openReport(path, params, isNewTab = '_self') { function openReport(path, params, isNewTab = '_self') {
if (typeof params === 'string') params = JSON.parse(params); if (typeof params === 'string') params = JSON.parse(params);
params = { params = Object.assign(
access_token: getTokenMultimedia(), {
...params, access_token: getTokenMultimedia(),
}; },
params
);
const query = new URLSearchParams(params).toString(); const query = new URLSearchParams(params).toString();
window.open(`api/${path}?${query}`, isNewTab); window.open(`api/${path}?${query}`, isNewTab);

View File

@ -17,7 +17,7 @@ export function useSession() {
let intervalId = null; let intervalId = null;
function setSession(data) { function setSession(data) {
const keepLogin = data.keepLogin; let keepLogin = data.keepLogin;
const storage = keepLogin ? localStorage : sessionStorage; const storage = keepLogin ? localStorage : sessionStorage;
storage.setItem(TOKEN, data.token); storage.setItem(TOKEN, data.token);
storage.setItem(TOKEN_MULTIMEDIA, data.tokenMultimedia); storage.setItem(TOKEN_MULTIMEDIA, data.tokenMultimedia);

View File

@ -47,7 +47,9 @@ export function useValidator() {
return !validator.isEmpty(value ? String(value) : '') || message; return !validator.isEmpty(value ? String(value) : '') || message;
}, },
required: (required, value) => { required: (required, value) => {
return required ? !!value || t('globals.fieldRequired') : null; return required
? value === 0 || !!value || t('globals.fieldRequired')
: null;
}, },
length: (value) => { length: (value) => {
const options = { const options = {

View File

@ -8,7 +8,7 @@ export function useVnConfirm() {
message, message,
promise, promise,
successFn, successFn,
customHTML = {}, customHTML = {}
) => { ) => {
const { component, props } = customHTML; const { component, props } = customHTML;
Dialog.create({ Dialog.create({
@ -19,7 +19,7 @@ export function useVnConfirm() {
message: message, message: message,
promise: promise, promise: promise,
}, },
{ customHTML: () => h(component, props) }, { customHTML: () => h(component, props) }
), ),
}).onOk(async () => { }).onOk(async () => {
if (successFn) successFn(); if (successFn) successFn();

View File

@ -343,20 +343,3 @@ input::-webkit-inner-spin-button {
.q-item__section--main ~ .q-item__section--side { .q-item__section--main ~ .q-item__section--side {
padding-inline: 0; padding-inline: 0;
} }
.calendars-header {
height: 45px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: $primary;
font-weight: bold;
font-size: 16px;
}
.calendars-container {
max-width: 800px;
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
}

File diff suppressed because one or more lines are too long

View File

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

View File

@ -30,12 +30,10 @@ export function isValidDate(date) {
export function toDateFormat(date, locale = 'es-ES', opts = {}) { export function toDateFormat(date, locale = 'es-ES', opts = {}) {
if (!isValidDate(date)) return ''; if (!isValidDate(date)) return '';
const format = { const format = Object.assign(
year: 'numeric', { year: 'numeric', month: '2-digit', day: '2-digit' },
month: '2-digit', opts
day: '2-digit', );
...opts,
};
return new Date(date).toLocaleDateString(locale, format); return new Date(date).toLocaleDateString(locale, format);
} }
@ -106,17 +104,17 @@ export function secondsToHoursMinutes(seconds, includeHSuffix = true) {
const hours = Math.floor(seconds / 3600); const hours = Math.floor(seconds / 3600);
const remainingMinutes = seconds % 3600; const remainingMinutes = seconds % 3600;
const minutes = Math.floor(remainingMinutes / 60); const minutes = Math.floor(remainingMinutes / 60);
const formattedHours = hours < 10 ? `0${hours}` : hours; const formattedHours = hours < 10 ? '0' + hours : hours;
const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes; const formattedMinutes = minutes < 10 ? '0' + minutes : minutes;
// Append "h." if includeHSuffix is true // Append "h." if includeHSuffix is true
const suffix = includeHSuffix ? ' h.' : ''; const suffix = includeHSuffix ? ' h.' : '';
// Return formatted string // Return formatted string
return `${formattedHours}:${formattedMinutes}${suffix}`; return formattedHours + ':' + formattedMinutes + suffix;
} }
export function getTimeDifferenceWithToday(date) { export function getTimeDifferenceWithToday(date) {
const today = Date.vnNew(); let today = Date.vnNew();
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
date = new Date(date); date = new Date(date);

View File

@ -5,12 +5,12 @@
* @return {Object} The fields as object * @return {Object} The fields as object
*/ */
function fieldsToObject(fields) { function fieldsToObject(fields) {
const fieldsObj = {}; let fieldsObj = {};
if (Array.isArray(fields)) { if (Array.isArray(fields)) {
for (const field of fields) fieldsObj[field] = true; for (let field of fields) fieldsObj[field] = true;
} else if (typeof fields == 'object') { } else if (typeof fields == 'object') {
for (const field in fields) { for (let field in fields) {
if (fields[field]) fieldsObj[field] = true; if (fields[field]) fieldsObj[field] = true;
} }
} }
@ -26,7 +26,7 @@ function fieldsToObject(fields) {
* @return {Array} The merged fields as an array * @return {Array} The merged fields as an array
*/ */
function mergeFields(src, dst) { function mergeFields(src, dst) {
const fields = {}; let fields = {};
Object.assign(fields, fieldsToObject(src), fieldsToObject(dst)); Object.assign(fields, fieldsToObject(src), fieldsToObject(dst));
return Object.keys(fields); return Object.keys(fields);
} }
@ -39,7 +39,7 @@ function mergeFields(src, dst) {
* @return {Array} The merged wheres * @return {Array} The merged wheres
*/ */
function mergeWhere(src, dst) { function mergeWhere(src, dst) {
const and = []; let and = [];
if (src) and.push(src); if (src) and.push(src);
if (dst) and.push(dst); if (dst) and.push(dst);
return simplifyOperation(and, 'and'); return simplifyOperation(and, 'and');
@ -53,7 +53,7 @@ function mergeWhere(src, dst) {
* @return {Object} The result filter * @return {Object} The result filter
*/ */
function mergeFilters(src, dst) { function mergeFilters(src, dst) {
const res = { ...dst }; let res = Object.assign({}, dst);
if (!src) return res; if (!src) return res;
@ -80,12 +80,12 @@ function simplifyOperation(operation, operator) {
} }
function buildFilter(params, builderFunc) { function buildFilter(params, builderFunc) {
const and = []; let and = [];
for (const param in params) { for (let param in params) {
const value = params[param]; let value = params[param];
if (value == null) continue; if (value == null) continue;
const expr = builderFunc(param, value); let expr = builderFunc(param, value);
if (expr) and.push(expr); if (expr) and.push(expr);
} }
return simplifyOperation(and, 'and'); return simplifyOperation(and, 'and');

View File

@ -1,14 +1,14 @@
export default function getDifferences(obj1, obj2) { export default function getDifferences(obj1, obj2) {
const diff = {}; let diff = {};
delete obj1.$index; delete obj1.$index;
delete obj2.$index; delete obj2.$index;
for (const key in obj1) { for (let key in obj1) {
if (obj2[key] && JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) { if (obj2[key] && JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {
diff[key] = obj2[key]; diff[key] = obj2[key];
} }
} }
for (const key in obj2) { for (let key in obj2) {
if ( if (
obj1[key] === undefined || obj1[key] === undefined ||
JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key]) JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])

View File

@ -6,7 +6,6 @@ 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';
import toNumber from './toNumber';
import toLowerCamel from './toLowerCamel'; import toLowerCamel from './toLowerCamel';
import dashIfEmpty from './dashIfEmpty'; import dashIfEmpty from './dashIfEmpty';
import dateRange from './dateRange'; import dateRange from './dateRange';
@ -35,7 +34,6 @@ export {
toRelativeDate, toRelativeDate,
toCurrency, toCurrency,
toPercentage, toPercentage,
toNumber,
dashIfEmpty, dashIfEmpty,
dateRange, dateRange,
getParamWhere, getParamWhere,

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