diff --git a/.eslintrc.js b/.eslintrc.js index c8bdecb1a..1d09a896f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -58,7 +58,7 @@ module.exports = { rules: { 'prefer-promise-reject-errors': 'off', 'no-unused-vars': 'warn', - + "vue/no-multiple-template-root": "off" , // allow debugger during development only 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', }, diff --git a/Jenkinsfile b/Jenkinsfile index 437332c4e..9dd72ccc3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -96,4 +96,4 @@ pipeline { } } } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 9db93eff3..ce9c4d7c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "salix-front", - "version": "23.52.01", + "version": "24.02.01", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "salix-front", - "version": "23.52.01", + "version": "24.02.01", "dependencies": { "@quasar/cli": "^2.3.0", "@quasar/extras": "^1.16.4", "axios": "^1.4.0", "chromium": "^3.0.3", + "croppie": "^2.6.5", "pinia": "^2.1.3", "quasar": "^2.12.0", "validator": "^13.9.0", @@ -3169,6 +3170,11 @@ "node": ">= 10" } }, + "node_modules/croppie": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/croppie/-/croppie-2.6.5.tgz", + "integrity": "sha512-IlChnVUGG5T3w2gRZIaQgBtlvyuYnlUWs2YZIXXR3H9KrlO1PtBT3j+ykxvy9eZIWhk+V5SpBmhCQz5UXKrEKQ==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", diff --git a/package.json b/package.json index 7c966dbb1..27ba190a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salix-front", - "version": "24.00.01", + "version": "24.8.0", "description": "Salix frontend", "productName": "Salix", "author": "Verdnatura", @@ -19,13 +19,13 @@ "@quasar/extras": "^1.16.4", "axios": "^1.4.0", "chromium": "^3.0.3", + "croppie": "^2.6.5", "pinia": "^2.1.3", "quasar": "^2.12.0", "validator": "^13.9.0", "vue": "^3.3.4", "vue-i18n": "^9.2.2", - "vue-router": "^4.2.1", - "vue-router-mock": "^0.2.0" + "vue-router": "^4.2.1" }, "devDependencies": { "@intlify/unplugin-vue-i18n": "^0.8.1", diff --git a/src/App.vue b/src/App.vue index 8e9bea2e4..d0d8c9358 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,10 +1,11 @@ + + + + +en: + title: New bank entity + subtitle: Please, ensure you put the correct data! + name: Name + swift: Swift + country: Country + id: Entity code +es: + title: Nueva entidad bancaria + subtitle: ¡Por favor, asegúrate de poner los datos correctos! + name: Nombre + swift: Swift + country: País + id: Código de la entidad + diff --git a/src/components/CreateDepartmentChild.vue b/src/components/CreateDepartmentChild.vue new file mode 100644 index 000000000..8f5b4b874 --- /dev/null +++ b/src/components/CreateDepartmentChild.vue @@ -0,0 +1,100 @@ + + + + + + + +es: + Name: Nombre + New department: Nuevo departamento + diff --git a/src/components/CreateNewCityForm.vue b/src/components/CreateNewCityForm.vue new file mode 100644 index 000000000..7326ea7a5 --- /dev/null +++ b/src/components/CreateNewCityForm.vue @@ -0,0 +1,72 @@ + + + + + +es: + New city: Nueva ciudad + Please, ensure you put the correct data!: ¡Por favor, asegúrese de poner los datos correctos! + Name: Nombre + Province: Provincia + diff --git a/src/components/CreateNewPostcodeForm.vue b/src/components/CreateNewPostcodeForm.vue new file mode 100644 index 000000000..ccffb2ec2 --- /dev/null +++ b/src/components/CreateNewPostcodeForm.vue @@ -0,0 +1,148 @@ + + + + + +es: + New postcode: Nuevo código postal + Please, ensure you put the correct data!: ¡Por favor, asegúrese de poner los datos correctos! + City: Población + Province: Provincia + Country: País + Postcode: Código postal + diff --git a/src/components/CreateNewProvinceForm.vue b/src/components/CreateNewProvinceForm.vue new file mode 100644 index 000000000..b972db2c9 --- /dev/null +++ b/src/components/CreateNewProvinceForm.vue @@ -0,0 +1,72 @@ + + + + + +es: + New province: Nueva provincia + Please, ensure you put the correct data!: ¡Por favor, asegúrese de poner los datos correctos! + Name: Nombre + Autonomy: Autonomía + diff --git a/src/components/CrudModel.vue b/src/components/CrudModel.vue index dcffc1a28..fde87bf58 100644 --- a/src/components/CrudModel.vue +++ b/src/components/CrudModel.vue @@ -231,15 +231,19 @@ function getDifferences(obj1, obj2) { delete obj2.$index; for (let key in obj1) { - if (obj2[key] && obj1[key] !== obj2[key]) { + if (obj2[key] && JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) { diff[key] = obj2[key]; } } for (let key in obj2) { - if (obj1[key] === undefined || obj1[key] !== obj2[key]) { + if ( + obj1[key] === undefined || + JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key]) + ) { diff[key] = obj2[key]; } } + return diff; } diff --git a/src/components/EditPictureForm.vue b/src/components/EditPictureForm.vue new file mode 100644 index 000000000..44d44587f --- /dev/null +++ b/src/components/EditPictureForm.vue @@ -0,0 +1,363 @@ + + + + + + + +es: + Edit photo: Editar foto + Select from computer: Seleccionar desde ordenador + Import from external URL: Importar desde URL externa + Vertical: Vertical + Normal: Normal + Panoramic: Panorámica + Orientation: Orientación + File: Fichero + This photo provider doesn't allow remote downloads: Este proveedor de fotos no permite descargas remotas + Rotate left: Girar a la izquierda + Rotate right: Girar a la derecha + Select an image: Selecciona una imagen + diff --git a/src/components/FetchData.vue b/src/components/FetchData.vue index f0d908972..4f5d7a57d 100644 --- a/src/components/FetchData.vue +++ b/src/components/FetchData.vue @@ -27,6 +27,10 @@ const $props = defineProps({ type: String, default: '', }, + params: { + type: Object, + default: null, + }, }); const emit = defineEmits(['onFetch']); @@ -38,18 +42,19 @@ onMounted(async () => { } }); -async function fetch() { +async function fetch(fetchFilter = {}) { try { - const filter = Object.assign({}, $props.filter); // eslint-disable-line vue/no-dupe-keys - if ($props.where) filter.where = $props.where; + const filter = Object.assign(fetchFilter, $props.filter); // eslint-disable-line vue/no-dupe-keys + if ($props.where && !fetchFilter.where) filter.where = $props.where; if ($props.sortBy) filter.order = $props.sortBy; if ($props.limit) filter.limit = $props.limit; const { data } = await axios.get($props.url, { - params: { filter: JSON.stringify(filter) }, + params: { filter: JSON.stringify(filter), ...$props.params }, }); emit('onFetch', data); + return data; } catch (e) { // } diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 453b3ffe1..594780220 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -55,9 +55,13 @@ const $props = defineProps({ description: 'Esto se usa principalmente para permitir guardar sin hacer cambios (Útil para la feature de clonar ya que en este caso queremos poder guardar de primeras)', }, + mapper: { + type: Function, + default: null, + }, }); -const emit = defineEmits(['onFetch']); +const emit = defineEmits(['onFetch', 'onDataSaved']); defineExpose({ save, @@ -71,9 +75,12 @@ onMounted(async () => { await fetch(); } - // Disparamos el watcher del form después de que se haya cargado la data inicial, si así se desea + // Si así se desea disparamos el watcher del form después de 100ms, asi darle tiempo de que se haya cargado la data inicial + // para evitar que detecte cambios cuando es data inicial default if ($props.observeFormChanges) { - startFormWatcher(); + setTimeout(() => { + startFormWatcher(); + }, 100); } }); @@ -83,8 +90,9 @@ onUnmounted(() => { const isLoading = ref(false); // Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas +const isResetting = ref(false); const hasChanges = ref(!$props.observeFormChanges); -const originalData = ref({...$props.formInitialData}); +const originalData = ref({ ...$props.formInitialData }); const formData = computed(() => state.get($props.model)); const formUrl = computed(() => $props.url); @@ -92,7 +100,8 @@ const startFormWatcher = () => { watch( () => formData.value, (val) => { - if (val) hasChanges.value = true; + hasChanges.value = !isResetting.value && val; + isResetting.value = false; }, { deep: true } ); @@ -114,25 +123,27 @@ async function fetch() { } async function save() { - if (!hasChanges.value) { + if ($props.observeFormChanges && !hasChanges.value) { notify('globals.noChanges', 'negative'); return; } isLoading.value = true; try { + const body = $props.mapper ? $props.mapper(formData.value) : formData.value; + let response; if ($props.urlCreate) { - await axios.post($props.urlCreate, formData.value); + response = await axios.post($props.urlCreate, body); notify('globals.dataCreated', 'positive'); } else { - await axios.patch($props.urlUpdate || $props.url, formData.value); + response = await axios.patch($props.urlUpdate || $props.url, body); } + emit('onDataSaved', formData.value, response?.data); + originalData.value = JSON.parse(JSON.stringify(formData.value)); + hasChanges.value = false; } catch (err) { notify('errors.create', 'negative'); } - - originalData.value = JSON.parse(JSON.stringify(formData.value)); - hasChanges.value = false; isLoading.value = false; } @@ -143,6 +154,7 @@ function reset() { emit('onFetch', state.get($props.model)); if ($props.observeFormChanges) { hasChanges.value = false; + isResetting.value = true; } } @@ -168,11 +180,7 @@ watch(formUrl, async () => { }); diff --git a/src/components/RegularizeStockForm.vue b/src/components/RegularizeStockForm.vue new file mode 100644 index 000000000..28236be17 --- /dev/null +++ b/src/components/RegularizeStockForm.vue @@ -0,0 +1,81 @@ + + + + + +es: + Warehouse: Almacén + Type the visible quantity: Introduce la cantidad visible + Regularize stock: Regularizar stock + diff --git a/src/components/UserPanel.vue b/src/components/UserPanel.vue index aee12d105..e0b6b86ed 100644 --- a/src/components/UserPanel.vue +++ b/src/components/UserPanel.vue @@ -4,15 +4,16 @@ import { Dark, Quasar } from 'quasar'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; import axios from 'axios'; - import { useState } from 'src/composables/useState'; import { useSession } from 'src/composables/useSession'; +import { localeEquivalence } from 'src/i18n/index'; const state = useState(); const session = useSession(); const router = useRouter(); const { t, locale } = useI18n(); - +import { useClipboard } from 'src/composables/useClipboard'; +const { copyText } = useClipboard(); const userLocale = computed({ get() { return locale.value; @@ -20,13 +21,11 @@ const userLocale = computed({ set(value) { locale.value = value; - if (value === 'en') value = 'en-GB'; + value = localeEquivalence[value] ?? value; - // FIXME: Dynamic imports from absolute paths are not compatible with vite: - // https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations try { - const langList = import.meta.glob('../../node_modules/quasar/lang/*.mjs'); - langList[`../../node_modules/quasar/lang/${value}.mjs`]().then((lang) => { + /* @vite-ignore */ + import(`../../node_modules/quasar/lang/${value}.mjs`).then((lang) => { Quasar.lang.set(lang.default); }); } catch (error) { @@ -82,8 +81,8 @@ function logout() { router.push('/login'); } -function copyUserToken(){ - navigator.clipboard.writeText(session.getToken()); +function copyUserToken() { + copyText(session.getToken(), { label: 'components.userPanel.copyToken' }); } @@ -126,8 +125,12 @@ function copyUserToken(){
{{ user.nickname }}
-
@{{ user.name }} -
+
+ @{{ user.name }} +
diff --git a/src/components/common/SendEmailDialog.vue b/src/components/common/SendEmailDialog.vue index 0f574a795..501c48a4d 100644 --- a/src/components/common/SendEmailDialog.vue +++ b/src/components/common/SendEmailDialog.vue @@ -1,7 +1,9 @@ + + + + + + es: + Check the columns you want to see: Marca las columnas que quieres ver + Visible columns: Columnas visibles + diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue new file mode 100644 index 000000000..76e91a5d8 --- /dev/null +++ b/src/components/common/VnInput.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/components/common/VnInputDate.vue b/src/components/common/VnInputDate.vue index a8e0d4a43..8e0ef2890 100644 --- a/src/components/common/VnInputDate.vue +++ b/src/components/common/VnInputDate.vue @@ -10,7 +10,11 @@ const props = defineProps({ readonly: { type: Boolean, default: false, - } + }, + isOutlined: { + type: Boolean, + default: false, + }, }); const emit = defineEmits(['update:modelValue']); const value = computed({ @@ -36,6 +40,16 @@ const formatDate = (dateString) => { date.getDate() )}`; }; + +const styleAttrs = computed(() => { + return props.isOutlined + ? { + dense: true, + outlined: true, + rounded: true, + } + : {}; +}); diff --git a/src/components/common/VnSelectCreate.vue b/src/components/common/VnSelectCreate.vue new file mode 100644 index 000000000..bdc3f5cc8 --- /dev/null +++ b/src/components/common/VnSelectCreate.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/src/components/common/VnSelectFilter.vue b/src/components/common/VnSelectFilter.vue index 35cb72cf2..accf92fc6 100644 --- a/src/components/common/VnSelectFilter.vue +++ b/src/components/common/VnSelectFilter.vue @@ -1,5 +1,7 @@