/* Admin → Messaging: broadcasts + parent threads */
function DispatchThread() {
const SEED = [
{ from:'dispatch', t:'Morning Marcus — Elm Village Dr is closed for repairs. Use the Riverstone detour for stop 3.' },
{ from:'driver', t:'Got it, taking the detour now.' },
{ from:'dispatch', t:'Thanks. Let us know if you fall behind schedule.' },
];
const [thread, setThread] = React.useState([]);
const [draft, setDraft] = React.useState('');
React.useEffect(() => {
JLBackend.seed({ dispatch: SEED });
setThread(JLBackend.get('dispatch', SEED));
return JLBackend.subscribe('dispatch', v => setThread(v || []));
}, []);
const send = () => {
const t = draft.trim(); if (!t) return;
JLBackend.update('dispatch', prev => [...(prev||[]), { from:'dispatch', t }], SEED);
setDraft('');
};
return (
Live}>
{thread.map((b,i)=>{ const me = b.from==='dispatch'; return (
{b.t}
); })}
setDraft(e.target.value)} onKeyDown={e=>{ if(e.key==='Enter') send(); }} placeholder="Message the driver…" style={{ flex:1, border:'1px solid var(--gray-200)', borderRadius:'999px', padding:'10px 15px', fontSize:13.5, fontFamily:'var(--font-sans)', outline:'none', color:'var(--ink)' }} />
);
}
function NotifReachPanel() {
const CATS = [
{ id:'alerts', label:'Emergency alerts', icon:'siren' },
{ id:'pickup', label:'Pickup & bus', icon:'car-front' },
{ id:'attend', label:'Attendance', icon:'calendar-check' },
{ id:'grades', label:'Grades', icon:'graduation-cap' },
{ id:'messages', label:'Messages', icon:'message-circle' },
{ id:'payments', label:'Payments', icon:'wallet' },
{ id:'events', label:'Events', icon:'calendar-days' },
];
const [prefs, setPrefs] = React.useState(null);
React.useEffect(() => {
const read = () => { try { setPrefs(JSON.parse(localStorage.getItem('jl_notif') || 'null')); } catch(e){} };
read();
const onStore = (e) => { if (e.key === 'jl_notif') read(); };
window.addEventListener('storage', onStore);
const iv = setInterval(read, 1500);
return () => { window.removeEventListener('storage', onStore); clearInterval(iv); };
}, []);
const Dot = ({ on }) => {on?'On':'Off'};
return (
live from parent app} pad={false}>
{!prefs ? (
This family hasn't set notification preferences yet — defaults apply (push on).
) : (
CategoryPushSMS
{CATS.map((c,i) => { const p = prefs[c.id] || {}; return (
{c.label}
); })}
)}
);
}
function MessagingScreen() {
const [tab, setTab] = React.useState('broadcasts');
const broadcasts = [
{ t:'Early dismissal this Friday at 1:00 PM', aud:'All Parents · Katy Middle', time:'2h ago', reach:'1,248 sent', icon:'megaphone', tint:'var(--blue-100)', ic:'var(--blue-500)' },
{ t:'Bus #7 running late on Route 3', aud:'Route 3 Families', time:'3h ago', reach:'19 sent', icon:'bus', tint:'var(--danger-100)', ic:'var(--danger-500)' },
{ t:'Spring book fair next week', aud:'All Parents', time:'1d ago', reach:'1,248 sent', icon:'book-open', tint:'var(--gold-100)', ic:'var(--gold-600)' },
];
const threads = [
{ n:'Jermaine Hatten', re:'Re: EJ pickup change', prev:'Thank you, that works for us.', time:'12m', c:'navy', unread:true },
{ n:'Ashley Brooks', re:'Bus stop question', prev:'Is the morning stop still at 7:40?', time:'1h', c:'blue', unread:true },
{ n:'Marcus Lee', re:'Lunch balance', prev:'Just added funds — thanks!', time:'4h', c:'green', unread:false },
];
return (
{/* composer */}
{/* parent threads */}
2 new}>
{threads.map((t,i)=>(
jlToast('Opening thread with '+t.n, { icon:'message-circle' })} style={{ display:'flex', alignItems:'flex-start', gap:11, padding:'11px 0', borderBottom: i
w[0]).join('')} color={t.c} size={34} />
{t.time}
{t.unread && }
))}
{/* live dispatch thread (wired to driver app) */}
{/* sent broadcasts */}
jlToast('Showing all broadcasts', { icon:'megaphone' })} style={{ fontSize:12.5, fontWeight:700, color:'var(--blue-500)', cursor:'pointer' }}>View All}>
{broadcasts.map((b,i)=>(
))}
);
}
const selStyle = { flex:1, border:'1px solid var(--gray-200)', borderRadius:10, padding:'10px 12px', fontSize:13.5, fontFamily:'var(--font-sans)', color:'var(--ink)', background:'#fff', outline:'none' };
Object.assign(window, { MessagingScreen });