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>
106 lines
6.7 KiB
JavaScript
106 lines
6.7 KiB
JavaScript
'use strict';
|
|
const assert = require('assert');
|
|
const { assignMembersToSlots, isAllowedTeamName, slotRoleText, scoreMatch } = require('./matching');
|
|
|
|
const SLOTS = [
|
|
'[LEADER, VOX 1, ACOUSTIC] MIX 1', '[VOX 2] MIX 2', '[VOX 3] MIX 3', '[VOX 4] MIX 4',
|
|
'[GUITAR, LEAD] MIX 5', '[BASS] MIX 6', '[VOX 5] MIX 7', '[PIANO]', '[DRUMS]',
|
|
'[KEYS 2]', '[CALL TO WORSHIP] MC2', 'STAGE MANAGER', '[MC] MC1', '[SPEAKER] HEADSET',
|
|
];
|
|
const M = (label) => ({ label, name: label });
|
|
const members = [
|
|
M('Vox 3'), M('Vox 2'), M('Vox 1'), M('Guitar / Lead'), M('Bass'), M('Piano'), M('Drums'), M('MC'), M('Keys 2'),
|
|
];
|
|
const out = assignMembersToSlots(SLOTS, members).map((m) => (m ? m.label : null));
|
|
|
|
// Numbered vocals land in the RIGHT slot (this is the swap bug we fixed).
|
|
assert.strictEqual(out[0], 'Vox 1', 'slot 0 → Vox 1');
|
|
assert.strictEqual(out[1], 'Vox 2', 'slot 1 → Vox 2');
|
|
assert.strictEqual(out[2], 'Vox 3', 'slot 2 → Vox 3');
|
|
assert.strictEqual(out[3], null, 'slot 3 (Vox 4) unfilled — nobody scheduled');
|
|
assert.strictEqual(out[4], 'Guitar / Lead', 'slot 4 → Guitar/Lead');
|
|
assert.strictEqual(out[5], 'Bass', 'slot 5 → Bass');
|
|
assert.strictEqual(out[6], null, 'slot 6 (Vox 5) unfilled');
|
|
assert.strictEqual(out[7], 'Piano', 'slot 7 → Piano');
|
|
assert.strictEqual(out[8], 'Drums', 'slot 8 → Drums');
|
|
assert.strictEqual(out[9], 'Keys 2', 'slot 9 → Keys 2');
|
|
assert.strictEqual(out[12], 'MC', 'slot 12 → MC');
|
|
|
|
// A different number never matches.
|
|
assert.strictEqual(scoreMatch(slotRoleText('[VOX 2] MIX 2'), 'Vox 3'), 0, 'Vox 3 must not match Vox 2 slot');
|
|
// Mic number no longer pollutes: Vox 7 must not grab "[VOX 5] MIX 7".
|
|
assert.strictEqual(scoreMatch(slotRoleText('[VOX 5] MIX 7'), 'Vox 7'), 0, 'Vox 7 must not match the MIX 7 slot');
|
|
|
|
// Team allow-list (singular stems match plurals/variants via substring).
|
|
assert.ok(isAllowedTeamName('Band', ['band', 'vocal']));
|
|
assert.ok(isAllowedTeamName('Vocals', ['band', 'vocal']));
|
|
assert.ok(isAllowedTeamName('Vocal Team', ['band', 'vocal']));
|
|
assert.ok(!isAllowedTeamName('Video', ['band', 'vocal', 'production']));
|
|
|
|
console.log('matching.test.js: all assertions passed ✓');
|
|
console.log('assignment:', JSON.stringify(out));
|
|
|
|
// --- bestLibraryMatch: folder-photo → PCO person by name ---
|
|
{
|
|
const { bestLibraryMatch } = require('./matching');
|
|
const name = (n) => (e) => (e ? e.name : null);
|
|
const lib = [
|
|
{ id: 'a', name: 'Alex' }, { id: 'g', name: 'AlexG' }, { id: 'bm', name: 'Brian M' },
|
|
{ id: 'c', name: 'Courtney' }, { id: 'gn', name: 'Gaitan' }, { id: 'wes', name: 'Wes' },
|
|
];
|
|
const pick = (n, list = lib) => { const e = bestLibraryMatch(n, list); return e ? e.name : null; };
|
|
assert.strictEqual(pick('Alex Gaitan'), 'Alex', 'first name matches "Alex"');
|
|
assert.strictEqual(pick('Brian Moniz'), 'Brian M', '"Brian M" matches "Brian Moniz"');
|
|
assert.strictEqual(pick('Courtney Maranda'), 'Courtney', 'first name matches');
|
|
assert.strictEqual(pick('Alexander Zinni'), null, '"Alex" must NOT grab "Alexander Zinni"');
|
|
assert.strictEqual(pick('Wes Carter'), 'Wes', 'first name matches "Wes"');
|
|
// surname-named photo
|
|
assert.strictEqual(pick('Someone Gaitan'), 'Gaitan', 'surname photo matches by last name');
|
|
// two people share a first name + only a first-name photo → ambiguous only if two matches tie;
|
|
// here a single "Alex" photo still maps to whoever's first name is exactly Alex.
|
|
assert.strictEqual(pick('Alex Johnson'), 'Alex', 'first-name photo maps to any exact-first-name person');
|
|
// exact full name beats a first-name photo
|
|
assert.strictEqual(pick('Alex Gaitan', [{ id: 'x', name: 'Alex Gaitan' }, { id: 'a', name: 'Alex' }]), 'Alex Gaitan', 'exact full name wins');
|
|
// tie → no match (two identical-scoring photos)
|
|
assert.strictEqual(bestLibraryMatch('Chris Smith', [{ id: '1', name: 'Chris' }, { id: '2', name: 'Chris' }]), null, 'ambiguous tie → null');
|
|
console.log('bestLibraryMatch: all assertions passed ✓');
|
|
}
|
|
|
|
// --- one spot per person: someone scheduled twice (Leader + Acoustic) fills one spot only ---
|
|
{
|
|
const P = (label, name, personId) => ({ label, name, personId });
|
|
const slots = ['[LEADER, VOX 1, ACOUSTIC] MIX 1', '[GUITAR, 2] MIX 7'];
|
|
const got = assignMembersToSlots(slots, [P('[Leader]', 'Mike', 'mp'), P('Guitar [Acoustic]', 'Mike', 'mp'), P('Guitar [Rhythm]', 'Craig', 'cs')]).map((m) => (m ? m.name : null));
|
|
assert.deepStrictEqual(got, ['Mike', 'Craig'], 'Mike once, Craig gets the guitar spot');
|
|
console.log('one spot per person: all assertions passed ✓');
|
|
}
|
|
|
|
// --- Planning Center notes place the leader and vocalists (real notes from Oct 25 / Nov 1, 2026) ---
|
|
{
|
|
const { noteScore } = require('./matching');
|
|
const NA_SLOTS = [
|
|
'[LEADER, VOX 1, ACOUSTIC] MIX 1', '[VOX 2] MIX 2', '[VOX 3] MIX 3', '[VOX 4] MIX 4', '[GUITAR, LEAD] MIX 5',
|
|
'[BASS] MIX 6', '[GUITAR, 2] MIX 7', '[VOX 5] MIX 8', '[PIANO]', '[DRUMS]', '[KEYS 2]',
|
|
];
|
|
const P = (label, name, personId, note) => ({ label, name, personId, note });
|
|
// Listed in the order Planning Center returned them — vocalists alphabetical, not by mic.
|
|
const oct25 = [
|
|
P('Vocalist', 'DJ', 'dj', 'Mix 3, VOX 3'), P('[Leader]', 'Mike', 'mp', 'Mix 1, VOX 1'), P('Guitar [Lead]', 'Yeremis', 'yc'),
|
|
P('Piano/Pad', 'Kevin', 'km'), P('Vocalist', 'Genesis', 'gg', 'Mix 4, VOX 4'), P('Guitar [Rhythm]', 'Craig', 'cs'),
|
|
P('Guitar [Acoustic]', 'Mike', 'mp'), P('Vocalist', 'Rosaliz', 'rg', 'Mix 2, VOX 2'), P('Drums', 'Luis', 'ld'),
|
|
P('Vocalist', 'Bianca', 'bb', 'Mix 8, VOX 5'), P('Guitar [Bass]', 'Joram', 'jl'),
|
|
];
|
|
const got = assignMembersToSlots(NA_SLOTS, oct25).map((m) => (m ? m.name : null));
|
|
assert.deepStrictEqual(got, ['Mike', 'Rosaliz', 'DJ', 'Genesis', 'Yeremis', 'Joram', 'Craig', 'Bianca', 'Kevin', 'Luis', null], 'Oct 25 by notes');
|
|
// Nov 1: leader note in different case, a drum note with no mix, an "NA" note.
|
|
const nov1 = [P('Guitar [Acoustic]', 'Mike', 'mp', 'NA'), P('Drums', 'Alex', 'ag', 'DP48, Drums'), P('[Leader]', 'Mike', 'mp', 'Mix 1, Vox 1')];
|
|
assert.deepStrictEqual(assignMembersToSlots(NA_SLOTS, nov1).map((m) => (m ? m.name : null)).filter(Boolean), ['Mike', 'Alex'], 'Nov 1: Mike once, Alex on drums');
|
|
assert.strictEqual(assignMembersToSlots(NA_SLOTS, nov1)[9].name, 'Alex', 'DP48, Drums → [DRUMS]');
|
|
// A note never pulls someone onto a different mix.
|
|
assert.strictEqual(noteScore('[VOX 2] MIX 2', 'Mix 8, VOX 5'), 0, 'Mix 8 note must not match MIX 2');
|
|
assert.strictEqual(noteScore('[VOX 5] MIX 8', 'Mix 8, VOX 5') > 0, true, 'Mix 8, VOX 5 matches [VOX 5] MIX 8');
|
|
assert.strictEqual(noteScore('[VOX 5] MIX 8', 'NA'), 0, 'NA says nothing');
|
|
assert.strictEqual(noteScore('[VOX 1] MIX 10', 'Mix 1, VOX 1') , 0, 'Mix 1 must not match MIX 10');
|
|
console.log('notes: all assertions passed ✓');
|
|
}
|