Merge pull request 'feat(WorkerPDA): refs #5926 send to docuware' (!1617) from 5926-signPdaPdf into dev
gitea/salix-front/pipeline/head There was a failure building this commit Details

Reviewed-on: #1617
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
This commit is contained in:
Alex Moreno 2025-03-24 06:52:35 +00:00
commit a29153fea2
8 changed files with 377 additions and 187 deletions

View File

@ -633,6 +633,7 @@ const rowCtrlClickFunction = computed(() => {
:data-key="$attrs['data-key']" :data-key="$attrs['data-key']"
:columns="columns" :columns="columns"
:redirect="redirect" :redirect="redirect"
v-bind="$attrs?.['table-filter']"
> >
<template <template
v-for="(_, slotName) in $slots" v-for="(_, slotName) in $slots"

View File

@ -4,12 +4,15 @@ import { vi, afterEach, beforeAll, describe, expect, it } from 'vitest';
describe('VnDmsList', () => { describe('VnDmsList', () => {
let vm; let vm;
const dms = { const dms = {
userFk: 1, userFk: 1,
name: 'DMS 1' name: 'DMS 1',
}; };
beforeAll(() => { beforeAll(() => {
vi.mock('src/composables/getUrl', () => ({
getUrl: vi.fn().mockResolvedValue(''),
}));
vi.spyOn(axios, 'get').mockResolvedValue({ data: [] }); vi.spyOn(axios, 'get').mockResolvedValue({ data: [] });
vm = createWrapper(VnDmsList, { vm = createWrapper(VnDmsList, {
props: { props: {
@ -18,8 +21,8 @@ describe('VnDmsList', () => {
filter: 'wd.workerFk', filter: 'wd.workerFk',
updateModel: 'Workers', updateModel: 'Workers',
deleteModel: 'WorkerDms', deleteModel: 'WorkerDms',
downloadModel: 'WorkerDms' downloadModel: 'WorkerDms',
} },
}).vm; }).vm;
}); });
@ -29,46 +32,45 @@ describe('VnDmsList', () => {
describe('setData()', () => { describe('setData()', () => {
const data = [ const data = [
{ {
userFk: 1, userFk: 1,
name: 'Jessica', name: 'Jessica',
lastName: 'Jones', lastName: 'Jones',
file: '4.jpg', file: '4.jpg',
created: '2021-07-28 21:00:00' created: '2021-07-28 21:00:00',
}, },
{ {
userFk: 2, userFk: 2,
name: 'Bruce', name: 'Bruce',
lastName: 'Banner', lastName: 'Banner',
created: '2022-07-28 21:00:00', created: '2022-07-28 21:00:00',
dms: { dms: {
userFk: 2, userFk: 2,
name: 'Bruce', name: 'Bruce',
lastName: 'BannerDMS', lastName: 'BannerDMS',
created: '2022-07-28 21:00:00', created: '2022-07-28 21:00:00',
file: '4.jpg', file: '4.jpg',
} },
}, },
{ {
userFk: 3, userFk: 3,
name: 'Natasha', name: 'Natasha',
lastName: 'Romanoff', lastName: 'Romanoff',
file: '4.jpg', file: '4.jpg',
created: '2021-10-28 21:00:00' created: '2021-10-28 21:00:00',
} },
] ];
it('Should replace objects that contain the "dms" property with the value of the same and sort by creation date', () => { it('Should replace objects that contain the "dms" property with the value of the same and sort by creation date', () => {
vm.setData(data); vm.setData(data);
expect([vm.rows][0][0].lastName).toEqual('BannerDMS'); expect([vm.rows][0][0].lastName).toEqual('BannerDMS');
expect([vm.rows][0][1].lastName).toEqual('Romanoff'); expect([vm.rows][0][1].lastName).toEqual('Romanoff');
}); });
}); });
describe('parseDms()', () => { describe('parseDms()', () => {
const resultDms = { ...dms, userId:1}; const resultDms = { ...dms, userId: 1 };
it('Should add properties that end with "Fk" by changing the suffix to "Id"', () => { it('Should add properties that end with "Fk" by changing the suffix to "Id"', () => {
const parsedDms = vm.parseDms(dms); const parsedDms = vm.parseDms(dms);
expect(parsedDms).toEqual(resultDms); expect(parsedDms).toEqual(resultDms);
@ -76,12 +78,12 @@ describe('VnDmsList', () => {
}); });
describe('showFormDialog()', () => { describe('showFormDialog()', () => {
const resultDms = { ...dms, userId:1}; const resultDms = { ...dms, userId: 1 };
it('should call fn parseDms() and set show true if dms is defined', () => { it('should call fn parseDms() and set show true if dms is defined', () => {
vm.showFormDialog(dms); vm.showFormDialog(dms);
expect(vm.formDialog.show).toEqual(true); expect(vm.formDialog.show).toEqual(true);
expect(vm.formDialog.dms).toEqual(resultDms); expect(vm.formDialog.dms).toEqual(resultDms);
}); });
}); });
}); });

View File

@ -6,10 +6,12 @@ const session = useSession();
const token = session.getToken(); const token = session.getToken();
describe('downloadFile', () => { describe('downloadFile', () => {
const baseUrl = 'http://localhost:9000';
let defaulCreateObjectURL; let defaulCreateObjectURL;
beforeAll(() => { beforeAll(() => {
vi.mock('src/composables/getUrl', () => ({
getUrl: vi.fn().mockResolvedValue(''),
}));
defaulCreateObjectURL = window.URL.createObjectURL; defaulCreateObjectURL = window.URL.createObjectURL;
window.URL.createObjectURL = vi.fn(() => 'blob:http://localhost:9000/blob-id'); window.URL.createObjectURL = vi.fn(() => 'blob:http://localhost:9000/blob-id');
}); });
@ -22,15 +24,14 @@ describe('downloadFile', () => {
headers: { 'content-disposition': 'attachment; filename="test-file.txt"' }, headers: { 'content-disposition': 'attachment; filename="test-file.txt"' },
}; };
vi.spyOn(axios, 'get').mockImplementation((url) => { vi.spyOn(axios, 'get').mockImplementation((url) => {
if (url == 'Urls/getUrl') return Promise.resolve({ data: baseUrl }); if (url.includes('downloadFile')) return Promise.resolve(res);
else if (url.includes('downloadFile')) return Promise.resolve(res);
}); });
await downloadFile(1); await downloadFile(1);
expect(axios.get).toHaveBeenCalledWith( expect(axios.get).toHaveBeenCalledWith(
`${baseUrl}/api/dms/1/downloadFile?access_token=${token}`, `/api/dms/1/downloadFile?access_token=${token}`,
{ responseType: 'blob' } { responseType: 'blob' },
); );
}); });
}); });

View File

@ -5,20 +5,30 @@ import { exportFile } from 'quasar';
const { getTokenMultimedia } = useSession(); const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia(); const token = getTokenMultimedia();
const appUrl = (await getUrl('', 'lilium')).replace('/#/', '');
export async function downloadFile(id, model = 'dms', urlPath = '/downloadFile', url) { export async function downloadFile(id, model = 'dms', urlPath = '/downloadFile', url) {
const appUrl = (await getUrl('', 'lilium')).replace('/#/', '');
const response = await axios.get( const response = await axios.get(
url ?? `${appUrl}/api/${model}/${id}${urlPath}?access_token=${token}`, url ?? `${appUrl}/api/${model}/${id}${urlPath}?access_token=${token}`,
{ responseType: 'blob' } { responseType: 'blob' }
); );
download(response);
}
export async function downloadDocuware(url, params) {
const response = await axios.get(`${appUrl}/api/` + url, {
responseType: 'blob',
params,
});
download(response);
}
function download(response) {
const contentDisposition = response.headers['content-disposition']; const contentDisposition = response.headers['content-disposition'];
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition); const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition);
const filename = const filename = matches?.[1] ? matches[1].replace(/['"]/g, '') : 'downloaded-file';
matches != null && matches[1]
? matches[1].replace(/['"]/g, '')
: 'downloaded-file';
exportFile(filename, response.data); exportFile(filename, response.data);
} }

View File

@ -646,6 +646,7 @@ worker:
model: Model model: Model
serialNumber: Serial number serialNumber: Serial number
removePDA: Deallocate PDA removePDA: Deallocate PDA
sendToTablet: Send to tablet
create: create:
lastName: Last name lastName: Last name
birth: Birth birth: Birth

View File

@ -731,6 +731,7 @@ worker:
model: Modelo model: Modelo
serialNumber: Número de serie serialNumber: Número de serie
removePDA: Desasignar PDA removePDA: Desasignar PDA
sendToTablet: Enviar a la tablet
create: create:
lastName: Apellido lastName: Apellido
birth: Fecha de nacimiento birth: Fecha de nacimiento

View File

@ -5,24 +5,25 @@ import { ref, computed } from 'vue';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useArrayData } from 'src/composables/useArrayData';
import { downloadDocuware } from 'src/composables/downloadFile';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModelPopup from 'src/components/FormModelPopup.vue'; import FormModelPopup from 'src/components/FormModelPopup.vue';
import { useVnConfirm } from 'composables/useVnConfirm';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnTable from 'src/components/VnTable/VnTable.vue';
const { t } = useI18n(); const { t } = useI18n();
const { notify } = useNotify(); const { notify } = useNotify();
const loadingDocuware = ref(true);
const paginate = ref(); const tableRef = ref();
const dialog = ref(); const dialog = ref();
const route = useRoute(); const route = useRoute();
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();
const routeId = computed(() => route.params.id); const routeId = computed(() => route.params.id);
const worker = computed(() => useArrayData('Worker').store.data);
const initialData = computed(() => { const initialData = computed(() => {
return { return {
userFk: routeId.value, userFk: routeId.value,
@ -31,154 +32,268 @@ const initialData = computed(() => {
}; };
}); });
const deallocatePDA = async (deviceProductionFk) => { const columns = computed(() => [
await axios.post(`Workers/${route.params.id}/deallocatePDA`, { {
pda: deviceProductionFk, align: 'center',
}); label: t('globals.state'),
notify(t('PDA deallocated'), 'positive'); name: 'state',
format: (row) => row?.docuware?.state,
paginate.value.fetch(); columnFilter: false,
}; chip: {
condition: (_, row) => !!row.docuware,
color: (row) => (isSigned(row) ? 'bg-positive' : 'bg-warning'),
},
visible: false,
},
{
align: 'right',
label: t('worker.pda.currentPDA'),
name: 'deviceProductionFk',
columnClass: 'shrink',
cardVisible: true,
},
{
align: 'left',
label: t('Model'),
name: 'modelFk',
format: ({ deviceProduction }) => deviceProduction.modelFk,
cardVisible: true,
},
{
align: 'right',
label: t('Serial number'),
name: 'serialNumber',
format: ({ deviceProduction }) => deviceProduction.serialNumber,
cardVisible: true,
},
{
align: 'left',
label: t('Current SIM'),
name: 'simFk',
cardVisible: true,
},
{
align: 'right',
name: 'actions',
columnFilter: false,
cardVisible: true,
},
]);
function reloadData() { function reloadData() {
initialData.value.deviceProductionFk = null; initialData.value.deviceProductionFk = null;
initialData.value.simFk = null; initialData.value.simFk = null;
paginate.value.fetch(); tableRef.value.reload();
}
async function fetchDocuware() {
loadingDocuware.value = true;
const id = `${worker.value?.lastName} ${worker.value?.firstName}`;
const rows = tableRef.value.CrudModelRef.formData;
const promises = rows.map(async (row) => {
const { data } = await axios.post(`Docuwares/${id}/checkFile`, {
fileCabinet: 'hr',
signed: false,
mergeFilter: [
{
DBName: 'TIPO_DOCUMENTO',
Value: ['PDA'],
},
{
DBName: 'FILENAME',
Value: [`${row.deviceProductionFk}-pda`],
},
],
});
row.docuware = data;
});
await Promise.allSettled(promises);
loadingDocuware.value = false;
}
async function sendToTablet(rows) {
const promises = rows.map(async (row) => {
await axios.post(`Docuwares/upload-pda-pdf`, {
ids: [row.deviceProductionFk],
});
row.docuware = true;
});
await Promise.allSettled(promises);
notify(t('PDF sended to signed'), 'positive');
tableRef.value.reload();
}
async function deallocatePDA(deviceProductionFk) {
await axios.post(`Workers/${route.params.id}/deallocatePDA`, {
pda: deviceProductionFk,
});
const index = tableRef.value.CrudModelRef.formData.findIndex(
(data) => data?.deviceProductionFk == deviceProductionFk,
);
delete tableRef.value.CrudModelRef.formData[index];
notify(t('PDA deallocated'), 'positive');
}
function isSigned(row) {
return row.docuware?.state === 'Firmado';
} }
</script> </script>
<template> <template>
<QPage class="column items-center q-pa-md centerCard"> <FetchData
<FetchData url="workers/getAvailablePda"
url="workers/getAvailablePda" @on-fetch="(data) => (deviceProductions = data)"
@on-fetch="(data) => (deviceProductions = data)" auto-load
auto-load />
/> <VnTable
<VnPaginate ref="tableRef"
ref="paginate" data-key="WorkerPda"
data-key="WorkerPda" url="DeviceProductionUsers"
url="DeviceProductionUsers" :user-filter="{ order: 'id' }"
:user-filter="{ where: { userFk: routeId } }" :filter="{ where: { userFk: routeId } }"
order="id" search-url="pda"
search-url="pda" auto-load
auto-load :columns="columns"
> @onFetch="fetchDocuware"
<template #body="{ rows }"> :hasSubToolbar="true"
<QCard :default-remove="false"
flat :default-reset="false"
bordered :default-save="false"
:key="row.id" :table="{
v-for="row of rows" 'row-key': 'deviceProductionFk',
class="card q-px-md q-mb-sm container" selection: 'multiple',
> }"
<VnRow> :table-filter="{ hiddenTags: ['userFk'] }"
<VnInput >
:label="t('worker.pda.currentPDA')" <template #moreBeforeActions>
:model-value="row?.deviceProductionFk"
disable
/>
<VnInput
:label="t('Model')"
:model-value="row?.deviceProduction?.modelFk"
disable
/>
<VnInput
:label="t('Serial number')"
:model-value="row?.deviceProduction?.serialNumber"
disable
/>
<VnInput
:label="t('Current SIM')"
:model-value="row?.simFk"
disable
/>
<QBtn
flat
icon="delete"
color="primary"
class="btn-delete"
@click="
openConfirmationModal(
t(`Remove PDA`),
t('Do you want to remove this PDA?'),
() => deallocatePDA(row.deviceProductionFk),
)
"
>
<QTooltip>
{{ t('worker.pda.removePDA') }}
</QTooltip>
</QBtn>
</VnRow>
</QCard>
</template>
</VnPaginate>
<QPageSticky :offset="[18, 18]">
<QBtn <QBtn
@click.stop="dialog.show()" :label="t('globals.refresh')"
icon="refresh"
@click="tableRef.reload()"
/>
<QBtn
:disable="!tableRef?.selected?.length"
:label="t('globals.send')"
icon="install_mobile"
@click="sendToTablet(tableRef?.selected)"
class="bg-primary"
/>
</template>
<template #column-actions="{ row }">
<QBtn
flat
icon="delete"
color="primary" color="primary"
fab @click="
icon="add" openConfirmationModal(
v-shortcut="'+'" t(`Remove PDA`),
t('Do you want to remove this PDA?'),
() => deallocatePDA(row.deviceProductionFk),
)
"
data-cy="workerPda-remove"
> >
<QDialog ref="dialog"> <QTooltip>
<FormModelPopup {{ t('worker.pda.removePDA') }}
:title="t('Add new device')" </QTooltip>
url-create="DeviceProductionUsers"
model="DeviceProductionUser"
:form-initial-data="initialData"
@on-data-saved="reloadData()"
>
<template #form-inputs="{ data }">
<VnRow>
<VnSelect
:label="t('worker.pda.newPDA')"
v-model="data.deviceProductionFk"
:options="deviceProductions"
option-label="id"
option-value="id"
id="deviceProductionFk"
hide-selected
data-cy="pda-dialog-select"
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel
>ID: {{ scope.opt?.id }}</QItemLabel
>
<QItemLabel caption>
{{ scope.opt?.modelFk }},
{{ scope.opt?.serialNumber }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnInput
v-model="data.simFk"
:label="t('SIM serial number')"
id="simSerialNumber"
use-input
/>
</VnRow>
</template>
</FormModelPopup>
</QDialog>
</QBtn> </QBtn>
<QTooltip> <QBtn
{{ t('globals.new') }} v-if="!isSigned(row)"
</QTooltip> :loading="loadingDocuware"
</QPageSticky> icon="install_mobile"
</QPage> flat
color="primary"
@click="
openConfirmationModal(
t('Sign PDA'),
t('Are you sure you want to send it?'),
() => sendToTablet([row]),
)
"
data-cy="workerPda-send"
>
<QTooltip>
{{ t('worker.pda.sendToTablet') }}
</QTooltip>
</QBtn>
<QBtn
v-if="isSigned(row)"
:loading="loadingDocuware"
icon="cloud_download"
flat
color="primary"
@click="
downloadDocuware('Docuwares/download-pda-pdf', {
file: row.deviceProductionFk + '-pda',
worker: worker?.lastName + ' ' + worker?.firstName,
})
"
data-cy="workerPda-download"
>
<QTooltip>
{{ t('worker.pda.download') }}
</QTooltip>
</QBtn>
</template>
</VnTable>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="dialog.show()" color="primary" fab icon="add" v-shortcut="'+'">
<QDialog ref="dialog">
<FormModelPopup
:title="t('Add new device')"
url-create="DeviceProductionUsers"
model="DeviceProductionUser"
:form-initial-data="initialData"
@on-data-saved="reloadData()"
>
<template #form-inputs="{ data }">
<VnRow>
<VnSelect
:label="t('PDA')"
v-model="data.deviceProductionFk"
:options="deviceProductions"
option-label="modelFk"
option-value="id"
id="deviceProductionFk"
hide-selected
data-cy="pda-dialog-select"
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel
>ID: {{ scope.opt?.id }}</QItemLabel
>
<QItemLabel caption>
{{ scope.opt?.modelFk }},
{{ scope.opt?.serialNumber }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect
url="Sims"
option-label="line"
option-value="code"
v-model="data.simFk"
:label="t('SIM serial number')"
id="simSerialNumber"
/>
</VnRow>
</template>
</FormModelPopup>
</QDialog>
</QBtn>
<QTooltip>
{{ t('globals.new') }}
</QTooltip>
</QPageSticky>
</template> </template>
<style lang="scss" scoped>
.btn-delete {
max-width: 4%;
margin-top: 30px;
}
</style>
<i18n> <i18n>
es: es:
Model: Modelo Model: Modelo
@ -190,4 +305,6 @@ es:
Do you want to remove this PDA?: ¿Desea eliminar este PDA? Do you want to remove this PDA?: ¿Desea eliminar este PDA?
You can only have one PDA: Solo puedes tener un PDA si no eres autonomo You can only have one PDA: Solo puedes tener un PDA si no eres autonomo
This PDA is already assigned to another user: Este PDA ya está asignado a otro usuario This PDA is already assigned to another user: Este PDA ya está asignado a otro usuario
Are you sure you want to send it?: ¿Seguro que quieres enviarlo?
Sign PDA: Firmar PDA
</i18n> </i18n>

View File

@ -1,23 +1,80 @@
describe('WorkerPda', () => { describe('WorkerPda', () => {
const select = '[data-cy="pda-dialog-select"]'; const deviceId = 4;
beforeEach(() => { beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer'); cy.login('developer');
cy.visit(`/#/worker/1110/pda`); cy.visit(`/#/worker/1110/pda`);
}); });
it('assign pda', () => { it('assign and delete pda', () => {
cy.addBtnClick(); creatNewPDA();
cy.get(select).click(); cy.checkNotification('Data created');
cy.get(select).type('{downArrow}{enter}'); cy.visit(`/#/worker/1110/pda`);
cy.get('.q-notification__message').should('have.text', 'Data created'); removeNewPDA();
cy.checkNotification('PDA deallocated');
}); });
it('delete pda', () => { it('send and download pdf to docuware', () => {
cy.get('.btn-delete').click(); //Send
cy.get( cy.intercept('POST', '/api/Docuwares/upload-pda-pdf', (req) => {
'.q-card__actions > .q-btn--unelevated > .q-btn__content > .block' req.reply({
).click(); statusCode: 200,
cy.get('.q-notification__message').should('have.text', 'PDA deallocated'); body: {},
});
});
creatNewPDA();
cy.dataCy('workerPda-send').click();
cy.clickConfirm();
cy.checkNotification('PDF sended to signed');
//Download
cy.intercept('POST', /\/api\/Docuwares\/Jones%20Jessica\/checkFile/, (req) => {
req.reply({
statusCode: 200,
body: {
id: deviceId,
state: 'Firmado',
},
});
});
cy.get('#st-actions').contains('refresh').click();
cy.intercept('GET', '/api/Docuwares/download-pda-pdf**', (req) => {
req.reply({
statusCode: 200,
body: {},
});
});
cy.dataCy('workerPda-download').click();
removeNewPDA();
}); });
it('send 2 pdfs to docuware', () => {
cy.intercept('POST', '/api/Docuwares/upload-pda-pdf', (req) => {
req.reply({
statusCode: 200,
body: {},
});
});
creatNewPDA();
creatNewPDA(2);
cy.selectRows([1, 2]);
cy.get('#st-actions').contains('Send').click();
cy.checkNotification('PDF sended to signed');
removeNewPDA();
});
function creatNewPDA(id = deviceId) {
cy.addBtnClick();
cy.selectOption('[data-cy="pda-dialog-select"]', id);
cy.saveCard();
}
function removeNewPDA() {
cy.dataCy('workerPda-remove').first().click();
cy.clickConfirm();
}
}); });