feat: refs #8304 added remove option to operator #1195

Merged
jtubau merged 27 commits from 8304-workerChangesAndFixes into dev 2025-02-05 12:25:27 +00:00
8 changed files with 244 additions and 71 deletions

View File

@ -17,7 +17,7 @@ import { useSession } from 'src/composables/useSession';
const route = useRoute();
const quasar = useQuasar();
const { t } = useI18n();
const rows = ref();
const rows = ref([]);
const dmsRef = ref();
const formDialog = ref({});
const token = useSession().getTokenMultimedia();
@ -389,6 +389,14 @@ defineExpose({
</div>
</template>
</QTable>
<div
v-else
class="info-row q-pa-md text-center"
>
<h5>
{{ t('No data to display') }}
</h5>
</div>
</template>
</VnPaginate>
<QDialog v-model="formDialog.show">

View File

@ -1,51 +1,78 @@
import { describe, it, expect, vi, beforeAll, afterEach, beforeEach } from 'vitest';
import {
describe,
it,
expect,
vi,
beforeAll,
afterEach,
beforeEach,
afterAll,
} from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper';
import VnNotes from 'src/components/ui/VnNotes.vue';
import vnDate from 'src/boot/vnDate';
describe('VnNotes', () => {
let vm;
let wrapper;
let spyFetch;
let postMock;
let expectedBody;
const mockData= {name: 'Tony', lastName: 'Stark', text: 'Test Note', observationTypeFk: 1};
function generateExpectedBody() {
expectedBody = {...vm.$props.body, ...{ text: vm.newNote.text, observationTypeFk: vm.newNote.observationTypeFk }};
}
async function setTestParams(text, observationType, type){
vm.newNote.text = text;
vm.newNote.observationTypeFk = observationType;
wrapper.setProps({ selectType: type });
}
beforeAll(async () => {
vi.spyOn(axios, 'get').mockReturnValue({ data: [] });
let patchMock;
let expectedInsertBody;
let expectedUpdateBody;
const defaultOptions = {
url: '/test',
body: { name: 'Tony', lastName: 'Stark' },
selectType: false,
saveUrl: null,
justInput: false,
};
function generateWrapper(
options = defaultOptions,
text = null,
observationType = null,
) {
vi.spyOn(axios, 'get').mockResolvedValue({ data: [] });
wrapper = createWrapper(VnNotes, {
propsData: {
url: '/test',
body: { name: 'Tony', lastName: 'Stark' },
}
propsData: options,
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
});
vm.newNote.text = text;
vm.newNote.observationTypeFk = observationType;
}
function createSpyFetch() {
spyFetch = vi.spyOn(vm.$refs.vnPaginateRef, 'fetch');
}
function generateExpectedBody() {
expectedInsertBody = {
...vm.$props.body,
...{ text: vm.newNote.text, observationTypeFk: vm.newNote.observationTypeFk },
};
expectedUpdateBody = { ...vm.$props.body, ...{ notes: vm.newNote.text } };
}
beforeEach(() => {
postMock = vi.spyOn(axios, 'post').mockResolvedValue(mockData);
spyFetch = vi.spyOn(vm.vnPaginateRef, 'fetch').mockImplementation(() => vi.fn());
postMock = vi.spyOn(axios, 'post');
patchMock = vi.spyOn(axios, 'patch');
});
afterEach(() => {
vi.clearAllMocks();
expectedBody = {};
expectedInsertBody = {};
expectedUpdateBody = {};
});
afterAll(() => {
vi.restoreAllMocks();
});
describe('insert', () => {
it('should not call axios.post and vnPaginateRef.fetch if newNote.text is null', async () => {
await setTestParams( null, null, true );
it('should not call axios.post and vnPaginateRef.fetch when newNote.text is null', async () => {
generateWrapper({ selectType: true });
createSpyFetch();
await vm.insert();
@ -53,8 +80,9 @@ describe('VnNotes', () => {
expect(spyFetch).not.toHaveBeenCalled();
});
it('should not call axios.post and vnPaginateRef.fetch if newNote.text is empty', async () => {
await setTestParams( "", null, false );
it('should not call axios.post and vnPaginateRef.fetch when newNote.text is empty', async () => {
generateWrapper(null, '');
createSpyFetch();
await vm.insert();
@ -62,8 +90,9 @@ describe('VnNotes', () => {
expect(spyFetch).not.toHaveBeenCalled();
});
it('should not call axios.post and vnPaginateRef.fetch if observationTypeFk is missing and selectType is true', async () => {
await setTestParams( "Test Note", null, true );
it('should not call axios.post and vnPaginateRef.fetch when observationTypeFk is null and selectType is true', async () => {
generateWrapper({ selectType: true }, 'Test Note');
createSpyFetch();
await vm.insert();
@ -71,37 +100,57 @@ describe('VnNotes', () => {
expect(spyFetch).not.toHaveBeenCalled();
});
it('should call axios.post and vnPaginateRef.fetch if observationTypeFk is missing and selectType is false', async () => {
await setTestParams( "Test Note", null, false );
it('should call axios.post and vnPaginateRef.fetch when observationTypeFk is missing and selectType is false', async () => {
generateWrapper(null, 'Test Note');
createSpyFetch();
generateExpectedBody();
await vm.insert();
expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody);
expect(spyFetch).toHaveBeenCalled();
});
it('should call axios.post and vnPaginateRef.fetch if observationTypeFk is setted and selectType is false', async () => {
await setTestParams( "Test Note", 1, false );
generateExpectedBody();
await vm.insert();
expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody);
expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedInsertBody);
expect(spyFetch).toHaveBeenCalled();
});
it('should call axios.post and vnPaginateRef.fetch when newNote is valid', async () => {
await setTestParams( "Test Note", 1, true );
generateWrapper({ selectType: true }, 'Test Note', 1);
createSpyFetch();
generateExpectedBody();
await vm.insert();
expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody);
expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedInsertBody);
expect(spyFetch).toHaveBeenCalled();
});
});
});
describe('update', () => {
it('should call axios.patch with saveUrl when saveUrl is set and justInput is true', async () => {
generateWrapper({
url: '/business',
justInput: true,
saveUrl: '/saveUrlTest',
});
generateExpectedBody();
await vm.update();
expect(patchMock).toHaveBeenCalledWith(vm.$props.saveUrl, expectedUpdateBody);
});
it('should call axios.patch with url when saveUrl is not set and justInput is true', async () => {
generateWrapper({
url: '/business',
body: { workerFk: 1110 },
justInput: true,
});
generateExpectedBody();
await vm.update();
expect(patchMock).toHaveBeenCalledWith(
`${vm.$props.url}/${vm.$props.body.workerFk}`,
expectedUpdateBody,
);
});
});
});

View File

@ -1,6 +1,6 @@
<script setup>
import axios from 'axios';
import { ref, reactive } from 'vue';
import { ref, reactive, useAttrs, computed } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
@ -16,12 +16,22 @@ import VnSelect from 'components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import VnInput from 'components/common/VnInput.vue';
const emit = defineEmits(['onFetch']);
Review

Tengo dudas de este componente porque le hemos metido la lógica de cuando es input, y tenemos que jugar con 2 condiciones
Mi propuesta es definir un componente tipo VnObservation que se comporte como cuando dices que es just-input=true, VnNoteType para el Vnelect y VnNote que tenga solo la lógica del textarea
Pero creo que son demasiados cambios para esta PR

Conclusión: VnNotes depende de 3 condiciones para renderizar que elemento u otro. Desacoplar la lógica

Tengo dudas de este componente porque le hemos metido la lógica de cuando es input, y tenemos que jugar con 2 condiciones Mi propuesta es definir un componente tipo VnObservation que se comporte como cuando dices que es just-input=true, VnNoteType para el Vnelect y VnNote que tenga solo la lógica del textarea Pero creo que son demasiados cambios para esta PR Conclusión: VnNotes depende de 3 condiciones para renderizar que elemento u otro. Desacoplar la lógica
Review

Yo lo dejaria en 2. Un VnNoteInput y un VnNote.
Donde el VnNoteInput seria el textArea.
Y el VnNote tener la logica, y este dividido en 2 partes.
Arriba un slot que por defecto contenga el VnNoteInput
Y abajo el VnPaginate

Yo lo dejaria en 2. Un VnNoteInput y un VnNote. Donde el VnNoteInput seria el textArea. Y el VnNote tener la logica, y este dividido en 2 partes. Arriba un slot que por defecto contenga el VnNoteInput Y abajo el VnPaginate
Review

Por tanto, lo dejamos así y creamos tarea con los comentarios que hemos puesto
Importante: referenciar la PR y ambos comentarios en la descripción de la tarea. Indicando: que la funcionalidad que fusionemos en esta PR debe quedar en el mismo estado. Añadir estos como checklist

Por tanto, lo dejamos así y creamos tarea con los comentarios que hemos puesto *Importante*: referenciar la PR y ambos comentarios en la descripción de la tarea. *Indicando*: que la funcionalidad que fusionemos en esta PR debe quedar en el mismo estado. Añadir estos como checklist
const $attrs = useAttrs();
const isRequired = computed(() => {
return Object.keys($attrs).includes('required')
});
const $props = defineProps({
url: { type: String, default: null },
saveUrl: {type: String, default: null},
filter: { type: Object, default: () => {} },
body: { type: Object, default: () => {} },
addNote: { type: Boolean, default: false },
selectType: { type: Boolean, default: false },
justInput: { type: Boolean, default: false },
});
const { t } = useI18n();
@ -29,6 +39,13 @@ const quasar = useQuasar();
const newNote = reactive({ text: null, observationTypeFk: null });
const observationTypes = ref([]);
const vnPaginateRef = ref();
let originalText;
function handleClick(e) {
if (e.shiftKey && e.key === 'Enter') return;
if ($props.justInput) confirmAndUpdate();
else insert();
}
async function insert() {
if (!newNote.text || ($props.selectType && !newNote.observationTypeFk)) return;
@ -41,8 +58,36 @@ async function insert() {
await axios.post($props.url, newBody);
await vnPaginateRef.value.fetch();
}
function confirmAndUpdate() {
if(!newNote.text && originalText)
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('New note is empty'),
message: t('Are you sure remove this note?'),
},
})
.onOk(update)
.onCancel(() => {
newNote.text = originalText;
});
else update();
}
async function update() {
originalText = newNote.text;
const body = $props.body;
const newBody = {
...body,
...{ notes: newNote.text },
};
await axios.patch(`${$props.saveUrl ?? `${$props.url}/${$props.body.workerFk}`}`, newBody);
}
onBeforeRouteLeave((to, from, next) => {
if (newNote.text)
if ((newNote.text && !$props.justInput) || (newNote.text !== originalText) && $props.justInput)
quasar.dialog({
component: VnConfirm,
componentProps: {
@ -53,6 +98,13 @@ onBeforeRouteLeave((to, from, next) => {
});
else next();
});
function fetchData([ data ]) {
newNote.text = data?.notes;
originalText = data?.notes;
emit('onFetch', data);
}
</script>
<template>
<FetchData
@ -62,8 +114,19 @@ onBeforeRouteLeave((to, from, next) => {
auto-load
@on-fetch="(data) => (observationTypes = data)"
/>
<QCard class="q-pa-xs q-mb-lg full-width" v-if="$props.addNote">
<QCardSection horizontal>
<FetchData
v-if="justInput"
:url="url"
:filter="filter"
@on-fetch="fetchData"
auto-load
/>
<QCard
class="q-pa-xs q-mb-lg full-width"
:class="{ 'just-input': $props.justInput }"
v-if="$props.addNote || $props.justInput"
>
<QCardSection horizontal v-if="!$props.justInput">
{{ t('New note') }}
</QCardSection>
<QCardSection class="q-px-xs q-my-none q-py-none">
@ -75,19 +138,19 @@ onBeforeRouteLeave((to, from, next) => {
v-model="newNote.observationTypeFk"
option-label="description"
style="flex: 0.15"
:required="true"
:required="isRequired"
@keyup.enter.stop="insert"
/>
<VnInput
v-model.trim="newNote.text"
type="textarea"
:label="t('Add note here...')"
:label="$props.justInput && newNote.text ? '' : t('Add note here...')"
filled
size="lg"
autogrow
@keyup.enter.stop="insert"
@keyup.enter.stop="handleClick"
:required="isRequired"
clearable
:required="true"
>
<template #append>
<QBtn
@ -95,7 +158,7 @@ onBeforeRouteLeave((to, from, next) => {
icon="save"
color="primary"
flat
@click="insert"
@click="handleClick"
class="q-mb-xs"
dense
data-cy="saveNote"
@ -106,6 +169,7 @@ onBeforeRouteLeave((to, from, next) => {
</QCardSection>
</QCard>
<VnPaginate
v-if="!$props.justInput"
:data-key="$props.url"
:url="$props.url"
order="created DESC"
@ -198,6 +262,11 @@ onBeforeRouteLeave((to, from, next) => {
}
}
}
.just-input {
padding-right: 18px;
margin-bottom: 2px;
box-shadow: none;
}
</style>
<i18n>
es:
@ -205,4 +274,6 @@ onBeforeRouteLeave((to, from, next) => {
New note: Nueva nota
Save (Enter): Guardar (Intro)
Observation type: Tipo de observación
New note is empty: La nueva nota esta vacia
Are you sure remove this note?: Estas seguro de quitar esta nota?
</i18n>

View File

@ -19,23 +19,26 @@ onMounted(() => {
const observer = new MutationObserver(
() =>
(hasContent.value =
actions.value?.childNodes?.length + data.value?.childNodes?.length)
actions.value?.childNodes?.length + data.value?.childNodes?.length),
);
if (actions.value) observer.observe(actions.value, opts);
if (data.value) observer.observe(data.value, opts);
});
onBeforeUnmount(() => stateStore.toggleSubToolbar());
const actionsChildCount = () => !!actions.value?.childNodes?.length;
onBeforeUnmount(() => stateStore.toggleSubToolbar() && hasSubToolbar);
</script>
<template>
<QToolbar
id="subToolbar"
class="justify-end sticky"
v-show="hasContent || $slots['st-actions'] || $slots['st-data']"
class="justify-end sticky"
>
<slot name="st-data">
<div id="st-data"></div>
<div id="st-data" :class="{ 'full-width': !actionsChildCount() }">
</div>
</slot>
<QSpace />
<slot name="st-actions">

View File

@ -23,5 +23,6 @@ const noteFilter = computed(() => {
:body="{ clientFk: route.params.id }"
style="overflow-y: auto"
:select-type="true"
required
/>
</template>

View File

@ -1,7 +1,8 @@
<script setup>
import { nextTick, ref, watch } from 'vue';
import { nextTick, ref, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useAcl } from 'src/composables/useAcl';
import WorkerCalendarFilter from 'pages/Worker/Card/WorkerCalendarFilter.vue';
import FetchData from 'components/FetchData.vue';
@ -9,10 +10,17 @@ import WorkerCalendarItem from 'pages/Worker/Card/WorkerCalendarItem.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import axios from 'axios';
import VnNotes from 'src/components/ui/VnNotes.vue';
import { useStateStore } from 'src/stores/useStateStore';
const stateStore = useStateStore();
const router = useRouter();
const route = useRoute();
const { t } = useI18n();
const acl = useAcl();
const canSeeNotes = computed(() =>
acl.hasAny([{ model: 'Worker', props: '__get__business', accessType: 'READ' }]),
);
const workerIsFreelance = ref();
const WorkerFreelanceRef = ref();
const workerCalendarFilterRef = ref(null);
@ -26,6 +34,10 @@ const contractHolidays = ref(null);
const yearHolidays = ref(null);
const eventsMap = ref({});
const festiveEventsMap = ref({});
const saveUrl = ref();
const body = {
workerFk: route.params.id,
jsegarra marked this conversation as resolved
Review

entiendo la buena practica pero al ser 1 sola linea, menor abajo
2 bien, pero 3 ya no

entiendo la buena practica pero al ser 1 sola linea, menor abajo 2 bien, pero 3 ya no
};
const onFetchActiveContract = (data) => {
if (!data) return;
@ -67,7 +79,7 @@ const onFetchAbsences = (data) => {
name: holidayName,
isFestive: true,
},
true
true,
);
});
}
@ -146,7 +158,7 @@ watch(
async () => {
await nextTick();
await activeContractRef.value.fetch();
}
},
);
watch([year, businessFk], () => refreshData());
</script>
@ -181,6 +193,20 @@ watch([year, businessFk], () => refreshData());
/>
</template>
</RightMenu>
<Teleport to="#st-data" v-if="stateStore.isSubToolbarShown() && canSeeNotes">
<VnNotes
:just-input="true"
:url="`Workers/${route.params.id}/business`"
:filter="{ fields: ['id', 'notes', 'workerFk'] }"
:save-url="saveUrl"
@on-fetch="
(data) => {
saveUrl = `Businesses/${data.id}`;
}
"
:body="body"
/>
</Teleport>
<QPage class="column items-center">
<QCard v-if="workerIsFreelance">
<QCardSection class="text-center">

View File

@ -180,8 +180,6 @@ const yearList = ref(generateYears());
:is-clearable="false"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
:label="t('Contract')"

View File

@ -1,7 +1,7 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { ref, computed } from 'vue';
import { ref, computed, watch } from 'vue';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
@ -19,6 +19,7 @@ const trainsData = ref([]);
const machinesData = ref([]);
const route = useRoute();
const routeId = computed(() => route.params.id);
const selected = ref([]);
jtubau marked this conversation as resolved
Review

No entiendo la propiedad selected que papel hace

No entiendo la propiedad selected que papel hace
Review

para pasarle a crudModel que operario es y que pueda eliminarlo con el botón del subtoolbar

para pasarle a crudModel que operario es y que pueda eliminarlo con el botón del subtoolbar
Review

Okey, veo la necesidad, pero la ejecución es compleja.
Se puede hacer con menos lineas

Okey, veo la necesidad, pero la ejecución es compleja. Se puede hacer con menos lineas
const initialData = computed(() => {
return {
@ -41,6 +42,21 @@ async function insert() {
await axios.post('Operators', initialData.value);
crudModelRef.value.reload();
}
watch(
Review

No veo la utilidad de este watch

No veo la utilidad de este watch
() => crudModelRef.value?.formData,
(formData) => {
if (formData && formData.length) {
if (JSON.stringify(selected.value) !== JSON.stringify(formData)) {
selected.value = formData;
}
} else if (selected.value.length > 0) {
selected.value = [];
}
},
{ immediate: true, deep: true }
);
</script>
<template>
jtubau marked this conversation as resolved
Review

Revisar esta sección porque los botones de la subtoolbar se ven raros, mas altos o comprimidos

Revisar esta sección porque los botones de la subtoolbar se ven raros, mas altos o comprimidos
@ -67,6 +83,7 @@ async function insert() {
:data-required="{ workerFk: route.params.id }"
ref="crudModelRef"
search-url="operator"
:selected="selected"
auto-load
>
<template #body="{ rows }">