'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 ✓'); }