} }); return itens; } function recSummary(recursos){ if(!recursos || !recursos.length) return ''; return recursos.map(i=>{ const n = getRecCatalog().find(e=>e.id===i.id)?.nome || 'Recurso'; return `${n} ×${i.qtd}`; }).join('; '); } // Regra: se “Lousa Interativa” estiver marcada, qualquer “projetor” fica indisponível function enforceMutualExclusion(){ const cat = getRecCatalog(); const lousa = cat.find(r=> r.nome.toLowerCase()==='lousa interativa'); const lousaChecked = lousa ? document.querySelector(`.rec-check[data-id="${lousa.id}"]`)?.checked : false; cat.forEach(rc=>{ if(isProjetor(rc.nome)){ const chk = document.querySelector(`.rec-check[data-id="${rc.id}"]`); const qty = document.querySelector(`.rec-qty[data-id="${rc.id}"]`); if(!chk || !qty) return; if(lousaChecked){ chk.checked = false; chk.disabled = true; qty.disabled = true; chk.title = 'Indisponível junto com Lousa Interativa'; }else{ chk.disabled = false; qty.disabled = !chk.checked; chk.title = ''; } } }); } /* =========================== Seções base =========================== */ function popularEscolas(sel){ sel.innerHTML=''; ESCOLAS.forEach(e=>{ const o=document.createElement('option'); o.value=e.id; o.textContent=`${e.nome} — ${e.municipio}`; sel.appendChild(o); }); } function popularSalas(sel, escolaId, incluirTodas=false){ sel.innerHTML=''; if(incluirTodas){ const t=document.createElement('option'); t.value=''; t.textContent='Todas'; sel.appendChild(t); } SALAS.filter(s=>!escolaId || s.escola_id===escolaId).forEach(s=>{ const o=document.createElement('option'); o.value=s.id; o.textContent=s.nome; sel.appendChild(o); }); } function setTab(tab){ const tabs = ['reservar','quadro','painel']; tabs.forEach(t=>{ $('#tab-'+t).classList.toggle('active', t===tab); $('#sec-'+t).style.display = (t===tab)?'block':'none'; }); if(tab==='quadro') renderQuadro(); if(tab==='painel') renderPainel(); } /* =========================== Regras / Reservar / Quadro =========================== */ function existeConflito({sala_id, data, ini, fim}){ const A1 = toMin(ini), A2 = toMin(fim); return state.reservas.some(r=>{ if(r.sala_id!==sala_id || r.data!==data) return false; const B1 = toMin(r.ini), B2 = toMin(r.fim); return overlap(A1,A2,B1,B2); }); } function reservar(){ const escola_id = $('#escola').value; const sala_id = $('#sala').value; const data = $('#data').value; const ini = $('#ini').value; const fim = $('#fim').value; const prof = $('#prof').value.trim(); const turma = $('#turma').value.trim(); const contato = $('#contato').value.trim(); const obs = $('#obs').value.trim(); const recursos = coletarRecursosSelecionados(); if(!escola_id || !sala_id || !data || !ini || !fim || !prof){ alert('Preencha Escola, Sala, Data, Início, Fim e Professor.'); return; } if(toMin(fim) <= toMin(ini)){ alert('Hora fim deve ser maior que hora início.'); return; } if(existeConflito({sala_id,data,ini,fim})){ alert('Conflito: já existe reserva nesta sala nesse intervalo.'); return; } // valida disponibilidade e regra da Lousa x Projetor const cat = getRecCatalog(); const temLousa = recursos.some(r=> cat.find(c=>c.id===r.id)?.nome.toLowerCase()==='lousa interativa'); const temProj = recursos.some(r=> isProjetor(cat.find(c=>c.id===r.id)?.nome)); if(temLousa && temProj){ alert('Não é necessário (nem permitido) reservar Projetor junto com Lousa Interativa.'); return; } for(const item of recursos){ const disp = disponibilidadeRecurso(item.id, data, ini, fim); if(item.qtd > disp){ const nome = cat.find(e=>e.id===item.id)?.nome || 'Recurso'; alert(`Sem disponibilidade para ${nome}. Restantes: ${disp}. Ajuste a quantidade ou horário.`); return; } } const novo = { id: crypto.randomUUID?.() || String(Date.now()), escola_id, sala_id, data, ini, fim, prof, turma, contato, obs, recursos, // [{id,qtd}] criado_em:new Date().toISOString() }; state.reservas.push(novo); save(state.reservas, STORAGE_KEY); alert('Reserva criada com sucesso!'); state.painel.data = data; state.painel.escola = escola_id; limparForm(); setTab('painel'); renderPainel(); atualizarDisponibilidadeRecurso(); } function limparForm(){ $('#data').value=''; $('#ini').value=''; $('#fim').value=''; $('#prof').value=''; $('#turma').value=''; $('#contato').value=''; $('#obs').value=''; $$('.rec-check').forEach(c=> { c.checked=false; c.disabled=false; c.title=''; }); $$('.rec-qty').forEach(q=>{ q.value=1; q.disabled=true; }); $('#rec-hint').textContent = 'Selecione data e horário para ver disponibilidade'; getRecCatalog().forEach(rc=> $('#ava-'+rc.id).textContent = `Estoque: ${rc.estoque}`); enforceMutualExclusion(); } function aplicarFiltro(){ state.filtro.escola = $('#q-escola').value; state.filtro.sala = $('#q-sala').value; state.filtro.ini = $('#q-ini').value; state.filtro.fim = $('#q-fim').value; state.filtro.prof = $('#q-prof').value.trim().toLowerCase(); renderQuadro(); } function limparFiltro(){ $('#q-escola').value=''; $('#q-sala').value=''; $('#q-ini').value=''; $('#q-fim').value=''; $('#q-prof').value=''; aplicarFiltro(); } function renderQuadro(){ const tgt = $('#quadro'); const list = state.reservas .filter(r=> !state.filtro.escola || r.escola_id===state.filtro.escola) .filter(r=> !state.filtro.sala || r.sala_id===state.filtro.sala) .filter(r=> !state.filtro.prof || r.prof.toLowerCase().includes(state.filtro.prof)) .filter(r=>{ if(state.filtro.ini && r.data < state.filtro.ini) return false; if(state.filtro.fim && r.data > state.filtro.fim) return false; return true; }) .sort((a,b)=> (a.data+a.ini).localeCompare(b.data+b.ini)); if(list.length===0){ tgt.innerHTML = '
Sem agendamentos no período/critério informado.
'; return; } let html = '
'+ ''+ ''; list.forEach(r=>{ const esc = ESCOLAS.find(e=>e.id===r.escola_id)?.nome || '-'; const sala= SALAS.find(s=>s.id===r.sala_id)?.nome || '-'; html += ``; }); html += '
DataInícioFimEscolaSalaProfessorTurmaRecursosContatoObs.
${fmtDataBR(r.data)} ${r.ini} ${r.fim} ${esc} ${sala} ${r.prof} ${r.turma||''} ${recSummary(r.recursos)||''} ${r.contato||''} ${(r.obs||'')}
'; tgt.innerHTML = html; } /* =========================== Export / ICS =========================== */ function exportCSV(){ const rows = [['Data','Início','Fim','Escola','Sala','Professor','Turma','Recursos','Contato','Observações']]; state.reservas .sort((a,b)=> (a.data+a.ini).localeCompare(b.data+b.ini)) .forEach(r=>{ const esc=ESCOLAS.find(e=>e.id===r.escola_id)?.nome||'-'; const sala=SALAS.find(s=>s.id===r.sala_id)?.nome||'-'; rows.push([fmtDataBR(r.data),r.ini,r.fim,esc,sala,r.prof,r.turma||'',recSummary(r.recursos),r.contato||'',(r.obs||'').replace(/\n/g,' ')]); }); const csv = rows.map(line=> line.map(v=> `"${String(v).replace(/"/g,'""')}"`).join(',') ).join('\r\n'); download('agendamentos.csv', csv, 'text/csv'); } function baixarICS(){ const escola_id = $('#escola').value; const sala_id = $('#sala').value; const data = $('#data').value; const ini = $('#ini').value; const fim = $('#fim').value; const prof = $('#prof').value.trim(); const recursos = recSummary(coletarRecursosSelecionados()); if(!escola_id || !sala_id || !data || !ini || !fim || !prof){ alert('Preencha Escola, Sala, Data, Início, Fim e Professor.'); return; } const esc = ESCOLAS.find(e=>e.id===escola_id)?.nome||'-'; const sala= SALAS.find(s=>s.id===sala_id)?.nome||'-'; const ics = makeICS({ titulo:`Reserva — ${esc} — ${sala}`, descricao:`Professor: ${prof}${recursos?('\\nRecursos: '+recursos):''}`, local:`${esc} — ${sala}`, dateISO:data, ini, fim }); download(`Reserva-${data}-${sala}.ics`, ics, 'text/calendar'); } /* =========================== Painel Visual (Dia) =========================== */ function horasDoDia(){ const slots = []; let t = toMin(DIA_INICIO), end = toMin(DIA_FIM); while(t <= end){ slots.push(fromMin(t)); t += SLOT_MIN; } return slots; } function renderPainel(){ const escolaId = state.painel.escola || ESCOLAS[0]?.id; const data = state.painel.data; $('#pv-escola').value = escolaId; $('#pv-data').value = data; const times = horasDoDia(); const timeCol = $('#pv-times'); timeCol.innerHTML = ''; times.forEach(h=>{ const div=document.createElement('div'); div.className='cell'; div.textContent=h; timeCol.appendChild(div); }); const salas = SALAS.filter(s=> s.escola_id===escolaId); const header = $('#pv-header'); header.style.gridTemplateColumns = `repeat(${salas.length}, 1fr)`; header.innerHTML=''; salas.forEach(s=>{ const c=document.createElement('div'); c.className='pv-col'; c.textContent=s.nome; header.appendChild(c); }); const canvas = $('#pv-canvas'); canvas.innerHTML=''; const rows = times.length-1; canvas.style.height = (rows*28 + 8) + 'px'; const reservasDia = state.reservas.filter(r=> r.escola_id===escolaId && r.data===data); reservasDia.forEach(r=>{ const salaIdx = salas.findIndex(s=> s.id===r.sala_id); if(salaIdx<0) return; const y1 = Math.max(0, Math.floor((toMin(r.ini)-toMin(DIA_INICIO))/SLOT_MIN)); const y2 = Math.min(rows, Math.ceil((toMin(r.fim)-toMin(DIA_INICIO))/SLOT_MIN)); const top = y1*28 + 2; const height = Math.max(26, (y2 - y1)*28 - 4); const width = (canvas.clientWidth / (salas.length)) - 8; const left = (canvas.clientWidth / (salas.length))*salaIdx + 4; const b = document.createElement('div'); b.className='pv-slot'; b.style.top = top+'px'; b.style.left = left+'px'; b.style.width = width+'px'; b.style.height = height+'px'; const rs = recSummary(r.recursos); b.title = `${r.ini}–${r.fim} • ${r.prof}${r.turma?(' • '+r.turma):''}${rs?(' • '+rs):''}`; b.innerHTML = `${r.ini}–${r.fim}
${r.prof}${r.turma?(' • '+r.turma):''}${rs?('
'+rs+''):''}`; canvas.appendChild(b); }); } /* =========================== Mini calendário =========================== */ function renderMiniCal(){ const cont = $('#mc-grid'); cont.innerHTML=''; const base = state.painel.mcDate; const {y,m,firstDow,days} = monthInfo(base); $('#mc-title').textContent = base.toLocaleDateString('pt-BR',{month:'long', year:'numeric'}); const week = ['D','S','T','Q','Q','S','S']; week.forEach(w=>{ const h=document.createElement('div'); h.className='mc-head'; h.textContent=w; cont.appendChild(h); }); for(let i=0;i r.escola_id===escolaId && r.data.startsWith(monthStr)).map(r=> r.data.split('-')[2])); for(let d=1; d<=days; d++){ const iso = `${y}-${pad2(m+1)}-${pad2(d)}`; const a = document.createElement('div'); a.className='mc-day'; a.textContent = d; if(diasCom.has(pad2(d))) a.classList.add('has'); if(iso===hojeISO()) a.classList.add('today'); a.style.cursor='pointer'; a.addEventListener('click', ()=>{ state.painel.data = iso; renderPainel(); }); cont.appendChild(a); } } /* =========================== Init =========================== */ (function init(){ ensureRecCatalog(); // carrega/garante catálogo (sem projetor) // selects reservar popularEscolas($('#escola')); popularSalas($('#sala'), $('#escola').value); $('#escola').addEventListener('change', e=> popularSalas($('#sala'), e.target.value)); // UI de recursos renderRecForm(); ['data','ini','fim'].forEach(id=> $('#'+id).addEventListener('change', atualizarDisponibilidadeRecurso)); // Gerenciador renderRecManager(); $('#rec-save').addEventListener('click', saveRecCatalog); $('#rec-reset').addEventListener('click', resetRecCatalog); // selects quadro const qEscola = $('#q-escola'); qEscola.innerHTML=''; ESCOLAS.forEach(e=>{ const o=document.createElement('option'); o.value=e.id; o.textContent=e.nome; qEscola.appendChild(o); }); const qSala = $('#q-sala'); qSala.innerHTML=''; SALAS.forEach(s=>{ const o=document.createElement('option'); o.value=s.id; o.textContent=s.nome; qSala.appendChild(o); }); // Painel Visual selects const pvEscola = $('#pv-escola'); pvEscola.innerHTML=''; ESCOLAS.forEach(e=>{ const o=document.createElement('option'); o.value=e.id; o.textContent=e.nome; pvEscola.appendChild(o); }); pvEscola.addEventListener('change', e=>{ state.painel.escola = e.target.value; renderMiniCal(); renderPainel(); }); // datas padrão $('#q-ini').value = hojeISO(); $('#q-fim').value = hojeISO(); state.painel.escola = ESCOLAS[0]?.id || ''; $('#pv-data').value = state.painel.data = hojeISO(); // ações $('#tab-reservar').addEventListener('click', ()=>setTab('reservar')); $('#tab-quadro').addEventListener('click', ()=>setTab('quadro')); $('#tab-painel').addEventListener('click', ()=>setTab('painel')); $('#btn-salvar').addEventListener('click', reservar); $('#btn-limpar').addEventListener('click', ()=>{ limparForm(); }); $('#btn-ics').addEventListener('click', baixarICS); $('#q-buscar').addEventListener('click', aplicarFiltro); $('#q-limpar').addEventListener('click', limparFiltro); $('#btn-export').addEventListener('click', exportCSV); // navegadores de dia (painel) $('#pv-prev').addEventListener('click', ()=>{ const d=new Date(state.painel.data); d.setDate(d.getDate()-1); state.painel.data=d.toISOString().slice(0,10); $('#pv-data').value=state.painel.data; renderPainel(); }); $('#pv-next').addEventListener('click', ()=>{ const d=new Date(state.painel.data); d.setDate(d.getDate()+1); state.painel.data=d.toISOString().slice(0,10); $('#pv-data').value=state.painel.data; renderPainel(); }); $('#pv-today').addEventListener('click', ()=>{ state.painel.data = hojeISO(); $('#pv-data').value=state.painel.data; renderPainel(); }); $('#pv-data').addEventListener('change', e=>{ state.painel.data = e.target.value; renderPainel(); }); // mini-calendário $('#mc-prev').addEventListener('click', ()=>{ const d=state.painel.mcDate; d.setMonth(d.getMonth()-1); renderMiniCal(); }); $('#mc-next').addEventListener('click', ()=>{ const d=state.painel.mcDate; d.setMonth(d.getMonth()+1); renderMiniCal(); }); // expiração automática limparAntigos(); // primeira render renderQuadro(); renderMiniCal(); renderPainel(); })();