aziendasanitaria-utils 1.2.49 → 1.2.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/package.json +1 -1
- package/src/narTsServices/Nar2.js +708 -43
package/README.md
CHANGED
|
@@ -252,6 +252,75 @@ const assistiti = await nar2.getAssistitiFromParams({
|
|
|
252
252
|
[Nar2.PARAMS.NOME]: 'MARIO',
|
|
253
253
|
[Nar2.PARAMS.AZIENDA]: '205'
|
|
254
254
|
});
|
|
255
|
+
|
|
256
|
+
// Elenco medici MMG/PLS, con dati dei rapporti individuali e degli indirizzi
|
|
257
|
+
const medici = await nar2.getMediciFromNar2({
|
|
258
|
+
soloAttivi: true,
|
|
259
|
+
aggiungiDatiRapporti: true, // allega `rapporti` e `indirizzi` a ogni medico
|
|
260
|
+
parallelsRapporti: 10 // richieste eseguite in parallelo
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Solo i rapporti / gli indirizzi di un medico dato il suo pf_id
|
|
264
|
+
const rapporti = await nar2.getRapportiMedicoFromId(5860, 'ME');
|
|
265
|
+
const indirizzi = await nar2.getIndirizziMedicoFromId(5860, 'ME');
|
|
266
|
+
|
|
267
|
+
// Lookup comuni (cm_id NAR2) per nome/CAP/ISTAT/catastale, con filtro esatto
|
|
268
|
+
// e fallback CAP generico per le città multi-CAP (98167 → 98100)
|
|
269
|
+
const {comune, candidati} = await nar2.getComuneNar2({cap: '98167'}); // → MESSINA (id 83)
|
|
270
|
+
|
|
271
|
+
// Aggiornamento residenza/domicilio (PUT /pazienti/{id}, dryRun default true).
|
|
272
|
+
// Sorgenti combinabili: daTs (entrambi dal Sistema TS), residenzaDaTs/domicilioDaTs
|
|
273
|
+
// (singola sezione da TS), residenza/domicilio personalizzati {via, civico, cap, comuneId|criterio}
|
|
274
|
+
const dry = await nar2.aggiornaIndirizziAssistito(cf, {daTs: true}); // ispeziona dry.diff/warnings
|
|
275
|
+
const sent = await nar2.aggiornaIndirizziAssistito(cf, {
|
|
276
|
+
residenzaDaTs: true,
|
|
277
|
+
domicilio: {via: 'VIA ROMA', civico: '10', cap: '98057'}, // comune risolto dal CAP
|
|
278
|
+
dryRun: false
|
|
279
|
+
});
|
|
280
|
+
// Vedi docs/CAMBIO_MEDICO_NAR2.md §5.b — la verifica indirizzi è lo step standard
|
|
281
|
+
// che precede il cambio medico (test interattivo: node ./test-cambio-medico.js)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Ogni rapporto è normalizzato così (include anche i rapporti non attivi; `attivo` è `true` solo se manca la data di fine):
|
|
285
|
+
|
|
286
|
+
```javascript
|
|
287
|
+
{
|
|
288
|
+
id: "75337",
|
|
289
|
+
categoria: "mmg", // "mmg" | "pls"
|
|
290
|
+
tipoRapporto: "MDB",
|
|
291
|
+
descrizioneRapporto: "Medico di base",
|
|
292
|
+
email: "...",
|
|
293
|
+
telefoni: ["3780818127", "3403896880"], // array, scarta i valori nulli
|
|
294
|
+
dataInizio: "25/05/2026", // dd/MM/yyyy
|
|
295
|
+
dataFine: null, // null = rapporto attivo
|
|
296
|
+
attivo: true,
|
|
297
|
+
domicilio: { // indirizzo studio; null se NAR non fornisce il link
|
|
298
|
+
comune: "MESSINA",
|
|
299
|
+
provincia: "ME",
|
|
300
|
+
cap: "98100",
|
|
301
|
+
indirizzo: "VIA CENTONZE",
|
|
302
|
+
numeroCivico: "72"
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Gli indirizzi del medico (legati al medico, non al rapporto) sono restituiti tutti in array, normalizzati così:
|
|
308
|
+
|
|
309
|
+
```javascript
|
|
310
|
+
{
|
|
311
|
+
progressivo: "1",
|
|
312
|
+
descrizione: "RESIDENZA", // può essere null
|
|
313
|
+
comune: "CANICATTI'",
|
|
314
|
+
provincia: "AG",
|
|
315
|
+
cap: "92024",
|
|
316
|
+
indirizzo: "VIA S. CORAZZINI, 9",
|
|
317
|
+
numeroCivico: "1",
|
|
318
|
+
dataAttivazione: "21/03/2065", // dd/MM/yyyy
|
|
319
|
+
dataDisattivazione: null,
|
|
320
|
+
residenza: true, // true se è l'indirizzo di residenza
|
|
321
|
+
telefoni: [],
|
|
322
|
+
email: null
|
|
323
|
+
}
|
|
255
324
|
```
|
|
256
325
|
|
|
257
326
|
#### Assistiti - Gestione Dati Assistiti
|
package/package.json
CHANGED
|
@@ -24,6 +24,8 @@ export class Nar2 {
|
|
|
24
24
|
static MEDICI_BY_AMBITO_AUTOCOMPLETE = "https://nar2.regione.sicilia.it/services/index.php/api/mediciByAmbito";
|
|
25
25
|
static MOTIVI_OPERAZIONE_FROM_TIPO = "https://nar2.regione.sicilia.it/services/index.php/api/motiviOperazioneFromMotivoRevoca/{tipoOp}";
|
|
26
26
|
static STAMPA_DOCUMENTO_URL = "https://nar2.regione.sicilia.it/services/index.php/api/stampe/{tipo}";
|
|
27
|
+
static GET_COMUNI_AUTOCOMPLETE = "https://nar2.regione.sicilia.it/services/index.php/api/comuni";
|
|
28
|
+
static GET_COMUNE_FROM_ID = "https://nar2.regione.sicilia.it/services/index.php/api/anagrafica/comune/{id}";
|
|
27
29
|
|
|
28
30
|
// Documenti di stampa dell'assistito (endpoint /stampe/{tipo}?paziente={pz_id} → application/pdf).
|
|
29
31
|
// Etichette UI nella pagina medico-di-base (ultimo rapporto, attivo o cessato).
|
|
@@ -92,6 +94,26 @@ export class Nar2 {
|
|
|
92
94
|
PAGINATION: 'pagination'
|
|
93
95
|
};
|
|
94
96
|
|
|
97
|
+
// Campi del record paziente inviati dalla UI NAR2 nella PUT /pazienti/{id}
|
|
98
|
+
// (tracciato di rete della pagina anagrafica, 2026-06-04). Il payload viene
|
|
99
|
+
// ricostruito prendendo questi campi dal record corrente del GET /pazienti/{id}:
|
|
100
|
+
// NON vanno inclusi i campi derivati/annidati (comune_residenza, storico_medici, ...).
|
|
101
|
+
static #CAMPI_PUT_PAZIENTE = [
|
|
102
|
+
"pz_cogn", "pz_nome", "pz_dt_nas", "pz_cfis", "pz_sesso", "pz_com_nas", "pz_citt",
|
|
103
|
+
"pz_fcertif", "pz_fcompl", "pz_fstato", "pz_dscadaire", "pz_aire", "pz_ise",
|
|
104
|
+
"pz_dt_dec", "pz_con_pers", "pz_con_parentela", "pz_con_tele", "pz_email",
|
|
105
|
+
"pz_tel1", "pz_tel2", "pz_tel3", "pz_tel1_nota", "pz_tel2_nota", "pz_tel3_nota",
|
|
106
|
+
"pz_flagpriv", "pz_com_dec", "pz_ind_res", "pz_com_res", "pz_nciv_res",
|
|
107
|
+
"pz_com_dom", "pz_ind_dom", "pz_nciv_dom", "pz_categoria_citt", "pz_asl_app",
|
|
108
|
+
"pz_reg_res", "pz_doc_dt_ril", "pz_doc_ril_da", "pz_doc_ril_di", "pz_documento",
|
|
109
|
+
"pz_doc_dt_sca", "pz_doc_tipo", "pz_asl_pro", "pz_com_imm", "pz_com_emi",
|
|
110
|
+
"pz_dt_emi", "pz_dt_imm", "pz_dis_pro", "pz_dt_ini_ssn", "pz_dt_end_ssn",
|
|
111
|
+
"pz_asl_ass", "pz_dt_ini_asl", "pz_dt_end_asl", "pz_distretto_ass", "pz_reg_assi",
|
|
112
|
+
"pz_ts_dt_ini", "pz_dscadtess", "pz_tsan", "pz_ts_asl_emi", "pz_drilstp",
|
|
113
|
+
"pz_dscadstp", "pz_stp", "pz_drileni", "pz_dscadeni", "pz_eni",
|
|
114
|
+
"pz_team_dt_ini", "pz_team_dtsca", "pz_team_tes", "pz_team_id", "pz_team_qualben"
|
|
115
|
+
];
|
|
116
|
+
|
|
95
117
|
static #token = null;
|
|
96
118
|
static #tokenPromise = null; // Variabile per la chiamata in corso
|
|
97
119
|
|
|
@@ -193,7 +215,7 @@ export class Nar2 {
|
|
|
193
215
|
async getSituazioniAssistenzialiAmmesse(codFiscale, config = {}) {
|
|
194
216
|
const {pmId = null, tipoMedico = Nar2.MEDICO_DI_BASE} = config;
|
|
195
217
|
const dati = await this.getDatiAssistitoNar2FromCf(codFiscale);
|
|
196
|
-
if (!dati || !dati.ok) return {ok: false, data: null};
|
|
218
|
+
if (!dati || !dati.ok) return {ok: false, data: null, error: dati?.error || "Assistito non trovato su NAR2", errorDetail: dati?.errorDetail ?? null};
|
|
197
219
|
const fullData = dati.fullData.data;
|
|
198
220
|
const eta = moment().diff(moment(fullData.pz_dt_nas, "YYYY-MM-DD HH:mm:ss"), "years");
|
|
199
221
|
const azId = fullData.comune_domicilio?._azienda?.[0]?.az_azie ?? "ME";
|
|
@@ -251,13 +273,13 @@ export class Nar2 {
|
|
|
251
273
|
let sm = sceltaMedico;
|
|
252
274
|
if (!sm) {
|
|
253
275
|
const dati = await this.getDatiAssistitoNar2FromCf(codFiscale);
|
|
254
|
-
if (!dati || !dati.ok) return {ok: false, data: null};
|
|
276
|
+
if (!dati || !dati.ok) return {ok: false, data: null, error: dati?.error || "Assistito non trovato su NAR2", errorDetail: dati?.errorDetail ?? null};
|
|
255
277
|
const pazienteId = dati.fullData.data.pz_id;
|
|
256
278
|
const azId = dati.fullData.data.comune_domicilio?._azienda?.[0]?.az_azie ?? "ME";
|
|
257
279
|
const res = await this.#getDataFromUrlIdOrParams(Nar2.GET_DATI_PAZIENTEMEDICO, {
|
|
258
280
|
replaceFromUrl: {id: pazienteId, tipo_medico: tipoMedico, az_id: azId}
|
|
259
281
|
});
|
|
260
|
-
if (!res.ok || !res.data?.sceltaMedico) return {ok: false, data: null};
|
|
282
|
+
if (!res.ok || !res.data?.sceltaMedico) return {ok: false, data: null, error: res.error || "Dati paziente-medico non disponibili su NAR2", errorDetail: res.errorDetail ?? null};
|
|
261
283
|
sm = res.data.sceltaMedico;
|
|
262
284
|
}
|
|
263
285
|
let tipi = Array.isArray(sm.tipoOperazioni_) ? sm.tipoOperazioni_ : [];
|
|
@@ -287,18 +309,20 @@ export class Nar2 {
|
|
|
287
309
|
async getMotiviOperazione(tipoOp = Nar2.TIPO_OP_REVOCA, config = {}) {
|
|
288
310
|
const {semplifica = false, retryVuoto = 3} = config;
|
|
289
311
|
let data = null;
|
|
312
|
+
let lastRes = null;
|
|
290
313
|
for (let i = 0; i <= retryVuoto; i++) {
|
|
291
314
|
const res = await this.#getDataFromUrlIdOrParams(Nar2.MOTIVI_OPERAZIONE_FROM_TIPO, {
|
|
292
315
|
replaceFromUrl: {tipoOp},
|
|
293
316
|
rawResponse: true
|
|
294
317
|
});
|
|
318
|
+
lastRes = res;
|
|
295
319
|
if (res.ok && Array.isArray(res.data)) {
|
|
296
320
|
data = res.data;
|
|
297
321
|
if (data.length > 0) break; // ok con motivi
|
|
298
322
|
}
|
|
299
323
|
if (i < retryVuoto) await new Promise((r) => setTimeout(r, 300));
|
|
300
324
|
}
|
|
301
|
-
if (!Array.isArray(data)) return {ok: false, data: null};
|
|
325
|
+
if (!Array.isArray(data)) return {ok: false, data: null, error: lastRes?.error || "Motivi operazione non disponibili su NAR2", errorDetail: lastRes?.errorDetail ?? null};
|
|
302
326
|
if (semplifica) data = data.map((m) => ({id: m.eg_id, codice: m.eg_cod, descrizione: m.eg_desc1}));
|
|
303
327
|
return {ok: true, data};
|
|
304
328
|
}
|
|
@@ -320,7 +344,7 @@ export class Nar2 {
|
|
|
320
344
|
async getMotiviPerTipoOperazione(codFiscale, config = {}) {
|
|
321
345
|
const {tipoMedico = Nar2.MEDICO_DI_BASE, semplifica = true, gruppo = null} = config;
|
|
322
346
|
const tipiRes = await this.getTipiOperazione(codFiscale, {tipoMedico, semplifica: false, gruppo});
|
|
323
|
-
if (!tipiRes.ok || !Array.isArray(tipiRes.data)) return {ok: false, data: null};
|
|
347
|
+
if (!tipiRes.ok || !Array.isArray(tipiRes.data)) return {ok: false, data: null, error: tipiRes.error || "Tipi operazione non disponibili su NAR2", errorDetail: tipiRes.errorDetail ?? null};
|
|
324
348
|
|
|
325
349
|
const data = await Promise.all(tipiRes.data.map(async (tp) => {
|
|
326
350
|
const motiviRes = await this.getMotiviOperazione(tp.eg_id, {semplifica});
|
|
@@ -380,7 +404,9 @@ export class Nar2 {
|
|
|
380
404
|
dataScelta = moment().format("YYYY-MM-DD"),
|
|
381
405
|
sitAssistenziale = 4
|
|
382
406
|
} = config;
|
|
383
|
-
const
|
|
407
|
+
const dati = await this.getDatiAssistitoNar2FromCf(codFiscaleAssistito);
|
|
408
|
+
if (!dati || !dati.ok || !dati.fullData?.data) return {ok: false, data: null, error: dati?.error || "Assistito non trovato su NAR2", errorDetail: dati?.errorDetail ?? null};
|
|
409
|
+
const fullData = dati.fullData.data;
|
|
384
410
|
const azienda = fullData.comune_domicilio?._azienda?.[0]?.az_azie ?? "ME";
|
|
385
411
|
return await this.#getDataFromUrlIdOrParams(Nar2.MEDICI_BY_AMBITO_AUTOCOMPLETE, {
|
|
386
412
|
getParams: {
|
|
@@ -458,29 +484,64 @@ export class Nar2 {
|
|
|
458
484
|
// 1) Dati assistito
|
|
459
485
|
const datiAssistito = await this.getDatiAssistitoNar2FromCf(codFiscale);
|
|
460
486
|
if (!datiAssistito || !datiAssistito.ok) {
|
|
461
|
-
return {ok: false, dryRun, payload: null, response: null, error: "Assistito non trovato su NAR2"};
|
|
487
|
+
return {ok: false, dryRun, payload: null, response: null, error: datiAssistito?.error || "Assistito non trovato su NAR2", errorDetail: datiAssistito?.errorDetail ?? null};
|
|
462
488
|
}
|
|
463
489
|
const fullData = datiAssistito.fullData.data;
|
|
464
490
|
const pzId = fullData.pz_id;
|
|
465
491
|
const eta = moment().diff(moment(fullData.pz_dt_nas, "YYYY-MM-DD HH:mm:ss"), "years");
|
|
466
492
|
const azId = fullData.comune_domicilio?._azienda?.[0]?.az_azie ?? "ME";
|
|
467
493
|
|
|
468
|
-
// 2) Ambito di domicilio (se non fornito):
|
|
469
|
-
|
|
494
|
+
// 2) Ambito di domicilio (se non fornito): SEMPRE dal catalogo CORRENTE
|
|
495
|
+
// (/ambitoDomTable → sr_id per il comune di domicilio, vedi spec
|
|
496
|
+
// CAMBIO_MEDICO_NAR2.md §8.1/§12). MAI dallo storico come fonte primaria:
|
|
497
|
+
// gli id ambito vengono riorganizzati nel tempo da NAR2 (es. scelta del 2000
|
|
498
|
+
// con dm_ambito_dom=147; oggi l'ambito valido per lo stesso comune è 3748) e
|
|
499
|
+
// un id stale viene rifiutato dalla POST /pazienti/sceltaMedico con 400.
|
|
500
|
+
let ambitoDom = idAmbitoDomicilio ?? null;
|
|
501
|
+
let ambitiRes = null;
|
|
470
502
|
if (!ambitoDom) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
503
|
+
ambitiRes = await this.getAmbitiDomicilioAssistito(codFiscale, {dividiAmbitiMMGPediatri: true});
|
|
504
|
+
const lista = (ambitiRes?.ok
|
|
505
|
+
? (tipoMedico === Nar2.PEDIATRA ? ambitiRes.data.ambiti.pediatri : ambitiRes.data.ambiti.mmg)
|
|
506
|
+
: null) ?? [];
|
|
507
|
+
const listaIds = lista.map((a) => a?.sr_id?.toString()).filter(Boolean);
|
|
508
|
+
if (listaIds.length === 1) {
|
|
509
|
+
ambitoDom = listaIds[0];
|
|
510
|
+
} else if (listaIds.length > 1) {
|
|
511
|
+
// Comune con più ambiti: disambiguo con l'ambito del rapporto del
|
|
512
|
+
// medico scelto, se compare nel catalogo. Su medici/{id} il campo è
|
|
513
|
+
// lo scalare dett_medico.dm_ambito (verificato live); in altri
|
|
514
|
+
// contesti la spec mostra l'oggetto annidato dett_medico.ambito
|
|
515
|
+
// {sr_id,...} — li leggo entrambi per robustezza.
|
|
516
|
+
const medicoRes = await this.getMedicoFromId(idMedico);
|
|
517
|
+
const ambitiMedico = (medicoRes?.ok ? (medicoRes.data?.rapporto_individuale ?? []) : [])
|
|
518
|
+
.map((ri) => (ri?.dett_medico?.dm_ambito ?? ri?.dett_medico?.ambito?.sr_id)?.toString())
|
|
519
|
+
.filter(Boolean);
|
|
520
|
+
ambitoDom = ambitiMedico.find((a) => listaIds.includes(a)) ?? null;
|
|
521
|
+
if (!ambitoDom) {
|
|
522
|
+
// Continuità: l'ambito della scelta ATTIVA, ma solo se ancora
|
|
523
|
+
// presente nel catalogo corrente (altrimenti è stale).
|
|
524
|
+
const attiva = (Array.isArray(fullData.storico_medici) ? fullData.storico_medici : [])
|
|
525
|
+
.find((s) => s?.pm_fstato === "A" && !s?.pm_dt_disable);
|
|
526
|
+
const ambitoAttiva = attiva?.dett_pazientemedico?.dm_ambito_dom?.toString() ?? null;
|
|
527
|
+
ambitoDom = (ambitoAttiva && listaIds.includes(ambitoAttiva)) ? ambitoAttiva : listaIds[0];
|
|
479
528
|
}
|
|
480
529
|
}
|
|
530
|
+
// Last resort (catalogo non disponibile): ambito della scelta attiva,
|
|
531
|
+
// potenzialmente stale ma meglio di niente. NB: scelta ATTIVA, non
|
|
532
|
+
// storico_medici[0] (l'array non è ordinato).
|
|
533
|
+
if (!ambitoDom) {
|
|
534
|
+
const attiva = (Array.isArray(fullData.storico_medici) ? fullData.storico_medici : [])
|
|
535
|
+
.find((s) => s?.pm_fstato === "A" && !s?.pm_dt_disable);
|
|
536
|
+
ambitoDom = attiva?.dett_pazientemedico?.dm_ambito_dom ?? null;
|
|
537
|
+
}
|
|
481
538
|
}
|
|
482
539
|
if (!ambitoDom) {
|
|
483
|
-
return {
|
|
540
|
+
return {
|
|
541
|
+
ok: false, dryRun, payload: null, response: null,
|
|
542
|
+
error: ambitiRes?.error || "Impossibile determinare l'ambito di domicilio",
|
|
543
|
+
errorDetail: ambitiRes?.errorDetail ?? null
|
|
544
|
+
};
|
|
484
545
|
}
|
|
485
546
|
const ambitoScelta = idAmbitoScelta ?? ambitoDom;
|
|
486
547
|
|
|
@@ -512,7 +573,7 @@ export class Nar2 {
|
|
|
512
573
|
// 4) Situazione assistenziale + motivo + tipo operazione (dal catalogo)
|
|
513
574
|
const sitAmmesse = await this.getSituazioniAssistenzialiAmmesse(codFiscale, {tipoMedico});
|
|
514
575
|
if (!sitAmmesse || !sitAmmesse.ok || !Array.isArray(sitAmmesse.data)) {
|
|
515
|
-
return {ok: false, dryRun, payload: null, response: null, error: "Impossibile recuperare situazioni assistenziali ammesse"};
|
|
576
|
+
return {ok: false, dryRun, payload: null, response: null, error: sitAmmesse?.error || "Impossibile recuperare situazioni assistenziali ammesse", errorDetail: sitAmmesse?.errorDetail ?? null};
|
|
516
577
|
}
|
|
517
578
|
let situazioneScelta;
|
|
518
579
|
if (idSituazioneAssistenziale) {
|
|
@@ -577,7 +638,380 @@ export class Nar2 {
|
|
|
577
638
|
payload,
|
|
578
639
|
response: result.fullResponse,
|
|
579
640
|
data: result.data,
|
|
580
|
-
error: result.ok ? undefined : "Errore durante la POST /pazienti/sceltaMedico"
|
|
641
|
+
error: result.ok ? undefined : (result.error || "Errore durante la POST /pazienti/sceltaMedico"),
|
|
642
|
+
errorDetail: result.ok ? undefined : (result.errorDetail ?? null)
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Ricerca comuni via autocomplete NAR2.
|
|
648
|
+
*
|
|
649
|
+
* Endpoint: GET /comuni?start=0&autocomplete=true&searchKey=...
|
|
650
|
+
* La ricerca è "contains" su nome (cm_desc), CAP (cm_ccap), codice ISTAT
|
|
651
|
+
* (cm_cistat) e codice catastale (cm_cfis). NB (verificato live):
|
|
652
|
+
* - searchKey vuota → 0 risultati (serve almeno un carattere);
|
|
653
|
+
* - il CAP in tabella è UNO solo per comune (il generico: Messina=98100),
|
|
654
|
+
* i CAP specifici delle città multi-CAP non matchano (vedi getComuneNar2).
|
|
655
|
+
*
|
|
656
|
+
* @param {string} searchKey - Chiave di ricerca (nome parziale, CAP, ISTAT, catastale).
|
|
657
|
+
* @param {Object} [config={}]
|
|
658
|
+
* @param {boolean} [config.semplifica=true] - Se true ritorna {id, nome, cap, provincia, istat, catastale, regione}; se false i record grezzi cm_*.
|
|
659
|
+
* @returns {Promise<{ok:boolean, data:Array|null, count:number|null}>} `count` = totale lato server (la pagina restituita può essere più corta).
|
|
660
|
+
*/
|
|
661
|
+
async getComuni(searchKey, config = {}) {
|
|
662
|
+
const {semplifica = true} = config;
|
|
663
|
+
const res = await this.#getDataFromUrlIdOrParams(Nar2.GET_COMUNI_AUTOCOMPLETE, {
|
|
664
|
+
getParams: {start: 0, autocomplete: true, searchKey: (searchKey ?? "").toString().trim()},
|
|
665
|
+
rawResponse: true // serve l'envelope completo per leggere anche `count`
|
|
666
|
+
});
|
|
667
|
+
if (!res.ok) return {ok: false, data: null, count: null, error: res.error, errorDetail: res.errorDetail ?? null};
|
|
668
|
+
const body = res.data ?? {};
|
|
669
|
+
if (body.status?.toString() !== "true" || !Array.isArray(body.result)) {
|
|
670
|
+
return {ok: false, data: null, count: null, error: Nar2.#extractErrorMessage(body) || "Risposta /comuni non valida", errorDetail: null};
|
|
671
|
+
}
|
|
672
|
+
const data = semplifica
|
|
673
|
+
? body.result.map((c) => ({
|
|
674
|
+
id: c.cm_id,
|
|
675
|
+
nome: c.cm_desc,
|
|
676
|
+
cap: c.cm_ccap,
|
|
677
|
+
provincia: c.cm_prov,
|
|
678
|
+
istat: c.cm_cistat,
|
|
679
|
+
catastale: c.cm_cfis,
|
|
680
|
+
regione: c.cm_reg
|
|
681
|
+
}))
|
|
682
|
+
: body.result;
|
|
683
|
+
return {ok: true, data, count: body.count ?? data.length};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Risolve un comune NAR2 (cm_id) a partire da UN criterio: nome, CAP,
|
|
688
|
+
* codice ISTAT (6 cifre con zero iniziale) o codice catastale/Belfiore.
|
|
689
|
+
* Applica un filtro ESATTO lato client sul criterio usato: la searchKey del
|
|
690
|
+
* server è "contains" su più campi e produce falsi positivi (es. CAP 98057
|
|
691
|
+
* matcha anche TERRANOVA DEI PASSERINI per l'ISTAT 098057).
|
|
692
|
+
*
|
|
693
|
+
* CAP delle città multi-CAP: la tabella NAR2 ha solo il CAP generico
|
|
694
|
+
* (98167 → 0 risultati). Se il CAP esatto non trova nulla si riprova col
|
|
695
|
+
* CAP generalizzato (ultime 2 cifre → "00": 98167→98100) e il risultato
|
|
696
|
+
* viene marcato `capGenerico: true`.
|
|
697
|
+
*
|
|
698
|
+
* Precedenza criteri (dal più univoco): catastale > istat > cap > nome.
|
|
699
|
+
*
|
|
700
|
+
* @param {Object} [criteri={}]
|
|
701
|
+
* @param {string} [criteri.nome] - Nome (anche parziale; match esatto se esiste, altrimenti i parziali).
|
|
702
|
+
* @param {string} [criteri.cap] - CAP a 5 cifre.
|
|
703
|
+
* @param {string} [criteri.istat] - Codice ISTAT 6 cifre (es. "083049").
|
|
704
|
+
* @param {string} [criteri.catastale] - Codice catastale (es. "F206").
|
|
705
|
+
* @returns {Promise<{ok:boolean, comune:Object|null, candidati:Array, capGenerico?:boolean}>}
|
|
706
|
+
* `comune` valorizzato solo se il match è univoco; `candidati` per la scelta interattiva.
|
|
707
|
+
*/
|
|
708
|
+
async getComuneNar2(criteri = {}) {
|
|
709
|
+
const norm = (s) => (s ?? "").toString().trim().toUpperCase();
|
|
710
|
+
const {nome = null, cap = null, istat = null, catastale = null} = criteri;
|
|
711
|
+
const ricerca = async (searchKey, campo, valore) => {
|
|
712
|
+
const res = await this.getComuni(searchKey);
|
|
713
|
+
if (!res.ok) return res;
|
|
714
|
+
return {ok: true, data: res.data.filter((c) => norm(c[campo]) === norm(valore))};
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
let candidati = null;
|
|
718
|
+
let capGenerico = false;
|
|
719
|
+
let lastRes = null;
|
|
720
|
+
if (catastale) {
|
|
721
|
+
lastRes = await ricerca(catastale, "catastale", catastale);
|
|
722
|
+
candidati = lastRes.ok ? lastRes.data : null;
|
|
723
|
+
} else if (istat) {
|
|
724
|
+
lastRes = await ricerca(istat, "istat", istat);
|
|
725
|
+
candidati = lastRes.ok ? lastRes.data : null;
|
|
726
|
+
} else if (cap) {
|
|
727
|
+
lastRes = await ricerca(cap, "cap", cap);
|
|
728
|
+
candidati = lastRes.ok ? lastRes.data : null;
|
|
729
|
+
if (lastRes.ok && candidati.length === 0 && /^\d{5}$/.test(norm(cap))) {
|
|
730
|
+
// CAP specifico di città multi-CAP: riprovo col CAP generico (98167 → 98100)
|
|
731
|
+
const generico = `${norm(cap).substring(0, 3)}00`;
|
|
732
|
+
if (generico !== norm(cap)) {
|
|
733
|
+
lastRes = await ricerca(generico, "cap", generico);
|
|
734
|
+
if (lastRes.ok && lastRes.data.length > 0) {
|
|
735
|
+
candidati = lastRes.data;
|
|
736
|
+
capGenerico = true;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
} else if (nome) {
|
|
741
|
+
const res = await this.getComuni(nome);
|
|
742
|
+
lastRes = res;
|
|
743
|
+
if (res.ok) {
|
|
744
|
+
const esatti = res.data.filter((c) => norm(c.nome) === norm(nome));
|
|
745
|
+
candidati = esatti.length > 0 ? esatti : res.data;
|
|
746
|
+
}
|
|
747
|
+
} else {
|
|
748
|
+
return {ok: false, comune: null, candidati: [], error: "Nessun criterio di ricerca fornito (nome, cap, istat o catastale)"};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (!candidati) return {ok: false, comune: null, candidati: [], error: lastRes?.error || "Ricerca comuni fallita", errorDetail: lastRes?.errorDetail ?? null};
|
|
752
|
+
if (candidati.length === 0) return {ok: false, comune: null, candidati: [], error: `Nessun comune trovato per ${JSON.stringify(criteri)}`};
|
|
753
|
+
return {ok: true, comune: candidati.length === 1 ? candidati[0] : null, candidati, ...(capGenerico ? {capGenerico} : {})};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Dettaglio di un comune per cm_id.
|
|
758
|
+
*
|
|
759
|
+
* Endpoint: GET /anagrafica/comune/{id} → record cm_* completo, incluso
|
|
760
|
+
* `uc_asl` (ASL di competenza territoriale, es. "281"), usato per il
|
|
761
|
+
* warning di coerenza ASL in aggiornaIndirizziAssistito.
|
|
762
|
+
*
|
|
763
|
+
* @param {string|number} cmId - cm_id interno NAR2.
|
|
764
|
+
* @returns {Promise<{ok:boolean, data:Object|null}>}
|
|
765
|
+
*/
|
|
766
|
+
async getComuneFromId(cmId) {
|
|
767
|
+
return await this.#getDataFromUrlIdOrParams(Nar2.GET_COMUNE_FROM_ID, {replaceFromUrl: {id: cmId}});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Compone la stringa indirizzo nel formato usato dal Sistema TS / NAR2:
|
|
772
|
+
* "PACE SALITA BISIGNANI 3 , 98167 MESSINA (ME)"
|
|
773
|
+
* (via civico, spazio-virgola-doppio spazio, CAP, doppio spazio, comune,
|
|
774
|
+
* doppio spazio, provincia tra parentesi; tutto maiuscolo).
|
|
775
|
+
* Il CAP è quello reale dell'indirizzo (es. 98167), non per forza il cm_ccap.
|
|
776
|
+
*
|
|
777
|
+
* @param {Object} p
|
|
778
|
+
* @param {string} p.via - Via/denominazione (es. "PACE SALITA BISIGNANI").
|
|
779
|
+
* @param {string|number|null} [p.civico=null] - Numero civico (accodato alla via).
|
|
780
|
+
* @param {string|number} p.cap - CAP a 5 cifre.
|
|
781
|
+
* @param {string} p.nomeComune - Nome comune (es. "MESSINA").
|
|
782
|
+
* @param {string} p.provincia - Sigla provincia (es. "ME").
|
|
783
|
+
* @returns {string} Indirizzo formattato.
|
|
784
|
+
*/
|
|
785
|
+
static formattaIndirizzoNar2({via, civico = null, cap, nomeComune, provincia}) {
|
|
786
|
+
const clean = (v) => (v === null || typeof v === "undefined") ? "" : v.toString().trim();
|
|
787
|
+
const viaCivico = [clean(via), clean(civico)].filter((x) => x !== "").join(" ");
|
|
788
|
+
return `${viaCivico} , ${clean(cap)} ${clean(nomeComune)} (${clean(provincia)})`.toUpperCase();
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Risolve un parametro indirizzo di aggiornaIndirizziAssistito in
|
|
793
|
+
* `{indirizzo, comuneId, comune}` pronti per il payload. Accetta:
|
|
794
|
+
* - {indirizzo: "già formattato", comuneId} → passa-through;
|
|
795
|
+
* - {via, civico?, cap, comuneId} → comune da id, stringa composta;
|
|
796
|
+
* - {via, civico?, cap, catastale|istat|nome} → comune risolto univocamente
|
|
797
|
+
* (il cap viene usato come criterio se non c'è altro), stringa composta.
|
|
798
|
+
*
|
|
799
|
+
* @param {Object} param - Parametro indirizzo (vedi sopra).
|
|
800
|
+
* @param {string} etichetta - "residenza" o "domicilio" (per i messaggi di errore).
|
|
801
|
+
* @returns {Promise<{ok:boolean, indirizzo?:string, comuneId?:string, comune?:Object, candidati?:Array}>}
|
|
802
|
+
*/
|
|
803
|
+
async #risolviParametroIndirizzo(param, etichetta) {
|
|
804
|
+
if (!param || typeof param !== "object")
|
|
805
|
+
return {ok: false, error: `Parametro ${etichetta} mancante o non valido`};
|
|
806
|
+
|
|
807
|
+
// comune: da id diretto o da criteri
|
|
808
|
+
let comune = null;
|
|
809
|
+
if (param.comuneId) {
|
|
810
|
+
const det = await this.getComuneFromId(param.comuneId);
|
|
811
|
+
if (!det.ok || !det.data)
|
|
812
|
+
return {ok: false, error: `Comune ${etichetta} non trovato per id=${param.comuneId}${det.error ? `: ${det.error}` : ""}`, errorDetail: det.errorDetail ?? null};
|
|
813
|
+
comune = {
|
|
814
|
+
id: det.data.cm_id, nome: det.data.cm_desc, cap: det.data.cm_ccap,
|
|
815
|
+
provincia: det.data.cm_prov, istat: det.data.cm_cistat,
|
|
816
|
+
catastale: det.data.cm_cfis, regione: det.data.cm_reg
|
|
817
|
+
};
|
|
818
|
+
} else {
|
|
819
|
+
const criteri = {};
|
|
820
|
+
if (param.catastale) criteri.catastale = param.catastale;
|
|
821
|
+
else if (param.istat) criteri.istat = param.istat;
|
|
822
|
+
else if (param.nome) criteri.nome = param.nome;
|
|
823
|
+
else if (param.cap) criteri.cap = param.cap;
|
|
824
|
+
if (Object.keys(criteri).length === 0)
|
|
825
|
+
return {ok: false, error: `Comune ${etichetta} non determinabile: fornire comuneId oppure catastale/istat/nome/cap`};
|
|
826
|
+
const res = await this.getComuneNar2(criteri);
|
|
827
|
+
if (!res.ok) return {ok: false, error: `Comune ${etichetta}: ${res.error}`, errorDetail: res.errorDetail ?? null};
|
|
828
|
+
if (!res.comune) {
|
|
829
|
+
return {
|
|
830
|
+
ok: false,
|
|
831
|
+
error: `Comune ${etichetta} ambiguo per ${JSON.stringify(criteri)}: ${res.candidati.map((c) => `${c.nome} (${c.provincia}, id=${c.id})`).join("; ")}`,
|
|
832
|
+
candidati: res.candidati
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
comune = res.comune;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// stringa indirizzo: passa-through o composizione nel formato TS
|
|
839
|
+
let indirizzo;
|
|
840
|
+
if (typeof param.indirizzo === "string" && param.indirizzo.trim() !== "") {
|
|
841
|
+
indirizzo = param.indirizzo.trim().toUpperCase();
|
|
842
|
+
} else {
|
|
843
|
+
if (!param.via || !param.cap)
|
|
844
|
+
return {ok: false, error: `Indirizzo ${etichetta} incompleto: servono almeno via e cap (oppure la stringa già formattata in 'indirizzo')`};
|
|
845
|
+
indirizzo = Nar2.formattaIndirizzoNar2({
|
|
846
|
+
via: param.via, civico: param.civico ?? null, cap: param.cap,
|
|
847
|
+
nomeComune: comune.nome, provincia: comune.provincia
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
return {ok: true, indirizzo, comuneId: comune.id?.toString(), comune};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Aggiorna residenza e/o domicilio di un assistito su NAR2 replicando la PUT
|
|
855
|
+
* della pagina anagrafica: PUT /pazienti/{pz_id} con il record completo
|
|
856
|
+
* (whitelist #CAMPI_PUT_PAZIENTE presa dal record corrente) e i soli campi
|
|
857
|
+
* indirizzo modificati (pz_ind_res/pz_com_res/pz_nciv_res, pz_ind_dom/
|
|
858
|
+
* pz_com_dom/pz_nciv_dom). Come la UI: civico dentro la stringa, pz_nciv_*=null,
|
|
859
|
+
* pz_com_* = cm_id interno NAR2 (NON il codice ISTAT).
|
|
860
|
+
*
|
|
861
|
+
* Sorgenti combinabili per ciascuna sezione (residenza, domicilio):
|
|
862
|
+
* - {daTs: true} → scorciatoia: residenza E domicilio = indirizzo del Sistema TS
|
|
863
|
+
* (p801recapitoTessera; comune dal catastale TS, fallback ISTAT)
|
|
864
|
+
* - {residenzaDaTs: true} → la sola residenza dall'indirizzo TS
|
|
865
|
+
* - {domicilioDaTs: true} → il solo domicilio dall'indirizzo TS
|
|
866
|
+
* - {residenza: {...}} → residenza da parametro
|
|
867
|
+
* - {domicilio: {...}} → domicilio da parametro
|
|
868
|
+
* Es.: {residenzaDaTs: true, domicilio: {...}} = residenza da TS + domicilio manuale.
|
|
869
|
+
* Conflitti (daTs con altri; *DaTs con il parametro della stessa sezione) → ok:false.
|
|
870
|
+
*
|
|
871
|
+
* Parametro indirizzo (residenza/domicilio): vedi #risolviParametroIndirizzo.
|
|
872
|
+
*
|
|
873
|
+
* Nessun campo ASL/regione viene toccato (comportamento identico alla UI):
|
|
874
|
+
* se l'ASL di competenza del comune (uc_asl) differisce da pz_asl_app viene
|
|
875
|
+
* aggiunto un warning non bloccante in `warnings[]`.
|
|
876
|
+
*
|
|
877
|
+
* @param {string} codFiscale - Codice fiscale dell'assistito.
|
|
878
|
+
* @param {Object} [config={}]
|
|
879
|
+
* @param {boolean} [config.dryRun=true] - DEFAULT TRUE: ritorna payload e diff senza inviare.
|
|
880
|
+
* @param {boolean} [config.daTs=false] - Residenza E domicilio dall'indirizzo TS (scorciatoia).
|
|
881
|
+
* @param {boolean} [config.residenzaDaTs=false] - La sola residenza dall'indirizzo TS.
|
|
882
|
+
* @param {boolean} [config.domicilioDaTs=false] - Il solo domicilio dall'indirizzo TS.
|
|
883
|
+
* @param {Object|null} [config.residenza=null] - Indirizzo residenza da parametro.
|
|
884
|
+
* @param {Object|null} [config.domicilio=null] - Indirizzo domicilio da parametro.
|
|
885
|
+
* @returns {Promise<{ok:boolean, dryRun:boolean, payload:Object|null, diff:Object|null, warnings:string[], response:Object|null, error?:string}>}
|
|
886
|
+
* `diff` = {campo: {da, a}} dei soli campi che cambiano rispetto al record corrente.
|
|
887
|
+
*/
|
|
888
|
+
async aggiornaIndirizziAssistito(codFiscale, config = {}) {
|
|
889
|
+
const {dryRun = true, daTs = false, residenzaDaTs = false, domicilioDaTs = false, residenza = null, domicilio = null} = config;
|
|
890
|
+
const warnings = [];
|
|
891
|
+
const ko = (error, extra = {}) => ({ok: false, dryRun, payload: null, diff: null, warnings, response: null, error, ...extra});
|
|
892
|
+
|
|
893
|
+
if (!daTs && !residenzaDaTs && !domicilioDaTs && !residenza && !domicilio)
|
|
894
|
+
return ko("Nessuna modifica richiesta: specificare daTs, residenzaDaTs, domicilioDaTs e/o residenza/domicilio");
|
|
895
|
+
if (daTs && (residenzaDaTs || domicilioDaTs || residenza || domicilio))
|
|
896
|
+
return ko("daTs è esclusivo: non combinare con residenzaDaTs/domicilioDaTs/residenza/domicilio");
|
|
897
|
+
if (residenzaDaTs && residenza)
|
|
898
|
+
return ko("residenzaDaTs e residenza sono alternativi");
|
|
899
|
+
if (domicilioDaTs && domicilio)
|
|
900
|
+
return ko("domicilioDaTs e domicilio sono alternativi");
|
|
901
|
+
|
|
902
|
+
// 1) Record corrente NAR2
|
|
903
|
+
const datiAssistito = await this.getDatiAssistitoNar2FromCf(codFiscale);
|
|
904
|
+
if (!datiAssistito || !datiAssistito.ok)
|
|
905
|
+
return ko(datiAssistito?.error || "Assistito non trovato su NAR2", {errorDetail: datiAssistito?.errorDetail ?? null});
|
|
906
|
+
const fullData = datiAssistito.fullData.data;
|
|
907
|
+
|
|
908
|
+
// 2) Indirizzo dal Sistema TS (se richiesto)
|
|
909
|
+
let nuovaResidenza = null; // {indirizzo, comuneId, comune}
|
|
910
|
+
let nuovoDomicilio = null;
|
|
911
|
+
if (daTs || residenzaDaTs || domicilioDaTs) {
|
|
912
|
+
const sogei = await this.getDatiAssistitoFromCfSuSogeiNew(codFiscale);
|
|
913
|
+
const tsData = sogei?.fullData?.data ?? null;
|
|
914
|
+
const indirizzoTs = (typeof tsData?.p801recapitoTessera === "string" && tsData.p801recapitoTessera.trim() !== "")
|
|
915
|
+
? tsData.p801recapitoTessera.trim() : null;
|
|
916
|
+
if (!sogei?.ok || sogei?.data?.okTs !== true || !indirizzoTs)
|
|
917
|
+
return ko(sogei?.data?.erroreTs || "Indirizzo Sistema TS non disponibile per l'assistito");
|
|
918
|
+
// comune TS: catastale (univoco su NAR2), fallback ISTAT
|
|
919
|
+
const catastaleTs = (tsData.p801codiceComuneResidenza ?? "").toString().trim();
|
|
920
|
+
const istatTs = (tsData.p801codiceistatiComuneResidenza ?? "").toString().trim();
|
|
921
|
+
let comuneRes = catastaleTs ? await this.getComuneNar2({catastale: catastaleTs}) : {ok: false, comune: null};
|
|
922
|
+
if (!comuneRes.ok || !comuneRes.comune)
|
|
923
|
+
comuneRes = istatTs ? await this.getComuneNar2({istat: istatTs}) : comuneRes;
|
|
924
|
+
if (!comuneRes.ok || !comuneRes.comune)
|
|
925
|
+
return ko(`Comune TS non risolvibile su NAR2 (catastale='${catastaleTs}', istat='${istatTs}')${comuneRes.error ? `: ${comuneRes.error}` : ""}`);
|
|
926
|
+
// normalizza nel formato concordato: l'API Sogei usa spaziatura semplice
|
|
927
|
+
// ("VIA X 3, 98167 MESSINA (ME)"), il formato target è quello della UI
|
|
928
|
+
// ("VIA X 3 , 98167 MESSINA (ME)") — scompongo e ricompongo
|
|
929
|
+
let indirizzoTsNormalizzato;
|
|
930
|
+
const m = indirizzoTs.match(/^(.+?)\s*,\s*(\d{5})\s+(.+?)\s+\(([A-Za-z]{2})\)\s*$/);
|
|
931
|
+
if (m) {
|
|
932
|
+
indirizzoTsNormalizzato = Nar2.formattaIndirizzoNar2({
|
|
933
|
+
via: m[1], cap: m[2],
|
|
934
|
+
nomeComune: comuneRes.comune.nome, provincia: comuneRes.comune.provincia
|
|
935
|
+
});
|
|
936
|
+
} else {
|
|
937
|
+
indirizzoTsNormalizzato = indirizzoTs.toUpperCase();
|
|
938
|
+
warnings.push(`Indirizzo TS non scomponibile ("${indirizzoTs}"): usato così com'è`);
|
|
939
|
+
}
|
|
940
|
+
const daSistemaTs = {indirizzo: indirizzoTsNormalizzato, comuneId: comuneRes.comune.id?.toString(), comune: comuneRes.comune};
|
|
941
|
+
if (daTs || residenzaDaTs) nuovaResidenza = daSistemaTs;
|
|
942
|
+
if (daTs || domicilioDaTs) nuovoDomicilio = daSistemaTs;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// 3) Indirizzi da parametro
|
|
946
|
+
if (residenza) {
|
|
947
|
+
const r = await this.#risolviParametroIndirizzo(residenza, "residenza");
|
|
948
|
+
if (!r.ok) return ko(r.error, {errorDetail: r.errorDetail ?? null, ...(r.candidati ? {candidati: r.candidati} : {})});
|
|
949
|
+
nuovaResidenza = r;
|
|
950
|
+
}
|
|
951
|
+
if (domicilio) {
|
|
952
|
+
const d = await this.#risolviParametroIndirizzo(domicilio, "domicilio");
|
|
953
|
+
if (!d.ok) return ko(d.error, {errorDetail: d.errorDetail ?? null, ...(d.candidati ? {candidati: d.candidati} : {})});
|
|
954
|
+
nuovoDomicilio = d;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// 4) Payload: whitelist dei campi della PUT UI + override dei soli campi toccati
|
|
958
|
+
const payloadData = {};
|
|
959
|
+
for (const campo of Nar2.#CAMPI_PUT_PAZIENTE)
|
|
960
|
+
payloadData[campo] = typeof fullData[campo] === "undefined" ? null : fullData[campo];
|
|
961
|
+
if (nuovaResidenza) {
|
|
962
|
+
payloadData.pz_ind_res = nuovaResidenza.indirizzo;
|
|
963
|
+
payloadData.pz_com_res = nuovaResidenza.comuneId;
|
|
964
|
+
payloadData.pz_nciv_res = null;
|
|
965
|
+
}
|
|
966
|
+
if (nuovoDomicilio) {
|
|
967
|
+
payloadData.pz_ind_dom = nuovoDomicilio.indirizzo;
|
|
968
|
+
payloadData.pz_com_dom = nuovoDomicilio.comuneId;
|
|
969
|
+
payloadData.pz_nciv_dom = null;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// diff: prima → dopo dei soli campi modificati
|
|
973
|
+
const diff = {};
|
|
974
|
+
for (const campo of ["pz_ind_res", "pz_com_res", "pz_nciv_res", "pz_ind_dom", "pz_com_dom", "pz_nciv_dom"]) {
|
|
975
|
+
const da = typeof fullData[campo] === "undefined" ? null : fullData[campo];
|
|
976
|
+
if ((da ?? null) !== (payloadData[campo] ?? null)) diff[campo] = {da, a: payloadData[campo]};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// 5) Warning coerenza ASL (best effort, non bloccante)
|
|
980
|
+
const daVerificare = [];
|
|
981
|
+
if (nuovaResidenza) daVerificare.push({etichetta: "residenza", ...nuovaResidenza});
|
|
982
|
+
if (nuovoDomicilio && nuovoDomicilio.comuneId !== nuovaResidenza?.comuneId)
|
|
983
|
+
daVerificare.push({etichetta: "domicilio", ...nuovoDomicilio});
|
|
984
|
+
for (const {etichetta, comuneId, comune} of daVerificare) {
|
|
985
|
+
try {
|
|
986
|
+
const det = await this.getComuneFromId(comuneId);
|
|
987
|
+
const ucAsl = det.ok ? (det.data?.uc_asl ?? null) : null;
|
|
988
|
+
if (!ucAsl)
|
|
989
|
+
warnings.push(`ASL di competenza del comune di ${etichetta} (${comune?.nome ?? comuneId}) non determinabile`);
|
|
990
|
+
else if (fullData.pz_asl_app && ucAsl.toString() !== fullData.pz_asl_app.toString())
|
|
991
|
+
warnings.push(`Il comune di ${etichetta} (${comune?.nome ?? comuneId}) ricade nell'ASL ${ucAsl}, diversa dall'ASL di appartenenza dell'assistito (${fullData.pz_asl_app}): i campi ASL NON vengono modificati`);
|
|
992
|
+
} catch (e) {
|
|
993
|
+
warnings.push(`Verifica ASL ${etichetta} non riuscita: ${e.message}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const payload = {data: payloadData};
|
|
998
|
+
|
|
999
|
+
// 6) Dry-run: ritorna senza inviare
|
|
1000
|
+
if (dryRun) return {ok: true, dryRun: true, payload, diff, warnings, response: null};
|
|
1001
|
+
|
|
1002
|
+
// 7) Submit: PUT /pazienti/{pz_id}
|
|
1003
|
+
const result = await this.#postDataToUrl(Nar2.GET_ASSISTITO_NAR_FROM_ID, payload, {
|
|
1004
|
+
method: "put",
|
|
1005
|
+
replaceFromUrl: {id: fullData.pz_id}
|
|
1006
|
+
});
|
|
1007
|
+
return {
|
|
1008
|
+
ok: result.ok,
|
|
1009
|
+
dryRun: false,
|
|
1010
|
+
payload, diff, warnings,
|
|
1011
|
+
response: result.fullResponse,
|
|
1012
|
+
data: result.data,
|
|
1013
|
+
error: result.ok ? undefined : (result.error || "Errore durante la PUT /pazienti/{id}"),
|
|
1014
|
+
errorDetail: result.ok ? undefined : (result.errorDetail ?? null)
|
|
581
1015
|
};
|
|
582
1016
|
}
|
|
583
1017
|
|
|
@@ -604,9 +1038,9 @@ export class Nar2 {
|
|
|
604
1038
|
if (includeFullData)
|
|
605
1039
|
out.data.fullData = data.data;
|
|
606
1040
|
return out;
|
|
607
|
-
} else return {ok: false, data: null};
|
|
1041
|
+
} else return {ok: false, data: null, error: data?.error || "Dati paziente-medico non disponibili su NAR2", errorDetail: data?.errorDetail ?? null};
|
|
608
1042
|
} catch (e) {
|
|
609
|
-
return {ok: false, data: null};
|
|
1043
|
+
return {ok: false, data: null, error: e.message, errorDetail: null};
|
|
610
1044
|
}
|
|
611
1045
|
}
|
|
612
1046
|
|
|
@@ -637,7 +1071,7 @@ export class Nar2 {
|
|
|
637
1071
|
let result = fullData;
|
|
638
1072
|
if (!result) {
|
|
639
1073
|
const datiRes = await this.getDatiAssistitoNar2FromCf(codFiscale);
|
|
640
|
-
if (!datiRes || !datiRes.ok) return {ok: false, data: null};
|
|
1074
|
+
if (!datiRes || !datiRes.ok) return {ok: false, data: null, error: datiRes?.error || "Assistito non trovato su NAR2", errorDetail: datiRes?.errorDetail ?? null};
|
|
641
1075
|
result = datiRes.fullData.data;
|
|
642
1076
|
}
|
|
643
1077
|
|
|
@@ -751,7 +1185,7 @@ export class Nar2 {
|
|
|
751
1185
|
let result = fullData;
|
|
752
1186
|
if (!result) {
|
|
753
1187
|
const datiRes = await this.getDatiAssistitoNar2FromCf(codFiscale);
|
|
754
|
-
if (!datiRes || !datiRes.ok) return {ok: false, data: null};
|
|
1188
|
+
if (!datiRes || !datiRes.ok) return {ok: false, data: null, error: datiRes?.error || "Assistito non trovato su NAR2", errorDetail: datiRes?.errorDetail ?? null};
|
|
755
1189
|
result = datiRes.fullData.data;
|
|
756
1190
|
}
|
|
757
1191
|
return {ok: true, data: Nar2.estraiRapportoMedicoAttivo(result, {categoria})};
|
|
@@ -808,7 +1242,7 @@ export class Nar2 {
|
|
|
808
1242
|
return {ok: true, data: out};
|
|
809
1243
|
}
|
|
810
1244
|
}
|
|
811
|
-
return {ok: false, data: null};
|
|
1245
|
+
return {ok: false, data: null, error: data?.error || "Ambiti di domicilio non disponibili su NAR2", errorDetail: data?.errorDetail ?? null};
|
|
812
1246
|
}
|
|
813
1247
|
|
|
814
1248
|
|
|
@@ -858,7 +1292,7 @@ export class Nar2 {
|
|
|
858
1292
|
}
|
|
859
1293
|
return {ok: true, data: out};
|
|
860
1294
|
}
|
|
861
|
-
return {ok: false, data: []};
|
|
1295
|
+
return {ok: false, data: [], error: data?.error || "Medici per ambito non disponibili su NAR2", errorDetail: data?.errorDetail ?? null};
|
|
862
1296
|
}
|
|
863
1297
|
|
|
864
1298
|
|
|
@@ -873,6 +1307,11 @@ export class Nar2 {
|
|
|
873
1307
|
* @param {boolean} [config.soloMMG=false] - Flag indicating whether to include only MMG records.
|
|
874
1308
|
* @param {string} [config.asl="281"] - The ASL code to filter the records (default is "281" for Messina).
|
|
875
1309
|
* @param {string} [config.azienda="ME"] - The company code to filter the records (default is "ME" for Messina).
|
|
1310
|
+
* @param {boolean} [config.aggiungiDatiRapporti=false] - Se true, per ogni medico recupera (con una sola
|
|
1311
|
+
* chiamata `medici/{id}`) sia l'elenco dei rapporti individuali (categoria, email, telefoni, date,
|
|
1312
|
+
* domicilio/indirizzo studio), allegato al campo `rapporti`, sia gli indirizzi del medico, allegati
|
|
1313
|
+
* al campo `indirizzi`. Include tutti i rapporti, anche quelli non attivi.
|
|
1314
|
+
* @param {number} [config.parallelsRapporti=10] - Numero di richieste rapporti eseguite in parallelo.
|
|
876
1315
|
*
|
|
877
1316
|
* @return {Promise<Object>} A promise that resolves to the retrieved medical data.
|
|
878
1317
|
*/
|
|
@@ -885,6 +1324,8 @@ export class Nar2 {
|
|
|
885
1324
|
soloMMG = false,
|
|
886
1325
|
asl = "281", // messina,
|
|
887
1326
|
azienda = "ME", //messina
|
|
1327
|
+
aggiungiDatiRapporti = false,
|
|
1328
|
+
parallelsRapporti = 10,
|
|
888
1329
|
} = config;
|
|
889
1330
|
let getParams = {
|
|
890
1331
|
"tipo_rapporto": "Medico_base",
|
|
@@ -917,6 +1358,24 @@ export class Nar2 {
|
|
|
917
1358
|
if (nascondiCessati)
|
|
918
1359
|
medici = medici.filter(m => m && (m.fine_rapporto === null || typeof m.fine_rapporto === "undefined"));
|
|
919
1360
|
|
|
1361
|
+
if (aggiungiDatiRapporti) {
|
|
1362
|
+
const concorrenza = Math.max(1, parallelsRapporti);
|
|
1363
|
+
for (let i = 0; i < medici.length; i += concorrenza) {
|
|
1364
|
+
const blocco = medici.slice(i, i + concorrenza);
|
|
1365
|
+
await Promise.all(blocco.map(async (m) => {
|
|
1366
|
+
// Una sola chiamata medici/{id} fornisce sia rapporti che indirizzi
|
|
1367
|
+
const datiMedico = m.pf_id ? await this.getMedicoFromId(m.pf_id, azienda) : null;
|
|
1368
|
+
if (datiMedico && datiMedico.ok) {
|
|
1369
|
+
m.rapporti = this.#estraiRapportiDaDatiMedico(datiMedico.data);
|
|
1370
|
+
m.indirizzi = this.#estraiIndirizziDaDatiMedico(datiMedico.data);
|
|
1371
|
+
} else {
|
|
1372
|
+
m.rapporti = [];
|
|
1373
|
+
m.indirizzi = [];
|
|
1374
|
+
}
|
|
1375
|
+
}));
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
920
1379
|
return {ok: true, data: medici};
|
|
921
1380
|
}
|
|
922
1381
|
return data;
|
|
@@ -929,6 +1388,7 @@ export class Nar2 {
|
|
|
929
1388
|
assistito = new Assistito();
|
|
930
1389
|
let datiAssistito = null;
|
|
931
1390
|
let datiIdAssistito;
|
|
1391
|
+
let lastException = null;
|
|
932
1392
|
const retry = 3;
|
|
933
1393
|
for (let i = 0; i < retry; i++) {
|
|
934
1394
|
try {
|
|
@@ -956,6 +1416,7 @@ export class Nar2 {
|
|
|
956
1416
|
}
|
|
957
1417
|
}
|
|
958
1418
|
} catch (e) {
|
|
1419
|
+
lastException = e;
|
|
959
1420
|
console.log("[getDatiAssistitoNar2FromCf] Eccezione durante recupero Nar2:", e.message);
|
|
960
1421
|
}
|
|
961
1422
|
if (datiAssistito && datiAssistito.ok) break;
|
|
@@ -1012,9 +1473,19 @@ export class Nar2 {
|
|
|
1012
1473
|
};
|
|
1013
1474
|
} else {
|
|
1014
1475
|
console.log(`[getDatiAssistitoNar2FromCf] Nar2 fallito dopo ${retry} tentativi per CF: ${codiceFiscale?.substring(0, 6)}***`);
|
|
1015
|
-
|
|
1476
|
+
// Determina il motivo più specifico possibile del fallimento (additivo, non cambia lo shape)
|
|
1477
|
+
const numRisultati = (!fallback && datiIdAssistito?.ok && Array.isArray(datiIdAssistito.data)) ? datiIdAssistito.data.length : null;
|
|
1478
|
+
const errMessage = datiAssistito?.error || datiIdAssistito?.error ||
|
|
1479
|
+
(numRisultati !== null && numRisultati !== 1 ? `La ricerca per codice fiscale su NAR2 ha restituito ${numRisultati} risultati` : null) ||
|
|
1480
|
+
(lastException ? `Eccezione durante il recupero da NAR2: ${lastException.message}` : null) ||
|
|
1481
|
+
"Nessun assistito trovato con il codice fiscale fornito";
|
|
1482
|
+
assistito.erroreNar2 = errMessage;
|
|
1016
1483
|
assistito.okNar2 = false;
|
|
1017
|
-
return {
|
|
1484
|
+
return {
|
|
1485
|
+
ok: false, data: null, fullData: datiIdAssistito,
|
|
1486
|
+
error: errMessage,
|
|
1487
|
+
errorDetail: datiAssistito?.errorDetail || datiIdAssistito?.errorDetail || null
|
|
1488
|
+
};
|
|
1018
1489
|
}
|
|
1019
1490
|
}
|
|
1020
1491
|
|
|
@@ -1040,7 +1511,7 @@ export class Nar2 {
|
|
|
1040
1511
|
* @param {Object} [config.replaceFromUrl] - Sostituzioni nei placeholder URL.
|
|
1041
1512
|
* @param {Object} [config.getParams] - Eventuali query params.
|
|
1042
1513
|
* @param {string} [config.method="post"] - Metodo HTTP (es. "post", "put"). `aggiornaSceltaMedico` richiede PUT.
|
|
1043
|
-
* @returns {Promise<{ok:boolean, data:any, fullResponse:Object|null}>}
|
|
1514
|
+
* @returns {Promise<{ok:boolean, data:any, fullResponse:Object|null, error:string|null, errorDetail:Object|null}>}
|
|
1044
1515
|
*/
|
|
1045
1516
|
async #postDataToUrl(url, body, config = {}) {
|
|
1046
1517
|
const {replaceFromUrl = null, getParams = null, method = "post"} = config;
|
|
@@ -1057,7 +1528,8 @@ export class Nar2 {
|
|
|
1057
1528
|
finalUrl = finalUrl.replace(`{${key}}`, (value === null || typeof value === "undefined") ? "null" : value.toString());
|
|
1058
1529
|
}
|
|
1059
1530
|
|
|
1060
|
-
let out = {ok: false, data: null, fullResponse: null};
|
|
1531
|
+
let out = {ok: false, data: null, fullResponse: null, error: null, errorDetail: null};
|
|
1532
|
+
let lastError = null;
|
|
1061
1533
|
for (let i = 0; i < this._maxRetry && !out.ok; i++) {
|
|
1062
1534
|
try {
|
|
1063
1535
|
await this.getToken();
|
|
@@ -1072,15 +1544,33 @@ export class Nar2 {
|
|
|
1072
1544
|
});
|
|
1073
1545
|
if (!response?.data?.status || response.data.status.toString() !== "true" ||
|
|
1074
1546
|
/token is (invalid|expired)/i.test(response.data.status.toString())) {
|
|
1547
|
+
// NAR2 risponde 200 con status != true: errore business (o token scaduto)
|
|
1548
|
+
lastError = {
|
|
1549
|
+
type: "nar2",
|
|
1550
|
+
statusCode: response.status ?? null,
|
|
1551
|
+
message: Nar2.#extractErrorMessage(response?.data) || `Risposta NAR2 con status='${response?.data?.status ?? "assente"}'`,
|
|
1552
|
+
response: response?.data ?? null
|
|
1553
|
+
};
|
|
1075
1554
|
await this.getToken({newToken: true});
|
|
1076
1555
|
} else {
|
|
1077
|
-
out = {ok: true, data: response.data.result, fullResponse: response.data};
|
|
1556
|
+
out = {ok: true, data: response.data.result, fullResponse: response.data, error: null, errorDetail: null};
|
|
1078
1557
|
}
|
|
1079
1558
|
} catch (e) {
|
|
1080
|
-
|
|
1559
|
+
lastError = {
|
|
1560
|
+
type: "http",
|
|
1561
|
+
statusCode: e.response?.status ?? null,
|
|
1562
|
+
message: Nar2.#extractErrorMessage(e.response?.data) || e.message,
|
|
1563
|
+
response: e.response?.data ?? null
|
|
1564
|
+
};
|
|
1565
|
+
console.log(`[#postDataToUrl] Errore ${httpMethod.toUpperCase()} ${finalUrl}: ${lastError.message}`);
|
|
1081
1566
|
await this.getToken({newToken: true});
|
|
1082
1567
|
}
|
|
1083
1568
|
}
|
|
1569
|
+
if (!out.ok && lastError) {
|
|
1570
|
+
out.error = lastError.message;
|
|
1571
|
+
out.errorDetail = {...lastError, method: httpMethod.toUpperCase(), url: finalUrl, tentativi: this._maxRetry};
|
|
1572
|
+
out.fullResponse = lastError.response;
|
|
1573
|
+
}
|
|
1084
1574
|
return out;
|
|
1085
1575
|
}
|
|
1086
1576
|
|
|
@@ -1092,8 +1582,9 @@ export class Nar2 {
|
|
|
1092
1582
|
replaceFromUrl = null,
|
|
1093
1583
|
rawResponse = false, // se true, accetta risposte non incapsulate in {status, result}
|
|
1094
1584
|
} = config;
|
|
1095
|
-
let out = {ok: false, data: null};
|
|
1585
|
+
let out = {ok: false, data: null, error: null, errorDetail: null};
|
|
1096
1586
|
let ok = false;
|
|
1587
|
+
let lastError = null;
|
|
1097
1588
|
|
|
1098
1589
|
// Build URL with get parameters if provided
|
|
1099
1590
|
let finalUrl = url;
|
|
@@ -1132,32 +1623,196 @@ export class Nar2 {
|
|
|
1132
1623
|
// se contiene un messaggio "token is invalid" rinnovo
|
|
1133
1624
|
const asStr = typeof response?.data === "string" ? response.data : JSON.stringify(response?.data ?? "");
|
|
1134
1625
|
if (/token is (invalid|expired)/i.test(asStr)) {
|
|
1626
|
+
lastError = {
|
|
1627
|
+
type: "nar2",
|
|
1628
|
+
statusCode: response.status ?? null,
|
|
1629
|
+
message: "Token NAR2 non valido o scaduto",
|
|
1630
|
+
response: response?.data ?? null
|
|
1631
|
+
};
|
|
1135
1632
|
await this.getToken({newToken: true});
|
|
1136
1633
|
} else {
|
|
1137
1634
|
ok = true;
|
|
1138
|
-
out = {ok: true, data: response.data};
|
|
1635
|
+
out = {ok: true, data: response.data, error: null, errorDetail: null};
|
|
1139
1636
|
}
|
|
1140
1637
|
} else if (!response?.data?.status || response.data.status.toString() !== "true" ||
|
|
1141
1638
|
/token is (invalid|expired)/i.test(response.data.status.toString())) {
|
|
1639
|
+
// NAR2 risponde 200 con status != true: errore business (o token scaduto)
|
|
1640
|
+
lastError = {
|
|
1641
|
+
type: "nar2",
|
|
1642
|
+
statusCode: response.status ?? null,
|
|
1643
|
+
message: Nar2.#extractErrorMessage(response?.data) || `Risposta NAR2 con status='${response?.data?.status ?? "assente"}'`,
|
|
1644
|
+
response: response?.data ?? null
|
|
1645
|
+
};
|
|
1142
1646
|
await this.getToken({newToken: true});
|
|
1143
1647
|
} else {
|
|
1144
1648
|
ok = true;
|
|
1145
|
-
out = {ok: true, data: response.data.result};
|
|
1649
|
+
out = {ok: true, data: response.data.result, error: null, errorDetail: null};
|
|
1146
1650
|
}
|
|
1147
1651
|
} catch (e) {
|
|
1652
|
+
lastError = {
|
|
1653
|
+
type: "http",
|
|
1654
|
+
statusCode: e.response?.status ?? null,
|
|
1655
|
+
message: Nar2.#extractErrorMessage(e.response?.data) || e.message,
|
|
1656
|
+
response: e.response?.data ?? null
|
|
1657
|
+
};
|
|
1148
1658
|
await this.getToken({newToken: true});
|
|
1149
1659
|
}
|
|
1150
1660
|
}
|
|
1151
1661
|
|
|
1662
|
+
if (!out.ok && lastError) {
|
|
1663
|
+
out.error = lastError.message;
|
|
1664
|
+
out.errorDetail = {...lastError, method: "GET", url: finalUrl, tentativi: this._maxRetry};
|
|
1665
|
+
}
|
|
1152
1666
|
return out;
|
|
1153
1667
|
}
|
|
1154
1668
|
|
|
1669
|
+
/**
|
|
1670
|
+
* Estrae un messaggio d'errore leggibile da un body di risposta NAR2/HTTP.
|
|
1671
|
+
* Accetta stringhe (troncate a 500 char) o oggetti con i campi comuni
|
|
1672
|
+
* message/error/msg/errore/result.
|
|
1673
|
+
*
|
|
1674
|
+
* @param {any} data - Body della risposta.
|
|
1675
|
+
* @returns {string|null} Messaggio estratto, o null se non determinabile.
|
|
1676
|
+
*/
|
|
1677
|
+
static #extractErrorMessage(data) {
|
|
1678
|
+
if (data === null || typeof data === "undefined") return null;
|
|
1679
|
+
if (typeof data === "string") {
|
|
1680
|
+
const s = data.trim();
|
|
1681
|
+
return s.length === 0 ? null : (s.length > 500 ? `${s.slice(0, 500)}…` : s);
|
|
1682
|
+
}
|
|
1683
|
+
if (typeof data !== "object") return String(data);
|
|
1684
|
+
const candidate = data.message || data.error || data.msg || data.errore ||
|
|
1685
|
+
(typeof data.result === "string" ? data.result : null);
|
|
1686
|
+
if (candidate) return typeof candidate === "string" ? candidate : JSON.stringify(candidate);
|
|
1687
|
+
if (data.status && data.status.toString() !== "true") return `status='${data.status}'`;
|
|
1688
|
+
return null;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1155
1691
|
async getAssistitoFromId(id) {
|
|
1156
1692
|
return await this.#getDataFromUrlIdOrParams(Nar2.GET_ASSISTITO_NAR_FROM_ID, {urlId: id});
|
|
1157
1693
|
}
|
|
1158
1694
|
|
|
1159
|
-
async getMedicoFromId(id) {
|
|
1160
|
-
|
|
1695
|
+
async getMedicoFromId(id, azienda = null) {
|
|
1696
|
+
const config = {urlId: id};
|
|
1697
|
+
if (azienda) config.getParams = {azienda};
|
|
1698
|
+
return await this.#getDataFromUrlIdOrParams(Nar2.GET_DATI_MEDICO_FROM_ID, config);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/**
|
|
1702
|
+
* Estrae l'elenco normalizzato dei rapporti individuali dai dati grezzi di un medico
|
|
1703
|
+
* (risultato di `getMedicoFromId` / endpoint `medici/{id}`).
|
|
1704
|
+
*
|
|
1705
|
+
* Include tutti i rapporti, anche quelli non attivi. Un rapporto è considerato attivo
|
|
1706
|
+
* solo se non ha data di fine (`ri_dt_disable` nullo).
|
|
1707
|
+
*
|
|
1708
|
+
* @param {Object} datiMedico - Oggetto restituito da `getMedicoFromId(...).data`.
|
|
1709
|
+
* @return {Array<Object>} Elenco rapporti normalizzati.
|
|
1710
|
+
*/
|
|
1711
|
+
#estraiRapportiDaDatiMedico(datiMedico) {
|
|
1712
|
+
const indirizziMedico = Array.isArray(datiMedico?.indirizzi) ? datiMedico.indirizzi : [];
|
|
1713
|
+
const rapporti = Array.isArray(datiMedico?.rapporto_individuale) ? datiMedico.rapporto_individuale : [];
|
|
1714
|
+
const fmt = (d) => d ? moment(d, "YYYY-MM-DD HH:mm:ss").format("DD/MM/YYYY") : null;
|
|
1715
|
+
|
|
1716
|
+
return rapporti.map(r => {
|
|
1717
|
+
let categoria = null;
|
|
1718
|
+
if (r.ri_categoria === Nar2.CAT_MMG) categoria = "mmg";
|
|
1719
|
+
else if (r.ri_categoria === Nar2.CAT_PEDIATRI) categoria = "pls";
|
|
1720
|
+
|
|
1721
|
+
const telefoni = [r.ri_telefono1, r.ri_telefono2, r.ri_telefono3]
|
|
1722
|
+
.map(t => (t ?? "").toString().trim())
|
|
1723
|
+
.filter(t => t !== "");
|
|
1724
|
+
|
|
1725
|
+
// Il domicilio (indirizzo studio) è collegato all'indirizzo del medico tramite il progressivo.
|
|
1726
|
+
// Quando NAR non fornisce il link, il domicilio resta null.
|
|
1727
|
+
const progr = r.indirizzo?.rd_progr ?? null;
|
|
1728
|
+
const ind = progr ? (indirizziMedico.find(i => i.pr_progr === progr) ?? null) : null;
|
|
1729
|
+
const comune = ind?.comune ?? null;
|
|
1730
|
+
const domicilio = ind ? {
|
|
1731
|
+
comune: comune?.cm_desc ?? null,
|
|
1732
|
+
provincia: comune?.cm_prov ?? null,
|
|
1733
|
+
cap: ind.pr_cap ?? comune?.cm_ccap ?? null,
|
|
1734
|
+
indirizzo: ind.pr_indi ?? null,
|
|
1735
|
+
numeroCivico: ind.pr_nciv ?? null,
|
|
1736
|
+
} : null;
|
|
1737
|
+
|
|
1738
|
+
return {
|
|
1739
|
+
id: r.ri_id ?? null,
|
|
1740
|
+
categoria,
|
|
1741
|
+
tipoRapporto: r.ri_rapporto ?? r.tipo_rapporto?.ra_cod ?? null,
|
|
1742
|
+
descrizioneRapporto: r.tipo_rapporto?.ra_desc ?? null,
|
|
1743
|
+
email: r.ri_email ?? null,
|
|
1744
|
+
telefoni,
|
|
1745
|
+
dataInizio: fmt(r.ri_dt_enable),
|
|
1746
|
+
dataFine: fmt(r.ri_dt_disable),
|
|
1747
|
+
attivo: !r.ri_dt_disable,
|
|
1748
|
+
domicilio,
|
|
1749
|
+
};
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* Recupera l'elenco normalizzato dei rapporti individuali di un medico dato il suo id NAR2.
|
|
1755
|
+
*
|
|
1756
|
+
* @param {string|number} id - Id persona del medico (`pf_id`).
|
|
1757
|
+
* @param {string} [azienda="ME"] - Codice azienda.
|
|
1758
|
+
* @return {Promise<Array<Object>>} Elenco rapporti normalizzati (vuoto in caso di errore).
|
|
1759
|
+
*/
|
|
1760
|
+
async getRapportiMedicoFromId(id, azienda = "ME") {
|
|
1761
|
+
const datiMedico = await this.getMedicoFromId(id, azienda);
|
|
1762
|
+
if (datiMedico && datiMedico.ok)
|
|
1763
|
+
return this.#estraiRapportiDaDatiMedico(datiMedico.data);
|
|
1764
|
+
return [];
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/**
|
|
1768
|
+
* Estrae l'elenco normalizzato degli indirizzi del medico dai dati grezzi
|
|
1769
|
+
* (risultato di `getMedicoFromId` / endpoint `medici/{id}`).
|
|
1770
|
+
*
|
|
1771
|
+
* Gli indirizzi sono legati al medico (non al rapporto). Corrispondono alla scheda
|
|
1772
|
+
* "Indirizzi" del portale NAR2 (`/operatore/medici/{id}/dati/indirizzi`).
|
|
1773
|
+
*
|
|
1774
|
+
* @param {Object} datiMedico - Oggetto restituito da `getMedicoFromId(...).data`.
|
|
1775
|
+
* @return {Array<Object>} Elenco indirizzi normalizzati.
|
|
1776
|
+
*/
|
|
1777
|
+
#estraiIndirizziDaDatiMedico(datiMedico) {
|
|
1778
|
+
const indirizzi = Array.isArray(datiMedico?.indirizzi) ? datiMedico.indirizzi : [];
|
|
1779
|
+
const fmt = (d) => d ? moment(d, "YYYY-MM-DD HH:mm:ss").format("DD/MM/YYYY") : null;
|
|
1780
|
+
|
|
1781
|
+
return indirizzi.map(ind => {
|
|
1782
|
+
const comune = ind?.comune ?? null;
|
|
1783
|
+
const telefoni = [ind.pr_telefono1, ind.pr_telefono2, ind.pr_telefono3]
|
|
1784
|
+
.map(t => (t ?? "").toString().trim())
|
|
1785
|
+
.filter(t => t !== "");
|
|
1786
|
+
|
|
1787
|
+
return {
|
|
1788
|
+
progressivo: ind.pr_progr ?? null,
|
|
1789
|
+
descrizione: ind.pr_desc ?? null,
|
|
1790
|
+
comune: comune?.cm_desc ?? null,
|
|
1791
|
+
provincia: comune?.cm_prov ?? null,
|
|
1792
|
+
cap: ind.pr_cap ?? comune?.cm_ccap ?? null,
|
|
1793
|
+
indirizzo: ind.pr_indi ?? null,
|
|
1794
|
+
numeroCivico: ind.pr_nciv ?? null,
|
|
1795
|
+
dataAttivazione: fmt(ind.pr_dt_enable),
|
|
1796
|
+
dataDisattivazione: fmt(ind.pr_dt_disable),
|
|
1797
|
+
residenza: ind.pr_residenza === "S",
|
|
1798
|
+
telefoni,
|
|
1799
|
+
email: ind.pr_email ?? null,
|
|
1800
|
+
};
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* Recupera l'elenco normalizzato degli indirizzi di un medico dato il suo id NAR2.
|
|
1806
|
+
*
|
|
1807
|
+
* @param {string|number} id - Id persona del medico (`pf_id`).
|
|
1808
|
+
* @param {string} [azienda="ME"] - Codice azienda.
|
|
1809
|
+
* @return {Promise<Array<Object>>} Elenco indirizzi normalizzati (vuoto in caso di errore).
|
|
1810
|
+
*/
|
|
1811
|
+
async getIndirizziMedicoFromId(id, azienda = "ME") {
|
|
1812
|
+
const datiMedico = await this.getMedicoFromId(id, azienda);
|
|
1813
|
+
if (datiMedico && datiMedico.ok)
|
|
1814
|
+
return this.#estraiIndirizziDaDatiMedico(datiMedico.data);
|
|
1815
|
+
return [];
|
|
1161
1816
|
}
|
|
1162
1817
|
|
|
1163
1818
|
async getNumAssistitiMedico(id) {
|
|
@@ -1331,7 +1986,7 @@ export class Nar2 {
|
|
|
1331
1986
|
|
|
1332
1987
|
const datiAssistito = await this.getDatiAssistitoNar2FromCf(codFiscale);
|
|
1333
1988
|
if (!datiAssistito || !datiAssistito.ok) {
|
|
1334
|
-
return {ok: false, dryRun, payload: null, response: null, error: "Assistito non trovato su NAR2"};
|
|
1989
|
+
return {ok: false, dryRun, payload: null, response: null, error: datiAssistito?.error || "Assistito non trovato su NAR2", errorDetail: datiAssistito?.errorDetail ?? null};
|
|
1335
1990
|
}
|
|
1336
1991
|
const full = datiAssistito.fullData.data;
|
|
1337
1992
|
const storico = Array.isArray(full.storico_medici) ? full.storico_medici : [];
|
|
@@ -1394,7 +2049,8 @@ export class Nar2 {
|
|
|
1394
2049
|
payload,
|
|
1395
2050
|
response: result.fullResponse,
|
|
1396
2051
|
data: result.data,
|
|
1397
|
-
error: result.ok ? undefined : "Errore durante la PUT /pazienti/aggiornaSceltaMedico"
|
|
2052
|
+
error: result.ok ? undefined : (result.error || "Errore durante la PUT /pazienti/aggiornaSceltaMedico"),
|
|
2053
|
+
errorDetail: result.ok ? undefined : (result.errorDetail ?? null)
|
|
1398
2054
|
};
|
|
1399
2055
|
}
|
|
1400
2056
|
|
|
@@ -1413,10 +2069,11 @@ export class Nar2 {
|
|
|
1413
2069
|
let {pzId = null} = config;
|
|
1414
2070
|
if (pzId == null) {
|
|
1415
2071
|
const dati = await this.getDatiAssistitoNar2FromCf(codFiscale);
|
|
1416
|
-
if (!dati || !dati.ok) return {ok: false, pdf: null, tipo};
|
|
2072
|
+
if (!dati || !dati.ok) return {ok: false, pdf: null, tipo, error: dati?.error || "Assistito non trovato su NAR2", errorDetail: dati?.errorDetail ?? null};
|
|
1417
2073
|
pzId = dati.fullData.data.pz_id;
|
|
1418
2074
|
}
|
|
1419
2075
|
const url = Nar2.STAMPA_DOCUMENTO_URL.replace("{tipo}", tipo);
|
|
2076
|
+
let lastError = null;
|
|
1420
2077
|
for (let i = 0; i < this._maxRetry; i++) {
|
|
1421
2078
|
try {
|
|
1422
2079
|
await this.getToken();
|
|
@@ -1429,13 +2086,15 @@ export class Nar2 {
|
|
|
1429
2086
|
if (resp.status === 200 && /pdf/i.test(ct)) {
|
|
1430
2087
|
return {ok: true, pdf: Buffer.from(resp.data), tipo};
|
|
1431
2088
|
}
|
|
2089
|
+
lastError = {type: "nar2", statusCode: resp.status ?? null, message: `Risposta non PDF (content-type: ${ct || "assente"})`, response: null};
|
|
1432
2090
|
await this.getToken({newToken: true});
|
|
1433
2091
|
} catch (e) {
|
|
2092
|
+
lastError = {type: "http", statusCode: e.response?.status ?? null, message: e.message, response: null};
|
|
1434
2093
|
console.log(`[getDocumentoPdf] Errore stampe/${tipo}: ${e.message}`);
|
|
1435
2094
|
await this.getToken({newToken: true});
|
|
1436
2095
|
}
|
|
1437
2096
|
}
|
|
1438
|
-
return {ok: false, pdf: null, tipo};
|
|
2097
|
+
return {ok: false, pdf: null, tipo, error: lastError?.message || null, errorDetail: lastError ? {...lastError, method: "GET", url, tentativi: this._maxRetry} : null};
|
|
1439
2098
|
}
|
|
1440
2099
|
|
|
1441
2100
|
/**
|
|
@@ -1459,7 +2118,7 @@ export class Nar2 {
|
|
|
1459
2118
|
} = config;
|
|
1460
2119
|
|
|
1461
2120
|
const dati = await this.getDatiAssistitoNar2FromCf(codFiscale);
|
|
1462
|
-
if (!dati || !dati.ok) return {ok: false, unico: null, documenti: [], error: "Assistito non trovato su NAR2"};
|
|
2121
|
+
if (!dati || !dati.ok) return {ok: false, unico: null, documenti: [], error: dati?.error || "Assistito non trovato su NAR2", errorDetail: dati?.errorDetail ?? null};
|
|
1463
2122
|
const pzId = dati.fullData.data.pz_id;
|
|
1464
2123
|
|
|
1465
2124
|
const risultati = [];
|
|
@@ -1497,11 +2156,16 @@ export class Nar2 {
|
|
|
1497
2156
|
}
|
|
1498
2157
|
}
|
|
1499
2158
|
|
|
2159
|
+
if (!ok) {
|
|
2160
|
+
const primoErrore = risultati.find((r) => !r.ok && r.error);
|
|
2161
|
+
return {ok, unico, documenti: risultati, error: primoErrore?.error || "Nessun documento disponibile su NAR2", errorDetail: primoErrore?.errorDetail ?? null};
|
|
2162
|
+
}
|
|
1500
2163
|
return {ok, unico, documenti: risultati};
|
|
1501
2164
|
}
|
|
1502
2165
|
|
|
1503
2166
|
async getDatiAssistitoFromCfSuSogeiNew(cf, assistito = null, fallback = false) {
|
|
1504
2167
|
let ok = false;
|
|
2168
|
+
let risultato = null; // valore di ritorno del ramo di successo (fix: prima non veniva mai ritornato)
|
|
1505
2169
|
const nullArray = (data) => {
|
|
1506
2170
|
return Array.isArray(data) && data.length === 0 ? "" : data;
|
|
1507
2171
|
};
|
|
@@ -1583,7 +2247,7 @@ export class Nar2 {
|
|
|
1583
2247
|
assistito.setTs(DATI.DATA_DECESSO, deceduto ? nullArray(out.data.data.p801dataDecesso) : null);
|
|
1584
2248
|
assistito.okTs = true;
|
|
1585
2249
|
assistito.fullDataTs = out.data;
|
|
1586
|
-
|
|
2250
|
+
risultato = {
|
|
1587
2251
|
ok: true,
|
|
1588
2252
|
fullData: out.data,
|
|
1589
2253
|
data: assistito
|
|
@@ -1602,6 +2266,7 @@ export class Nar2 {
|
|
|
1602
2266
|
data: assistito
|
|
1603
2267
|
};
|
|
1604
2268
|
}
|
|
2269
|
+
return risultato;
|
|
1605
2270
|
}
|
|
1606
2271
|
|
|
1607
2272
|
|