172 lines
5.8 KiB
JavaScript
172 lines
5.8 KiB
JavaScript
/* 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 = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 8h13l-3-3"/><path d="M20 16H7l3 3"/></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 || ''));
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|