}
});
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 += '
| Data | Início | Fim | Escola | Sala | Professor | Turma | Recursos | Contato | Obs. |
|---|
| ${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();
})();