Band Lineup 1.12.0: Sync puts the leader and vocalists on the mic their Planning Center note names
The booth writes each vocalist's and the leader's mic into their Planning
Center note every week ("Mix 8, VOX 5", "Mix 1, Vox 1"), because those
change week to week and the position name ("Vocalist") carries no number.
Sync ignored the notes and dropped the singers onto Vox 2-5 in whatever
order Planning Center listed them.
Sync now reads each team member's note and places anyone whose note names
a spot there first: the mix number must agree with the slot's MIX, and the
role ("VOX 5", "Drums") is scored like a position. "NA" or a blank note
says nothing. Everyone else is placed by position as before. Tests use the
real Oct 25 and Nov 1 notes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -117,9 +117,61 @@ function scoreMatch(slotLabel, positionLabel) {
|
||||
* member or null. `MIN_SCORE` keeps a weak coincidence from claiming a slot.
|
||||
*/
|
||||
const MIN_SCORE = 60;
|
||||
|
||||
/** The mix numbers a label names ("[VOX 5] MIX 8" → {"8"}; "Mix 8, VOX 5" → {"8"}). */
|
||||
function mixNumbers(value) {
|
||||
const out = new Set(); const re = /\bmix\s*(\d+)\b/gi; let m;
|
||||
while ((m = re.exec(String(value || '')))) out.add(m[1]);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* How well a person's Planning Center note names a spot. The booth writes the mic/role into each
|
||||
* vocalist's and the leader's note every week ("Mix 8, VOX 5", "DP48, Drums"), because those
|
||||
* change week to week and the position name ("Vocalist") doesn't say which mic. A note that names a
|
||||
* different mix than the spot is never a match; the role part ("VOX 5") is scored like a position.
|
||||
* 0 means the note says nothing usable about this spot ("NA", blank, or no overlap).
|
||||
*/
|
||||
function noteScore(slotLabel, note) {
|
||||
const text = String(note || '').trim();
|
||||
if (!text || /^n\/?a$/i.test(text)) return 0;
|
||||
const slotMix = mixNumbers(slotLabel); const noteMix = mixNumbers(text);
|
||||
let mixBonus = 0;
|
||||
if (noteMix.size && slotMix.size) {
|
||||
if (![...noteMix].some((n) => slotMix.has(n))) return 0;
|
||||
mixBonus = 100;
|
||||
}
|
||||
const role = text.replace(/\bmix\s*\d+\b/gi, ' ');
|
||||
const roleScore = scoreMatch(slotRoleText(slotLabel), role);
|
||||
if (!roleScore && !mixBonus) return 0;
|
||||
if (noteMix.size && slotMix.size && roleScore === 0 && role.replace(/[^a-z0-9]/gi, '')) {
|
||||
// The mix agrees but the role contradicts it (e.g. "Mix 8, Keys" on a VOX spot) — trust neither.
|
||||
const roleTokens = labelTokens(role);
|
||||
if (roleTokens.length) return 0;
|
||||
}
|
||||
return mixBonus + roleScore;
|
||||
}
|
||||
|
||||
function assignMembersToSlots(slots, members) {
|
||||
const pool = members.slice();
|
||||
return slots.map((slotLabel) => {
|
||||
const result = slots.map(() => null);
|
||||
const take = (chosen) => {
|
||||
// One spot per person: someone scheduled twice (Leader + Acoustic) must not fill a second spot.
|
||||
const key = chosen.personId || '';
|
||||
for (let i = pool.length - 1; i >= 0; i -= 1) if (pool[i] === chosen || (key && pool[i].personId === key)) pool.splice(i, 1);
|
||||
};
|
||||
// Pass 1 — people whose note names their spot go exactly there, best-fitting pairs first.
|
||||
const pairs = [];
|
||||
slots.forEach((slotLabel, s) => pool.forEach((m) => { const sc = noteScore(slotLabel, m.note); if (sc >= MIN_SCORE) pairs.push({ s, m, sc }); }));
|
||||
pairs.sort((a, b) => b.sc - a.sc);
|
||||
for (const { s, m } of pairs) {
|
||||
if (result[s] || !pool.includes(m)) continue;
|
||||
result[s] = m; take(m);
|
||||
}
|
||||
// Pass 2 — everyone else by Planning Center position, into the spots still open.
|
||||
return result.map((placed, s) => {
|
||||
if (placed) return placed;
|
||||
const slotLabel = slots[s];
|
||||
const role = slotRoleText(slotLabel);
|
||||
let bestIndex = -1;
|
||||
let bestScore = MIN_SCORE - 1;
|
||||
@@ -128,10 +180,8 @@ function assignMembersToSlots(slots, members) {
|
||||
if (score > bestScore) { bestScore = score; bestIndex = i; }
|
||||
}
|
||||
if (bestIndex < 0) return null;
|
||||
const chosen = pool.splice(bestIndex, 1)[0];
|
||||
// One spot per person: someone scheduled twice (Leader + Acoustic) must not fill a second spot.
|
||||
const key = chosen.personId || '';
|
||||
if (key) for (let i = pool.length - 1; i >= 0; i -= 1) if (pool[i].personId === key) pool.splice(i, 1);
|
||||
const chosen = pool[bestIndex];
|
||||
take(chosen);
|
||||
return chosen;
|
||||
});
|
||||
}
|
||||
@@ -198,5 +248,7 @@ module.exports = {
|
||||
numbersIn,
|
||||
scoreMatch,
|
||||
assignMembersToSlots,
|
||||
noteScore,
|
||||
mixNumbers,
|
||||
MIN_SCORE,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user