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: [
{
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

View File

@ -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: {

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 { 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 () => {
</QBanner>
<QForm v-if="formData" @submit="save" @reset="reset" class="q-pa-md">
<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>
<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" />
<QInnerLoading
: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 { t } = useI18n();
const entity = ref();
onMounted(() => fetch());
onMounted(async () => {
await fetch();
});
const emit = defineEmits(['onFetch']);
const entity = ref();
async function fetch() {
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,
default: 500,
},
skeleton: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['onFetch', 'onPaginate']);
@ -144,7 +148,10 @@ async function onLoad(...params) {
{{ t('No results found') }}
</h5>
</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">
<QItem v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
<QItemSection class="q-pa-md">
@ -164,7 +171,7 @@ async function onLoad(...params) {
</QCard>
</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>
<div v-if="isLoading" class="info-row q-pa-md text-center">
<QSpinner color="orange" size="md" />

View File

@ -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,
};
}

View File

@ -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,
};
}
}

View File

@ -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);
}

View File

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

View File

@ -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',

View File

@ -44,17 +44,6 @@ onMounted(async () => {
<LeftMenu source="card" />
<QSeparator />
<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
active-class="text-primary"
clickable
@ -68,8 +57,13 @@ onMounted(async () => {
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage class="q-pa-md">
<RouterView></RouterView>
<QPage>
<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>
</QPageContainer>
</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
Details: Detalles
Notes: Notas
Development: Trazabilidad
Action: Acción
</i18n>

View File

@ -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);
};
</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 { 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,6 +36,7 @@ const linesFilter = {
},
};
const claimLinesForm = ref();
const claim = ref(null);
async function onFetchClaim(data) {
claim.value = data;
@ -46,6 +47,7 @@ async function onFetchClaim(data) {
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,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() {
quasar
.dialog({
@ -191,10 +152,8 @@ function showImportDialog() {
}
</script>
<template>
<QPageSticky position="top" :offset="[0, 0]" expand>
<Teleport to="#st-data" v-if="stateStore.isSubToolbarShown()">
<QToolbar class="bg-dark text-white">
<QToolbarTitle> {{ t('Claimed lines') }} </QToolbarTitle>
<QSpace />
<div class="row q-gutter-md">
<div>
{{ t('Amount') }}
@ -211,7 +170,7 @@ function showImportDialog() {
</div>
</div>
</QToolbar>
</QPageSticky>
</Teleport>
<FetchData
:url="`Claims/${route.params.id}`"
@ -221,11 +180,16 @@ function showImportDialog() {
/>
<div class="column items-center">
<div class="list">
<VnPaginate
<CrudModel
data-key="ClaimLines"
ref="claimLinesForm"
:url="`Claims/${route.params.id}/lines`"
save-url="ClaimBeginnings/crud"
:filter="linesFilter"
@on-fetch="onFetch"
v-model:selected="selected"
:default-save="false"
:default-reset="false"
auto-load
>
<template #body="{ rows }">
@ -361,46 +325,12 @@ function showImportDialog() {
</template>
</QTable>
</template>
</VnPaginate>
</CrudModel>
</div>
</div>
<Teleport
v-if="stateStore.isHeaderMounted() && !$q.screen.lt.sm"
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 position="bottom-right" :offset="[25, 25]">
<QBtn fab color="primary" icon="add" @click="showImportDialog()" />
</QPageSticky>
</template>
@ -421,7 +351,6 @@ en:
You are about to remove <strong>{count}</strong> row |
You are about to remove <strong>{count}</strong> rows'
es:
Claimed lines: Líneas reclamadas
Delivered: Entregado
Quantity: Cantidad
Claimed: Reclamada

View File

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

View File

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

View File

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

View File

@ -1,11 +1,13 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { useRoute } from 'vue-router';
import CustomerDescriptor from './CustomerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
const stateStore = useStateStore();
const route = useRoute();
const { t } = useI18n();
</script>
<template>
@ -25,8 +27,13 @@ const { t } = useI18n();
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage class="q-pa-md">
<RouterView></RouterView>
<QPage>
<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>
</QPageContainer>
</template>

View File

@ -25,8 +25,13 @@ const { t } = useI18n();
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage class="q-pa-md">
<RouterView></RouterView>
<QPage>
<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>
</QPageContainer>
</template>

View File

@ -25,8 +25,13 @@ const { t } = useI18n();
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage class="q-pa-md">
<RouterView></RouterView>
<QPage>
<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>
</QPageContainer>
</template>

View File

@ -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);

View File

@ -25,8 +25,13 @@ const { t } = useI18n();
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage class="q-pa-md">
<RouterView></RouterView>
<QPage>
<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>
</QPageContainer>
</template>

View File

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

View File

@ -18,6 +18,7 @@ export default {
'ClaimPhotos',
'ClaimLog',
'ClaimNotes',
'ClaimDevelopment',
],
},
children: [
@ -101,6 +102,16 @@ export default {
},
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',
path: 'log',

View File

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

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', () => {
let vm;
beforeAll(() => {
vm = createWrapper(ClaimLines, {
global: {
@ -13,25 +12,26 @@ describe('ClaimLines', () => {
mocks: {
fetch: vi.fn(),
},
}
},
}).vm;
});
beforeEach(() => {
vm.claim = {
id: 1,
ticketFk: 1
}
ticketFk: 1,
};
vm.store.data = [
{
id: 1,
quantity: 10,
sale: {
id: 1, discount: 0
}
}
]
})
id: 1,
discount: 0,
},
},
];
});
afterEach(() => {
vi.clearAllMocks();
@ -42,13 +42,17 @@ describe('ClaimLines', () => {
vi.spyOn(axios, 'post').mockResolvedValue({ data: true });
vi.spyOn(vm.quasar, 'notify');
const canceller = new AbortController()
const canceller = new AbortController();
await vm.updateDiscount({ saleFk: 1, discount: 5, canceller });
const expectedData = { salesIds: [1], newDiscount: 5 }
expect(axios.post).toHaveBeenCalledWith('Tickets/1/updateDiscount', expectedData, {
signal: canceller.signal
})
const expectedData = { salesIds: [1], newDiscount: 5 };
expect(axios.post).toHaveBeenCalledWith(
'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 () => {
vi.spyOn(vm.quasar, 'notify');
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.objectContaining({
message: 'Discount updated',
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'
type: 'positive',
})
);
});

View File

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

View File

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