diff --git a/.eslintrc.js b/.eslintrc.js index 09dc09c1e..c8bdecb1a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -64,7 +64,7 @@ module.exports = { }, overrides: [ { - files: ['test/cypress/**/*.spec.{js,ts}'], + files: ['test/cypress/**/*.*'], extends: [ // Add Cypress-specific lint rules, globals and Cypress plugin // See https://github.com/cypress-io/eslint-plugin-cypress#rules diff --git a/cypress.config.js b/cypress.config.js index 31aad6a86..2b5b40d08 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -7,7 +7,7 @@ module.exports = defineConfig({ screenshotsFolder: 'test/cypress/screenshots', supportFile: 'test/cypress/support/index.js', videosFolder: 'test/cypress/videos', - video: true, + video: false, specPattern: 'test/cypress/integration/*.spec.js', experimentalRunAllSpecs: true, component: { diff --git a/package-lock.json b/package-lock.json index 2e96d2ccf..a3a9dcc63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "salix-front", - "version": "23.36.01", + "version": "23.40.01", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index b713c906a..3e26b483b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salix-front", - "version": "23.36.01", + "version": "23.40.01", "description": "Salix frontend", "productName": "Salix", "author": "Verdnatura", @@ -9,7 +9,7 @@ "lint": "eslint --ext .js,.vue ./", "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore", "test:e2e": "cypress open", - "test:e2e:ci": "cypress run --browser chromium", + "test:e2e:ci": "cd ../salix && gulp docker && cd ../salix-front && cypress run --browser chromium", "test": "echo \"See package.json => scripts for available tests.\" && exit 0", "test:unit": "vitest", "test:unit:ci": "vitest run" diff --git a/src/boot/axios.js b/src/boot/axios.js index bdc661ae2..c58cc2d08 100644 --- a/src/boot/axios.js +++ b/src/boot/axios.js @@ -46,7 +46,7 @@ const onResponseError = (error) => { message = responseError.message; } - switch (response.status) { + switch (response?.status) { case 500: message = 'errors.statusInternalServerError'; break; @@ -58,7 +58,7 @@ const onResponseError = (error) => { break; } - if (session.isLoggedIn() && response.status === 401) { + if (session.isLoggedIn() && response?.status === 401) { session.destroy(); const hash = window.location.hash; const url = hash.slice(1); diff --git a/src/components/CrudModel.vue b/src/components/CrudModel.vue new file mode 100644 index 000000000..2a4982fce --- /dev/null +++ b/src/components/CrudModel.vue @@ -0,0 +1,324 @@ + + + + + { + "en": { + "confirmDeletion": "Confirm deletion", + "confirmDeletionMessage": "Are you sure you want to delete this?" + }, + "es": { + "confirmDeletion": "Confirmar eliminación", + "confirmDeletionMessage": "Seguro que quieres eliminar?" + } + } + diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 9d0916a8e..540c37d01 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -4,12 +4,14 @@ import { onMounted, onUnmounted, computed, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useQuasar } from 'quasar'; import { useState } from 'src/composables/useState'; +import { useStateStore } from 'stores/useStateStore'; import { useValidator } from 'src/composables/useValidator'; import SkeletonForm from 'components/ui/SkeletonForm.vue'; const quasar = useQuasar(); -const { t } = useI18n(); const state = useState(); +const stateStore = useStateStore(); +const { t } = useI18n(); const { validate } = useValidator(); const $props = defineProps({ @@ -29,6 +31,10 @@ const $props = defineProps({ type: String, default: null, }, + defaultActions: { + type: Boolean, + default: true, + }, }); const emit = defineEmits(['onFetch']); @@ -45,17 +51,21 @@ onUnmounted(() => { const isLoading = ref(false); const hasChanges = ref(false); -const formData = computed(() => state.get($props.model)); const originalData = ref(); +const formData = computed(() => state.get($props.model)); const formUrl = computed(() => $props.url); +function tMobile(...args) { + if (!quasar.platform.is.mobile) return t(...args); +} + async function fetch() { const { data } = await axios.get($props.url, { params: { filter: $props.filter }, }); state.set($props.model, data); - originalData.value = Object.assign({}, data); + originalData.value = data && JSON.parse(JSON.stringify(data)); watch(formData.value, () => (hasChanges.value = true)); @@ -72,13 +82,18 @@ async function save() { isLoading.value = true; await axios.patch($props.urlUpdate || $props.url, formData.value); - originalData.value = formData.value; + originalData.value = JSON.parse(JSON.stringify(formData.value)); hasChanges.value = false; isLoading.value = false; } function reset() { state.set($props.model, originalData.value); + originalData.value = JSON.parse(JSON.stringify(originalData.value)); + + watch(formData.value, () => (hasChanges.value = true)); + + emit('onFetch', state.get($props.model)); hasChanges.value = false; } // eslint-disable-next-line vue/no-dupe-keys @@ -109,20 +124,31 @@ watch(formUrl, async () => { -
- - - - -
+ +
+ + + + + +
+
+import { ref, toRefs, watch, computed } from 'vue'; +const emit = defineEmits(['update:modelValue', 'update:options']); + +const $props = defineProps({ + modelValue: { + type: [String, Number], + default: null, + }, + options: { + type: Array, + default: () => [], + }, + optionLabel: { + type: String, + default: '', + }, +}); +const { optionLabel, options } = toRefs($props); +const myOptions = ref([]); +const myOptionsOriginal = ref([]); +const vnSelectRef = ref(null); + +function setOptions(data) { + myOptions.value = JSON.parse(JSON.stringify(data)); + myOptionsOriginal.value = JSON.parse(JSON.stringify(data)); +} +setOptions(options.value); + +const filter = (val, options) => { + const search = val.toLowerCase(); + + if (val === '') return options; + + return options.filter((row) => { + const id = row.id; + const name = row[$props.optionLabel].toLowerCase(); + + const idMatches = id == search; + const nameMatches = name.indexOf(search) > -1; + + return idMatches || nameMatches; + }); +}; + +const filterHandler = (val, update) => { + update( + () => { + myOptions.value = filter(val, myOptionsOriginal.value); + }, + (ref) => { + if (val !== '' && ref.options.length > 0) { + ref.setOptionIndex(-1); + ref.moveOptionSelection(1, true); + } + } + ); +}; + +watch(options, (newValue) => { + setOptions(newValue); +}); + +const value = computed({ + get() { + return $props.modelValue; + }, + set(value) { + emit('update:modelValue', value); + }, +}); + + + diff --git a/src/components/ui/CardDescriptor.vue b/src/components/ui/CardDescriptor.vue index 65a89ee28..f63b75de6 100644 --- a/src/components/ui/CardDescriptor.vue +++ b/src/components/ui/CardDescriptor.vue @@ -29,12 +29,14 @@ const $props = defineProps({ const slots = useSlots(); const { t } = useI18n(); +const entity = ref(); -onMounted(() => fetch()); +onMounted(async () => { + await fetch(); +}); const emit = defineEmits(['onFetch']); -const entity = ref(); async function fetch() { const params = {}; diff --git a/src/components/ui/SkeletonTable.vue b/src/components/ui/SkeletonTable.vue new file mode 100644 index 000000000..d58253f90 --- /dev/null +++ b/src/components/ui/SkeletonTable.vue @@ -0,0 +1,50 @@ + + diff --git a/src/components/ui/VnPaginate.vue b/src/components/ui/VnPaginate.vue index 434ebf232..d21d073f2 100644 --- a/src/components/ui/VnPaginate.vue +++ b/src/components/ui/VnPaginate.vue @@ -46,6 +46,10 @@ const props = defineProps({ type: Number, default: 500, }, + skeleton: { + type: Boolean, + default: true, + }, }); const emit = defineEmits(['onFetch', 'onPaginate']); @@ -137,14 +141,9 @@ async function onLoad(...params) {
-
- {{ t('No results found') }} -
-
-
@@ -164,7 +163,7 @@ async function onLoad(...params) {
- +
diff --git a/src/composables/tMobile.js b/src/composables/tMobile.js new file mode 100644 index 000000000..a6a000b81 --- /dev/null +++ b/src/composables/tMobile.js @@ -0,0 +1,8 @@ +import { useQuasar } from 'quasar'; +import { useI18n } from 'vue-i18n'; + +export function tMobile(...args) { + const quasar = useQuasar(); + const { t } = useI18n(); + if (!quasar.platform.is.mobile) return t(...args); +} diff --git a/src/composables/useArrayData.js b/src/composables/useArrayData.js index c7808f9a8..4535cde0f 100644 --- a/src/composables/useArrayData.js +++ b/src/composables/useArrayData.js @@ -38,11 +38,11 @@ export function useArrayData(key, userOptions) { 'limit', 'skip', 'userParams', - 'userFilter' + 'userFilter', ]; if (typeof userOptions === 'object') { for (const option in userOptions) { - const isEmpty = userOptions[option] == null || userOptions[option] == '' + const isEmpty = userOptions[option] == null || userOptions[option] == ''; if (isEmpty || !allowedOptions.includes(option)) continue; if (Object.prototype.hasOwnProperty.call(store, option)) { @@ -73,7 +73,7 @@ export function useArrayData(key, userOptions) { Object.assign(params, store.userParams); - store.isLoading = true + store.isLoading = true; const response = await axios.get(store.url, { signal: canceller.signal, params, @@ -94,7 +94,7 @@ export function useArrayData(key, userOptions) { updateStateParams(); } - store.isLoading = false + store.isLoading = false; canceller = null; } @@ -153,8 +153,8 @@ export function useArrayData(key, userOptions) { }); } - const totalRows = computed(() => store.data && store.data.length || 0); - const isLoading = computed(() => store.isLoading || false) + const totalRows = computed(() => (store.data && store.data.length) || 0); + const isLoading = computed(() => store.isLoading || false); return { fetch, @@ -167,6 +167,6 @@ export function useArrayData(key, userOptions) { hasMoreData, totalRows, updateStateParams, - isLoading + isLoading, }; } diff --git a/src/composables/useValidator.js b/src/composables/useValidator.js index ef2dcbd90..bc48332a2 100644 --- a/src/composables/useValidator.js +++ b/src/composables/useValidator.js @@ -3,15 +3,13 @@ import { useI18n } from 'vue-i18n'; import axios from 'axios'; import validator from 'validator'; - const models = ref(null); export function useValidator() { if (!models.value) fetch(); function fetch() { - axios.get('Schemas/ModelInfo') - .then(response => models.value = response.data) + axios.get('Schemas/ModelInfo').then((response) => (models.value = response.data)); } function validate(propertyRule) { @@ -38,19 +36,18 @@ export function useValidator() { const { t } = useI18n(); const validations = function (validation) { - return { presence: (value) => { let message = `Value can't be empty`; if (validation.message) - message = t(validation.message) || validation.message + message = t(validation.message) || validation.message; - return !validator.isEmpty(value ? String(value) : '') || message + return !validator.isEmpty(value ? String(value) : '') || message; }, length: (value) => { const options = { min: validation.min || validation.is, - max: validation.max || validation.is + max: validation.max || validation.is, }; value = String(value); @@ -69,14 +66,14 @@ export function useValidator() { }, numericality: (value) => { if (validation.int) - return validator.isInt(value) || 'Value should be integer' - return validator.isNumeric(value) || 'Value should be a number' + return validator.isInt(value) || 'Value should be integer'; + return validator.isNumeric(value) || 'Value should be a number'; }, - custom: (value) => validation.bindedFunction(value) || 'Invalid value' + custom: (value) => validation.bindedFunction(value) || 'Invalid value', }; }; return { - validate + validate, }; -} \ No newline at end of file +} diff --git a/src/css/app.scss b/src/css/app.scss index 3c8cc50b6..0f04c9ad8 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -32,10 +32,16 @@ body.body--light { --vn-text: #000000; --vn-gray: #f5f5f5; --vn-label: #5f5f5f; + --vn-dark: white; } body.body--dark { --vn-text: #ffffff; --vn-gray: #313131; --vn-label: #a8a8a8; + --vn-dark: #292929; +} + +.bg-vn-dark { + background-color: var(--vn-dark); } diff --git a/src/i18n/en/index.js b/src/i18n/en/index.js index 62704bf8c..cfd20716b 100644 --- a/src/i18n/en/index.js +++ b/src/i18n/en/index.js @@ -266,6 +266,7 @@ export default { lines: 'Lines', rma: 'RMA', photos: 'Photos', + development: 'Development', log: 'Audit logs', notes: 'Notes', }, diff --git a/src/i18n/es/index.js b/src/i18n/es/index.js index 1cef961db..532c1bb3b 100644 --- a/src/i18n/es/index.js +++ b/src/i18n/es/index.js @@ -264,6 +264,7 @@ export default { basicData: 'Datos básicos', lines: 'Líneas', rma: 'RMA', + development: 'Trazabilidad', photos: 'Fotos', log: 'Registros de auditoría', notes: 'Notas', diff --git a/src/pages/Claim/Card/ClaimCard.vue b/src/pages/Claim/Card/ClaimCard.vue index 9f1ecc416..03b9889f0 100644 --- a/src/pages/Claim/Card/ClaimCard.vue +++ b/src/pages/Claim/Card/ClaimCard.vue @@ -44,17 +44,6 @@ onMounted(async () => { - - - - - {{ t('Development') }} - { - - + + +
+ +
+
+
@@ -80,6 +74,5 @@ es: You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente Details: Detalles Notes: Notas - Development: Trazabilidad Action: Acción diff --git a/src/pages/Claim/Card/ClaimDescriptor.vue b/src/pages/Claim/Card/ClaimDescriptor.vue index 914de2eb2..af7e84d38 100644 --- a/src/pages/Claim/Card/ClaimDescriptor.vue +++ b/src/pages/Claim/Card/ClaimDescriptor.vue @@ -3,6 +3,8 @@ import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { toDate } from 'src/filters'; +import { useState } from 'src/composables/useState'; + import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue'; @@ -19,6 +21,7 @@ const $props = defineProps({ }); const route = useRoute(); +const state = useState(); const { t } = useI18n(); const entityId = computed(() => { @@ -67,6 +70,7 @@ function stateColor(code) { const data = ref(useCardDescription()); const setData = (entity) => { data.value = useCardDescription(entity.client.name, entity.id); + state.set('ClaimDescriptor', entity); }; diff --git a/src/pages/Claim/Card/ClaimDevelopment.vue b/src/pages/Claim/Card/ClaimDevelopment.vue new file mode 100644 index 000000000..0c83bdadd --- /dev/null +++ b/src/pages/Claim/Card/ClaimDevelopment.vue @@ -0,0 +1,266 @@ + + + + + + +es: + Reason: Motivo + Result: Consecuencia + Responsible: Responsable + Worker: Trabajador + Redelivery: Devolución + diff --git a/src/pages/Claim/Card/ClaimLines.vue b/src/pages/Claim/Card/ClaimLines.vue index 9d2a12804..c03291b85 100644 --- a/src/pages/Claim/Card/ClaimLines.vue +++ b/src/pages/Claim/Card/ClaimLines.vue @@ -6,9 +6,8 @@ import { useQuasar } from 'quasar'; import { useRoute } from 'vue-router'; import { useArrayData } from 'composables/useArrayData'; import { useStateStore } from 'stores/useStateStore'; -import VnPaginate from 'components/ui/VnPaginate.vue'; +import CrudModel from 'components/CrudModel.vue'; import FetchData from 'components/FetchData.vue'; -import VnConfirm from 'components/ui/VnConfirm.vue'; import { toDate, toCurrency, toPercentage } from 'filters/index'; import VnDiscount from 'components/common/vnDiscount.vue'; @@ -17,6 +16,7 @@ import ClaimLinesImport from './ClaimLinesImport.vue'; const quasar = useQuasar(); const route = useRoute(); const { t } = useI18n(); + const stateStore = useStateStore(); const arrayData = useArrayData('ClaimLines'); const store = arrayData.store; @@ -36,16 +36,17 @@ const linesFilter = { }, }; +const claimLinesForm = ref(); const claim = ref(null); async function onFetchClaim(data) { claim.value = data; - fetchMana(); } const amount = ref(0); const amountClaimed = ref(0); async function onFetch(rows) { + if (!rows || rows.length) return; amount.value = rows.reduce( (acumulator, { sale }) => acumulator + sale.price * sale.quantity, 0 @@ -141,60 +142,20 @@ function onUpdateDiscount(response) { }); } -async function confirmRemove() { - const rows = selected.value; - const count = rows.length; - - if (count === 0) { - return quasar.notify({ - message: 'You must select at least one row', - type: 'warning', - }); - } - - quasar - .dialog({ - component: VnConfirm, - componentProps: { - title: t('Delete claimed sales'), - message: t('You are about to remove {count} rows', count, { count }), - data: { rows }, - promise: remove, - }, - }) - .onOk(() => { - for (const row of rows) { - const orgData = store.data; - const index = orgData.findIndex((item) => item.id === row.id); - store.data.splice(index, 1); - selected.value = []; - } - }); -} - -async function remove({ rows }) { - if (!rows.length) return; - const body = { deletes: rows.map((row) => row.id) }; - await axios.post(`ClaimBeginnings/crud`, body); - quasar.notify({ - type: 'positive', - message: t('globals.rowRemoved'), - }); -} - function showImportDialog() { quasar .dialog({ component: ClaimLinesImport, + componentProps: { + ticketId: claim.value.ticketFk, + }, }) - .onOk(() => arrayData.refresh()); + .onOk(() => claimLinesForm.value.reload()); } - +
- -
- - {{ t('globals.remove') }} - - - {{ t('globals.add') }} - - -
-
- - - - - - - - - + + @@ -421,7 +353,6 @@ en: You are about to remove {count} row | You are about to remove {count} rows' es: - Claimed lines: Líneas reclamadas Delivered: Entregado Quantity: Cantidad Claimed: Reclamada diff --git a/src/pages/Claim/Card/ClaimLinesImport.vue b/src/pages/Claim/Card/ClaimLinesImport.vue index 26e59bbc0..be8914eec 100644 --- a/src/pages/Claim/Card/ClaimLinesImport.vue +++ b/src/pages/Claim/Card/ClaimLinesImport.vue @@ -14,6 +14,13 @@ const route = useRoute(); const quasar = useQuasar(); const { t } = useI18n(); +const $props = defineProps({ + ticketId: { + type: Number, + required: true, + }, +}); + const columns = computed(() => [ { name: 'delivered', @@ -99,7 +106,7 @@ function cancel() { diff --git a/src/pages/Wagon/WagonCreate.vue b/src/pages/Wagon/WagonCreate.vue index 3f7824975..123e01d36 100644 --- a/src/pages/Wagon/WagonCreate.vue +++ b/src/pages/Wagon/WagonCreate.vue @@ -20,7 +20,7 @@ const $props = defineProps({ }); const entityId = computed(() => $props.id || route.params.id); -let wagonTypes; +let wagonTypes = []; let originalData = {}; const wagon = ref({}); const filteredWagonTypes = ref(wagonTypes); diff --git a/src/pages/Worker/Card/WorkerCard.vue b/src/pages/Worker/Card/WorkerCard.vue index 3d6b46e11..972eb52ec 100644 --- a/src/pages/Worker/Card/WorkerCard.vue +++ b/src/pages/Worker/Card/WorkerCard.vue @@ -25,8 +25,13 @@ const { t } = useI18n(); - - + + +
+ +
+
+
diff --git a/src/pages/Worker/Card/WorkerSummary.vue b/src/pages/Worker/Card/WorkerSummary.vue index 05ccdc373..7c8accc5d 100644 --- a/src/pages/Worker/Card/WorkerSummary.vue +++ b/src/pages/Worker/Card/WorkerSummary.vue @@ -1,6 +1,5 @@