/* PCO Plan — which service type THIS dashboard is looking at. The module's server follows one service type at a time: whichever the admin's priority order lands on. A dashboard may want a different one on screen — the other campus, the youth plan — without changing what the server follows or what any other screen sees. That choice belongs to the browser, so it lives here: one value for the whole page, kept in localStorage and shared between every PCO Plan tile on it, so switching from one tile moves them all together. */ const KEY = 'pco-plan:viewed-service-type'; /** One bus per page, however many tiles import this. */ function bus() { if (!window.__pcoPlanViewed) { let value = ''; try { value = String(localStorage.getItem(KEY) || ''); } catch { value = ''; // private window, storage off: the choice just won't persist } window.__pcoPlanViewed = { value, listeners: new Set() }; } return window.__pcoPlanViewed; } /** '' means "whatever the server is following" — the normal case. */ export function viewedType() { return bus().value; } export function setViewedType(id) { const b = bus(); const next = String(id || ''); if (next === b.value) return; b.value = next; try { if (next) localStorage.setItem(KEY, next); else localStorage.removeItem(KEY); } catch { /* not persisted, still shared for this page */ } for (const fn of [...b.listeners]) { try { fn(next); } catch (err) { console.error('[pco-plan] viewed-type listener failed:', err); } } } /** Returns an unsubscribe — call it from the tile's stop(). */ export function onViewedType(fn) { const b = bus(); b.listeners.add(fn); return () => b.listeners.delete(fn); } /** * The plan a tile should draw, given the whole state and this page's choice: * the one the server follows, or one of the others it also reads. * * `live` is false for anything but the followed one — the ProPresenter match, * the service clock and the timers belong to the service actually running, so * a tile looking elsewhere must not pretend to have them. */ export function viewOf(state) { const empty = { id: '', name: '', plan: null, items: [], teams: [], teamsError: '', live: true, lastError: '' }; if (!state) return empty; const wanted = viewedType(); const activeId = state.serviceType?.id || ''; if (!wanted || wanted === activeId) { return { id: activeId, name: state.serviceType?.name || '', plan: state.plan, items: state.items || [], teams: state.teams || [], teamsError: state.teamsError || '', live: true, lastError: '', }; } const other = (state.others || {})[wanted]; if (!other) { // Ticked off in admin since this dashboard chose it, or not read yet. return { ...empty, id: wanted, live: false, lastError: 'That service type is no longer selected' }; } return { id: other.id, name: other.name, plan: other.plan, items: other.items || [], teams: other.teams || [], teamsError: other.teamsError || '', live: false, lastError: other.lastError || '', }; } /** Every service type a dashboard may switch to, in the admin's priority order. */ export function choices(state) { const out = []; const activeId = state?.serviceType?.id || ''; for (const id of state?.serviceTypes || []) { if (id === activeId) out.push({ id, name: state.serviceType?.name || id, following: true }); else out.push({ id, name: (state.others || {})[id]?.name || id, following: false }); } return out; } const SWITCH_SVG = ''; /** * The title-bar control that switches this dashboard between the selected * service types. Added by every PCO Plan tile, because switching from any of * them moves all of them; it hides itself whenever there is only one service * type to choose from, which is the usual case. * * Returns { sync } — call it when the state arrives so the control appears, * disappears or re-labels itself. */ export function addServiceTypeSwitch({ moduleApi, getState }) { const menu = moduleApi.header.addMenu({ icon: SWITCH_SVG, title: 'Service type shown on this dashboard', build(el) { el.classList.add('pp-menu'); const state = getState(); const wanted = viewedType(); const rows = [ { id: '', name: 'Whatever is live', following: false, auto: true }, ...choices(state), ]; for (const row of rows) { const item = document.createElement('button'); item.type = 'button'; item.className = 'pp-menu-item' + (row.id === wanted ? ' is-on' : ''); const tick = document.createElement('span'); tick.className = 'pp-menu-tick'; tick.textContent = '✓'; const name = document.createElement('span'); name.className = 'pp-menu-name'; name.textContent = row.name; item.append(tick, name); if (row.following) { const note = document.createElement('span'); note.className = 'pp-menu-note'; note.textContent = 'live'; item.appendChild(note); } item.addEventListener('click', () => { setViewedType(row.id); menu.close(); }); el.appendChild(item); } }, }); return { sync() { const state = getState(); // One service type is no choice at all — rule 11, nothing in the bar // that has nothing to do. const many = (state?.serviceTypes || []).length > 1; menu.button.hidden = !many; const wanted = viewedType(); menu.button.classList.toggle('active', Boolean(wanted) && wanted !== (state?.serviceType?.id || '')); }, }; }