Supervisors can split an event's named attendees into allocation sets (rooms, small groups, buses) with capacity, gender and leader per group, using a drag-and-drop board or auto-allocate (keep bookings together or split them). Adds printable group and master lists (medical view audited), the group on tickets and in My Events, messaging by group, a "My groups" page for leaders, and an optional per-event gender question for named attendees. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
3.9 KiB
JavaScript
85 lines
3.9 KiB
JavaScript
const { joinBlocker, autoAllocate, cleanGender } = require('../src/utils/allocation');
|
|
|
|
const person = (id, registrationId, gender = null) => ({ id, name: id, registrationId, gender });
|
|
const groupsOf = (result) => {
|
|
const out = {};
|
|
for (const p of result.placements) (out[p.groupId] = out[p.groupId] || []).push(p.attendeeId);
|
|
return out;
|
|
};
|
|
|
|
describe('cleanGender', () => {
|
|
test('accepts male/female in any case, blank as null', () => {
|
|
expect(cleanGender('Male')).toBe('male');
|
|
expect(cleanGender('')).toBeNull();
|
|
expect(() => cleanGender('other')).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('joinBlocker', () => {
|
|
test('capacity', () => {
|
|
expect(joinBlocker({ name: 'Cabin 1', capacity: 2 }, 1, [person('a', 'r')])).toBeNull();
|
|
expect(joinBlocker({ name: 'Cabin 1', capacity: 2 }, 2, [person('a', 'r')])).toMatch(/full/);
|
|
});
|
|
|
|
test('gender', () => {
|
|
expect(joinBlocker({ name: 'Boys', gender: 'male' }, 0, [person('a', 'r', 'male')])).toBeNull();
|
|
expect(joinBlocker({ name: 'Boys', gender: 'male' }, 0, [person('a', 'r', 'female')])).toMatch(/males only/);
|
|
expect(joinBlocker({ name: 'Boys', gender: 'male' }, 0, [person('a', 'r')])).toMatch(/no gender recorded/);
|
|
});
|
|
});
|
|
|
|
describe('autoAllocate', () => {
|
|
test('fills groups evenly', () => {
|
|
const groups = [{ id: 'g1', name: 'A' }, { id: 'g2', name: 'B' }];
|
|
const attendees = ['a', 'b', 'c', 'd'].map(id => person(id, id));
|
|
const r = autoAllocate({ groups, attendees });
|
|
const g = groupsOf(r);
|
|
expect(g.g1.length).toBe(2);
|
|
expect(g.g2.length).toBe(2);
|
|
expect(r.unplaced).toEqual([]);
|
|
});
|
|
|
|
test('respects capacity and reports who could not be placed', () => {
|
|
const groups = [{ id: 'g1', name: 'A', capacity: 1 }];
|
|
const r = autoAllocate({ groups, attendees: [person('a', 'r1'), person('b', 'r2')] });
|
|
expect(r.placements.length).toBe(1);
|
|
expect(r.unplaced.length).toBe(1);
|
|
});
|
|
|
|
test('respects gender', () => {
|
|
const groups = [{ id: 'boys', name: 'Boys', gender: 'male' }, { id: 'girls', name: 'Girls', gender: 'female' }];
|
|
const r = autoAllocate({ groups, attendees: [person('a', 'r1', 'male'), person('b', 'r2', 'female'), person('c', 'r3')] });
|
|
expect(groupsOf(r)).toEqual({ boys: ['a'], girls: ['b'] });
|
|
expect(r.unplaced).toEqual(['c']);
|
|
});
|
|
|
|
test('keeps a registration together when a group can take them all', () => {
|
|
const groups = [{ id: 'g1', name: 'A', capacity: 3 }, { id: 'g2', name: 'B', capacity: 3 }];
|
|
const attendees = [person('s1', 'fam'), person('s2', 'fam'), person('x', 'solo')];
|
|
const g = groupsOf(autoAllocate({ groups, attendees, mode: 'together' }));
|
|
const famGroup = Object.keys(g).find(k => g[k].includes('s1'));
|
|
expect(g[famGroup]).toContain('s2');
|
|
});
|
|
|
|
test('places siblings one by one when no group fits them all', () => {
|
|
const groups = [{ id: 'boys', name: 'Boys', gender: 'male' }, { id: 'girls', name: 'Girls', gender: 'female' }];
|
|
const r = autoAllocate({ groups, attendees: [person('bro', 'fam', 'male'), person('sis', 'fam', 'female')], mode: 'together' });
|
|
expect(groupsOf(r)).toEqual({ boys: ['bro'], girls: ['sis'] });
|
|
});
|
|
|
|
test('split mode spreads a registration across groups', () => {
|
|
const groups = [{ id: 'g1', name: 'A' }, { id: 'g2', name: 'B' }];
|
|
const attendees = [person('f1', 'friends'), person('f2', 'friends')];
|
|
const g = groupsOf(autoAllocate({ groups, attendees, mode: 'split' }));
|
|
expect(g.g1.length).toBe(1);
|
|
expect(g.g2.length).toBe(1);
|
|
});
|
|
|
|
test('counts people already placed and leaves them where they are', () => {
|
|
const groups = [{ id: 'g1', name: 'A', capacity: 2 }, { id: 'g2', name: 'B', capacity: 2 }];
|
|
const attendees = [person('a', 'r1'), person('b', 'r2'), person('c', 'r3')];
|
|
const r = autoAllocate({ groups, attendees, assigned: new Map([['a', 'g1'], ['b', 'g1']]) });
|
|
expect(r.placements).toEqual([{ attendeeId: 'c', groupId: 'g2' }]);
|
|
});
|
|
});
|