aziendasanitaria-utils 1.2.39 → 1.2.41
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/package.json +3 -2
- package/src/Procedure.js +5 -4
- package/src/Utils.js +59 -5
- package/src/m/FlussoM.js +267 -35
- package/src/narTsServices/Medici.js +98 -95
- package/src/narTsServices/Nar2.js +155 -27
- package/src/siad/FlussoSIAD.js +276 -31
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=14.0.0"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.2.
|
|
6
|
+
"version": "1.2.41",
|
|
7
7
|
"repository": "deduzzo/aziendasanitaria-utils",
|
|
8
8
|
"description": "Un utility per gestire i flussi sanitari Siciliani e non solo..",
|
|
9
9
|
"main": "index.js",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"test": "echo \"Error: no test specified\"",
|
|
20
20
|
"run-private": "node ./private_example.js",
|
|
21
|
-
"bun-run-private": "bun ./private_example.js"
|
|
21
|
+
"bun-run-private": "bun ./private_example.js",
|
|
22
|
+
"genera-strutture": "node ./load_strutture.js"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"@freiraum/msgreader": "^1.1.1",
|
package/src/Procedure.js
CHANGED
|
@@ -132,8 +132,9 @@ class Procedure {
|
|
|
132
132
|
for (let assistito of assistitiNar[codReg].assistiti) {
|
|
133
133
|
allAssistitiDistrettuali[distretto].nar.push({...assistito, ...medico});
|
|
134
134
|
}
|
|
135
|
-
for (let
|
|
136
|
-
|
|
135
|
+
for (let assistiti of assistitiTs[codReg] ?? []) {
|
|
136
|
+
if (assistiti)
|
|
137
|
+
allAssistitiDistrettuali[distretto].ts.push({...assistiti, ...medico});
|
|
137
138
|
}
|
|
138
139
|
allAssistitiDistrettuali[distretto].codRegNar[codReg] = assistitiNar[codReg].assistiti;
|
|
139
140
|
allAssistitiDistrettuali[distretto].codRegTs[codReg] = assistitiTs[codReg];
|
|
@@ -188,7 +189,7 @@ class Procedure {
|
|
|
188
189
|
*
|
|
189
190
|
* @return {Promise<number>} Returns 0 upon successful processing.
|
|
190
191
|
*/
|
|
191
|
-
static async getControlliEsenzione(pathElenco, anno,impostazioniServizi, config={}){
|
|
192
|
+
static async getControlliEsenzione(pathElenco, anno, impostazioniServizi, config = {}) {
|
|
192
193
|
let {
|
|
193
194
|
colonnaProtocolli = "PROTOCOLLO",
|
|
194
195
|
colonnaEsenzione = "ESENZIONE",
|
|
@@ -394,7 +395,7 @@ class Procedure {
|
|
|
394
395
|
}
|
|
395
396
|
}
|
|
396
397
|
}
|
|
397
|
-
if (Object.values(outData) >0)
|
|
398
|
+
if (Object.values(outData) > 0)
|
|
398
399
|
outFinal.push(outData);
|
|
399
400
|
da = da.add(1, "month");
|
|
400
401
|
} while (da.isSameOrBefore(a) && !singoloCedolino);
|
package/src/Utils.js
CHANGED
|
@@ -853,13 +853,67 @@ const riunisciJsonDaTag = async (path, tag, filter = null) => {
|
|
|
853
853
|
return out;
|
|
854
854
|
}
|
|
855
855
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
856
|
+
/**
|
|
857
|
+
* Riunisci dati da tutti i file Excel trovati ricorsivamente in `path`.
|
|
858
|
+
*
|
|
859
|
+
* - Cerca tutti i file `.xlsx` all'interno della cartella `path` (ricorsivamente),
|
|
860
|
+
* applicando opzionalmente un `filter` sui nomi dei file.
|
|
861
|
+
* - Per ogni file trovato legge il primo foglio usando `getObjectFromFileExcel`.
|
|
862
|
+
* - Se `config.tags` è un array non vuoto, per ogni `tag` crea un array in output
|
|
863
|
+
* e ci inserisce i valori corrispondenti (per ogni riga prende `row[tag]`).
|
|
864
|
+
* - Se `config.tags` è vuoto, costruisce l'output con tutte le colonne presenti
|
|
865
|
+
* nella prima riga del primo file (`data[0]`).
|
|
866
|
+
* - Se `config.salvaInNuovoFileExcel` è true, salva il risultato in
|
|
867
|
+
* un file `riunito.xlsx` nella cartella `path`.
|
|
868
|
+
*
|
|
869
|
+
* @param {string} path - Cartella radice dove cercare i file Excel.
|
|
870
|
+
* @param {Object} [config={}] - Configurazione opzionale.
|
|
871
|
+
* @param {string[]} [config.tags=[]] - Array di colonne (nomi) da raccogliere.
|
|
872
|
+
* @param {string|null} [config.filter=null] - Filtro per i nomi dei file (opzionale).
|
|
873
|
+
* @param {boolean} [config.salvaInNuovoFileExcel=false] - Se true salva l'output in `riunito.xlsx`.
|
|
874
|
+
* @param {string|null} [config.soloValoriUniciPerCampo=null] - Se specificato, mantiene solo righe con valori unici per questo campo.
|
|
875
|
+
* @returns {Promise<Object>} Oggetto con chiavi corrispondenti ai tag (o colonne) e valori come array dei relativi valori.
|
|
876
|
+
*
|
|
877
|
+
* @example
|
|
878
|
+
* // Unisce le colonne "nome" e "cognome" da tutti gli xlsx nella cartella
|
|
879
|
+
* const out = await riunisciExcelDaTag(`/Users/deduzzo/dati`, { tags: ['nome','cognome'] });
|
|
880
|
+
*/
|
|
881
|
+
const riunisciExcelDaTag = async (folderPath, config = {}) => {
|
|
882
|
+
let {
|
|
883
|
+
tags = [],
|
|
884
|
+
filter = null,
|
|
885
|
+
salvaInNuovoFileExcel = false,
|
|
886
|
+
soloValoriUniciPerCampo = null
|
|
887
|
+
} = config;
|
|
888
|
+
let files = getAllFilesRecursive(folderPath, ".xlsx", filter);
|
|
889
|
+
let out = [];
|
|
860
890
|
for (let file of files) {
|
|
861
891
|
let data = await getObjectFromFileExcel(file);
|
|
862
|
-
|
|
892
|
+
if (tags.length === 0) {
|
|
893
|
+
// get all keys from data[0]
|
|
894
|
+
let keys = Object.keys(data[0]);
|
|
895
|
+
tags = keys;
|
|
896
|
+
}
|
|
897
|
+
// add every object of data but only the tags we want, ex. if tag is ['cf', 'data'] we want to put only object type {cf: 'xxx', data: 'yyy'} in out
|
|
898
|
+
for (let obj of data) {
|
|
899
|
+
let objTemp = {};
|
|
900
|
+
tags.forEach(tag => {
|
|
901
|
+
objTemp[tag] = obj[tag];
|
|
902
|
+
});
|
|
903
|
+
// prevent to push empty object
|
|
904
|
+
if (Object.values(objTemp).some(value => value !== ""))
|
|
905
|
+
out.push(objTemp);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (soloValoriUniciPerCampo) {
|
|
909
|
+
let out2 = {};
|
|
910
|
+
for (let riga of out)
|
|
911
|
+
if (!out2.hasOwnProperty(riga[soloValoriUniciPerCampo]))
|
|
912
|
+
out2[riga[soloValoriUniciPerCampo]] = riga;
|
|
913
|
+
out = Object.values(out2);
|
|
914
|
+
}
|
|
915
|
+
if (salvaInNuovoFileExcel) {
|
|
916
|
+
await scriviOggettoSuNuovoFileExcel(folderPath + path.sep + "riunito.xlsx", out);
|
|
863
917
|
}
|
|
864
918
|
return out;
|
|
865
919
|
}
|
package/src/m/FlussoM.js
CHANGED
|
@@ -10,6 +10,7 @@ import {DatiStruttureProgettoTs} from "./DatiStruttureProgettoTs.js";
|
|
|
10
10
|
import ExcelJS from "exceljs"
|
|
11
11
|
import loki from 'lokijs';
|
|
12
12
|
import {hashFile} from "hasha";
|
|
13
|
+
import * as cheerio from 'cheerio';
|
|
13
14
|
|
|
14
15
|
export class FlussoM {
|
|
15
16
|
/**
|
|
@@ -403,27 +404,76 @@ export class FlussoM {
|
|
|
403
404
|
}
|
|
404
405
|
|
|
405
406
|
|
|
406
|
-
#
|
|
407
|
-
|
|
408
|
-
|
|
407
|
+
// DEPRECATO: sostituito da #loadStruttureFromLatestFile()
|
|
408
|
+
// #loadStruttureFromFlowlookDB() {
|
|
409
|
+
// const buffer = fs.readFileSync(this._settings.flowlookDBFilePath);
|
|
410
|
+
// const reader = new MDBReader(buffer);
|
|
411
|
+
// const strutture = reader.getTable(this._settings.flowlookDBTableSTS11).getData();
|
|
412
|
+
// let struttureFiltrate = strutture.filter(p => p["CodiceAzienda"] === this._settings.codiceAzienda && p["CodiceRegione"] === this._settings.codiceRegione && (parseInt(p["Anno"]) >= (new Date().getFullYear()) - 2));
|
|
413
|
+
// let mancanti = []
|
|
414
|
+
// let struttureOut = {}
|
|
415
|
+
// struttureFiltrate.forEach(p => {
|
|
416
|
+
// if (this._settings.datiStruttureRegione.comuniDistretti.hasOwnProperty(p["CodiceComune"])) {
|
|
417
|
+
// struttureOut[p['CodiceStruttura']] = {
|
|
418
|
+
// codiceRegione: p['CodiceRegione'],
|
|
419
|
+
// codiceAzienda: p['CodiceAzienda'],
|
|
420
|
+
// denominazione: p['DenominazioneStruttura'],
|
|
421
|
+
// codiceComune: p['CodiceComune'],
|
|
422
|
+
// idDistretto: this._settings.datiStruttureRegione.comuniDistretti[p["CodiceComune"]],
|
|
423
|
+
// dataUltimoAggiornamento: moment(p['DataAggiornamento'], 'DD/MM/YYYY')
|
|
424
|
+
// };
|
|
425
|
+
// } else
|
|
426
|
+
// mancanti.push(p);
|
|
427
|
+
// })
|
|
428
|
+
// return struttureOut;
|
|
429
|
+
// }
|
|
430
|
+
|
|
431
|
+
#loadStruttureFromLatestFile() {
|
|
432
|
+
const struttureDir = path.join("data", "strutture");
|
|
433
|
+
if (!fs.existsSync(struttureDir)) {
|
|
434
|
+
throw new Error(`Cartella ${struttureDir} non trovata. Eseguire prima la generazione delle strutture con load_strutture.js`);
|
|
435
|
+
}
|
|
409
436
|
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
437
|
+
const latestFile = fs.readdirSync(struttureDir).find(f => f.startsWith("LATEST_"));
|
|
438
|
+
if (!latestFile) {
|
|
439
|
+
throw new Error(`Nessun file LATEST_ trovato in ${struttureDir}. Eseguire prima la generazione delle strutture con load_strutture.js`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const strutture = JSON.parse(fs.readFileSync(path.join(struttureDir, latestFile), "utf-8"));
|
|
443
|
+
|
|
444
|
+
// Costruisci reverse lookup: nome distretto -> id numerico
|
|
445
|
+
const reverseDistretti = {};
|
|
446
|
+
for (const [id, nome] of Object.entries(this._settings.datiStruttureRegione.distretti)) {
|
|
447
|
+
reverseDistretti[nome.toLowerCase()] = parseInt(id);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const struttureOut = {};
|
|
451
|
+
for (const s of strutture) {
|
|
452
|
+
let idDistretto = null;
|
|
453
|
+
if (s.distretto) {
|
|
454
|
+
const distLower = s.distretto.toLowerCase();
|
|
455
|
+
// Match esatto
|
|
456
|
+
idDistretto = reverseDistretti[distLower];
|
|
457
|
+
// Match parziale (es. "Barcellona P.G." contiene "barcellona")
|
|
458
|
+
if (idDistretto === undefined) {
|
|
459
|
+
const found = Object.entries(reverseDistretti).find(([nome]) =>
|
|
460
|
+
distLower.startsWith(nome) || nome.startsWith(distLower)
|
|
461
|
+
);
|
|
462
|
+
if (found) idDistretto = found[1];
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
struttureOut[s.codice] = {
|
|
467
|
+
denominazione: s.denominazione,
|
|
468
|
+
codiceComune: s.codCatastale,
|
|
469
|
+
idDistretto: idDistretto,
|
|
470
|
+
distretto: s.distretto,
|
|
471
|
+
ambito: s.ambito,
|
|
472
|
+
stato: s.stato
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
console.log(`Caricate ${Object.keys(struttureOut).length} strutture da ${latestFile}`);
|
|
427
477
|
return struttureOut;
|
|
428
478
|
}
|
|
429
479
|
|
|
@@ -476,7 +526,7 @@ export class FlussoM {
|
|
|
476
526
|
}
|
|
477
527
|
|
|
478
528
|
async #ottieniStatDaFileFlussoM(file) {
|
|
479
|
-
let strutture = this.#
|
|
529
|
+
let strutture = this.#loadStruttureFromLatestFile();
|
|
480
530
|
let ricetteInFile = await this.#elaboraFileFlussoM(file, this._starts);
|
|
481
531
|
let warn = "";
|
|
482
532
|
if (ricetteInFile.error) {
|
|
@@ -486,13 +536,13 @@ export class FlussoM {
|
|
|
486
536
|
let verificaDateStruttura = this.#checkMeseAnnoStruttura(Object.values(ricetteInFile.ricette))
|
|
487
537
|
ricetteInFile.codiceStruttura = verificaDateStruttura.codiceStruttura;
|
|
488
538
|
ricetteInFile.file = file;
|
|
489
|
-
ricetteInFile.idDistretto = strutture[verificaDateStruttura.codiceStruttura]?.idDistretto
|
|
539
|
+
ricetteInFile.idDistretto = strutture[verificaDateStruttura.codiceStruttura]?.idDistretto?.toString() ?? (ricetteInFile.datiDaFile?.idDistretto ?? "X");
|
|
490
540
|
ricetteInFile.annoPrevalente = verificaDateStruttura.meseAnnoPrevalente.substr(2, 4);
|
|
491
541
|
ricetteInFile.mesePrevalente = verificaDateStruttura.meseAnnoPrevalente.substr(0, 2);
|
|
492
542
|
ricetteInFile.date = _.omitBy(verificaDateStruttura.date, _.isNil);
|
|
493
543
|
if (!strutture.hasOwnProperty(verificaDateStruttura.codiceStruttura)) {
|
|
494
|
-
console.log("STRUTTURA " + verificaDateStruttura.codiceStruttura + " non presente
|
|
495
|
-
warn = "STRUTTURA " + verificaDateStruttura.codiceStruttura + " non presente
|
|
544
|
+
console.log("STRUTTURA " + verificaDateStruttura.codiceStruttura + " non presente nel file strutture")
|
|
545
|
+
warn = "STRUTTURA " + verificaDateStruttura.codiceStruttura + " non presente nel file strutture"
|
|
496
546
|
}
|
|
497
547
|
return {errore: false, warning: (warn === "" ? false : warn), out: ricetteInFile}
|
|
498
548
|
}
|
|
@@ -536,7 +586,7 @@ export class FlussoM {
|
|
|
536
586
|
|
|
537
587
|
async generaReportDaStats(salvaFileHtml = true, salvaFileExcel = true) {
|
|
538
588
|
let idDistretti = Object.keys(this._settings.datiStruttureRegione.distretti);
|
|
539
|
-
let strutture = this.#
|
|
589
|
+
let strutture = this.#loadStruttureFromLatestFile();
|
|
540
590
|
let files = utils.getAllFilesRecursive(this._settings.out_folder, '.mstats');
|
|
541
591
|
const table = {
|
|
542
592
|
name: '',
|
|
@@ -897,11 +947,19 @@ export class FlussoM {
|
|
|
897
947
|
}
|
|
898
948
|
let lunghezzaRiga = utils.verificaLunghezzaRiga(this._starts);
|
|
899
949
|
const outputFile = nomeFile === "" ? (outFolder + path.sep + '190205_000_XXXX_XX_M_AL_20XX_XX_XX.TXT') : outFolder + path.sep + nomeFile;
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
950
|
+
|
|
951
|
+
// Rimuovi file esistente per evitare append duplicato
|
|
952
|
+
if (fs.existsSync(outputFile)) {
|
|
953
|
+
fs.unlinkSync(outputFile);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
console.log(`unisciFileTxt: ${allFiles.length} file da ${inFolder}`);
|
|
957
|
+
|
|
958
|
+
const logger = fs.createWriteStream(outputFile, { flags: 'w' });
|
|
959
|
+
const writeLine = (line) => logger.write(`${line}\n`);
|
|
960
|
+
let totalLines = 0;
|
|
961
|
+
|
|
962
|
+
for (const file of allFiles) {
|
|
905
963
|
const fileStream = fs.createReadStream(file);
|
|
906
964
|
const rl = readline.createInterface({
|
|
907
965
|
input: fileStream,
|
|
@@ -911,7 +969,6 @@ export class FlussoM {
|
|
|
911
969
|
let nLine = 0;
|
|
912
970
|
for await (const line of rl) {
|
|
913
971
|
writeLine(line);
|
|
914
|
-
// Each line in input.txt will be successively available here as `line`.
|
|
915
972
|
nLine++;
|
|
916
973
|
if (line.length !== lunghezzaRiga) {
|
|
917
974
|
console.log("file: " + file);
|
|
@@ -920,10 +977,14 @@ export class FlussoM {
|
|
|
920
977
|
errors.push({file: file, lunghezza: line.length, linea: nLine})
|
|
921
978
|
}
|
|
922
979
|
}
|
|
980
|
+
totalLines += nLine;
|
|
923
981
|
}
|
|
924
982
|
logger.end();
|
|
983
|
+
|
|
984
|
+
const fileSize = fs.existsSync(outputFile) ? fs.statSync(outputFile).size : 0;
|
|
985
|
+
console.log(`unisciFileTxt: ${totalLines} righe totali, ${(fileSize / 1024).toFixed(1)} KB -> ${outputFile}`);
|
|
986
|
+
|
|
925
987
|
if (errors.length === 0) {
|
|
926
|
-
//verifica
|
|
927
988
|
console.log("verifica.. ");
|
|
928
989
|
errors = [...errors, ...await this.#processLineByLine(outputFile, lunghezzaRiga)]
|
|
929
990
|
if (errors.length === 0)
|
|
@@ -933,7 +994,7 @@ export class FlussoM {
|
|
|
933
994
|
console.table(errors);
|
|
934
995
|
}
|
|
935
996
|
}
|
|
936
|
-
return {error: errors.length !== 0, errors: errors}
|
|
997
|
+
return {error: errors.length !== 0, errors: errors, totalFiles: allFiles.length, totalLines, fileSize}
|
|
937
998
|
}
|
|
938
999
|
|
|
939
1000
|
async inviaMailAiDistretti(distretti, meseAnno = "", mailGlobale = "", nomeFileCompleto = "CONSEGNE_GLOBALI") {
|
|
@@ -1440,7 +1501,7 @@ export class FlussoM {
|
|
|
1440
1501
|
|
|
1441
1502
|
|
|
1442
1503
|
async generaFileExcelPerAnno(nomeFile, anno, cosaGenerare = [FlussoM.PER_STRUTTURA_ANNO_MESE, FlussoM.TAB_CONSEGNE_PER_CONTENUTO, FlussoM.TAB_CONSEGNE_PER_NOME_FILE, FlussoM.TAB_DIFFERENZE_CONTENUTO_NOMEFILE]) {
|
|
1443
|
-
const strutture = this.#
|
|
1504
|
+
const strutture = this.#loadStruttureFromLatestFile();
|
|
1444
1505
|
let files = utils.getAllFilesRecursive(this._settings.out_folder, '.mstats');
|
|
1445
1506
|
let data = [];
|
|
1446
1507
|
for (let file of files) {
|
|
@@ -1616,7 +1677,7 @@ export class FlussoM {
|
|
|
1616
1677
|
}
|
|
1617
1678
|
}
|
|
1618
1679
|
|
|
1619
|
-
const strutture = this.#
|
|
1680
|
+
const strutture = this.#loadStruttureFromLatestFile();
|
|
1620
1681
|
|
|
1621
1682
|
const buffer = fs.readFileSync(this.settings.flowlookDBFilePath);
|
|
1622
1683
|
const reader = new MDBReader(buffer);
|
|
@@ -1748,4 +1809,175 @@ export class FlussoM {
|
|
|
1748
1809
|
}
|
|
1749
1810
|
}
|
|
1750
1811
|
|
|
1812
|
+
/**
|
|
1813
|
+
* Esegue lo scraping di una pagina HTML dell'elenco strutture del Progetto Tessera Sanitaria
|
|
1814
|
+
* e genera un file JSON con i dati estratti.
|
|
1815
|
+
* @param {string} htmlContent - Il contenuto HTML della pagina
|
|
1816
|
+
* @param {string} outputDir - La directory di output (default: "data")
|
|
1817
|
+
* @returns {Array<{codice: string, denominazione: string, indirizzo: string, localita: string, piva: string, stato: string}>}
|
|
1818
|
+
*/
|
|
1819
|
+
static scrapingStruttureProgettoTs(htmlContent, outputDir = "data/strutture") {
|
|
1820
|
+
const strutture = [];
|
|
1821
|
+
const isHTML = /<tr[\s>]/i.test(htmlContent) || /<td[\s>]/i.test(htmlContent);
|
|
1822
|
+
|
|
1823
|
+
if (isHTML) {
|
|
1824
|
+
const $ = cheerio.load(htmlContent);
|
|
1825
|
+
$('tr').each((i, row) => {
|
|
1826
|
+
const tds = $(row).find('td');
|
|
1827
|
+
if (tds.length < 5) return;
|
|
1828
|
+
|
|
1829
|
+
const codice = $(tds[0]).text().trim();
|
|
1830
|
+
if (!codice || codice === "Codice") return;
|
|
1831
|
+
|
|
1832
|
+
const denominazione = $(tds[1]).text().trim();
|
|
1833
|
+
const indirizzo = $(tds[2]).text().trim();
|
|
1834
|
+
const localita = $(tds[3]).text().trim();
|
|
1835
|
+
const piva = $(tds[4]).text().trim();
|
|
1836
|
+
|
|
1837
|
+
const rigaTesto = $(row).text();
|
|
1838
|
+
const stato = rigaTesto.includes("Struttura chiusa") ? "chiuso" : "attivo";
|
|
1839
|
+
|
|
1840
|
+
strutture.push({codice, denominazione, indirizzo, localita, piva, stato});
|
|
1841
|
+
});
|
|
1842
|
+
} else {
|
|
1843
|
+
// Parsing testo piano (copia-incolla dal browser)
|
|
1844
|
+
const lines = htmlContent.split('\n');
|
|
1845
|
+
for (const line of lines) {
|
|
1846
|
+
const trimmed = line.trim();
|
|
1847
|
+
if (!trimmed || !/^\d{6}\s/.test(trimmed)) continue;
|
|
1848
|
+
|
|
1849
|
+
const stato = trimmed.includes("Struttura chiusa") ? "chiuso" : "attivo";
|
|
1850
|
+
const cleaned = trimmed.replace(/\s+Visualizza Dettaglio.*$/, '');
|
|
1851
|
+
|
|
1852
|
+
// Prova prima con tab, poi con 4+ spazi
|
|
1853
|
+
let parts = cleaned.split('\t').map(p => p.trim()).filter(p => p);
|
|
1854
|
+
if (parts.length < 5) {
|
|
1855
|
+
parts = cleaned.split(/\s{4,}/).map(p => p.trim()).filter(p => p);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
if (parts.length >= 5) {
|
|
1859
|
+
strutture.push({
|
|
1860
|
+
codice: parts[0],
|
|
1861
|
+
denominazione: parts[1],
|
|
1862
|
+
indirizzo: parts[2],
|
|
1863
|
+
localita: parts[3],
|
|
1864
|
+
piva: parts[4],
|
|
1865
|
+
stato
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// Arricchimento con codice catastale e distretto dal CSV ambiti
|
|
1872
|
+
const csvPath = path.join(path.dirname(outputDir), "ambiti_distretti_messina.csv");
|
|
1873
|
+
if (fs.existsSync(csvPath)) {
|
|
1874
|
+
const csvContent = fs.readFileSync(csvPath, "utf-8");
|
|
1875
|
+
const ambiti = {};
|
|
1876
|
+
csvContent.split('\n').slice(1).forEach(line => {
|
|
1877
|
+
const parts = line.split(',');
|
|
1878
|
+
if (parts.length >= 3) {
|
|
1879
|
+
const ambito = parts[0].trim();
|
|
1880
|
+
if (ambito) {
|
|
1881
|
+
ambiti[ambito] = { codCatastale: parts[1].trim(), distretto: parts[2].trim() };
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
// Carica mapping manuale da file JSON esterno
|
|
1887
|
+
const mappingPath = path.join(path.dirname(outputDir), "mapping_strutture_extra.json");
|
|
1888
|
+
let mappingVarianti = {};
|
|
1889
|
+
let mappingPerCodice = {};
|
|
1890
|
+
if (fs.existsSync(mappingPath)) {
|
|
1891
|
+
const mappingData = JSON.parse(fs.readFileSync(mappingPath, "utf-8"));
|
|
1892
|
+
mappingVarianti = mappingData.per_comune || {};
|
|
1893
|
+
mappingPerCodice = mappingData.per_codice_struttura || {};
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// Normalizza stringa per match fuzzy (rimuove apostrofi, trattini, spazi multipli)
|
|
1897
|
+
const normalizza = (s) => s.toUpperCase().replace(/['\-\u2019\u0060]/g, '').replace(/\s+/g, ' ').trim();
|
|
1898
|
+
|
|
1899
|
+
// Costruisci indice normalizzato degli ambiti
|
|
1900
|
+
const ambitiNorm = {};
|
|
1901
|
+
for (const [key, val] of Object.entries(ambiti)) {
|
|
1902
|
+
ambitiNorm[normalizza(key)] = { ...val, ambitoOriginale: key };
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
let matchati = 0;
|
|
1906
|
+
let nonMatchati = [];
|
|
1907
|
+
|
|
1908
|
+
for (const s of strutture) {
|
|
1909
|
+
// Estrai comune dalla localita (rimuovi provincia es. "(ME)")
|
|
1910
|
+
let comune = s.localita.replace(/\s*\([A-Z]{2}\)\s*$/, '').trim();
|
|
1911
|
+
|
|
1912
|
+
// 0. Match per codice struttura (override manuale)
|
|
1913
|
+
if (mappingPerCodice[s.codice]) {
|
|
1914
|
+
comune = mappingPerCodice[s.codice];
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// 1. Match diretto
|
|
1918
|
+
if (ambiti[comune]) {
|
|
1919
|
+
s.codCatastale = ambiti[comune].codCatastale;
|
|
1920
|
+
s.distretto = ambiti[comune].distretto;
|
|
1921
|
+
s.ambito = comune;
|
|
1922
|
+
matchati++;
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// 2. Match via mapping manuale per comune
|
|
1927
|
+
const mappato = mappingVarianti[comune];
|
|
1928
|
+
if (mappato && ambiti[mappato]) {
|
|
1929
|
+
s.codCatastale = ambiti[mappato].codCatastale;
|
|
1930
|
+
s.distretto = ambiti[mappato].distretto;
|
|
1931
|
+
s.ambito = mappato;
|
|
1932
|
+
matchati++;
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
// 3. Match fuzzy normalizzato (apostrofi, trattini)
|
|
1937
|
+
const comuneNorm = normalizza(comune);
|
|
1938
|
+
if (ambitiNorm[comuneNorm]) {
|
|
1939
|
+
const found = ambitiNorm[comuneNorm];
|
|
1940
|
+
s.codCatastale = found.codCatastale;
|
|
1941
|
+
s.distretto = found.distretto;
|
|
1942
|
+
s.ambito = found.ambitoOriginale;
|
|
1943
|
+
matchati++;
|
|
1944
|
+
continue;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// Non trovato
|
|
1948
|
+
s.codCatastale = null;
|
|
1949
|
+
s.distretto = null;
|
|
1950
|
+
s.ambito = null;
|
|
1951
|
+
nonMatchati.push({ codice: s.codice, denominazione: s.denominazione, localita: s.localita });
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
console.log(`Matching ambiti: ${matchati}/${strutture.length} matchati, ${nonMatchati.length} non matchati`);
|
|
1955
|
+
if (nonMatchati.length > 0) {
|
|
1956
|
+
console.log("Strutture senza match ambito:");
|
|
1957
|
+
nonMatchati.forEach(s => console.log(` ${s.codice} - ${s.denominazione} - ${s.localita}`));
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
const timestamp = moment().format("YYYYMMDD_HHmmss");
|
|
1962
|
+
const nomeFile = path.join(outputDir, `strutture_ts_${timestamp}.json`);
|
|
1963
|
+
|
|
1964
|
+
if (!fs.existsSync(outputDir)) {
|
|
1965
|
+
fs.mkdirSync(outputDir, {recursive: true});
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
fs.writeFileSync(nomeFile, JSON.stringify(strutture, null, 2), "utf-8");
|
|
1969
|
+
console.log(`Salvate ${strutture.length} strutture in ${nomeFile}`);
|
|
1970
|
+
|
|
1971
|
+
// Rimuovi eventuali file LATEST_ precedenti e crea il nuovo
|
|
1972
|
+
const files = fs.readdirSync(outputDir);
|
|
1973
|
+
files.filter(f => f.startsWith("LATEST_")).forEach(f => {
|
|
1974
|
+
fs.unlinkSync(path.join(outputDir, f));
|
|
1975
|
+
});
|
|
1976
|
+
const latestFile = path.join(outputDir, `LATEST_strutture_ts_${timestamp}.json`);
|
|
1977
|
+
fs.copyFileSync(nomeFile, latestFile);
|
|
1978
|
+
console.log(`Creato ${latestFile}`);
|
|
1979
|
+
|
|
1980
|
+
return strutture;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1751
1983
|
}
|