Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion app.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,7 @@
.orc-row .orc-hd{font-weight:600;margin-bottom:4px}.orc-row .orc-prod{font-weight:400;color:var(--text-3,#6b7280);margin-left:4px}
.orc-row .orc-line{padding:2px 0}.orc-row .orc-line.ok{color:#0d9488}.orc-row .orc-line.wait{color:#2563eb}.orc-row .orc-line.open{color:#b45309}.orc-row .orc-line.off{color:var(--text-3,#6b7280)}
.orc-row .orc-link{display:inline-block;margin-top:6px;color:#0d9488;font-weight:600;text-decoration:none;min-height:24px}
.orc-row .orc-link + .orc-link{margin-left:14px}
.lbl.sub{font-weight:400;fill:#6b7280;font-size:12px;stroke-width:2.6}
/* POSTI NUMERATI (06/10): il numero nella sedia, la fila ai capi, la legenda. Non sono .lbl: «nomi
nascosti» e la passata anti-collisione del PDF non li toccano — il numero è il posto, non un nome. */
Expand Down
10 changes: 9 additions & 1 deletion index.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -42037,6 +42037,13 @@ <h2>Esporta PDF in scala</h2>
if(g){ if(typeof setSvgInner==="function") setSvgInner(g, m); else g.innerHTML=m; }
if(typeof renderProps==="function") renderProps();
}
/* «Cerca musicista» (V1/b): apre il matching di Orchestre già sul ruolo di questa postazione. Solo se c'è un posto da coprire. */
function orcCercaLink(productionId, it, rows){
if(!productionId || !it || !it.id || !rows || !rows.length) return null;
var open=rows.some(function(r){ var st=String(r&&r.slot_status||"open"); return st==="open"||st==="replaced"||st==="cancelled"; });
if(!open) return null;
return "/orchestre/admin/produzioni/scheda/?id="+encodeURIComponent(productionId)+"&t=matching&item="+encodeURIComponent(it.id);
}
/* la riga «Organico» nel pannello dell'elemento: cosa dice Orchestre di questa postazione */
function renderOrcRow(it){
var row=document.getElementById("pOrcRow"); if(!row) return;
Expand All @@ -42047,7 +42054,8 @@ <h2>Esporta PDF in scala</h2>
var prod=rows?rows[0]:orcSeats.productions[0];
var h='<div class="orc-hd">Organico <span class="orc-prod">'+esc(String(prod.production_title||""))+'</span></div>';
if(rows){ rows.forEach(function(r){ var who=String(r.musician_name||"").trim(); h+='<div class="orc-line '+orcSeatClass(r)+'">'+esc((r.role_name?r.role_name+" \u00b7 ":"")+((who && r.seat_no!=null)?"posto "+r.seat_no+" \u00b7 ":"")+orcSeatLine(r))+'</div>'; });
h+='<a class="orc-link" href="/orchestre/admin/produzioni/scheda/?id='+encodeURIComponent(prod.production_id)+'&t=organico">Apri l’organico →</a>'; }
var cerca=orcCercaLink(prod.production_id, it, rows);
h+='<a class="orc-link" href="/orchestre/admin/produzioni/scheda/?id='+encodeURIComponent(prod.production_id)+'&t=organico">Apri l’organico →</a>'+(cerca?'<a class="orc-link orc-cerca" href="'+esc(cerca)+'">Cerca musicista →</a>':''); }
else { h+='<div class="orc-line off">Postazione non ancora nell’organico</div><a class="orc-link" href="/orchestre/admin/produzioni/scheda/?id='+encodeURIComponent(prod.production_id)+'&t=stageplot">Importa le postazioni →</a>'; }
row.innerHTML=h;
}
Expand Down
11 changes: 11 additions & 0 deletions orchestre/src/domain/staffing.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ export function groupStaffing(rows) {
return out;
}

/* Dal disegno all'organico (V1/b): la postazione `itemId` dell'editor a quale ruolo e a quali posti corrisponde.
Restituisce { role, section, slots, target } o null: `target` è il posto da cercare (il primo non coperto, altrimenti il primo). */
export function roleForItem(sections, itemId) {
if (!itemId) return null;
for (const section of sections || []) for (const role of section.roles) {
const slots = role.slots.filter((x) => x.item_id === itemId);
if (slots.length) return { role, section, slots, target: slots.find((x) => x.status === "open" || x.status === "replaced" || x.status === "cancelled") || slots[0] };
}
return null;
}

/* Conteggi per il riepilogo: posti, confermati, scoperti. */
export function staffingCounts(sections) {
let seats = 0, filled = 0, open = 0;
Expand Down
12 changes: 11 additions & 1 deletion orchestre/src/pages/produzione.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BASE } from "../config.js";
import { esc, el, toast, confirm, setState, errMsg, fmtDate, fmtDateTime, avvia } from "../ui.js";
import { requireStaff, mountTopbar } from "../auth.js";
import { tabs, REP_KIND } from "../nav.js";
import { PROD_STATUS, PROD_STATUS_PILL, PROD_KIND, DATE_KIND, PART, SLOT_STATUS, SLOT_PILL, EVENT, INV_STATUS, INV_PILL, INV_EVENT, TEMPLATES, templateSeats, groupStaffing, staffingCounts, suggestedStatus } from "../domain/staffing.js";
import { roleForItem, PROD_STATUS, PROD_STATUS_PILL, PROD_KIND, DATE_KIND, PART, SLOT_STATUS, SLOT_PILL, EVENT, INV_STATUS, INV_PILL, INV_EVENT, TEMPLATES, templateSeats, groupStaffing, staffingCounts, suggestedStatus } from "../domain/staffing.js";
import * as api from "../api/productions.js";
import * as match from "../api/matching.js";
import * as inv from "../api/invitations.js";
Expand Down Expand Up @@ -377,13 +377,23 @@ async function paintMatching() {
const roles = sections.flatMap((s) => s.roles.map((r) => ({ ...r, section: s.name, open: r.slots.filter((x) => x.status === "open").length })));
panel.innerHTML = "";
if (!roles.length) { panel.appendChild(el(`<div class="empty">Prima definisci l'organico: il matching lavora su un ruolo alla volta.</div>`)); return; }
/* deep link dall'editor: ?item=<postazione> sceglie il ruolo e mette in evidenza il posto (una volta sola) */
const fromItem = q.get("item") ? roleForItem(sections, q.get("item")) : null;
if (fromItem) mRole = fromItem.role.id;
if (!mRole || !roles.find((r) => r.id === mRole)) mRole = (roles.find((r) => r.open > 0) || roles[0]).id;
const head = el(`<section class="card"><div class="form-row">
<div class="field"><label for="mRole">Ruolo</label><select id="mRole">${roles.map((r) => `<option value="${r.id}">${esc(r.section + " · " + r.name)} (${r.open} ${r.open === 1 ? "scoperto" : "scoperti"} su ${r.slots.length})</option>`).join("")}</select></div>
<div class="field"><label>Pesi</label><span class="pill">${esc(mRuleset.name || "Pesi")} · v${mRuleset.version}</span></div>
<button type="button" class="btn primary" id="mGo">Calcola</button></div>
<p class="small muted">In cima chi va bene per il ruolo, in ordine; accanto a ognuno il perché. Chi non si può convocare resta in fondo, con il motivo. Ogni calcolo resta salvato: la convocazione citerà questa proposta.</p></section>`);
head.querySelector("#mRole").value = mRole;
if (fromItem) {
const t = fromItem.target, lab = t.item_label || fromItem.role.name;
const note = el(`<p class="small focus-seat" role="status"></p>`);
note.textContent = "Dalla postazione «" + lab + "» del palco: posto " + t.seat_no + " di «" + fromItem.role.name + "» (" + (SLOT_STATUS[t.status] || t.status).toLowerCase() + (t.musician_name ? ", ora " + t.musician_name : "") + ").";
head.appendChild(note);
q.delete("item"); history.replaceState(null, "", "?id=" + p.id + "&t=matching&role=" + mRole);
}
head.querySelector("#mRole").onchange = (e) => { mRole = e.target.value; history.replaceState(null, "", "?id=" + p.id + "&t=matching&role=" + mRole); loadLastRun(); };
head.querySelector("#mGo").onclick = compute;
panel.appendChild(head);
Expand Down
8 changes: 8 additions & 0 deletions orchestre/test/pages.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -787,3 +787,11 @@ test("se il server non risponde la pagina lo dice, e Orchestre non dipende dalle
assert.match(wf, /- name: Convocazioni Orchestre \(orc-notify\)\n(?:\s*#[^\n]*\n)*\s+if: always\(\)\n/, "il passo di Orchestre gira comunque");
});


/* V1/b: «Cerca musicista» dall'editor porta il matching sul ruolo della postazione (?item=), una volta sola. */
test("la scheda produzione risolve ?item= in ruolo e posto, e lo consuma", () => {
const prod = readFileSync(join(root, "orchestre/src/pages/produzione.js"), "utf8");
assert.match(prod, /const fromItem = q\.get\("item"\) \? roleForItem\(sections, q\.get\("item"\)\) : null;\s*\n\s*if \(fromItem\) mRole = fromItem\.role\.id;/, "il ruolo viene dalla postazione");
assert.match(prod, /q\.delete\("item"\); history\.replaceState\(null, "", "\?id=" \+ p\.id \+ "&t=matching&role=" \+ mRole\)/, "una volta sola: poi l'indirizzo è quello di sempre");
assert.match(prod, /note\.textContent = "Dalla postazione «"/, "dice da dove arriva, col testo messo da textContent");
});
12 changes: 11 additions & 1 deletion orchestre/test/staffing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { TEMPLATES, templateSeats, groupStaffing, staffingCounts, suggestedStatus, PROD_STATUS } from "../src/domain/staffing.js";
import { TEMPLATES, templateSeats, groupStaffing, roleForItem, staffingCounts, suggestedStatus, PROD_STATUS } from "../src/domain/staffing.js";

const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..");

Expand Down Expand Up @@ -54,3 +54,13 @@ test("suggestedStatus non tocca gli stati terminali e segue i posti", () => {
assert.equal(suggestedStatus({ seats: 3, filled: 0, open: 3 }, "done"), "done");
assert.equal(suggestedStatus({ seats: 3, filled: 3, open: 0 }, "cancelled"), "cancelled");
});

test("roleForItem: dalla postazione dell'editor al ruolo e al posto da cercare", () => {
const sec = [{ name: "Archi", roles: [
{ id: "r1", name: "Violini primi", slots: [{ id: "s1", seat_no: 1, status: "confirmed", item_id: "A" }, { id: "s2", seat_no: 2, status: "open", item_id: "A" }, { id: "s3", seat_no: 3, status: "open", item_id: null }] },
{ id: "r2", name: "Viole", slots: [{ id: "s4", seat_no: 1, status: "confirmed", item_id: "B" }] }] }];
const a = roleForItem(sec, "A");
assert.equal(a.role.id, "r1"); assert.equal(a.slots.length, 2); assert.equal(a.target.id, "s2", "il primo posto scoperto");
assert.equal(roleForItem(sec, "B").target.id, "s4", "tutti coperti: il primo");
assert.equal(roleForItem(sec, "zzz"), null); assert.equal(roleForItem(sec, ""), null); assert.equal(roleForItem(null, "A"), null);
});
1 change: 1 addition & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1428,6 +1428,7 @@ svg.names-hidden .orc-name{display:none}
.orc-row .orc-hd{font-weight:600;margin-bottom:4px}.orc-row .orc-prod{font-weight:400;color:var(--text-3,#6b7280);margin-left:4px}
.orc-row .orc-line{padding:2px 0}.orc-row .orc-line.ok{color:#0d9488}.orc-row .orc-line.wait{color:#2563eb}.orc-row .orc-line.open{color:#b45309}.orc-row .orc-line.off{color:var(--text-3,#6b7280)}
.orc-row .orc-link{display:inline-block;margin-top:6px;color:#0d9488;font-weight:600;text-decoration:none;min-height:24px}
.orc-row .orc-link + .orc-link{margin-left:14px}
.lbl.sub{font-weight:400;fill:#6b7280;font-size:12px;stroke-width:2.6}
/* POSTI NUMERATI (06/10): il numero nella sedia, la fila ai capi, la legenda. Non sono .lbl: «nomi
nascosti» e la passata anti-collisione del PDF non li toccano — il numero è il posto, non un nome. */
Expand Down
15 changes: 15 additions & 0 deletions test/engines.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25871,6 +25871,21 @@ t("il layer layOrcSeats sta in sceneMarkup ma non nell'export né nel documento"
ok(A.window.__scenePrint !== true, "il flag di stampa è tornato spento dopo l'export");
A.orcSeats.pid = null; A.orcSeats.byItem = {};
});
t("«Cerca musicista» (V1/b): link al matching di Orchestre sulla postazione, solo se c'è un posto da coprire", () => {
const it = { id: "itm-1" };
const prod = "11111111-1111-4111-8111-111111111111";
eq(A.orcCercaLink(prod, it, [{ slot_status: "open" }]), "/orchestre/admin/produzioni/scheda/?id=" + prod + "&t=matching&item=itm-1");
ok(A.orcCercaLink(prod, it, [{ slot_status: "replaced" }]) !== null, "sostituito = da coprire");
ok(A.orcCercaLink(prod, it, [{ slot_status: "confirmed" }, { slot_status: "open" }]) !== null, "due posti: basta uno scoperto");
eq(A.orcCercaLink(prod, it, [{ slot_status: "confirmed" }]), null);
eq(A.orcCercaLink(prod, it, [{ slot_status: "invited" }]), null);
eq(A.orcCercaLink(prod, it, []), null); eq(A.orcCercaLink(prod, it, null), null);
eq(A.orcCercaLink("", it, [{ slot_status: "open" }]), null); eq(A.orcCercaLink(prod, { id: "" }, [{ slot_status: "open" }]), null);
ok(A.orcCercaLink(prod, { id: "a&b=c" }, [{ slot_status: "open" }]).indexOf("item=a%26b%3Dc") > -1, "l'id è codificato");
const row = appjs.slice(appjs.indexOf("function renderOrcRow("), appjs.indexOf("function deleteSelGuarded("));
ok(/orcCercaLink\(prod\.production_id, it, rows\)/.test(row) && row.indexOf("Cerca musicista") > -1, "la riga Organico la mostra");
ok(row.indexOf("Cerca musicista") < row.indexOf("Importa le postazioni"), "e solo nel ramo con le postazioni già nell'organico");
});
t("eliminare una postazione con una persona sopra passa SEMPRE dalla guardia, e l'annuncio arriva dopo", () => {
ok(appjs.indexOf('getElementById("pDel").addEventListener("click", deleteSelGuarded)') > -1, "bottone dell'elemento");
/* il collaudo del 10/09 ha trovato questa scappatoia: «Elimina» sulla selezione multipla cancellava
Expand Down
Loading