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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "engines": {
4
4
  "node": ">=14.0.0"
5
5
  },
6
- "version": "1.2.39",
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 assistito of assistitiTs[codReg]) {
136
- allAssistitiDistrettuali[distretto].ts.push({...assistito, ...medico});
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
- const riunisciExcelDaTag = async (path, tag, filter = null) => {
857
- let files = getAllFilesRecursive(path, ".xlsx", filter);
858
- let out = {};
859
- out[tag] = [];
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
- out[tag].push(...data);
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
- #loadStruttureFromFlowlookDB() {
407
- const buffer = fs.readFileSync(this._settings.flowlookDBFilePath);
408
- const reader = new MDBReader(buffer);
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 strutture = reader.getTable(this._settings.flowlookDBTableSTS11).getData();
411
- let struttureFiltrate = strutture.filter(p => p["CodiceAzienda"] === this._settings.codiceAzienda && p["CodiceRegione"] === this._settings.codiceRegione && (parseInt(p["Anno"]) >= (new Date().getFullYear()) - 2));
412
- let mancanti = []
413
- let struttureOut = {}
414
- struttureFiltrate.forEach(p => {
415
- if (this._settings.datiStruttureRegione.comuniDistretti.hasOwnProperty(p["CodiceComune"])) {
416
- struttureOut[p['CodiceStruttura']] = {
417
- codiceRegione: p['CodiceRegione'],
418
- codiceAzienda: p['CodiceAzienda'],
419
- denominazione: p['DenominazioneStruttura'],
420
- codiceComune: p['CodiceComune'],
421
- idDistretto: this._settings.datiStruttureRegione.comuniDistretti[p["CodiceComune"]],
422
- dataUltimoAggiornamento: moment(p['DataAggiornamento'], 'DD/MM/YYYY')
423
- };
424
- } else
425
- mancanti.push(p);
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.#loadStruttureFromFlowlookDB();
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.toString() ?? (ricetteInFile.datiDaFile?.idDistretto ?? "X");
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 sul FLOWLOOK")
495
- warn = "STRUTTURA " + verificaDateStruttura.codiceStruttura + " non presente sul FLOWLOOK"
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.#loadStruttureFromFlowlookDB();
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
- var logger = fs.createWriteStream(outputFile, {
901
- flags: 'a' // 'a' means appending (old data will be preserved)
902
- })
903
- var writeLine = (line) => logger.write(`${line}\n`);
904
- for (var file of allFiles) {
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.#loadStruttureFromFlowlookDB();
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.#loadStruttureFromFlowlookDB();
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
  }