5673-claim-development & crudModel #71

Merged
alexm merged 33 commits from 5673-claim-development into dev 2023-09-28 07:46:02 +00:00
34 changed files with 1171 additions and 362 deletions

View File

@ -64,7 +64,7 @@ module.exports = {
}, },
overrides: [ overrides: [
{ {
files: ['test/cypress/**/*.spec.{js,ts}'], files: ['test/cypress/**/*.*'],
extends: [ extends: [
// Add Cypress-specific lint rules, globals and Cypress plugin // Add Cypress-specific lint rules, globals and Cypress plugin
// See https://github.com/cypress-io/eslint-plugin-cypress#rules // See https://github.com/cypress-io/eslint-plugin-cypress#rules

View File

@ -7,7 +7,7 @@ module.exports = defineConfig({
screenshotsFolder: 'test/cypress/screenshots', screenshotsFolder: 'test/cypress/screenshots',
supportFile: 'test/cypress/support/index.js', supportFile: 'test/cypress/support/index.js',
videosFolder: 'test/cypress/videos', videosFolder: 'test/cypress/videos',
video: true, video: false,
specPattern: 'test/cypress/integration/*.spec.js', specPattern: 'test/cypress/integration/*.spec.js',
experimentalRunAllSpecs: true, experimentalRunAllSpecs: true,
component: { component: {

View File

@ -0,0 +1,322 @@
<script setup>
import axios from 'axios';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useValidator } from 'src/composables/useValidator';
import { useStateStore } from 'stores/useStateStore';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import SkeletonTable from 'components/ui/SkeletonTable.vue';
const quasar = useQuasar();
const stateStore = useStateStore();
const { t } = useI18n();
const { validate } = useValidator();
const $props = defineProps({
model: {
type: String,
default: '',
},
url: {
type: String,
default: '',
},
saveUrl: {
type: String,
default: null,
},
primaryKey: {
type: String,
default: 'id',
},
dataRequired: {
type: Object,
default: () => {},
},
defaultSave: {
type: Boolean,
default: true,
},
defaultReset: {
type: Boolean,
default: true,
},
defaultRemove: {
type: Boolean,
default: true,
},
selected: {
type: Object,
default: null,
},
saveFn: {
type: Function,
default: null,
},
});
const isLoading = ref(false);
const hasChanges = ref(false);
const originalData = ref();
const vnPaginateRef = ref();
const formData = ref();
const formUrl = computed(() => $props.url);
const emit = defineEmits(['onFetch', 'update:selected']);
defineExpose({
reload,
insert,
remove,
onSubmit,
reset,
hasChanges,
});
function tMobile(...args) {
if (!quasar.platform.is.mobile) return t(...args);
}
async function fetch(data) {
if (data && Array.isArray(data)) {
let $index = 0;
data.map((d) => (d.$index = $index++));
}
originalData.value = data && JSON.parse(JSON.stringify(data));
formData.value = data && JSON.parse(JSON.stringify(data));
watch(formData, () => (hasChanges.value = true), { deep: true });
emit('onFetch', data);
}
async function reset() {
await fetch(originalData.value);
hasChanges.value = false;
}
// eslint-disable-next-line vue/no-dupe-keys
function filter(value, update, filterOptions) {
update(
() => {
const { options, filterFn, field } = filterOptions;
options.value = filterFn(options, value, field);
},
(ref) => {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
);
}
async function onSubmit() {
if (!hasChanges.value) {
return quasar.notify({
type: 'negative',
message: t('globals.noChanges'),
});
}
isLoading.value = true;
await saveChanges();
}
async function saveChanges(data) {
if ($props.saveFn) return $props.saveFn(data, getChanges);
const changes = data || getChanges();
try {
await axios.post($props.saveUrl || $props.url + '/crud', changes);
} catch (e) {
return (isLoading.value = false);
}
originalData.value = JSON.parse(JSON.stringify(formData.value));
if (changes.creates?.length) await vnPaginateRef.value.fetch();
hasChanges.value = false;
isLoading.value = false;
}
async function insert() {
const $index = formData.value.length
? formData.value[formData.value.length - 1].$index + 1
: 0;
formData.value.push(Object.assign({ $index }, $props.dataRequired));
hasChanges.value = true;
}
async function remove(data) {
if (!data.length)
return quasar.notify({
type: 'warning',
message: t('globals.noChanges'),
alexm marked this conversation as resolved
Review

Esto lo dejo asi por si en un futuro se quiere hacer que puedas borrar una linea en especifico (como esta en salix que al final de la linea esta el boton de borrar) Pero teniendo los checkbox de momento no lo veo necesario

Esto lo dejo asi por si en un futuro se quiere hacer que puedas borrar una linea en especifico (como esta en salix que al final de la linea esta el boton de borrar) Pero teniendo los checkbox de momento no lo veo necesario
});
const pk = $props.primaryKey;
let ids = data.map((d) => d[pk]).filter(Boolean);
let preRemove = data.map((d) => (d[pk] ? null : d.$index)).filter(Boolean);
let newData = formData.value;
if (preRemove.length) {
newData = newData.filter(
(form) => !preRemove.some((index) => index == form.$index)
);
const changes = getChanges();
if (!changes.creates?.length && !changes.updates?.length)
hasChanges.value = false;
fetch(newData);
}
if (ids.length) {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('confirmDeletion'),
message: t('confirmDeletionMessage'),
newData,
ids,
},
})
.onOk(async () => {
await saveChanges({ deletes: ids });
newData = newData.filter((form) => !ids.some((id) => id == form[pk]));
fetch(newData);
});
}
emit('update:selected', []);
}
function getChanges() {
const updates = [];
const creates = [];
const pk = $props.primaryKey;
for (const [i, row] of formData.value.entries()) {
if (!row[pk]) {
creates.push(row);
} else if (originalData.value) {
const data = getDifferences(originalData.value[i], row);
if (!isEmpty(data)) {
updates.push({
data,
where: { [pk]: row[pk] },
});
}
}
}
const changes = { updates, creates };
for (let prop in changes) {
if (changes[prop].length === 0) changes[prop] = undefined;
}
return changes;
}
function getDifferences(obj1, obj2) {
let diff = {};
delete obj1.$index;
delete obj2.$index;
for (let key in obj1) {
if (obj2[key] && obj1[key] !== obj2[key]) {
diff[key] = obj2[key];
}
}
for (let key in obj2) {
if (obj1[key] === undefined || obj1[key] !== obj2[key]) {
diff[key] = obj2[key];
}
}
return diff;
}
function isEmpty(obj) {
if (obj == null) return true;
if (obj === undefined) return true;
if (Object.keys(obj).length === 0) return true;
if (obj.length > 0) return false;
}
async function reload() {
vnPaginateRef.value.fetch();
}
watch(formUrl, async () => {
originalData.value = null;
reset();
});
</script>
<template>
<VnPaginate
:url="url"
v-bind="$attrs"
@on-fetch="fetch"
:skeleton="false"
ref="vnPaginateRef"
>
<template #body v-if="formData">
<slot
name="body"
:rows="formData"
:validate="validate"
:filter="filter"
></slot>
</template>
</VnPaginate>
<SkeletonTable v-if="!formData" />
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<QBtnGroup push class="q-gutter-x-sm">
<slot name="moreActions" />
<QBtn
:label="tMobile('globals.remove')"
color="primary"
icon="delete"
flat
@click="remove(selected)"
:disable="!selected?.length"
:title="t('globals.remove')"
v-if="$props.defaultRemove"
/>
<QBtn
:label="tMobile('globals.reset')"
color="primary"
icon="restart_alt"
flat
@click="reset"
:disable="!hasChanges"
:title="t('globals.reset')"
v-if="$props.defaultReset"
/>
<QBtn
:label="tMobile('globals.save')"
color="primary"
icon="save"
@click="onSubmit"
:disable="!hasChanges"
:title="t('globals.save')"
v-if="$props.defaultSave"
/>
</QBtnGroup>
</Teleport>
<QInnerLoading
:showing="isLoading"
:label="t && t('globals.pleaseWait')"
color="primary"
/>
</template>
<i18n>
{
"en": {
"confirmDeletion": "Confirm deletion",
"confirmDeletionMessage": "Are you sure you want to delete this?"
},
"es": {
"confirmDeletion": "Confirmar eliminación",
"confirmDeletionMessage": "Seguro que quieres eliminar?"
}
}
</i18n>

View File

@ -4,12 +4,14 @@ import { onMounted, onUnmounted, computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useStateStore } from 'stores/useStateStore';
import { useValidator } from 'src/composables/useValidator'; import { useValidator } from 'src/composables/useValidator';
import SkeletonForm from 'components/ui/SkeletonForm.vue'; import SkeletonForm from 'components/ui/SkeletonForm.vue';
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n();
const state = useState(); const state = useState();
const stateStore = useStateStore();
const { t } = useI18n();
const { validate } = useValidator(); const { validate } = useValidator();
const $props = defineProps({ const $props = defineProps({
@ -29,6 +31,10 @@ const $props = defineProps({
type: String, type: String,
default: null, default: null,
}, },
defaultActions: {
type: Boolean,
default: true,
},
}); });
const emit = defineEmits(['onFetch']); const emit = defineEmits(['onFetch']);
@ -45,17 +51,21 @@ onUnmounted(() => {
const isLoading = ref(false); const isLoading = ref(false);
const hasChanges = ref(false); const hasChanges = ref(false);
const formData = computed(() => state.get($props.model));
const originalData = ref(); const originalData = ref();
const formData = computed(() => state.get($props.model));
const formUrl = computed(() => $props.url); const formUrl = computed(() => $props.url);
function tMobile(...args) {
if (!quasar.platform.is.mobile) return t(...args);
}
async function fetch() { async function fetch() {
const { data } = await axios.get($props.url, { const { data } = await axios.get($props.url, {
params: { filter: $props.filter }, params: { filter: $props.filter },
}); });
state.set($props.model, data); state.set($props.model, data);
originalData.value = Object.assign({}, data); originalData.value = data && JSON.parse(JSON.stringify(data));
watch(formData.value, () => (hasChanges.value = true)); watch(formData.value, () => (hasChanges.value = true));
@ -72,13 +82,18 @@ async function save() {
isLoading.value = true; isLoading.value = true;
await axios.patch($props.urlUpdate || $props.url, formData.value); await axios.patch($props.urlUpdate || $props.url, formData.value);
originalData.value = formData.value; originalData.value = JSON.parse(JSON.stringify(formData.value));
hasChanges.value = false; hasChanges.value = false;
isLoading.value = false; isLoading.value = false;
} }
function reset() { function reset() {
state.set($props.model, originalData.value); 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; hasChanges.value = false;
} }
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
@ -109,20 +124,31 @@ watch(formUrl, async () => {
</QBanner> </QBanner>
<QForm v-if="formData" @submit="save" @reset="reset" class="q-pa-md"> <QForm v-if="formData" @submit="save" @reset="reset" class="q-pa-md">
<slot name="form" :data="formData" :validate="validate" :filter="filter"></slot> <slot name="form" :data="formData" :validate="validate" :filter="filter"></slot>
<div class="q-mt-lg">
<slot name="actions">
<QBtn :label="t('globals.save')" type="submit" color="primary" />
<QBtn
:label="t('globals.reset')"
type="reset"
class="q-ml-sm"
color="primary"
flat
:disable="!hasChanges"
/>
</slot>
</div>
</QForm> </QForm>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<div v-if="$props.defaultActions">
<QBtnGroup push class="q-gutter-x-sm">
<slot name="moreActions" />
<QBtn
:label="tMobile('globals.reset')"
color="primary"
icon="restart_alt"
flat
@click="reset"
:disable="!hasChanges"
:title="t('globals.reset')"
/>
<QBtn
:label="tMobile('globals.save')"
color="primary"
icon="save"
@click="save"
Outdated
Review

$props.saveFn que le puedas pasar una función(data)

$props.saveFn que le puedas pasar una función(data)
:disable="!hasChanges"
:title="t('globals.save')"
/>
</QBtnGroup>
</div>
</Teleport>
<SkeletonForm v-if="!formData" /> <SkeletonForm v-if="!formData" />
<QInnerLoading <QInnerLoading
:showing="isLoading" :showing="isLoading"

View File

@ -0,0 +1,76 @@
<script setup>
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([]);
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);
});
};
watch(options, (newValue) => {
setOptions(newValue);
});
const value = computed({
get() {
return $props.modelValue;
},
set(value) {
emit('update:modelValue', value);
},
});
</script>
<template>
<QSelect
v-model="value"
:options="myOptions"
:option-label="optionLabel"
v-bind="$attrs"
emit-value
map-options
use-input
@filter="filterHandler"
clearable
clear-icon="close"
/>
</template>

View File

@ -29,12 +29,14 @@ const $props = defineProps({
const slots = useSlots(); const slots = useSlots();
const { t } = useI18n(); const { t } = useI18n();
const entity = ref();
onMounted(() => fetch()); onMounted(async () => {
await fetch();
});
const emit = defineEmits(['onFetch']); const emit = defineEmits(['onFetch']);
const entity = ref();
async function fetch() { async function fetch() {
const params = {}; const params = {};

View File

@ -0,0 +1,50 @@
<template>
<div class="q-pa-md w">
<div class="row q-gutter-md q-mb-md">
<div class="col-1">
<QSkeleton type="rect" square />
</div>
<div class="col">
<QSkeleton type="rect" square />
</div>
<div class="col">
<QSkeleton type="rect" square />
</div>
<div class="col">
<QSkeleton type="rect" square />
</div>
<div class="col">
<QSkeleton type="rect" square />
</div>
<div class="col">
<QSkeleton type="rect" square />
</div>
</div>
<div class="row q-gutter-md q-mb-md" v-for="n in 5" :key="n">
<div class="col-1">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.w {
width: 80vw;
}
</style>

View File

@ -46,6 +46,10 @@ const props = defineProps({
type: Number, type: Number,
default: 500, default: 500,
}, },
skeleton: {
type: Boolean,
default: true,
},
}); });
const emit = defineEmits(['onFetch', 'onPaginate']); const emit = defineEmits(['onFetch', 'onPaginate']);
@ -144,7 +148,10 @@ async function onLoad(...params) {
{{ t('No results found') }} {{ t('No results found') }}
</h5> </h5>
</div> </div>
<div v-if="props.autoLoad && !store.data" class="card-list q-gutter-y-md"> <div
v-if="props.skeleton && props.autoLoad && !store.data"
class="card-list q-gutter-y-md"
>
<QCard class="card" v-for="$index in $props.limit" :key="$index"> <QCard class="card" v-for="$index in $props.limit" :key="$index">
<QItem v-ripple class="q-pa-none items-start cursor-pointer q-hoverable"> <QItem v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
<QItemSection class="q-pa-md"> <QItemSection class="q-pa-md">
@ -164,7 +171,7 @@ async function onLoad(...params) {
</QCard> </QCard>
</div> </div>
</div> </div>
<QInfiniteScroll v-if="store.data" @load="onLoad" :offset="offset"> <QInfiniteScroll v-if="store.data" @load="onLoad" :offset="offset" class="full-width">
<slot name="body" :rows="store.data"></slot> <slot name="body" :rows="store.data"></slot>
<div v-if="isLoading" class="info-row q-pa-md text-center"> <div v-if="isLoading" class="info-row q-pa-md text-center">
<QSpinner color="orange" size="md" /> <QSpinner color="orange" size="md" />

View File

@ -38,11 +38,11 @@ export function useArrayData(key, userOptions) {
'limit', 'limit',
'skip', 'skip',
'userParams', 'userParams',
'userFilter' 'userFilter',
]; ];
if (typeof userOptions === 'object') { if (typeof userOptions === 'object') {
for (const option in userOptions) { 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 (isEmpty || !allowedOptions.includes(option)) continue;
if (Object.prototype.hasOwnProperty.call(store, option)) { if (Object.prototype.hasOwnProperty.call(store, option)) {
@ -73,7 +73,7 @@ export function useArrayData(key, userOptions) {
Object.assign(params, store.userParams); Object.assign(params, store.userParams);
store.isLoading = true store.isLoading = true;
const response = await axios.get(store.url, { const response = await axios.get(store.url, {
signal: canceller.signal, signal: canceller.signal,
params, params,
@ -94,7 +94,7 @@ export function useArrayData(key, userOptions) {
updateStateParams(); updateStateParams();
} }
store.isLoading = false store.isLoading = false;
canceller = null; canceller = null;
} }
@ -153,8 +153,8 @@ export function useArrayData(key, userOptions) {
}); });
} }
const totalRows = computed(() => store.data && store.data.length || 0); const totalRows = computed(() => (store.data && store.data.length) || 0);
const isLoading = computed(() => store.isLoading || false) const isLoading = computed(() => store.isLoading || false);
return { return {
fetch, fetch,
@ -167,6 +167,6 @@ export function useArrayData(key, userOptions) {
hasMoreData, hasMoreData,
totalRows, totalRows,
updateStateParams, updateStateParams,
isLoading isLoading,
}; };
} }

View File

@ -3,15 +3,13 @@ import { useI18n } from 'vue-i18n';
import axios from 'axios'; import axios from 'axios';
import validator from 'validator'; import validator from 'validator';
const models = ref(null); const models = ref(null);
export function useValidator() { export function useValidator() {
if (!models.value) fetch(); if (!models.value) fetch();
function fetch() { function fetch() {
axios.get('Schemas/ModelInfo') axios.get('Schemas/ModelInfo').then((response) => (models.value = response.data));
.then(response => models.value = response.data)
} }
function validate(propertyRule) { function validate(propertyRule) {
@ -38,19 +36,18 @@ export function useValidator() {
const { t } = useI18n(); const { t } = useI18n();
const validations = function (validation) { const validations = function (validation) {
return { return {
presence: (value) => { presence: (value) => {
let message = `Value can't be empty`; let message = `Value can't be empty`;
if (validation.message) 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) => { length: (value) => {
const options = { const options = {
min: validation.min || validation.is, min: validation.min || validation.is,
max: validation.max || validation.is max: validation.max || validation.is,
}; };
value = String(value); value = String(value);
@ -69,14 +66,14 @@ export function useValidator() {
}, },
numericality: (value) => { numericality: (value) => {
if (validation.int) if (validation.int)
return validator.isInt(value) || 'Value should be integer' return validator.isInt(value) || 'Value should be integer';
return validator.isNumeric(value) || 'Value should be a number' return validator.isNumeric(value) || 'Value should be a number';
}, },
custom: (value) => validation.bindedFunction(value) || 'Invalid value' custom: (value) => validation.bindedFunction(value) || 'Invalid value',
}; };
}; };
return { return {
validate validate,
}; };
} }

View File

@ -32,10 +32,16 @@ body.body--light {
--vn-text: #000000; --vn-text: #000000;
--vn-gray: #f5f5f5; --vn-gray: #f5f5f5;
--vn-label: #5f5f5f; --vn-label: #5f5f5f;
--vn-dark: white;
} }
body.body--dark { body.body--dark {
--vn-text: #ffffff; --vn-text: #ffffff;
--vn-gray: #313131; --vn-gray: #313131;
--vn-label: #a8a8a8; --vn-label: #a8a8a8;
--vn-dark: #292929;
}
.bg-vn-dark {
background-color: var(--vn-dark);
} }

View File

@ -266,6 +266,7 @@ export default {
lines: 'Lines', lines: 'Lines',
rma: 'RMA', rma: 'RMA',
photos: 'Photos', photos: 'Photos',
development: 'Development',
log: 'Audit logs', log: 'Audit logs',
notes: 'Notes', notes: 'Notes',
}, },

View File

@ -264,6 +264,7 @@ export default {
basicData: 'Datos básicos', basicData: 'Datos básicos',
lines: 'Líneas', lines: 'Líneas',
rma: 'RMA', rma: 'RMA',
development: 'Trazabilidad',
photos: 'Fotos', photos: 'Fotos',
log: 'Registros de auditoría', log: 'Registros de auditoría',
notes: 'Notas', notes: 'Notas',

View File

@ -44,17 +44,6 @@ onMounted(async () => {
<LeftMenu source="card" /> <LeftMenu source="card" />
<QSeparator /> <QSeparator />
<QList> <QList>
<QItem
active-class="text-primary"
clickable
v-ripple
:href="`${salixUrl}/development`"
>
<QItemSection avatar>
<QIcon name="vn:traceability"></QIcon>
</QItemSection>
<QItemSection>{{ t('Development') }}</QItemSection>
</QItem>
<QItem <QItem
active-class="text-primary" active-class="text-primary"
clickable clickable
@ -68,8 +57,13 @@ onMounted(async () => {
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<QPageContainer> <QPageContainer>
<QPage class="q-pa-md"> <QPage>
<RouterView></RouterView> <QToolbar class="bg-vn-dark justify-end">
<div id="st-data"></div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<div class="q-pa-md"><RouterView></RouterView></div>
</QPage> </QPage>
</QPageContainer> </QPageContainer>
</template> </template>
@ -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 You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
Details: Detalles Details: Detalles
Notes: Notas Notes: Notas
Development: Trazabilidad
Action: Acción Action: Acción
</i18n> </i18n>

View File

@ -3,6 +3,8 @@ import { ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import { useState } from 'src/composables/useState';
import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue'; import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue'; import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue';
@ -19,6 +21,7 @@ const $props = defineProps({
}); });
const route = useRoute(); const route = useRoute();
const state = useState();
const { t } = useI18n(); const { t } = useI18n();
const entityId = computed(() => { const entityId = computed(() => {
@ -67,6 +70,7 @@ function stateColor(code) {
const data = ref(useCardDescription()); const data = ref(useCardDescription());
const setData = (entity) => { const setData = (entity) => {
data.value = useCardDescription(entity.client.name, entity.id); data.value = useCardDescription(entity.client.name, entity.id);
state.set('ClaimDescriptor', entity);
}; };
</script> </script>

View File

@ -0,0 +1,203 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import CrudModel from 'components/CrudModel.vue';
import FetchData from 'components/FetchData.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
const route = useRoute();
const { t } = useI18n();
const claimDevelopmentForm = ref();
const claimReasons = ref([]);
const claimResults = ref([]);
const claimResponsibles = ref([]);
const claimRedeliveries = ref([]);
const workers = ref([]);
const selected = ref([]);
const developmentsFilter = {
fields: [
'id',
'claimFk',
'claimReasonFk',
'claimResultFk',
'claimResponsibleFk',
'workerFk',
'claimRedeliveryFk',
],
where: {
claimFk: route.params.id,
},
};
const columns = computed(() => [
{
name: 'claimReason',
label: t('Reason'),
field: (row) => row.claimReasonFk,
sortable: true,
options: claimReasons.value,
required: true,
model: 'claimReasonFk',
optionValue: 'id',
optionLabel: 'description',
},
{
name: 'claimResult',
label: t('Result'),
field: (row) => row.claimResultFk,
sortable: true,
options: claimResults.value,
required: true,
model: 'claimResultFk',
optionValue: 'id',
optionLabel: 'description',
},
{
name: 'claimResponsible',
label: t('Responsible'),
field: (row) => row.claimResponsibleFk,
sortable: true,
options: claimResponsibles.value,
required: true,
model: 'claimResponsibleFk',
optionValue: 'id',
optionLabel: 'description',
},
{
name: 'worker',
label: t('Worker'),
field: (row) => row.workerFk,
sortable: true,
options: workers.value,
model: 'workerFk',
optionValue: 'id',
optionLabel: 'nickname',
},
{
name: 'claimRedelivery',
label: t('Redelivery'),
field: (row) => row.claimRedeliveryFk,
sortable: true,
options: claimRedeliveries.value,
required: true,
model: 'claimRedeliveryFk',
optionValue: 'id',
optionLabel: 'description',
},
]);
</script>
<template>
<FetchData
url="ClaimReasons"
order="description"
@on-fetch="(data) => (claimReasons = data)"
auto-load
/>
<FetchData
url="ClaimResults"
order="description"
@on-fetch="(data) => (claimResults = data)"
auto-load
/>
<FetchData
url="ClaimResponsibles"
order="description"
@on-fetch="(data) => (claimResponsibles = data)"
auto-load
/>
<FetchData
url="ClaimRedeliveries"
order="description"
@on-fetch="(data) => (claimRedeliveries = data)"
auto-load
/>
<FetchData
url="Workers/activeWithInheritedRole"
:where="{ role: 'employee' }"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<CrudModel
data-key="ClaimDevelopments"
url="ClaimDevelopments"
model="claimDevelopment"
:filter="developmentsFilter"
ref="claimDevelopmentForm"
:data-required="{ claimFk: route.params.id }"
v-model:selected="selected"
auto-load
>
<template #body="{ rows }">
<QTable
:columns="columns"
:rows="rows"
:pagination="{ rowsPerPage: 0 }"
row-key="$index"
selection="multiple"
hide-pagination
v-model:selected="selected"
:grid="$q.screen.lt.md"
>
<template #body-cell="{ row, col }">
<QTd auto-width>
<VnSelectFilter
:label="col.label"
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
/>
</QTd>
</template>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard bordered flat>
<QCardSection>
<QCheckbox v-model="props.selected" dense />
</QCardSection>
<QSeparator />
<QList dense>
<QItem v-for="col in props.cols" :key="col.name">
<QItemSection>
<VnSelectFilter
:label="col.label"
v-model="props.row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
dense
/>
</QItemSection>
</QItem>
</QList>
</QCard>
</div>
</template>
</QTable>
</template>
</CrudModel>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn fab color="primary" icon="add" @click="claimDevelopmentForm.insert()" />
</QPageSticky>
</template>
<style lang="scss" scoped>
.grid-style-transition {
transition: transform 0.28s, background-color 0.28s;
}
.maxwidth {
width: 100%;
}
</style>
<i18n>
es:
Reason: Motivo
Result: Consecuencia
Responsible: Responsable
Worker: Trabajador
Redelivery: Devolución
</i18n>

View File

@ -6,9 +6,8 @@ import { useQuasar } from 'quasar';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useStateStore } from 'stores/useStateStore'; 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 FetchData from 'components/FetchData.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import { toDate, toCurrency, toPercentage } from 'filters/index'; import { toDate, toCurrency, toPercentage } from 'filters/index';
import VnDiscount from 'components/common/vnDiscount.vue'; import VnDiscount from 'components/common/vnDiscount.vue';
@ -17,6 +16,7 @@ import ClaimLinesImport from './ClaimLinesImport.vue';
const quasar = useQuasar(); const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
const arrayData = useArrayData('ClaimLines'); const arrayData = useArrayData('ClaimLines');
const store = arrayData.store; const store = arrayData.store;
@ -36,6 +36,7 @@ const linesFilter = {
}, },
}; };
const claimLinesForm = ref();
const claim = ref(null); const claim = ref(null);
async function onFetchClaim(data) { async function onFetchClaim(data) {
claim.value = data; claim.value = data;
@ -46,6 +47,7 @@ async function onFetchClaim(data) {
const amount = ref(0); const amount = ref(0);
const amountClaimed = ref(0); const amountClaimed = ref(0);
async function onFetch(rows) { async function onFetch(rows) {
if (!rows || rows.length) return;
amount.value = rows.reduce( amount.value = rows.reduce(
(acumulator, { sale }) => acumulator + sale.price * sale.quantity, (acumulator, { sale }) => acumulator + sale.price * sale.quantity,
0 0
@ -141,47 +143,6 @@ 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() { function showImportDialog() {
quasar quasar
.dialog({ .dialog({
@ -191,10 +152,8 @@ function showImportDialog() {
} }
</script> </script>
<template> <template>
<QPageSticky position="top" :offset="[0, 0]" expand> <Teleport to="#st-data" v-if="stateStore.isSubToolbarShown()">
<QToolbar class="bg-dark text-white"> <QToolbar class="bg-dark text-white">
<QToolbarTitle> {{ t('Claimed lines') }} </QToolbarTitle>
<QSpace />
<div class="row q-gutter-md"> <div class="row q-gutter-md">
<div> <div>
{{ t('Amount') }} {{ t('Amount') }}
@ -211,7 +170,7 @@ function showImportDialog() {
</div> </div>
</div> </div>
</QToolbar> </QToolbar>
</QPageSticky> </Teleport>
<FetchData <FetchData
:url="`Claims/${route.params.id}`" :url="`Claims/${route.params.id}`"
@ -221,11 +180,16 @@ function showImportDialog() {
/> />
<div class="column items-center"> <div class="column items-center">
<div class="list"> <div class="list">
<VnPaginate <CrudModel
data-key="ClaimLines" data-key="ClaimLines"
ref="claimLinesForm"
:url="`Claims/${route.params.id}/lines`" :url="`Claims/${route.params.id}/lines`"
save-url="ClaimBeginnings/crud"
:filter="linesFilter" :filter="linesFilter"
@on-fetch="onFetch" @on-fetch="onFetch"
v-model:selected="selected"
:default-save="false"
:default-reset="false"
auto-load auto-load
> >
<template #body="{ rows }"> <template #body="{ rows }">
@ -361,46 +325,12 @@ function showImportDialog() {
</template> </template>
</QTable> </QTable>
</template> </template>
</VnPaginate> </CrudModel>
</div> </div>
</div> </div>
<Teleport <QPageSticky position="bottom-right" :offset="[25, 25]">
v-if="stateStore.isHeaderMounted() && !$q.screen.lt.sm" <QBtn fab color="primary" icon="add" @click="showImportDialog()" />
to="#actions-prepend"
>
<div class="row q-gutter-x-sm">
<QBtn
v-if="selected.length > 0"
@click="confirmRemove"
icon="delete"
color="primary"
flat
dense
rounded
>
<QTooltip bottom> {{ t('globals.remove') }} </QTooltip>
</QBtn>
<QBtn @click="showImportDialog" icon="add" color="primary" flat dense rounded>
<QTooltip bottom> {{ t('globals.add') }} </QTooltip>
</QBtn>
<QSeparator vertical />
</div>
</Teleport>
<!-- v-if="quasar.platform.is.mobile" -->
<QPageSticky v-if="$q.screen.lt.sm" position="bottom" :offset="[0, 0]" expand>
<QToolbar class="bg-primary text-white q-pa-none">
<QTabs class="full-width" align="justify" inline-label narrow-indicator>
<QTab @click="showImportDialog" icon="add" :label="t('globals.add')" />
<QSeparator vertical inset />
<QTab
@click="confirmRemove"
icon="delete"
:label="t('globals.remove')"
:disable="selected.length === 0"
/>
</QTabs>
</QToolbar>
</QPageSticky> </QPageSticky>
</template> </template>
@ -421,7 +351,6 @@ en:
You are about to remove <strong>{count}</strong> row | You are about to remove <strong>{count}</strong> row |
You are about to remove <strong>{count}</strong> rows' You are about to remove <strong>{count}</strong> rows'
es: es:
Claimed lines: Líneas reclamadas
Delivered: Entregado Delivered: Entregado
Quantity: Cantidad Quantity: Cantidad
Claimed: Reclamada Claimed: Reclamada

View File

@ -25,7 +25,7 @@ const body = {
}; };
</script> </script>
<template> <template>
<div class="col items-center"> <div class="column items-center">
<VnNotes <VnNotes
:add-note="true" :add-note="true"
:id="id" :id="id"

View File

@ -4,7 +4,6 @@ import { ref, computed } from 'vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { useSession } from 'composables/useSession'; import { useSession } from 'composables/useSession';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
@ -12,7 +11,6 @@ import FetchData from 'components/FetchData.vue';
const router = useRouter(); const router = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore();
const session = useSession(); const session = useSession();
const token = session.getToken(); const token = session.getToken();
@ -237,59 +235,20 @@ function onDrag() {
</div> </div>
</div> </div>
<Teleport <QPageSticky position="bottom-right" :offset="[25, 25]">
v-if="stateStore.isHeaderMounted() && !quasar.platform.is.mobile" <label for="fileInput">
to="#actions-prepend" <QBtn fab @click="inputFile.nativeEl.click()" icon="add" color="primary">
> <QInput
<div class="row q-gutter-x-sm"> ref="inputFile"
<label for="fileInput"> type="file"
<QBtn style="display: none"
@click="inputFile.nativeEl.click()" multiple
icon="add" v-model="files"
color="primary" @update:model-value="create()"
dense />
rounded <QTooltip bottom> {{ t('globals.add') }} </QTooltip>
> </QBtn>
<QInput </label>
ref="inputFile"
type="file"
style="display: none"
multiple
v-model="files"
@update:model-value="create()"
/>
<QTooltip bottom> {{ t('globals.add') }} </QTooltip>
</QBtn>
</label>
<QSeparator vertical />
</div>
</Teleport>
<QPageSticky
v-if="quasar.platform.is.mobile"
position="bottom"
:offset="[0, 0]"
expand
>
<QToolbar class="bg-primary text-white q-pa-none">
<QTabs class="full-width" align="justify" inline-label narrow-indicator>
<QTab
@click="inputFile.nativeEl.click()"
icon="add_circle"
:label="t('globals.add')"
>
<QInput
ref="inputFile"
type="file"
style="display: none"
multiple
v-model="files"
@update:model-value="create()"
/>
<QTooltip bottom> {{ t('globals.add') }} </QTooltip>
</QTab>
</QTabs>
</QToolbar>
</QPageSticky> </QPageSticky>
<!-- MULTIMEDIA DIALOG START--> <!-- MULTIMEDIA DIALOG START-->

View File

@ -1,48 +1,34 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { ref } from 'vue'; import { watch, ref, computed, onUnmounted, onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useRoute } from 'vue-router'; import CrudModel from 'components/CrudModel.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useState } from 'src/composables/useState';
import { useStateStore } from 'stores/useStateStore';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import FetchData from 'components/FetchData.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
const quasar = useQuasar(); const quasar = useQuasar();
const route = useRoute(); const state = useState();
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const selected = ref([]);
const arrayData = useArrayData('ClaimRma'); const claimRmaRef = ref();
const claim = computed(() => state.get('ClaimDescriptor'));
const claim = ref(); const claimRmaFilter = {
const claimFilter = { include: {
fields: ['rma'], relation: 'worker',
}; scope: {
include: {
async function onFetch(data) { relation: 'user',
claim.value = data;
const filter = {
include: {
relation: 'worker',
scope: {
include: {
relation: 'user',
},
}, },
}, },
order: 'created DESC', },
where: { order: 'created DESC',
code: claim.value.rma, where: {
}, code: claim.value?.rma,
}; },
};
arrayData.applyFilter({ filter });
}
async function addRow() { async function addRow() {
if (!claim.value.rma) { if (!claim.value.rma) {
@ -56,7 +42,7 @@ async function addRow() {
}; };
await axios.post(`ClaimRmas`, formData); await axios.post(`ClaimRmas`, formData);
await arrayData.refresh(); await claimRmaRef.value.reload();
quasar.notify({ quasar.notify({
type: 'positive', type: 'positive',
@ -65,38 +51,33 @@ async function addRow() {
}); });
} }
function confirmRemove(id) { onMounted(() => {
quasar if (claim.value) claimRmaRef.value.reload();
.dialog({ });
component: VnConfirm, watch(
componentProps: { claim,
data: { id }, () => {
promise: remove, claimRmaRef.value.reload();
}, },
}) { deep: true }
.onOk(async () => await arrayData.refresh()); );
}
async function remove({ id }) {
await axios.delete(`ClaimRmas/${id}`);
quasar.notify({
type: 'positive',
message: t('globals.rowRemoved'),
});
}
</script> </script>
<template> <template>
<FetchData
:url="`Claims/${route.params.id}`"
:filter="claimFilter"
@on-fetch="onFetch"
auto-load
/>
<div class="column items-center"> <div class="column items-center">
<div class="list"> <div class="list">
<VnPaginate data-key="ClaimRma" url="ClaimRmas"> <CrudModel
data-key="ClaimRma"
url="ClaimRmas"
model="ClaimRma"
:filter="claimRmaFilter"
v-model:selected="selected"
ref="claimRmaRef"
:default-save="false"
:default-reset="false"
:default-remove="false"
>
<template #body="{ rows }"> <template #body="{ rows }">
<QCard class="card"> <QCard>
<template v-for="(row, index) of rows" :key="row.id"> <template v-for="(row, index) of rows" :key="row.id">
<QItem class="q-pa-none items-start"> <QItem class="q-pa-none items-start">
<QItemSection class="q-pa-md"> <QItemSection class="q-pa-md">
@ -107,7 +88,7 @@ async function remove({ id }) {
{{ t('claim.rma.user') }} {{ t('claim.rma.user') }}
</QItemLabel> </QItemLabel>
<QItemLabel> <QItemLabel>
{{ row.worker.user.name }} {{ row?.worker?.user?.name }}
</QItemLabel> </QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -133,7 +114,7 @@ async function remove({ id }) {
round round
color="orange" color="orange"
icon="vn:bin" icon="vn:bin"
@click="confirmRemove(row.id)" @click="claimRmaRef.remove([row])"
> >
<QTooltip>{{ t('globals.remove') }}</QTooltip> <QTooltip>{{ t('globals.remove') }}</QTooltip>
</QBtn> </QBtn>
@ -143,33 +124,11 @@ async function remove({ id }) {
</template> </template>
</QCard> </QCard>
</template> </template>
</VnPaginate> </CrudModel>
</div> </div>
</div> </div>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<Teleport <QBtn fab color="primary" icon="add" @click="addRow()" />
v-if="stateStore.isHeaderMounted() && !quasar.platform.is.mobile"
to="#actions-prepend"
>
<div class="row q-gutter-x-sm">
<QBtn @click="addRow()" icon="add" color="primary" dense rounded>
<QTooltip bottom> {{ t('globals.add') }} </QTooltip>
</QBtn>
<QSeparator vertical />
</div>
</Teleport>
<QPageSticky
v-if="quasar.platform.is.mobile"
position="bottom"
:offset="[0, 0]"
expand
>
<QToolbar class="bg-primary text-white q-pa-none">
<QTabs class="full-width" align="justify" inline-label narrow-indicator>
<QTab @click="addRow()" icon="add_circle" :label="t('globals.add')" />
</QTabs>
</QToolbar>
</QPageSticky> </QPageSticky>
</template> </template>
@ -178,16 +137,6 @@ async function remove({ id }) {
width: 100%; width: 100%;
max-width: 60em; max-width: 60em;
} }
.q-toolbar {
background-color: $grey-9;
}
.sticky-page {
padding-top: 66px;
}
.q-page-sticky {
z-index: 2998;
}
</style> </style>
<i18n> <i18n>

View File

@ -1,11 +1,13 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useRoute } from 'vue-router';
import CustomerDescriptor from './CustomerDescriptor.vue'; import CustomerDescriptor from './CustomerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue'; import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
const stateStore = useStateStore(); const stateStore = useStateStore();
const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
</script> </script>
<template> <template>
@ -25,8 +27,13 @@ const { t } = useI18n();
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<QPageContainer> <QPageContainer>
<QPage class="q-pa-md"> <QPage>
<RouterView></RouterView> <QToolbar class="bg-vn-dark justify-end">
<div id="st-data"></div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<div class="q-pa-md"><RouterView></RouterView></div>
</QPage> </QPage>
</QPageContainer> </QPageContainer>
</template> </template>

View File

@ -25,8 +25,13 @@ const { t } = useI18n();
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<QPageContainer> <QPageContainer>
<QPage class="q-pa-md"> <QPage>
<RouterView></RouterView> <QToolbar class="bg-vn-dark justify-end">
<div id="st-data"></div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<div class="q-pa-md"><RouterView></RouterView></div>
</QPage> </QPage>
</QPageContainer> </QPageContainer>
</template> </template>

View File

@ -25,8 +25,13 @@ const { t } = useI18n();
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<QPageContainer> <QPageContainer>
<QPage class="q-pa-md"> <QPage>
<RouterView></RouterView> <QToolbar class="bg-vn-dark justify-end">
<div id="st-data"></div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<div class="q-pa-md"><RouterView></RouterView></div>
</QPage> </QPage>
</QPageContainer> </QPageContainer>
</template> </template>

View File

@ -20,7 +20,7 @@ const $props = defineProps({
}); });
const entityId = computed(() => $props.id || route.params.id); const entityId = computed(() => $props.id || route.params.id);
let wagonTypes; let wagonTypes = [];
let originalData = {}; let originalData = {};
const wagon = ref({}); const wagon = ref({});
const filteredWagonTypes = ref(wagonTypes); const filteredWagonTypes = ref(wagonTypes);

View File

@ -25,8 +25,13 @@ const { t } = useI18n();
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<QPageContainer> <QPageContainer>
<QPage class="q-pa-md"> <QPage>
<RouterView></RouterView> <QToolbar class="bg-vn-dark justify-end">
<div id="st-data"></div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<div class="q-pa-md"><RouterView></RouterView></div>
</QPage> </QPage>
</QPageContainer> </QPageContainer>
</template> </template>

View File

@ -1,6 +1,5 @@
<script setup> <script setup>
import axios from 'axios'; import { ref, onMounted, computed } from 'vue';
import { ref, onMounted, computed, onUpdated } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import CardSummary from 'components/ui/CardSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';

View File

@ -18,6 +18,7 @@ export default {
'ClaimPhotos', 'ClaimPhotos',
'ClaimLog', 'ClaimLog',
'ClaimNotes', 'ClaimNotes',
'ClaimDevelopment',
], ],
}, },
children: [ children: [
@ -101,6 +102,16 @@ export default {
}, },
component: () => import('src/pages/Claim/Card/ClaimPhoto.vue'), component: () => import('src/pages/Claim/Card/ClaimPhoto.vue'),
}, },
{
name: 'ClaimDevelopment',
path: 'development',
meta: {
title: 'development',
icon: 'vn:traceability',
roles: ['claimManager'],
},
component: () => import('src/pages/Claim/Card/ClaimDevelopment.vue'),
},
{ {
name: 'ClaimLog', name: 'ClaimLog',
path: 'log', path: 'log',

View File

@ -30,6 +30,13 @@ export const useStateStore = defineStore('stateStore', () => {
return rightDrawer.value; return rightDrawer.value;
} }
function isSubToolbarShown() {
return (
!!document.querySelector('#st-data') &&
!!document.querySelector('#st-actions')
);
}
return { return {
leftDrawer, leftDrawer,
rightDrawer, rightDrawer,
@ -39,5 +46,6 @@ export const useStateStore = defineStore('stateStore', () => {
toggleRightDrawer, toggleRightDrawer,
isLeftDrawerShown, isLeftDrawerShown,
isRightDrawerShown, isRightDrawerShown,
isSubToolbarShown,
}; };
}); });

View File

@ -0,0 +1,53 @@
/// <reference types="cypress" />
describe('ClaimDevelopment', () => {
const claimId = 1;
const firstLineReason = 'tbody > :nth-child(1) > :nth-child(2)';
const thirdRow = 'tbody > :nth-child(3)';
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit(`/#/claim/${claimId}/development`);
});
it('should reset line', () => {
cy.selectOption(firstLineReason, 'Novato');
cy.resetCard();
cy.getValue(firstLineReason).should('have.text', 'Prisas');
});
it('should edit line', () => {
cy.selectOption(firstLineReason, 'Novato');
cy.saveCard();
cy.reload();
cy.getValue(firstLineReason).should('have.text', 'Novato');
//Restart data
cy.selectOption(firstLineReason, 'Prisas');
cy.saveCard();
});
it('should add and remove new line', () => {
//add row
cy.addCard();
cy.get(thirdRow).should('exist');
const rowData = [false, 'Novato', 'Roces', 'Compradores', 'employeeNick', 'Tour'];
cy.fillRow(thirdRow, rowData);
cy.saveCard();
cy.validateRow(thirdRow, rowData);
cy.reload();
cy.validateRow(thirdRow, rowData);
//remove row
cy.fillRow(thirdRow, [true]);
cy.removeCard();
cy.clickConfirm();
cy.get(thirdRow).should('not.exist');
cy.reload();
cy.get(thirdRow).should('not.exist');
});
});

View File

@ -40,4 +40,91 @@ Cypress.Commands.add('login', (user) => {
window.localStorage.setItem('token', response.body.token); window.localStorage.setItem('token', response.body.token);
}); });
}); });
Cypress.Commands.add('waitForElement', (element) => {
Review

He fet tots estos comandos per a facilitar el fer els e2e i te funcions com getValue que ja te mira que tipo de componente es i teu trau.

Soles els he adaptat per als QSelects i QCheckbox que era el cas que necesitava si me para adaptar mes componentes ja se me fea mes jaleo pq no els gaste realment en un e2e

He fet tots estos comandos per a facilitar el fer els e2e i te funcions com getValue que ja te mira que tipo de componente es i teu trau. Soles els he adaptat per als QSelects i QCheckbox que era el cas que necesitava si me para adaptar mes componentes ja se me fea mes jaleo pq no els gaste realment en un e2e
cy.get(element, { timeout: 2000 }).should('be.visible');
});
Cypress.Commands.add('getValue', (selector) => {
cy.get(selector).then(($el) => {
if ($el.find('.q-checkbox__inner').length > 0) {
return cy.get(selector + '.q-checkbox__inner');
}
// Si es un QSelect
else if ($el.find('.q-select__dropdown-icon').length) {
return cy.get(
selector +
'> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > span'
);
} else {
// Puedes añadir un log o lanzar un error si el elemento no es reconocido
cy.log('Elemento no soportado');
}
});
});
// Fill Inputs
Cypress.Commands.add('selectOption', (selector, option) => {
cy.get(selector).find('.q-select__dropdown-icon').click();
cy.get('.q-menu .q-item').contains(option).click();
});
Cypress.Commands.add('checkOption', (selector) => {
cy.wrap(selector).find('.q-checkbox__inner').click();
});
// Global buttons
Cypress.Commands.add('saveCard', () => {
cy.get('[title="Save"]').click();
cy.get('[title="Save"]').should('have.class', 'disabled');
});
Cypress.Commands.add('resetCard', () => {
cy.get('[title="Reset"]').click();
});
Cypress.Commands.add('removeCard', () => {
cy.get('[title="Remove"]').click();
});
Cypress.Commands.add('addCard', () => {
cy.waitForElement('tbody');
cy.get('.q-page-sticky > div > .q-btn').click();
});
Cypress.Commands.add('clickConfirm', () => {
cy.get('.q-btn--unelevated > .q-btn__content > .block').click();
});
Cypress.Commands.add('fillRow', (rowSelector, data) => {
// Usar el selector proporcionado para obtener la fila deseada
cy.waitForElement('tbody');
cy.get(rowSelector).as('currentRow');
data.forEach((value, index) => {
if (value === null) return;
cy.get('@currentRow')
.find('td')
.eq(index)
.then((td) => {
if (td.find('.q-select__dropdown-icon').length) {
cy.selectOption(td, value);
}
if (td.find('.q-checkbox__inner').length && value) {
cy.checkOption(td);
}
});
});
});
Cypress.Commands.add('validateRow', (rowSelector, expectedValues) => {
cy.waitForElement('tbody');
cy.get(rowSelector).within(() => {
for (const [index, value] of expectedValues.entries()) {
cy.log('CHECKING ', index, value);
if (typeof value == 'boolean') {
const prefix = value ? '' : 'not.';
cy.getValue(`:nth-child(${index + 1})`).should(`${prefix}be.checked`);
continue;
}
cy.getValue(`:nth-child(${index + 1})`).should('have.text', value);
}
});
});
// registerCommands(); // registerCommands();

View File

@ -0,0 +1,120 @@
import { createWrapper } from 'app/test/vitest/helper';
import CrudModel from 'components/CrudModel.vue';
import { vi, afterEach, beforeEach, beforeAll, describe, expect, it } from 'vitest';
describe('CrudModel', () => {
let vm;
beforeAll(() => {
vm = createWrapper(CrudModel, {
global: {
stubs: [
'vnPaginate',
'useState',
'arrayData',
'useStateStore',
'vue-i18n',
],
mocks: {
validate: vi.fn(),
},
},
propsData: {
dataRequired: {
fk: 1,
},
dataKey: 'crudModelKey',
model: 'crudModel',
url: 'crudModelUrl',
},
}).vm;
});
beforeEach(() => {
vm.fetch([]);
});
afterEach(() => {
vi.clearAllMocks();
});
describe('insert()', () => {
it('should new element in list with index 0 if formData not has data', () => {
vm.insert();
expect(vm.formData.length).toEqual(1);
expect(vm.formData[0].fk).toEqual(1);
expect(vm.formData[0].$index).toEqual(0);
});
});
describe('getChanges()', () => {
it('should return correct updates and creates', async () => {
vm.fetch([
{ id: 1, name: 'New name one' },
{ id: 2, name: 'New name two' },
{ id: 3, name: 'Bruce Wayne' },
]);
vm.originalData = [
{ id: 1, name: 'Tony Starks' },
{ id: 2, name: 'Jessica Jones' },
{ id: 3, name: 'Bruce Wayne' },
];
vm.insert();
const result = vm.getChanges();
const expected = {
creates: [
{
$index: 3,
fk: 1,
},
],
updates: [
{
data: {
name: 'New name one',
},
where: {
id: 1,
},
},
{
data: {
name: 'New name two',
},
where: {
id: 2,
},
},
],
};
expect(result).toEqual(expected);
});
});
describe('getDifferences()', () => {
Outdated
Review

Soles me ha faltat este test. Pq com es el dialogo al donarli a Acceptar qui borra. Hi ha que mockejar el componente de dialogo i tal i no me ha donat temps.

Soles me ha faltat este test. Pq com es el dialogo al donarli a Acceptar qui borra. Hi ha que mockejar el componente de dialogo i tal i no me ha donat temps.
it('should return the differences between two objects', async () => {
const obj1 = {
a: 1,
b: 2,
c: 3,
};
const obj2 = {
a: null,
b: 4,
d: 5,
};
const result = vm.getDifferences(obj1, obj2);
expect(result).toEqual({
a: null,
b: 4,
d: 5,
});
});
});
});

View File

@ -5,7 +5,6 @@ import ClaimLines from 'pages/Claim/Card/ClaimLines.vue';
describe('ClaimLines', () => { describe('ClaimLines', () => {
let vm; let vm;
beforeAll(() => { beforeAll(() => {
vm = createWrapper(ClaimLines, { vm = createWrapper(ClaimLines, {
global: { global: {
@ -13,25 +12,26 @@ describe('ClaimLines', () => {
mocks: { mocks: {
fetch: vi.fn(), fetch: vi.fn(),
}, },
} },
}).vm; }).vm;
}); });
beforeEach(() => { beforeEach(() => {
vm.claim = { vm.claim = {
id: 1, id: 1,
ticketFk: 1 ticketFk: 1,
} };
vm.store.data = [ vm.store.data = [
{ {
id: 1, id: 1,
quantity: 10, quantity: 10,
sale: { sale: {
id: 1, discount: 0 id: 1,
} discount: 0,
} },
] },
}) ];
});
afterEach(() => { afterEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
@ -42,13 +42,17 @@ describe('ClaimLines', () => {
vi.spyOn(axios, 'post').mockResolvedValue({ data: true }); vi.spyOn(axios, 'post').mockResolvedValue({ data: true });
vi.spyOn(vm.quasar, 'notify'); vi.spyOn(vm.quasar, 'notify');
const canceller = new AbortController() const canceller = new AbortController();
await vm.updateDiscount({ saleFk: 1, discount: 5, canceller }); await vm.updateDiscount({ saleFk: 1, discount: 5, canceller });
const expectedData = { salesIds: [1], newDiscount: 5 } const expectedData = { salesIds: [1], newDiscount: 5 };
expect(axios.post).toHaveBeenCalledWith('Tickets/1/updateDiscount', expectedData, { expect(axios.post).toHaveBeenCalledWith(
signal: canceller.signal 'Tickets/1/updateDiscount',
}) expectedData,
{
signal: canceller.signal,
}
);
}); });
}); });
@ -56,37 +60,14 @@ describe('ClaimLines', () => {
it('should make a POST request and then set the discount on the original row', async () => { it('should make a POST request and then set the discount on the original row', async () => {
vi.spyOn(vm.quasar, 'notify'); vi.spyOn(vm.quasar, 'notify');
vm.onUpdateDiscount({ discount: 5, rowIndex: 0 }); vm.onUpdateDiscount({ discount: 5, rowIndex: 0 });
const firstRow = vm.store.data[0] const firstRow = vm.store.data[0];
expect(firstRow.sale.discount).toEqual(5) expect(firstRow.sale.discount).toEqual(5);
expect(vm.quasar.notify).toHaveBeenCalledWith( expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
message: 'Discount updated', message: 'Discount updated',
type: 'positive' type: 'positive',
})
);
});
});
describe('remove()', () => {
it('should make a POST request and then call to the quasar notify() method', async () => {
vi.spyOn(axios, 'post').mockResolvedValue({ data: true });
vi.spyOn(vm.quasar, 'notify');
await vm.remove({
rows: [
{ id: 1 }
]
});
const expectedData = { deletes: [1] }
expect(axios.post).toHaveBeenCalledWith('ClaimBeginnings/crud', expectedData)
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Row removed',
type: 'positive'
}) })
); );
}); });

View File

@ -7,9 +7,11 @@ describe('WagonCreate', () => {
const entityId = 1; const entityId = 1;
beforeAll(() => { beforeAll(() => {
vmEdit = createWrapper(WagonCreate, {propsData: { vmEdit = createWrapper(WagonCreate, {
propsData: {
id: entityId, id: entityId,
}}).vm; },
}).vm;
vmCreate = createWrapper(WagonCreate).vm; vmCreate = createWrapper(WagonCreate).vm;
}); });
@ -29,9 +31,7 @@ describe('WagonCreate', () => {
await vmCreate.onSubmit(); await vmCreate.onSubmit();
expect(axios.patch).toHaveBeenCalledWith( expect(axios.patch).toHaveBeenCalledWith(`Wagons`, vmCreate.wagon);
`Wagons`, vmCreate.wagon
);
}); });
it('should update a wagon', async () => { it('should update a wagon', async () => {
@ -46,9 +46,7 @@ describe('WagonCreate', () => {
await vmEdit.onSubmit(); await vmEdit.onSubmit();
expect(axios.patch).toHaveBeenCalledWith( expect(axios.patch).toHaveBeenCalledWith(`Wagons`, vmEdit.wagon);
`Wagons`, vmEdit.wagon
);
}); });
}); });
@ -88,16 +86,12 @@ describe('WagonCreate', () => {
describe('fetch()', () => { describe('fetch()', () => {
it('should fetch data', async () => { it('should fetch data', async () => {
vi.spyOn(axios, 'get').mockResolvedValue({ data: true }); vi.spyOn(axios, 'get').mockResolvedValue({ data: [] });
await vmEdit.fetch(); await vmEdit.fetch();
expect(axios.get).toHaveBeenCalledWith( expect(axios.get).toHaveBeenCalledWith(`WagonTypes`);
`WagonTypes` expect(axios.get).toHaveBeenCalledWith(`Wagons/${entityId}`);
);
expect(axios.get).toHaveBeenCalledWith(
`Wagons/${entityId}`
);
}); });
}); });
}); });

View File

@ -5,6 +5,7 @@ import { vi } from 'vitest';
import { i18n } from 'src/boot/i18n'; import { i18n } from 'src/boot/i18n';
import { Notify, Dialog } from 'quasar'; import { Notify, Dialog } from 'quasar';
import axios from 'axios'; import axios from 'axios';
import * as useValidator from 'src/composables/useValidator';
installQuasarPlugin({ installQuasarPlugin({
plugins: { plugins: {
@ -34,6 +35,10 @@ vi.mock('vue-router', () => ({
}), }),
})); }));
vi.spyOn(useValidator, 'useValidator').mockImplementation(() => {
return { validate: vi.fn(), fetch: vi.fn() };
});
class FormDataMock { class FormDataMock {
append() { append() {
vi.fn(); vi.fn();
@ -64,6 +69,10 @@ export function createWrapper(component, options) {
global: { global: {
plugins: [i18n, pinia], plugins: [i18n, pinia],
}, },
mocks: {
t: (tKey) => tKey,
$t: (tKey) => tKey,
},
}; };
const mountOptions = Object.assign({}, defaultOptions); const mountOptions = Object.assign({}, defaultOptions);