Show markup
<bmx-shift-roster id="ex-sr" label="Willow Court care home, two weeks" today="2026-10-14" locale="en-GB" file-name="willow-court-roster"></bmx-shift-roster>
<p class="note" id="ex-sr-out" role="status">Fifteen staff, four shifts and the cover the home needs, with the first two days planned. Choose a shift above the grid and drag across days to paint it, type E, L, D or N in a cell, or press Auto-fill and see what it could not cover and why.</p>
<div class="row" style="gap: 0.75rem; flex-wrap: wrap; align-items: center; margin-block: 0.5rem 1.5rem">
<button type="button" id="ex-sr-fill">Auto-fill the second week only</button>
<label><input type="checkbox" id="ex-sr-readonly" /> Read only</label>
</div>
<script type="module">
await customElements.whenDefined('bmx-shift-roster');
const el = document.getElementById('ex-sr');
const out = document.getElementById('ex-sr-out');
el.roster = {
start: '2026-10-12',
days: 14,
currency: 'GBP',
rules: { minRestHours: 11, maxWeekHours: 48, maxDaysInRow: 6, maxNightsInRow: 4 },
shifts: [
{ id: 'early', code: 'E', name: 'Early', start: '07:00', end: '14:30', breakMinutes: 30, skills: [] },
{ id: 'late', code: 'L', name: 'Late', start: '14:00', end: '22:00', breakMinutes: 30, skills: [] },
{ id: 'long', code: 'D', name: 'Long day', start: '07:00', end: '19:30', breakMinutes: 60, skills: [] },
{ id: 'night', code: 'N', name: 'Night', start: '21:45', end: '07:15', breakMinutes: 45, skills: [] },
],
people: [
{ id: 'p1', name: 'Grace Okafor', role: 'Nurse', skills: ['RN', 'Meds'], minHours: 36, maxHours: 48, rate: 24.5, absences: [] },
{ id: 'p2', name: 'Tomasz Nowak', role: 'Nurse', skills: ['RN', 'Meds'], minHours: 36, maxHours: 48, rate: 24.5, absences: [{ from: '2026-10-19', to: '2026-10-21', reason: 'Annual leave', kind: 'hard' }] },
{ id: 'p3', name: 'Priya Raman', role: 'Nurse', skills: ['RN', 'Meds'], minHours: 22, maxHours: 30, rate: 25, absences: [] },
{ id: 'p4', name: 'Hannah Clarke', role: 'Nurse (bank)', skills: ['RN', 'Meds'], maxHours: 36, rate: 29, absences: [{ from: '2026-10-24', to: '2026-10-25', reason: 'Prefers weekends off', kind: 'preference' }] },
{ id: 'p15', name: 'Agency nurse', role: 'Nurse (agency)', skills: ['RN', 'Meds'], maxHours: 24, rate: 41, absences: [] },
{ id: 'p5', name: 'Samuel Mensah', role: 'Senior carer', skills: ['Meds', 'Senior'], minHours: 36, maxHours: 48, rate: 15.2, absences: [] },
{ id: 'p6', name: 'Lucy Fairbairn', role: 'Senior carer', skills: ['Meds', 'Senior'], minHours: 36, maxHours: 48, rate: 15.2, absences: [] },
{ id: 'p7', name: 'Daniel Byrne', role: 'Carer', skills: ['Meds'], minHours: 30, maxHours: 45, rate: 13.4, absences: [] },
{ id: 'p8', name: 'Aisha Rahman', role: 'Carer', skills: [], minHours: 30, maxHours: 45, rate: 12.6, absences: [{ from: '2026-10-15', reason: 'Training day', kind: 'hard' }] },
{ id: 'p9', name: 'Owen Davies', role: 'Carer', skills: [], minHours: 30, maxHours: 45, rate: 12.6, absences: [] },
{ id: 'p10', name: 'Maria Santos', role: 'Carer', skills: ['Meds'], minHours: 24, maxHours: 36, rate: 13.4, absences: [{ from: '2026-10-18', reason: 'Asked for Sunday off', kind: 'preference' }] },
{ id: 'p11', name: 'Kofi Asante', role: 'Carer (nights)', skills: [], minHours: 30, maxHours: 45, rate: 13.1, absences: [] },
{ id: 'p12', name: 'Emma Lindqvist', role: 'Carer', skills: [], minHours: 16, maxHours: 24, rate: 12.6, absences: [] },
{ id: 'p13', name: 'Jack Harper', role: 'Carer', skills: [], minHours: 30, maxHours: 45, rate: 12.6, absences: [{ from: '2026-10-22', to: '2026-10-25', reason: 'Annual leave', kind: 'hard' }] },
{ id: 'p14', name: 'Chloe Martin', role: 'Carer (bank)', skills: [], maxHours: 30, rate: 14, absences: [] },
],
demand: [
{ id: 'rn-day', label: 'Nurse on duty', skill: 'RN', from: '07:00', to: '22:00', need: 1 },
{ id: 'rn-night', label: 'Nurse at night', skill: 'RN', from: '22:00', to: '07:00', need: 1 },
{ id: 'meds', label: 'Medication rounds', skill: 'Meds', from: '07:30', to: '10:00', need: 2 },
{ id: 'morning', label: 'Mornings', weekdays: [1, 2, 3, 4, 5], from: '07:00', to: '14:00', need: 4 },
{ id: 'morning-weekend', label: 'Weekend mornings', weekdays: [0, 6], from: '07:00', to: '14:00', need: 3 },
{ id: 'afternoon', label: 'Afternoons and evenings', from: '14:00', to: '22:00', need: 3 },
{ id: 'night', label: 'Nights', from: '22:00', to: '07:00', need: 2 },
],
assignments: [
{ id: 'a1', person: 'p1', date: '2026-10-12', shift: 'long', locked: true },
{ id: 'a2', person: 'p1', date: '2026-10-13', shift: 'long', locked: true },
{ id: 'a3', person: 'p3', date: '2026-10-12', shift: 'night', locked: true },
{ id: 'a4', person: 'p3', date: '2026-10-13', shift: 'night', locked: true },
{ id: 'a5', person: 'p5', date: '2026-10-12', shift: 'early' },
{ id: 'a6', person: 'p6', date: '2026-10-12', shift: 'late' },
{ id: 'a7', person: 'p6', date: '2026-10-13', shift: 'early' },
{ id: 'a8', person: 'p7', date: '2026-10-12', shift: 'early' },
{ id: 'a9', person: 'p8', date: '2026-10-12', shift: 'early' },
{ id: 'a10', person: 'p9', date: '2026-10-12', shift: 'late' },
{ id: 'a11', person: 'p11', date: '2026-10-12', shift: 'night', locked: true },
{ id: 'a12', person: 'p11', date: '2026-10-13', shift: 'night', locked: true },
{ id: 'a13', person: 'p10', date: '2026-10-13', shift: 'late' },
{ id: 'a14', person: 'p8', date: '2026-10-15', shift: 'early' },
],
};
const said = {
assign: 'Shift set',
unassign: 'Shift removed',
paint: 'Painted',
swap: 'Moved',
lock: 'Lock changed',
fill: 'Auto-filled',
clear: 'Unlocked shifts cleared',
repeat: 'First week repeated',
period: 'Period changed',
undo: 'Undone',
redo: 'Redone',
set: 'Roster replaced',
};
el.addEventListener('bmxChange', async e => {
if (e.detail.reason === 'fill') return;
const problems = await el.check();
const errors = problems.filter(p => p.severity === 'error').length;
const totals = await el.getTotals();
out.textContent = `${said[e.detail.reason] ?? 'Changed'}. ${totals.shifts} shifts, ${Math.round(totals.hours)} hours, £${Math.round(totals.cost).toLocaleString('en-GB')}; ${errors} problem${errors === 1 ? '' : 's'} to fix.`;
});
el.addEventListener('bmxAutoFill', e => {
out.textContent = `Auto-fill added ${e.detail.added.length} shifts. ${e.detail.gaps.length ? `${e.detail.gaps.length} gaps are left - the reasons are under the grid.` : 'Every need is covered.'}`;
});
document.getElementById('ex-sr-fill').addEventListener('click', () => {
const dates = Array.from({ length: 7 }, (_, i) => new Date(Date.UTC(2026, 9, 19 + i)).toISOString().slice(0, 10));
el.autoFill({ dates });
});
document.getElementById('ex-sr-readonly').addEventListener('change', e => (el.readonly = e.target.checked));
</script>