diff --git a/app.js b/app.js
index 5928fc0..89dc273 100644
--- a/app.js
+++ b/app.js
@@ -21419,6 +21419,8 @@ function ricordaRecenteCatalogo(k, nome, over){
kw:"scaletta setlist set list brani canzoni pezzi ordine durata durate pausa bis intro cambio palco"});
entries.push({nome:"Nuova variante", dim:"copia della scena attiva, da cambiare", noQuick:true, iconHtml:VAR_ICON, action:function(){ if(typeof createVariant==="function") createVariant(); },
kw:"variante varianti scena scene versione alternativa seconda formazione"});
+ entries.push({nome:"Importa organico", dim:"da una riga o da Excel, con anteprima", noQuick:true, iconHtml:LISTA_ICON, action:function(){ orgImportApri(); },
+ kw:"importa importare incolla organico orchestra formazione foglio convocazione csv excel archi formula sezioni"}); /* G1 fase 2 (13/11): fuori dal menu File, che resta essenziale */
entries.push({nome:"Frequenze radio (RF)", dim:"si scrivono su ogni radiomic e in-ear", noQuick:true, iconHtml:RF_ICON, action:openRfFreq,
kw:"frequenza frequenze radiofrequenza radiofrequenze radio radiomicrofoni banda mhz lista rf"});
["metro"].forEach(function(k){
@@ -24562,6 +24564,7 @@ var ORCH_DEF = [
];
var pendingOut = null;
+var pendingDopo = null; /* callback di chi chiama placeOut (import organico, 13/11): gira a posti creati, anche dopo «Continua» dell'avviso */
var pendingReviewModal = "orchModal";
var ORCH_PRESETS = [
{id:"quartetto", nome:"Quartetto archi — 2.0.1.1", hint:"Quartetto classico: 2 violini, viola e violoncello. Ideale per piccoli palchi o cerimonie.", counts:{v1:2,v2:0,vle:1,vc:1}},
@@ -24818,6 +24821,7 @@ function applyOrchestraPreset(presetId){
/* Piazza una lista di postazioni (out, coord. relative al riferimento) sul palco.
Riusato da organico orchestra e da tutte le formazioni tipo. */
function placeOut(out, fitStage, clearFirst, force){
+ var _dopo=pendingDopo; pendingDopo=null;
var m=measureOrchestraOut(out), fp=frontPad(m);
if(fitStage){
var nw=Math.max(800, Math.ceil((2*m.maxX+160)/50)*50);
@@ -24829,13 +24833,13 @@ function placeOut(out, fitStage, clearFirst, force){
var C={x:state.stage.w/2, y:state.stage.d-fp};
var assessment=assessOut(out, state.stage.w, state.stage.d, fp);
if(!force && (assessment.tooSmall || assessment.relevantOverlaps>0)){
- pendingOut={out:out, fitStage:fitStage, clearFirst:clearFirst};
+ pendingOut={out:out, fitStage:fitStage, clearFirst:clearFirst, dopo:_dopo};
var msg=[];
if(assessment.tooSmall) msg.push("Il palco è troppo piccolo: alcune postazioni escono dall'area utile.");
if(assessment.relevantOverlaps>0) msg.push("La disposizione automatica crea "+assessment.overlaps+" sovrapposizion"+(assessment.overlaps===1?"e":"i")+" tra postazioni.");
msg.push("Puoi continuare e poi sistemare a mano, oppure rivedere l'organico.");
document.getElementById("orchWarnText").textContent=msg.join(" ");
- document.getElementById(pendingReviewModal||"orchModal").hidden=true;
+ var _rev=document.getElementById(pendingReviewModal||"orchModal"); if(_rev) _rev.hidden=true; /* il modale da cui si arriva può non esistere (import chiamato senza aprire la finestra) */
document.getElementById("orchWarnModal").hidden=false;
return false;
}
@@ -24854,6 +24858,7 @@ function placeOut(out, fitStage, clearFirst, force){
if(!clearFirst && state.items.length) avoidGroupOverlap(news); /* non sovrapporsi a ciò che è già sul palco */
news.forEach(function(it){ state.items.push(it); });
clearSelection(); save();
+ if(_dopo) _dopo();
return true;
}
/* sposta il nuovo gruppo DIETRO gli elementi esistenti che gli stanno davanti; allarga il palco sul fondo se serve */
@@ -24885,6 +24890,190 @@ function genOrchestra(counts, fitStage, clearFirst, force){
return placeOut(buildOrchestraOut(counts), fitStage, clearFirst, force);
}
+/* ═══ IMPORT DELL'ORGANICO (13/11/2026, G1 fase 2) ═══
+ L'organico dei fogli di convocazione si incolla, non si ricostruisce a mano: una riga «cr4-tr2-trb3-tu1; fl2-o2-cl2-fg2», la formula degli archi
+ «8-6-4-4-2» (violini I · II · viole · celli · bassi; con meno numeri si fermano prima), oppure righe «sezione, strumento, n.» (CSV, o copiate da Excel).
+ Il testo diventa un conto per sezione (le stesse di ORCH_DEF), si guarda in anteprima e SOLO dopo si creano i posti con lo stesso generatore
+ dell'organico orchestra; «Annulla» (toast o ⌘Z) rimette il palco com'era. Con le colonne «Completo · Medio · Ridotto» si sceglie quale disegnare.
+ DATI PERSONALI: non si leggono. Di una riga contano solo sezione, strumento e quantità; celle con @, IBAN, telefoni, «contratto», «cachet», «CV»,
+ «sostituto», «nominativo» si saltano e si contano; il contenuto di righe non riconosciute non si mostra. Niente nasce nel documento: nessun campo nuovo. */
+var ORG_IMP_ALIAS=(function(){
+ var A={}, add=function(id,l){ l.split("|").forEach(function(k){ A[k]=id; }); };
+ add("v1","v1|vl1|vln1|vn1|vni|violini 1|violino 1|violini primi|primi violini|violino primo");
+ add("v2","v2|vl2|vln2|vn2|vnii|violini 2|violino 2|violini secondi|secondi violini");
+ add("vle","vle|vla|viola|viole|vle"); add("vc","vc|vcl|vcello|celli|cello|violoncello|violoncelli");
+ add("cb","cb|cbs|contrabbasso|contrabbassi");
+ add("fl","fl|flauto|flauti"); add("ob","o|ob|oboe|oboi"); add("cl","cl|clar|clarinetto|clarinetti"); add("fg","fg|fag|fagotto|fagotti");
+ add("cor","cr|cor|corno|corni|hn"); add("tr","tr|tromba|trombe|tp"); add("tbn","trb|tbn|trbn|trombone|tromboni"); add("tba","tu|tba|tuba|tube");
+ add("arp","arp|arpa|arpe|hp"); add("pf","pf|pianoforte|pianoforti|piano"); add("cel","cel|celesta");
+ add("tim","timp|tim|timpani|timpano"); add("gc","gc|gran cassa|grancassa"); add("pia","pia|piatti"); add("rul","rul|rullante");
+ add("cmp","cmp|campane|campane tubolari"); add("tam","tam|tamtam|tam tam");
+ return A;
+})();
+var ORG_IMP_MAX=40;
+var ORG_IMP_GRUPPO={v1:"v1",v2:"v2",vle:"vle",vc:"vc",cb:"cb",fl:"fl",ob:"ob",cl:"cl",fg:"fg",cor:"cor",tr:"tr",tbn:"tbn",tba:"tba",arp:"arp",pf:"pf",cel:"cel",tim:"tim"}; /* sezioni che il foglio Organico sa riconoscere nel disegno */
+function orgImpNorm(s){
+ s=String(s==null?"":s).toLowerCase();
+ try{ s=s.normalize("NFD").replace(/[̀-ͯ]/g,""); }catch(e){}
+ s=s.replace(/[.'’_]/g,"").replace(/[\-–]/g," ").replace(/\s+/g," ").trim();
+ return s.replace(/ iii$/," 3").replace(/ ii$/," 2").replace(/ i$/," 1");
+}
+function orgImpRisolvi(nome){
+ var s=orgImpNorm(String(nome||"").split("/")[0].replace(/\([^)]*\)/g,""));
+ return Object.prototype.hasOwnProperty.call(ORG_IMP_ALIAS,s) ? ORG_IMP_ALIAS[s] : null;
+}
+var ORG_IMP_PRIV=/@|\biban\b|\b[a-z]{2}\d{2}[a-z0-9]{11,}\b|(?:\+|\b00)\d[\d ]{8,}\d|\b3\d{2}[ .\-]?\d{6,7}\b|\b0\d{1,3}[ \/.\-]?\d{5,8}\b|\b(?:cv|curriculum|contratt\w*|compens\w*|cachet|fattur\w*|sostitut\w*|nominativ\w*|codice fiscale)\b/i;
+function orgImportParse(testo){
+ var R={varianti:[], ignorati:[], righeNo:[], privati:0, tagliati:[]}, vm={};
+ var NOMI={completo:"Completo", medio:"Medio", ridotto:"Ridotto", "":"Organico"};
+ function va(k){ if(!vm[k]){ vm[k]={id:k, nome:NOMI[k], counts:{}, tot:0}; R.varianti.push(vm[k]); } return vm[k]; }
+ function agg(k,id,n){ var v=va(k); v.counts[id]=(v.counts[id]||0)+n; }
+ function isInt(s){ return /^\d{1,2}$/.test(String(s).trim()); }
+ var cur="", hdr=null;
+ String(testo||"").split(/\r?\n/).forEach(function(riga,ri){
+ var line=riga.trim(); if(!line) return;
+ var m=/^(completo|medio|ridotto)\s*[:=]\s*(.*)$/i.exec(line);
+ if(m){ cur=m[1].toLowerCase(); line=m[2].trim(); if(!line) return; }
+ var cells=line.split(/\t|;|,|\|/).map(function(c){ return c.trim(); });
+ if(!/\d/.test(line) && cells.length>=2 && /\b(sezione|strumento|strumenti|gruppo)\b/i.test(line)){ /* intestazione: quale colonna è «n.», quali le varianti */
+ hdr={nCol:-1, vcols:[]};
+ cells.forEach(function(c,i){ var n=orgImpNorm(c);
+ if(/^(n|n°|numero|quantita|qta|posti)$/.test(n)) hdr.nCol=i;
+ else if(n==="completo"||n==="medio"||n==="ridotto") hdr.vcols.push({idx:i, k:n}); });
+ return;
+ }
+ cells=cells.map(function(c){ if(c && ORG_IMP_PRIV.test(c)){ R.privati++; return ""; } return c; }); /* cella per cella: la riga resta, il dato personale no */
+ if(cells.length>=2 && cells.some(function(c){ return /^\d+$/.test(c); })){ /* RIGA DI TABELLA: sezione, strumento, n. (un numero solo di cifre: una quantità sensata ne ha 1-2) */
+ var id=null;
+ for(var i=cells.length-1;i>=0 && !id;i--){ if(cells[i] && !/^\d+$/.test(cells[i])) id=orgImpRisolvi(cells[i]); }
+ if(!id){ R.righeNo.push(ri+1); return; }
+ var letto=false;
+ if(hdr && hdr.vcols.length) hdr.vcols.forEach(function(vc){ if(isInt(cells[vc.idx]||"")){ agg(vc.k,id,+cells[vc.idx]); letto=true; } });
+ if(letto) return;
+ var ni=(hdr && hdr.nCol>=0 && isInt(cells[hdr.nCol]||"")) ? hdr.nCol : cells.findIndex(isInt);
+ if(ni<0){ R.righeNo.push(ri+1); return; }
+ agg(cur,id,+cells[ni]);
+ return;
+ }
+ /* RIGA IN LINEA: formule degli archi + «sigla+numero» */
+ var s=cells.join(" ; ").replace(/(^|[^\w])(\d{1,2}(?:\s*[-–.]\s*\d{1,2}){3,4})(?![\w])/g, function(_t,pre,f){
+ f.split(/\s*[-–.]\s*/).forEach(function(n,i){ agg(cur,["v1","v2","vle","vc","cb"][i],+n); });
+ return pre+" ; ";
+ });
+ s.split(/[;,|]+/).forEach(function(chunk){
+ chunk=chunk.trim(); if(!chunk) return;
+ var words=[], pn=null;
+ function emetti(nome,n){ var id=orgImpRisolvi(nome); if(id) agg(cur,id,n); else R.ignorati.push(String(nome).slice(0,16)); }
+ function chiudi(){ if(words.length){ emetti(words.join(" "), pn!==null ? pn : 1); words=[]; pn=null; } }
+ chunk.split(/[\s\-–]+/).filter(Boolean).forEach(function(t){
+ var ld=/^([a-zà-ÿ]+?)\.?(\d{1,2})$/i.exec(t);
+ if(isInt(t)){
+ if(words.length){ if(pn!==null){ emetti(words.join(" "),pn); words=[]; pn=+t; } else { emetti(words.join(" "),+t); words=[]; } }
+ else if(pn!==null){ R.ignorati.push(String(pn)); pn=+t; } else pn=+t;
+ } else if(ld){ chiudi(); emetti(ld[1], +ld[2]); }
+ else words.push(t);
+ });
+ if(words.length) chiudi(); else if(pn!==null) R.ignorati.push(String(pn));
+ });
+ });
+ R.varianti.forEach(function(v){
+ Object.keys(v.counts).forEach(function(k){ if(v.counts[k]>ORG_IMP_MAX){ R.tagliati.push(k); v.counts[k]=ORG_IMP_MAX; } if(!v.counts[k]) delete v.counts[k]; });
+ v.tot=orchTotal(v.counts);
+ });
+ R.varianti=R.varianti.filter(function(v){ return v.tot>0; });
+ R.ignorati=R.ignorati.filter(function(x,i,a){ return a.indexOf(x)===i; });
+ R.tagliati=R.tagliati.filter(function(x,i,a){ return a.indexOf(x)===i; });
+ return R;
+}
+/* quanti posti di ogni sezione ci sono ORA nel disegno (solo le sezioni che il foglio Organico riconosce) */
+function orgImportDisegnato(){
+ var D=organicoDati(), r={};
+ Object.keys(D.righe).forEach(function(k){ var x=D.righe[k]; r[x.id]=(r[x.id]||0)+x.n; });
+ var out={}; Object.keys(ORG_IMP_GRUPPO).forEach(function(id){ out[id]=r[ORG_IMP_GRUPPO[id]]||0; });
+ return out;
+}
+/* «organico dichiarato ≠ disegnato»: sezioni dove il conto importato e quello del disegno non coincidono */
+function orgImportDifferenze(counts){
+ var d=orgImportDisegnato(), diff=[];
+ ORCH_DEF.forEach(function(s){ if(!ORG_IMP_GRUPPO[s.id]) return; var a=counts[s.id]||0, b=d[s.id]||0; if(a!==b) diff.push(s.nome+": "+a+" dichiarati, "+b+" disegnati"); });
+ return diff;
+}
+function orgImportApplica(counts, opz){
+ opz=opz||{};
+ if(!orchTotal(counts)) return false;
+ pendingReviewModal="orgImpModal"; /* se il palco è troppo piccolo o la disposizione si sovrappone, «Rivedi» torna a questa finestra */
+ pendingDopo=function(){
+ try{ fit(); }catch(e){}
+ var diff=orgImportDifferenze(counts);
+ showToastAnnulla("Creati "+orchTotal(counts)+" posti dall'organico importato"+(diff.length ? " · dichiarato ≠ disegnato: "+diff.slice(0,3).join("; ")+(diff.length>3?"…":"") : ""));
+ };
+ return placeOut(buildOrchestraOut(counts), !!opz.fit, !!opz.clear, false);
+}
+function showToastAnnulla(msg){
+ try{ if(window.__toast) window.__toast(msg, false, {label:"Annulla", run:function(){ undo(); }}); }catch(e){}
+}
+function orgImportApri(){
+ var ov=document.getElementById("orgImpModal");
+ if(!ov){
+ ov=document.createElement("div"); ov.className="modal"; ov.id="orgImpModal"; ov.hidden=true;
+ ov.setAttribute("role","dialog"); ov.setAttribute("aria-modal","true"); ov.setAttribute("aria-labelledby","orgImpTitolo");
+ ov.innerHTML='
Importa l\'organico
'
+ + '
Incolla una riga come cr4-tr2-trb3-tu1; fl2-o2-cl2-fg2, la formula degli archi 8-6-4-4-2 (violini I · II · viole · celli · bassi) oppure righe «sezione, strumento, n.» copiate da Excel. Con le colonne Completo · Medio · Ridotto scegli quale disegnare. Contano solo sezione, strumento e quantità: nomi di persone e dati personali non vengono letti.
'
+ + ''
+ + ''
+ + ''
+ + ''
+ + ''
+ + '
';
+ document.body.appendChild(ov);
+ var ta=ov.querySelector("#orgImpTesto"), sel=ov.querySelector("#orgImpVarSel"), go=ov.querySelector("#orgImpGo");
+ var P=null;
+ function scelta(){ if(!P) return null; for(var i=0;i'+esc(v.nome)+' ('+v.tot+')'; }).join("");
+ if(prima && P.varianti.some(function(v){ return v.id===prima; })) sel.value=prima;
+ else { var c=P.varianti.filter(function(v){ return v.id==="completo"; })[0]; if(c) sel.value="completo"; }
+ ov.querySelector("#orgImpVar").hidden=P.varianti.length<2;
+ var v=scelta(), now=orgImportDisegnato(), clear=ov.querySelector("#orgImpClear").checked;
+ if(v){
+ var righe=ORCH_DEF.filter(function(s){ return v.counts[s.id]; }), gia=0;
+ h+='
Sul palco ci sono già dei posti di queste sezioni: restano, e il disegno avrà più posti di quelli dichiarati. Per sostituirli spunta «Svuota il palco».
';
+ } else h+='
'+(ta.value.trim() ? "Non ho trovato nessuna sezione da importare." : "Incolla il testo qui sopra: vedrai subito cosa verrebbe creato.")+'
';
+ var note=[];
+ if(P.ignorati.length) note.push("Non riconosciuti: "+P.ignorati.slice(0,8).map(esc).join(", ")+(P.ignorati.length>8?"…":""));
+ if(P.righeNo.length) note.push("Righe non riconosciute (contenuto non mostrato): "+P.righeNo.slice(0,10).join(", "));
+ if(P.privati) note.push(P.privati+" dat"+(P.privati===1?"o personale saltato":"i personali saltati")+", non letti");
+ if(P.tagliati.length) note.push("Oltre "+ORG_IMP_MAX+" per sezione: ridotto a "+ORG_IMP_MAX);
+ if(note.length) h+='
kw:"scaletta setlist set list brani canzoni pezzi ordine durata durate pausa bis intro cambio palco"});
entries.push({nome:"Nuova variante", dim:"copia della scena attiva, da cambiare", noQuick:true, iconHtml:VAR_ICON, action:function(){ if(typeof createVariant==="function") createVariant(); },
kw:"variante varianti scena scene versione alternativa seconda formazione"});
+ entries.push({nome:"Importa organico", dim:"da una riga o da Excel, con anteprima", noQuick:true, iconHtml:LISTA_ICON, action:function(){ orgImportApri(); },
+ kw:"importa importare incolla organico orchestra formazione foglio convocazione csv excel archi formula sezioni"}); /* G1 fase 2 (13/11): fuori dal menu File, che resta essenziale */
entries.push({nome:"Frequenze radio (RF)", dim:"si scrivono su ogni radiomic e in-ear", noQuick:true, iconHtml:RF_ICON, action:openRfFreq,
kw:"frequenza frequenze radiofrequenza radiofrequenze radio radiomicrofoni banda mhz lista rf"});
["metro"].forEach(function(k){
@@ -26817,6 +26819,7 @@
Esporta PDF in scala
];
var pendingOut = null;
+var pendingDopo = null; /* callback di chi chiama placeOut (import organico, 13/11): gira a posti creati, anche dopo «Continua» dell'avviso */
var pendingReviewModal = "orchModal";
var ORCH_PRESETS = [
{id:"quartetto", nome:"Quartetto archi — 2.0.1.1", hint:"Quartetto classico: 2 violini, viola e violoncello. Ideale per piccoli palchi o cerimonie.", counts:{v1:2,v2:0,vle:1,vc:1}},
@@ -27073,6 +27076,7 @@
Esporta PDF in scala
/* Piazza una lista di postazioni (out, coord. relative al riferimento) sul palco.
Riusato da organico orchestra e da tutte le formazioni tipo. */
function placeOut(out, fitStage, clearFirst, force){
+ var _dopo=pendingDopo; pendingDopo=null;
var m=measureOrchestraOut(out), fp=frontPad(m);
if(fitStage){
var nw=Math.max(800, Math.ceil((2*m.maxX+160)/50)*50);
@@ -27084,13 +27088,13 @@
Esporta PDF in scala
var C={x:state.stage.w/2, y:state.stage.d-fp};
var assessment=assessOut(out, state.stage.w, state.stage.d, fp);
if(!force && (assessment.tooSmall || assessment.relevantOverlaps>0)){
- pendingOut={out:out, fitStage:fitStage, clearFirst:clearFirst};
+ pendingOut={out:out, fitStage:fitStage, clearFirst:clearFirst, dopo:_dopo};
var msg=[];
if(assessment.tooSmall) msg.push("Il palco è troppo piccolo: alcune postazioni escono dall'area utile.");
if(assessment.relevantOverlaps>0) msg.push("La disposizione automatica crea "+assessment.overlaps+" sovrapposizion"+(assessment.overlaps===1?"e":"i")+" tra postazioni.");
msg.push("Puoi continuare e poi sistemare a mano, oppure rivedere l'organico.");
document.getElementById("orchWarnText").textContent=msg.join(" ");
- document.getElementById(pendingReviewModal||"orchModal").hidden=true;
+ var _rev=document.getElementById(pendingReviewModal||"orchModal"); if(_rev) _rev.hidden=true; /* il modale da cui si arriva può non esistere (import chiamato senza aprire la finestra) */
document.getElementById("orchWarnModal").hidden=false;
return false;
}
@@ -27109,6 +27113,7 @@
Esporta PDF in scala
if(!clearFirst && state.items.length) avoidGroupOverlap(news); /* non sovrapporsi a ciò che è già sul palco */
news.forEach(function(it){ state.items.push(it); });
clearSelection(); save();
+ if(_dopo) _dopo();
return true;
}
/* sposta il nuovo gruppo DIETRO gli elementi esistenti che gli stanno davanti; allarga il palco sul fondo se serve */
@@ -27140,6 +27145,190 @@
Esporta PDF in scala
return placeOut(buildOrchestraOut(counts), fitStage, clearFirst, force);
}
+/* ═══ IMPORT DELL'ORGANICO (13/11/2026, G1 fase 2) ═══
+ L'organico dei fogli di convocazione si incolla, non si ricostruisce a mano: una riga «cr4-tr2-trb3-tu1; fl2-o2-cl2-fg2», la formula degli archi
+ «8-6-4-4-2» (violini I · II · viole · celli · bassi; con meno numeri si fermano prima), oppure righe «sezione, strumento, n.» (CSV, o copiate da Excel).
+ Il testo diventa un conto per sezione (le stesse di ORCH_DEF), si guarda in anteprima e SOLO dopo si creano i posti con lo stesso generatore
+ dell'organico orchestra; «Annulla» (toast o ⌘Z) rimette il palco com'era. Con le colonne «Completo · Medio · Ridotto» si sceglie quale disegnare.
+ DATI PERSONALI: non si leggono. Di una riga contano solo sezione, strumento e quantità; celle con @, IBAN, telefoni, «contratto», «cachet», «CV»,
+ «sostituto», «nominativo» si saltano e si contano; il contenuto di righe non riconosciute non si mostra. Niente nasce nel documento: nessun campo nuovo. */
+var ORG_IMP_ALIAS=(function(){
+ var A={}, add=function(id,l){ l.split("|").forEach(function(k){ A[k]=id; }); };
+ add("v1","v1|vl1|vln1|vn1|vni|violini 1|violino 1|violini primi|primi violini|violino primo");
+ add("v2","v2|vl2|vln2|vn2|vnii|violini 2|violino 2|violini secondi|secondi violini");
+ add("vle","vle|vla|viola|viole|vle"); add("vc","vc|vcl|vcello|celli|cello|violoncello|violoncelli");
+ add("cb","cb|cbs|contrabbasso|contrabbassi");
+ add("fl","fl|flauto|flauti"); add("ob","o|ob|oboe|oboi"); add("cl","cl|clar|clarinetto|clarinetti"); add("fg","fg|fag|fagotto|fagotti");
+ add("cor","cr|cor|corno|corni|hn"); add("tr","tr|tromba|trombe|tp"); add("tbn","trb|tbn|trbn|trombone|tromboni"); add("tba","tu|tba|tuba|tube");
+ add("arp","arp|arpa|arpe|hp"); add("pf","pf|pianoforte|pianoforti|piano"); add("cel","cel|celesta");
+ add("tim","timp|tim|timpani|timpano"); add("gc","gc|gran cassa|grancassa"); add("pia","pia|piatti"); add("rul","rul|rullante");
+ add("cmp","cmp|campane|campane tubolari"); add("tam","tam|tamtam|tam tam");
+ return A;
+})();
+var ORG_IMP_MAX=40;
+var ORG_IMP_GRUPPO={v1:"v1",v2:"v2",vle:"vle",vc:"vc",cb:"cb",fl:"fl",ob:"ob",cl:"cl",fg:"fg",cor:"cor",tr:"tr",tbn:"tbn",tba:"tba",arp:"arp",pf:"pf",cel:"cel",tim:"tim"}; /* sezioni che il foglio Organico sa riconoscere nel disegno */
+function orgImpNorm(s){
+ s=String(s==null?"":s).toLowerCase();
+ try{ s=s.normalize("NFD").replace(/[̀-ͯ]/g,""); }catch(e){}
+ s=s.replace(/[.'’_]/g,"").replace(/[\-–]/g," ").replace(/\s+/g," ").trim();
+ return s.replace(/ iii$/," 3").replace(/ ii$/," 2").replace(/ i$/," 1");
+}
+function orgImpRisolvi(nome){
+ var s=orgImpNorm(String(nome||"").split("/")[0].replace(/\([^)]*\)/g,""));
+ return Object.prototype.hasOwnProperty.call(ORG_IMP_ALIAS,s) ? ORG_IMP_ALIAS[s] : null;
+}
+var ORG_IMP_PRIV=/@|\biban\b|\b[a-z]{2}\d{2}[a-z0-9]{11,}\b|(?:\+|\b00)\d[\d ]{8,}\d|\b3\d{2}[ .\-]?\d{6,7}\b|\b0\d{1,3}[ \/.\-]?\d{5,8}\b|\b(?:cv|curriculum|contratt\w*|compens\w*|cachet|fattur\w*|sostitut\w*|nominativ\w*|codice fiscale)\b/i;
+function orgImportParse(testo){
+ var R={varianti:[], ignorati:[], righeNo:[], privati:0, tagliati:[]}, vm={};
+ var NOMI={completo:"Completo", medio:"Medio", ridotto:"Ridotto", "":"Organico"};
+ function va(k){ if(!vm[k]){ vm[k]={id:k, nome:NOMI[k], counts:{}, tot:0}; R.varianti.push(vm[k]); } return vm[k]; }
+ function agg(k,id,n){ var v=va(k); v.counts[id]=(v.counts[id]||0)+n; }
+ function isInt(s){ return /^\d{1,2}$/.test(String(s).trim()); }
+ var cur="", hdr=null;
+ String(testo||"").split(/\r?\n/).forEach(function(riga,ri){
+ var line=riga.trim(); if(!line) return;
+ var m=/^(completo|medio|ridotto)\s*[:=]\s*(.*)$/i.exec(line);
+ if(m){ cur=m[1].toLowerCase(); line=m[2].trim(); if(!line) return; }
+ var cells=line.split(/\t|;|,|\|/).map(function(c){ return c.trim(); });
+ if(!/\d/.test(line) && cells.length>=2 && /\b(sezione|strumento|strumenti|gruppo)\b/i.test(line)){ /* intestazione: quale colonna è «n.», quali le varianti */
+ hdr={nCol:-1, vcols:[]};
+ cells.forEach(function(c,i){ var n=orgImpNorm(c);
+ if(/^(n|n°|numero|quantita|qta|posti)$/.test(n)) hdr.nCol=i;
+ else if(n==="completo"||n==="medio"||n==="ridotto") hdr.vcols.push({idx:i, k:n}); });
+ return;
+ }
+ cells=cells.map(function(c){ if(c && ORG_IMP_PRIV.test(c)){ R.privati++; return ""; } return c; }); /* cella per cella: la riga resta, il dato personale no */
+ if(cells.length>=2 && cells.some(function(c){ return /^\d+$/.test(c); })){ /* RIGA DI TABELLA: sezione, strumento, n. (un numero solo di cifre: una quantità sensata ne ha 1-2) */
+ var id=null;
+ for(var i=cells.length-1;i>=0 && !id;i--){ if(cells[i] && !/^\d+$/.test(cells[i])) id=orgImpRisolvi(cells[i]); }
+ if(!id){ R.righeNo.push(ri+1); return; }
+ var letto=false;
+ if(hdr && hdr.vcols.length) hdr.vcols.forEach(function(vc){ if(isInt(cells[vc.idx]||"")){ agg(vc.k,id,+cells[vc.idx]); letto=true; } });
+ if(letto) return;
+ var ni=(hdr && hdr.nCol>=0 && isInt(cells[hdr.nCol]||"")) ? hdr.nCol : cells.findIndex(isInt);
+ if(ni<0){ R.righeNo.push(ri+1); return; }
+ agg(cur,id,+cells[ni]);
+ return;
+ }
+ /* RIGA IN LINEA: formule degli archi + «sigla+numero» */
+ var s=cells.join(" ; ").replace(/(^|[^\w])(\d{1,2}(?:\s*[-–.]\s*\d{1,2}){3,4})(?![\w])/g, function(_t,pre,f){
+ f.split(/\s*[-–.]\s*/).forEach(function(n,i){ agg(cur,["v1","v2","vle","vc","cb"][i],+n); });
+ return pre+" ; ";
+ });
+ s.split(/[;,|]+/).forEach(function(chunk){
+ chunk=chunk.trim(); if(!chunk) return;
+ var words=[], pn=null;
+ function emetti(nome,n){ var id=orgImpRisolvi(nome); if(id) agg(cur,id,n); else R.ignorati.push(String(nome).slice(0,16)); }
+ function chiudi(){ if(words.length){ emetti(words.join(" "), pn!==null ? pn : 1); words=[]; pn=null; } }
+ chunk.split(/[\s\-–]+/).filter(Boolean).forEach(function(t){
+ var ld=/^([a-zà-ÿ]+?)\.?(\d{1,2})$/i.exec(t);
+ if(isInt(t)){
+ if(words.length){ if(pn!==null){ emetti(words.join(" "),pn); words=[]; pn=+t; } else { emetti(words.join(" "),+t); words=[]; } }
+ else if(pn!==null){ R.ignorati.push(String(pn)); pn=+t; } else pn=+t;
+ } else if(ld){ chiudi(); emetti(ld[1], +ld[2]); }
+ else words.push(t);
+ });
+ if(words.length) chiudi(); else if(pn!==null) R.ignorati.push(String(pn));
+ });
+ });
+ R.varianti.forEach(function(v){
+ Object.keys(v.counts).forEach(function(k){ if(v.counts[k]>ORG_IMP_MAX){ R.tagliati.push(k); v.counts[k]=ORG_IMP_MAX; } if(!v.counts[k]) delete v.counts[k]; });
+ v.tot=orchTotal(v.counts);
+ });
+ R.varianti=R.varianti.filter(function(v){ return v.tot>0; });
+ R.ignorati=R.ignorati.filter(function(x,i,a){ return a.indexOf(x)===i; });
+ R.tagliati=R.tagliati.filter(function(x,i,a){ return a.indexOf(x)===i; });
+ return R;
+}
+/* quanti posti di ogni sezione ci sono ORA nel disegno (solo le sezioni che il foglio Organico riconosce) */
+function orgImportDisegnato(){
+ var D=organicoDati(), r={};
+ Object.keys(D.righe).forEach(function(k){ var x=D.righe[k]; r[x.id]=(r[x.id]||0)+x.n; });
+ var out={}; Object.keys(ORG_IMP_GRUPPO).forEach(function(id){ out[id]=r[ORG_IMP_GRUPPO[id]]||0; });
+ return out;
+}
+/* «organico dichiarato ≠ disegnato»: sezioni dove il conto importato e quello del disegno non coincidono */
+function orgImportDifferenze(counts){
+ var d=orgImportDisegnato(), diff=[];
+ ORCH_DEF.forEach(function(s){ if(!ORG_IMP_GRUPPO[s.id]) return; var a=counts[s.id]||0, b=d[s.id]||0; if(a!==b) diff.push(s.nome+": "+a+" dichiarati, "+b+" disegnati"); });
+ return diff;
+}
+function orgImportApplica(counts, opz){
+ opz=opz||{};
+ if(!orchTotal(counts)) return false;
+ pendingReviewModal="orgImpModal"; /* se il palco è troppo piccolo o la disposizione si sovrappone, «Rivedi» torna a questa finestra */
+ pendingDopo=function(){
+ try{ fit(); }catch(e){}
+ var diff=orgImportDifferenze(counts);
+ showToastAnnulla("Creati "+orchTotal(counts)+" posti dall'organico importato"+(diff.length ? " · dichiarato ≠ disegnato: "+diff.slice(0,3).join("; ")+(diff.length>3?"…":"") : ""));
+ };
+ return placeOut(buildOrchestraOut(counts), !!opz.fit, !!opz.clear, false);
+}
+function showToastAnnulla(msg){
+ try{ if(window.__toast) window.__toast(msg, false, {label:"Annulla", run:function(){ undo(); }}); }catch(e){}
+}
+function orgImportApri(){
+ var ov=document.getElementById("orgImpModal");
+ if(!ov){
+ ov=document.createElement("div"); ov.className="modal"; ov.id="orgImpModal"; ov.hidden=true;
+ ov.setAttribute("role","dialog"); ov.setAttribute("aria-modal","true"); ov.setAttribute("aria-labelledby","orgImpTitolo");
+ ov.innerHTML='
Importa l\'organico
'
+ + '
Incolla una riga come cr4-tr2-trb3-tu1; fl2-o2-cl2-fg2, la formula degli archi 8-6-4-4-2 (violini I · II · viole · celli · bassi) oppure righe «sezione, strumento, n.» copiate da Excel. Con le colonne Completo · Medio · Ridotto scegli quale disegnare. Contano solo sezione, strumento e quantità: nomi di persone e dati personali non vengono letti.
'
+ + ''
+ + ''
+ + ''
+ + ''
+ + ''
+ + '
';
+ document.body.appendChild(ov);
+ var ta=ov.querySelector("#orgImpTesto"), sel=ov.querySelector("#orgImpVarSel"), go=ov.querySelector("#orgImpGo");
+ var P=null;
+ function scelta(){ if(!P) return null; for(var i=0;i'+esc(v.nome)+' ('+v.tot+')'; }).join("");
+ if(prima && P.varianti.some(function(v){ return v.id===prima; })) sel.value=prima;
+ else { var c=P.varianti.filter(function(v){ return v.id==="completo"; })[0]; if(c) sel.value="completo"; }
+ ov.querySelector("#orgImpVar").hidden=P.varianti.length<2;
+ var v=scelta(), now=orgImportDisegnato(), clear=ov.querySelector("#orgImpClear").checked;
+ if(v){
+ var righe=ORCH_DEF.filter(function(s){ return v.counts[s.id]; }), gia=0;
+ h+='
Sul palco ci sono già dei posti di queste sezioni: restano, e il disegno avrà più posti di quelli dichiarati. Per sostituirli spunta «Svuota il palco».
';
+ } else h+='
'+(ta.value.trim() ? "Non ho trovato nessuna sezione da importare." : "Incolla il testo qui sopra: vedrai subito cosa verrebbe creato.")+'
';
+ var note=[];
+ if(P.ignorati.length) note.push("Non riconosciuti: "+P.ignorati.slice(0,8).map(esc).join(", ")+(P.ignorati.length>8?"…":""));
+ if(P.righeNo.length) note.push("Righe non riconosciute (contenuto non mostrato): "+P.righeNo.slice(0,10).join(", "));
+ if(P.privati) note.push(P.privati+" dat"+(P.privati===1?"o personale saltato":"i personali saltati")+", non letti");
+ if(P.tagliati.length) note.push("Oltre "+ORG_IMP_MAX+" per sezione: ridotto a "+ORG_IMP_MAX);
+ if(note.length) h+='
'+note.join(" ")+'
';
+ ov.querySelector("#orgImpAnte").innerHTML=h;
+ go.disabled=!v;
+ }
+ ta.addEventListener("input", aggiorna); sel.addEventListener("change", aggiorna);
+ ov.querySelector("#orgImpClear").addEventListener("change", aggiorna);
+ ov.__agg=aggiorna;
+ function chiudi(){ ov.hidden=true; }
+ ov.querySelector("#orgImpNo").addEventListener("click", chiudi);
+ ov.addEventListener("click", function(e){ if(e.target===ov) chiudi(); });
+ ov.addEventListener("keydown", function(e){ if(e.key==="Escape") chiudi(); });
+ go.addEventListener("click", function(){
+ var v=scelta(); if(!v) return;
+ var r=orgImportApplica(v.counts, {clear:ov.querySelector("#orgImpClear").checked, fit:ov.querySelector("#orgImpFit").checked});
+ if(r!==false) chiudi();
+ });
+ }
+ ov.querySelector("#orgImpTesto").value=""; ov.querySelector("#orgImpClear").checked=false; ov.querySelector("#orgImpFit").checked=false;
+ ov.__agg(); ov.hidden=false;
+ try{ ov.querySelector("#orgImpTesto").focus(); }catch(e){}
+}
+
/* ============ FORMAZIONI TIPO (band, coro, combinazioni) ============ */
/* convenzione coord.: origine = centro fronte palco; -y = verso il fondo, +y = verso il pubblico;
-x = lato DX palco, +x = lato SX palco (come la scena). */
@@ -27559,17 +27748,20 @@
Esporta PDF in scala
});
document.getElementById("orchWarnReview").addEventListener("click", function(){
document.getElementById("orchWarnModal").hidden=true;
- document.getElementById(pendingReviewModal||"orchModal").hidden=false;
- });
- document.getElementById("orchWarnContinue").addEventListener("click", function(){
- if(!pendingOut) return;
- placeOut(pendingOut.out, pendingOut.fitStage, pendingOut.clearFirst, true);
- pendingOut=null;
- document.getElementById("orchWarnModal").hidden=true;
- fit();
+ var _rev=document.getElementById(pendingReviewModal||"orchModal"); if(_rev) _rev.hidden=false;
});
+ document.getElementById("orchWarnContinue").addEventListener("click", orchWarnContinua);
})();
+function orchWarnContinua(){
+ if(!pendingOut) return;
+ var po=pendingOut; pendingOut=null;
+ pendingDopo=po.dopo||null;
+ placeOut(po.out, po.fitStage, po.clearFirst, true);
+ document.getElementById("orchWarnModal").hidden=true;
+ fit();
+}
+
function readStringCounts(){
return {
v1:Math.max(0,parseInt(document.getElementById("str_v1").value,10)||0),
diff --git a/test/engines.test.mjs b/test/engines.test.mjs
index ee271ca..3e1b850 100644
--- a/test/engines.test.mjs
+++ b/test/engines.test.mjs
@@ -72,7 +72,7 @@ const A = loadApp();
/* ---- mini test runner ---- */
let pass = 0, fail = 0;
-function t(name, fn) { try { fn(); pass++; console.log(" ✓ " + name); } catch (e) { fail++; console.log(" ✗ " + name + "\n " + e.message); } }
+function t(name, fn) { if (process.env.SOLO && !name.includes(process.env.SOLO)) return; try { fn(); pass++; console.log(" ✓ " + name); } catch (e) { fail++; console.log(" ✗ " + name + "\n " + e.message); } }
function eq(got, exp, msg) { const g = JSON.stringify(got), e = JSON.stringify(exp); if (g !== e) throw new Error((msg || "") + " atteso " + e + ", ottenuto " + g); }
function ok(cond, msg) { if (!cond) throw new Error(msg || "condizione falsa"); }
function throws(fn, code) {
@@ -32215,6 +32215,100 @@ t("editor: in alto a sinistra il logo stageplot.it, vettoriale, che porta alla h
ok(/@media \(min-width:881px\) and \(max-width:1210px\)\{[^}]*header \.brand \.brand-full\{display:none\}/.test(stylesCss), "fra 881 e 1210 px solo il simbolo");
});
+/* ═══ IMPORT DELL'ORGANICO (13/11/2026, G1 fase 2): una riga o una tabella diventa il conto per sezione, anteprima, poi i posti; dati personali mai letti ═══ */
+const impC = (t, k) => { const P = A.orgImportParse(t); return (P.varianti.find((v) => v.id === (k || "")) || { counts: {} }).counts; };
+t("Import organico: la riga d'esempio del piano crea esattamente quei posti", () => {
+ eq(impC("cr4-tr2-trb3-tu1; fl2-o2-cl2-fg2"), { cor: 4, tr: 2, tbn: 3, tba: 1, fl: 2, ob: 2, cl: 2, fg: 2 }, "sigle col numero attaccato, separate da trattini e punto e virgola");
+ eq(impC("12-12-6-6-4"), { v1: 12, v2: 12, vle: 6, vc: 6, cb: 4 }, "formula degli archi coi trattini");
+ eq(impC("8-6-4-4"), { v1: 8, v2: 6, vle: 4, vc: 4 }, "con quattro numeri il contrabbasso non c'è");
+ eq(impC("10.8.6.6.3"), { v1: 10, v2: 8, vle: 6, vc: 6, cb: 3 }, "anche col punto (12.10.8.6.4, come nei preset)");
+ eq(impC("2 flauti, 2 oboi, 4 corni, gran cassa 1, tam-tam"), { fl: 2, ob: 2, cor: 4, gc: 1, tam: 1 }, "numero davanti, nomi estesi, nome di due parole, strumento senza numero = 1");
+ eq(impC("cr4-tr2; 12-10-8-6-4; arpa; timpani 1"), { v1: 12, v2: 10, vle: 8, vc: 6, cb: 4, cor: 4, tr: 2, arp: 1, tim: 1 }, "righe miste (la formula degli archi si legge per prima)");
+ eq(impC("1-2-3"), {}, "tre numeri non sono una formula degli archi");
+});
+t("Import organico: tabella «sezione, strumento, n.» (CSV, TSV, ;) e colonne Completo · Medio · Ridotto", () => {
+ eq(impC("Sezione,Strumento,N.\nARCHI,Violini I,12\nARCHI,Violini II,10\nFIATI,Corni,4\nLEGNI,Flauto/Ottavino,2"), { v1: 12, v2: 10, cor: 4, fl: 2 }, "CSV con intestazione; «Flauto/Ottavino» è un flauto");
+ eq(impC("Sezione\tStrumento\tN.\nARCHI\tViole\t6\nARCHI\tViole\t2"), { vle: 8 }, "copiato da Excel (tabulazioni); le righe dello stesso strumento si sommano");
+ eq(impC("ARCHI;Violini 1;7\nARCHI;Violoncelli;4"), { v1: 7, vc: 4 }, "senza intestazione, con il punto e virgola");
+ const P = A.orgImportParse("Sezione;Strumento;Completo;Medio;Ridotto\nARCHI;Violini I;12;8;4\nFIATI;Corni;4;2;2");
+ eq(P.varianti.map((v) => v.id), ["completo", "medio", "ridotto"], "tre varianti");
+ eq(P.varianti.map((v) => v.counts), [{ v1: 12, cor: 4 }, { v1: 8, cor: 2 }, { v1: 4, cor: 2 }], "ognuna con i suoi numeri");
+ const Q = A.orgImportParse("Ridotto: 6-5-4-3-2; fl1\nCompleto: 12-10-8-6-4; fl2");
+ eq(Q.varianti.map((v) => [v.id, v.counts.v1, v.counts.fl]), [["ridotto", 6, 1], ["completo", 12, 2]], "varianti anche in linea («Completo: …»)");
+});
+t("Import organico: dati personali mai letti né mostrati; righe non riconosciute solo per numero; tetto per sezione", () => {
+ const T = "Sezione,Strumento,N.\nARCHI,Viole,6,mario.rossi@example.org,IBAN IT60X0542811101000000123456\nARCHI,Violini,3\nFIATI,Corni,2,contratto firmato\nFIATI,Tube,1,+39 333 1234567\nRossi Mario,Cantante,1\nARCHI,Celli,350";
+ const P = A.orgImportParse(T);
+ eq(P.varianti[0].counts, { vle: 6, cor: 2, tba: 1 }, "la riga resta, la cella personale no (celli 350: più di 2 cifre non è una quantità, la riga si scarta)");
+ ok(P.privati >= 4, "le celle personali sono contate (" + P.privati + ")");
+ const J = JSON.stringify(P);
+ ok(!/mario|rossi|example|IT60|contratto|1234567/i.test(J), "nulla di personale nel risultato: " + J);
+ eq(P.righeNo.length >= 2, true, "le righe non riconosciute (violini senza sezione, cantante…) sono elencate");
+ ok(P.righeNo.every((n) => typeof n === "number"), "per numero di riga, mai col contenuto");
+ eq(P.righeNo, [3, 6, 7], "righe 3 (violini senza sezione), 6 (cantante) e 7 (celli con quantità di 3 cifre)");
+ const iban = A.orgImportParse("FIATI,Corni,2,coordinate IBAN da inviare\nFIATI,Tube,1,+39 06 1234 5678\nFIATI,Trombe,2,CV allegato");
+ eq([iban.varianti[0].counts, iban.privati], [{ cor: 2, tba: 1, tr: 2 }, 3], "la parola IBAN, un fisso con prefisso, «CV»: celle saltate, righe tenute");
+ const sost = A.orgImportParse("FIATI,Corno (sostituto Rossi),2");
+ eq([sost.varianti, sost.righeNo, sost.privati], [[], [1], 1], "uno strumento con il nome di un sostituto nella stessa cella: la riga si scarta (e si dice quale), non si importa a metà");
+ const lin = A.orgImportParse("cr4; IBAN IT60X0542811101000000123456; fl2");
+ eq([lin.varianti[0].counts, lin.privati, lin.ignorati], [{ cor: 4, fl: 2 }, 1, []], "anche in linea: il pezzo personale si salta, non finisce fra i «non riconosciuti»");
+ const pipe = A.orgImportParse("cr4 | IBAN IT60X0542811101000000123456 | fl2");
+ eq([pipe.varianti[0].counts, pipe.privati, pipe.ignorati], [{ cor: 4, fl: 2 }, 1, []], "anche con la barra verticale come separatore");
+ const due = A.orgImportParse("4 2 flauti");
+ eq([due.varianti[0].counts, due.ignorati], [{ fl: 2 }, ["4"]], "due numeri di fila: il primo resta senza nome e si dice");
+ eq(impC("Sezione,Strumento,Leggii,N.\nARCHI,Violini I,6,12"), { v1: 12 }, "la colonna «N.» dell'intestazione vince su un altro numero della riga");
+ eq(impC("2 flauti 2 oboi 4 corni"), { fl: 2, ob: 2, cor: 4 }, "numero davanti, senza virgole");
+ eq(impC("cor 4 tr 2 fl 2"), { cor: 4, tr: 2, fl: 2 }, "sigla e numero, senza virgole");
+ eq(A.orgImportParse("fl0 cr0").varianti, [], "tutto a zero: niente da importare");
+ eq(impC("fl99"), { fl: 40 }, "oltre 40 per sezione si ferma a 40");
+ eq(A.orgImportParse("fl99").tagliati, ["fl"], "e lo dice");
+ eq(A.orgImportParse("sax2 fl2 pippo").ignorati, ["sax", "pippo"], "gli strumenti che il generatore non ha si dichiarano (sax) invece di sparire");
+ eq(A.orgImportParse("").varianti, [], "testo vuoto: nessuna variante");
+});
+t("Import organico: i posti creati ridanno gli stessi numeri nella pagina «Organico»; dichiarato ≠ disegnato se il palco ha già altro", () => {
+ const vuoto = () => A.loadDoc({ titolo: "Prova", stage: { w: 1600, d: 1000, blocks: [{ x: 0, y: 0, w: 1600, d: 1000 }] }, inputs: [], outputs: [], items: [] });
+ vuoto(); const prima = Object.keys(A.state).sort();
+ const c = impC("12-12-6-6-4; cr4-tr2-trb3-tu1; fl2-o2-cl2-fg2; arpa; timpani");
+ const toast = []; A.window.__toast = (m, e, az) => toast.push([m, az && az.label]);
+ A.orgImportApplica(c, { clear: true, fit: true });
+ if (A.pendingOut) A.orchWarnContinua();
+ const L = A.organicoList(), righe = Object.fromEntries(L.rows.filter((r) => r.tipo === "riga").map((r) => [r.nome, r.n]));
+ eq(L.formula, "12-12-6-6-4", "formula archi");
+ eq([righe["Violini I"], righe["Violini II"], righe["Viole"], righe["Violoncelli"], righe["Contrabbassi"]], [12, 12, 6, 6, 4], "archi");
+ eq([righe["Corni"], righe["Trombe"], righe["Tromboni"], righe["Tuba"], righe["Flauti"], righe["Oboi"], righe["Clarinetti"], righe["Fagotti"], righe["Arpa"], righe["Timpani"]], [4, 2, 3, 1, 2, 2, 2, 2, 1, 1], "fiati, legni, arpa, timpani");
+ eq(A.orgImportDifferenze(c), [], "dichiarato = disegnato");
+ eq(toast.length, 1, "un solo messaggio"); eq(toast[0][1], "Annulla", "con il pulsante «Annulla»");
+ ok(/^Creati 60 posti/.test(toast[0][0]) && !/dichiarato ≠/.test(toast[0][0]), "senza avviso: " + toast[0][0]);
+ eq(Object.keys(A.state).sort(), prima, "nessun campo nuovo nel documento");
+ vuoto(); ok(A.orgImportDifferenze(c).length > 0, "palco vuoto: dichiarato più del disegnato, e si vede");
+ eq(A.orgImportApplica({}, {}), false, "nessun posto dichiarato: non si crea niente"); eq(!!A.pendingOut, false, "e non si apre nessun avviso");
+ toast.length = 0; A.orgImportApplica(c, { clear: true, fit: true });
+ eq([!!A.pendingOut, A.state.items.length, toast.length], [true, 0, 0], "con sovrapposizioni l'avviso viene PRIMA: nessun posto creato e nessun messaggio finché non si preme «Continua»");
+ A.placeOut([{ type: "flauto", x: 0, y: 0 }], false, false, true); eq(toast.length, 0, "un altro posizionamento, nel frattempo, non eredita il messaggio dell'import");
+ A.orchWarnContinua(); eq([!!A.pendingOut, toast.length], [false, 1], "dopo «Continua» i posti ci sono e arriva il messaggio con «Annulla»");
+ vuoto(); toast.length = 0;
+ /* palco con altro già dentro e senza svuotare: i numeri non coincidono, e lo dice */
+ s5Fix(orgFix()); toast.length = 0;
+ A.orgImportApplica(impC("12-12-6-6-4"), { clear: false, fit: false }); if (A.pendingOut) A.orchWarnContinua();
+ ok(A.orgImportDifferenze(impC("12-12-6-6-4")).length > 0, "palco non svuotato: dichiarato ≠ disegnato");
+ ok(/dichiarato ≠ disegnato/.test(toast[0][0]), "il messaggio lo dice: " + toast[0][0]);
+ /* e la stessa importazione con «svuota» torna pari */
+ A.orgImportApplica(impC("12-12-6-6-4"), { clear: true, fit: true }); if (A.pendingOut) A.orchWarnContinua();
+ eq(A.orgImportDifferenze(impC("12-12-6-6-4")), [], "svuotando il palco il disegno coincide");
+});
+t("Import organico: si trova dalla ricerca («importa organico»), il menu File resta essenziale, nessun documento cambia e niente va nel database", () => {
+ const trovati = (q) => A.__spSearch(q).map((e) => e.nome);
+ ok(trovati("importa organico").includes("Importa organico"), "«importa organico» lo trova: " + trovati("importa organico"));
+ ok(trovati("incolla").includes("Importa organico") && trovati("excel").includes("Importa organico"), "anche «incolla» e «excel»");
+ eq(trovati("violino").includes("Importa organico"), false, "ma non ruba i risultati di uno strumento");
+ const html = readFileSync(join(root, "app/index.html"), "utf8");
+ ok(!/data-file="orgimp"/.test(html), "niente voce nel menu File (tetto di sette, decisione di Simone)");
+ const js = readFileSync(join(root, "app.js"), "utf8"), src = js.slice(js.indexOf("function orgImportParse"), js.indexOf("function orgImportApri"));
+ ok(src.length > 500, "premessa: il blocco si trova");
+ ok(!/state\.[a-zA-Z]+\s*=[^=]/.test(src.replace(/state\.items/g, "")), "il parser e l'applicazione non scrivono campi nuovi in state");
+ ok(!/fetch\(|supabase|localStorage|sessionStorage/.test(src), "niente rete, niente archivio: il testo incollato resta sulla pagina");
+});
+
console.log("\n" + (fail === 0 ? "✓ TUTTI VERDI" : "✗ " + fail + " FALLITI") + " — " + pass + " passati, " + fail + " falliti.");
process.exit(fail === 0 ? 0 : 1);