/** * Supera Glia - Tab Grade de Horários * @module tabs/turmas/GradeTab * * Exibe a grade de horários das turmas em formato de tabela * com dias da semana nas colunas e salas nas linhas. */ (function() { 'use strict'; const { API_BASE } = SuperaGlia.config; SuperaGlia.initGradeTab = function() { const { useState, useEffect } = SuperaGlia.hooks; const { Icon, perfilColors, salaColors, AlunoDetalhesModal } = SuperaGlia; SuperaGlia.GradeTab = ({ dados, carregarDados }) => { // Estado local - carrega dados próprios como no HTML original const [calendario, setCalendario] = useState([]); const [ausencias, setAusencias] = useState([]); const [loading, setLoading] = useState(true); const [selectedTurma, setSelectedTurma] = useState(null); const [alunosTurma, setAlunosTurma] = useState([]); const [loadingAlunos, setLoadingAlunos] = useState(false); const [alunoSelecionado, setAlunoSelecionado] = useState(null); // Filtros const [turnoFiltro, setTurnoFiltro] = useState('todos'); const [salaFiltro, setSalaFiltro] = useState('todas'); const dias = ['Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado']; const salas = ['Neurônio', 'Glia', 'Online']; // Carregar dados do calendário na montagem useEffect(() => { carregarCalendario(); }, []); // Carregar dados do endpoint /calendario (como o original) const carregarCalendario = async () => { setLoading(true); try { const [calRes, ausRes] = await Promise.all([ fetch(`${API_BASE}/calendario`).then(r => r.json()), fetch(`${API_BASE}/ausencias`).then(r => r.json()).catch(() => ({ ausencias: [] })) ]); setCalendario(calRes.turmas || []); setAusencias(ausRes.ausencias || []); } catch (e) { console.error('Erro ao carregar calendário:', e); } setLoading(false); }; // Carregar alunos de uma turma específica const carregarAlunosTurma = async (turma) => { setSelectedTurma(turma); setLoadingAlunos(true); try { const res = await fetch(`${API_BASE}/turma-alunos?id=${turma.id}`); const data = await res.json(); // Enriquecer com dados de ausência const alunosEnriquecidos = (data.alunos || []).map(aluno => { const ausencia = ausencias.find(a => { if (a.cod_aluno && aluno.cod_aluno && String(a.cod_aluno) === String(aluno.cod_aluno)) return true; if (a.cod_aluno && aluno.id && String(a.cod_aluno) === String(aluno.id)) return true; const nomeAluno = (aluno.nome || '').toUpperCase().trim(); const nomeAusencia = (a.nome || '').toUpperCase().trim(); if (nomeAluno && nomeAusencia && nomeAluno === nomeAusencia) return true; return false; }); if (ausencia) { return { ...aluno, ausente_ate: ausencia.ausente_ate, motivo_ausencia: ausencia.motivo_ausencia, parcela_atual: ausencia.parcela_atual || aluno.parcela_atual }; } return aluno; }); setAlunosTurma(alunosEnriquecidos); } catch (e) { console.error('Erro ao carregar alunos:', e); setAlunosTurma([]); } setLoadingAlunos(false); }; // Fechar modal da turma const fecharModalTurma = () => { setSelectedTurma(null); setAlunosTurma([]); }; // Obter turmas por dia e sala const getTurmasPorDiaESala = (dia, sala) => { return calendario.filter(t => { // Aplicar filtros if (turnoFiltro !== 'todos') { const hora = parseInt(t.horario?.split(':')[0] || '12'); if (turnoFiltro === 'manha' && hora >= 12) return false; if (turnoFiltro === 'tarde' && (hora < 12 || hora >= 18)) return false; if (turnoFiltro === 'noite' && hora < 18) return false; } if (salaFiltro !== 'todas' && t.sala !== salaFiltro) return false; // Filtrar por dia e sala return t.dia_nome === dia && (t.sala === sala || (sala === 'Online' && t.sala === 'OnLine')); }); }; // Calcular vagas por ausência para uma turma const getVagasAusencia = (turma) => { const hoje = new Date(); hoje.setHours(0, 0, 0, 0); const ausenciasTurma = ausencias.filter(a => { if (!a.ausente_ate) return false; const ausenteAte = new Date(a.ausente_ate); ausenteAte.setHours(23, 59, 59, 999); if (ausenteAte < hoje) return false; const turmaAluno = (a.turma || '').toLowerCase(); const salaTurma = (turma.sala || '').toLowerCase(); const diaNome = (turma.dia_nome || '').toLowerCase().substring(0, 3); const hora = turma.horario?.split(' - ')[0]?.split(':')[0] || ''; const mesmaSala = turmaAluno.includes(salaTurma) || (salaTurma === 'online' && turmaAluno.includes('online')); const mesmoDia = turmaAluno.includes(diaNome) || (turma.dia_nome === 'Segunda' && turmaAluno.startsWith('2ª')) || (turma.dia_nome === 'Terça' && turmaAluno.startsWith('3ª')) || (turma.dia_nome === 'Quarta' && turmaAluno.startsWith('4ª')) || (turma.dia_nome === 'Quinta' && turmaAluno.startsWith('5ª')) || (turma.dia_nome === 'Sexta' && turmaAluno.startsWith('6ª')) || (turma.dia_nome === 'Sábado' && turmaAluno.includes('sábado')); const mesmoHorario = turmaAluno.includes(`(${hora}h`) || turmaAluno.includes(`(${hora}:`); return mesmaSala && mesmoDia && mesmoHorario; }); return ausenciasTurma.length; }; const getOcupacaoColor = (ocupacao, capacidade) => { const percent = (ocupacao / capacidade) * 100; if (percent >= 90) return 'bg-red-500'; if (percent >= 70) return 'bg-amber-500'; return 'bg-emerald-500'; }; const getVagasColor = (vagas) => { if (vagas <= 0) return 'text-red-600 bg-red-50'; if (vagas <= 3) return 'text-amber-600 bg-amber-50'; return 'text-emerald-600 bg-emerald-50'; }; // Contar turmas totais const totalTurmas = calendario.filter(t => { if (turnoFiltro !== 'todos') { const hora = parseInt(t.horario?.split(':')[0] || '12'); if (turnoFiltro === 'manha' && hora >= 12) return false; if (turnoFiltro === 'tarde' && (hora < 12 || hora >= 18)) return false; if (turnoFiltro === 'noite' && hora < 18) return false; } if (salaFiltro !== 'todas' && t.sala !== salaFiltro) return false; return true; }).length; // Loading if (loading) { return React.createElement('div', { className: 'flex items-center justify-center h-64' }, React.createElement('div', { className: 'text-center' }, React.createElement('div', { className: 'w-10 h-10 border-4 border-orange-200 border-t-orange-500 rounded-full animate-spin mx-auto mb-4' }), React.createElement('p', { className: 'text-gray-500' }, 'Carregando calendário...') ) ); } return React.createElement('div', { className: 'space-y-4' }, // Filtros React.createElement('div', { className: 'bg-white rounded-xl p-4 shadow-sm border border-gray-100' }, React.createElement('div', { className: 'flex flex-wrap gap-4 items-center justify-between' }, React.createElement('div', { className: 'flex gap-4' }, React.createElement('div', null, React.createElement('label', { className: 'block text-xs text-gray-500 mb-1' }, 'Turno'), React.createElement('select', { value: turnoFiltro, onChange: e => setTurnoFiltro(e.target.value), className: 'px-3 py-2 border border-gray-200 rounded-lg text-sm' }, React.createElement('option', { value: 'todos' }, 'Todos'), React.createElement('option', { value: 'manha' }, 'Manhã'), React.createElement('option', { value: 'tarde' }, 'Tarde'), React.createElement('option', { value: 'noite' }, 'Noite') ) ), React.createElement('div', null, React.createElement('label', { className: 'block text-xs text-gray-500 mb-1' }, 'Sala'), React.createElement('select', { value: salaFiltro, onChange: e => setSalaFiltro(e.target.value), className: 'px-3 py-2 border border-gray-200 rounded-lg text-sm' }, React.createElement('option', { value: 'todas' }, 'Todas'), salas.map(s => React.createElement('option', { key: s, value: s }, s)) ) ) ), React.createElement('div', { className: 'flex items-center gap-3' }, React.createElement('span', { className: 'text-sm text-gray-500' }, `${totalTurmas} turmas`), React.createElement('button', { onClick: carregarCalendario, className: 'px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded-lg text-sm font-medium transition-colors flex items-center gap-2' }, React.createElement(Icon, { name: 'refresh-cw', className: 'w-4 h-4' }), 'Atualizar' ) ) ) ), // Legenda React.createElement('div', { className: 'bg-white rounded-xl p-4 shadow-sm border border-gray-100' }, React.createElement('div', { className: 'flex flex-wrap items-center gap-4 text-sm' }, React.createElement('span', { className: 'font-medium text-gray-700' }, 'Legenda:'), React.createElement('div', { className: 'flex items-center gap-2' }, React.createElement('div', { className: 'w-3 h-3 rounded-full bg-emerald-500' }), React.createElement('span', { className: 'text-gray-600' }, 'Disponível') ), React.createElement('div', { className: 'flex items-center gap-2' }, React.createElement('div', { className: 'w-3 h-3 rounded-full bg-amber-500' }), React.createElement('span', { className: 'text-gray-600' }, 'Quase lotada') ), React.createElement('div', { className: 'flex items-center gap-2' }, React.createElement('div', { className: 'w-3 h-3 rounded-full bg-red-500' }), React.createElement('span', { className: 'text-gray-600' }, 'Lotada') ), React.createElement('div', { className: 'flex items-center gap-2' }, React.createElement('div', { className: 'w-3 h-3 rounded-full bg-blue-500' }), React.createElement('span', { className: 'text-gray-600' }, 'Vaga por ausência') ) ) ), // Grid do calendário (tabela) React.createElement('div', { className: 'bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden' }, React.createElement('div', { className: 'overflow-x-auto' }, React.createElement('table', { className: 'w-full min-w-[900px]' }, // Header com dias da semana React.createElement('thead', null, React.createElement('tr', { className: 'bg-gray-50' }, React.createElement('th', { className: 'p-3 text-left text-sm font-semibold text-gray-700 border-b w-24' }, 'Sala'), dias.map(dia => React.createElement('th', { key: dia, className: 'p-3 text-center text-sm font-semibold text-gray-700 border-b border-l' }, dia)) ) ), // Body com salas e turmas React.createElement('tbody', null, salas.map(sala => { const salaColor = salaColors[sala] || salaColors['Neurônio']; return React.createElement('tr', { key: sala, className: 'border-b last:border-b-0' }, React.createElement('td', { className: `p-3 font-medium ${salaColor.light} ${salaColor.text} border-r` }, React.createElement('div', { className: 'flex items-center gap-2' }, React.createElement('div', { className: `w-3 h-3 rounded-full ${salaColor.bg}` }), sala ) ), dias.map(dia => { const turmasDia = getTurmasPorDiaESala(dia, sala); return React.createElement('td', { key: `${sala}-${dia}`, className: 'p-2 border-l align-top' }, React.createElement('div', { className: 'space-y-2 min-h-[80px]' }, turmasDia.length === 0 ? React.createElement('div', { className: 'text-center text-gray-300 text-xs py-4' }, 'Sem turma') : turmasDia.map(turma => { const vagasAusencia = getVagasAusencia(turma); return React.createElement('button', { key: turma.id, onClick: () => carregarAlunosTurma(turma), className: `turma-card w-full p-2 rounded-lg border ${selectedTurma?.id === turma.id ? 'ring-2 ring-orange-500 border-orange-300' : 'border-gray-200 hover:border-gray-300'} bg-white text-left` }, React.createElement('div', { className: 'flex items-center justify-between mb-1' }, React.createElement('span', { className: 'text-xs font-medium text-gray-900' }, turma.horario), React.createElement('div', { className: 'flex items-center gap-1' }, React.createElement('span', { className: `text-xs px-1.5 py-0.5 rounded ${getVagasColor(turma.vagas)}` }, `${turma.vagas} vagas`), vagasAusencia > 0 && React.createElement('span', { className: 'text-xs px-1.5 py-0.5 rounded bg-blue-100 text-blue-700', title: 'Vagas temporárias por ausência' }, `+${vagasAusencia}` ) ) ), // Barra de ocupação React.createElement('div', { className: 'flex items-center gap-2' }, React.createElement('div', { className: 'flex-1 h-1.5 bg-gray-100 rounded-full overflow-hidden' }, React.createElement('div', { className: `h-full ${getOcupacaoColor(turma.ocupacao, turma.capacidade)}`, style: { width: `${Math.min(100, (turma.ocupacao / turma.capacidade) * 100)}%` } }) ), React.createElement('span', { className: 'text-xs text-gray-500' }, `${turma.ocupacao}/${turma.capacidade}`) ), // Barra azul se houver vagas por ausência vagasAusencia > 0 && React.createElement('div', { className: 'flex items-center gap-2 mt-1' }, React.createElement('div', { className: 'flex-1 h-1 bg-gray-100 rounded-full overflow-hidden' }, React.createElement('div', { className: 'h-full bg-blue-400', style: { width: `${Math.min(100, ((turma.ocupacao - vagasAusencia) / turma.capacidade) * 100)}%` } }) ), React.createElement('span', { className: 'text-xs text-blue-600' }, `${turma.ocupacao - vagasAusencia}/${turma.capacidade}`) ), // Perfil React.createElement('div', { className: 'mt-1' }, React.createElement('span', { className: `text-xs px-1.5 py-0.5 rounded ${(perfilColors[turma.perfil] || perfilColors['60+']).badge}` }, turma.perfil) ) ); }) ) ); }) ); }) ) ) ) ), // ========== MODAL POPUP DOS ALUNOS DA TURMA ========== selectedTurma && React.createElement('div', { className: 'fixed inset-0 z-50 flex items-center justify-center p-4', style: { backgroundColor: 'rgba(0, 0, 0, 0.5)' }, onClick: (e) => { // Fechar ao clicar no overlay (fundo escuro) if (e.target === e.currentTarget) fecharModalTurma(); } }, React.createElement('div', { className: 'bg-white rounded-2xl shadow-2xl w-full max-w-4xl max-h-[85vh] overflow-hidden animate-in fade-in zoom-in duration-200', onClick: (e) => e.stopPropagation() // Evitar fechar ao clicar no modal }, // Header do modal React.createElement('div', { className: 'flex items-center justify-between p-5 border-b border-gray-100 bg-gradient-to-r from-orange-50 to-white' }, React.createElement('div', null, React.createElement('h3', { className: 'text-xl font-bold text-gray-900 flex items-center gap-2' }, React.createElement(Icon, { name: 'users', className: 'w-5 h-5 text-orange-500' }), `${selectedTurma.sala} - ${selectedTurma.dia_nome}` ), React.createElement('p', { className: 'text-sm text-gray-500 mt-1' }, `${selectedTurma.horario} • ${selectedTurma.perfil} • ${selectedTurma.ocupacao}/${selectedTurma.capacidade} alunos` ) ), React.createElement('button', { onClick: fecharModalTurma, className: 'p-2 hover:bg-gray-100 rounded-lg transition-colors', title: 'Fechar' }, React.createElement(Icon, { name: 'x', className: 'w-6 h-6 text-gray-500' }) ) ), // Legenda dos chips React.createElement('div', { className: 'flex items-center gap-4 px-5 py-3 bg-gray-50 border-b border-gray-100 text-xs text-gray-500' }, React.createElement('span', null, 'Clique em um aluno para ver detalhes'), React.createElement('div', { className: 'flex items-center gap-1' }, React.createElement('div', { className: 'w-2 h-2 rounded-full bg-blue-400' }), React.createElement('span', null, 'Aluno ausente') ) ), // Conteúdo do modal (lista de alunos) React.createElement('div', { className: 'p-5 overflow-y-auto', style: { maxHeight: 'calc(85vh - 140px)' } }, loadingAlunos ? React.createElement('div', { className: 'text-center py-12' }, React.createElement('div', { className: 'w-10 h-10 border-4 border-orange-200 border-t-orange-500 rounded-full animate-spin mx-auto mb-4' }), React.createElement('p', { className: 'text-gray-500' }, 'Carregando alunos...') ) : alunosTurma.length === 0 ? React.createElement('div', { className: 'text-center py-12' }, React.createElement(Icon, { name: 'users', className: 'w-12 h-12 text-gray-300 mx-auto mb-3' }), React.createElement('p', { className: 'text-gray-500' }, 'Nenhum aluno nesta turma') ) : React.createElement('div', { className: 'grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3' }, alunosTurma.map(aluno => { const colors = perfilColors[aluno.perfil] || perfilColors['60+']; const isAusente = aluno.ausente_ate && new Date(aluno.ausente_ate) >= new Date(); const formatDateShort = (d) => { if (!d) return ''; const date = new Date(d); return date.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' }); }; return React.createElement('button', { key: aluno.id || aluno.cod_aluno, onClick: () => setAlunoSelecionado(aluno), className: `p-3 rounded-xl border-2 ${isAusente ? 'border-blue-300 bg-blue-50' : 'border-gray-200 hover:border-orange-300 bg-white'} text-left transition-all hover:shadow-md` }, React.createElement('p', { className: 'text-sm font-semibold text-gray-900 truncate' }, aluno.nome), React.createElement('div', { className: 'flex items-center justify-between mt-2' }, React.createElement('span', { className: `text-xs px-2 py-1 rounded-full ${colors.badge}` }, aluno.perfil), isAusente && React.createElement('span', { className: 'text-xs text-blue-600 font-medium' }, `até ${formatDateShort(aluno.ausente_ate)}`) ) ); }) ) ) ) ), // Modal de detalhes do aluno (mantém o existente) alunoSelecionado && React.createElement(AlunoDetalhesModal, { aluno: alunoSelecionado, onClose: () => setAlunoSelecionado(null), onUpdate: carregarCalendario }) ); }; }; })();