import React, { useState, useEffect, useCallback } from 'react'; import { DoorOpen, Wallet, Clock, CalendarClock, CheckCircle, RefreshCw, MapPin, Inbox, BarChart3, Filter, ChevronLeft, ChevronRight, CalendarDays, Wrench, X } from 'lucide-react'; import { HybridBackend } from '../services/backend.ts'; import { useToast } from '../contexts/ToastContext.tsx'; import { useConfirm } from '../contexts/ConfirmContext.tsx'; import { PageHeader } from '../components/PageHeader.tsx'; const fmtBRL = (v: number) => 'R$ ' + Number(v || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 }); const dataBR = (d: string) => (d || '').slice(0, 10).split('-').reverse().join('/'); // ─── Agenda semanal visual da sala ──────────────────────────────────────────── const DIAS = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom']; const H_INI = 7, H_FIM = 22, ROW_H = 40; // 07h–22h, 40px/hora const segOf = (d: Date) => { const x = new Date(d); const dow = (x.getDay() + 6) % 7; x.setHours(0, 0, 0, 0); x.setDate(x.getDate() - dow); return x; }; const sameDay = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); const AgendaSala: React.FC<{ reservas: any[]; onBloquear?: () => void }> = ({ reservas, onBloquear }) => { const [weekStart, setWeekStart] = useState(() => segOf(new Date())); const dias = Array.from({ length: 7 }, (_, i) => { const d = new Date(weekStart); d.setDate(d.getDate() + i); return d; }); const ativas = reservas.filter(r => r.status === 'pendente' || r.status === 'confirmada'); const fimSemana = new Date(weekStart); fimSemana.setDate(fimSemana.getDate() + 6); const label = `${weekStart.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' })} – ${fimSemana.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' })}`; const blocosDoDia = (dia: Date) => ativas.map(r => { const ini = new Date(r.inicio), fim = new Date(r.fim); if (!sameDay(ini, dia)) return null; const top = Math.max(0, (ini.getHours() + ini.getMinutes() / 60 - H_INI) * ROW_H); const height = Math.max(16, ((fim.getTime() - ini.getTime()) / 3600000) * ROW_H); const manut = r.tipo === 'manutencao'; const conf = r.status === 'confirmada'; const cls = manut ? 'bg-gray-200 border-gray-400 text-gray-700' : conf ? 'bg-teal-100 border-teal-300 text-teal-800' : 'bg-amber-100 border-amber-300 text-amber-800'; const legenda = manut ? (r.motivo || 'Manutenção') : (r.solicitante_nome || r.clinica_nome || 'Reserva'); return (

{ini.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}

{legenda}

); }); return (

Agenda da sala

{onBloquear && ( )} {label}
{/* Cabeçalho dos dias */}
{dias.map((d, i) => { const hoje = sameDay(d, new Date()); return (

{DIAS[i]}

{d.getDate()}

); })}
{/* Grade de horas */}
{/* coluna de horas */}
{Array.from({ length: H_FIM - H_INI }, (_, h) => (
{String(H_INI + h).padStart(2, '0')}h
))}
{dias.map((d, i) => (
{Array.from({ length: H_FIM - H_INI }, (_, h) =>
)} {blocosDoDia(d)}
))}
Confirmada Pendente Manutenção
); }; // Modal: bloquear a sala para manutenção (data + faixa de horário + motivo). const BloqueioModal: React.FC<{ onClose: () => void; onConfirm: (inicio: string, fim: string, motivo: string) => Promise }> = ({ onClose, onConfirm }) => { const toast = useToast(); const hojeYmd = new Date().toISOString().split('T')[0]; const [data, setData] = useState(hojeYmd); const [hIni, setHIni] = useState('08:00'); const [hFim, setHFim] = useState('09:00'); const [motivo, setMotivo] = useState(''); const [saving, setSaving] = useState(false); const salvar = async () => { if (!data || !hIni || !hFim) { toast.error('PREENCHA DATA E HORÁRIOS.'); return; } const inicio = new Date(`${data}T${hIni}`); const fim = new Date(`${data}T${hFim}`); if (fim <= inicio) { toast.error('O FIM DEVE SER DEPOIS DO INÍCIO.'); return; } setSaving(true); try { await onConfirm(inicio.toISOString(), fim.toISOString(), motivo.trim()); } catch (e: any) { toast.error((e?.message || 'ERRO AO BLOQUEAR').toUpperCase()); } finally { setSaving(false); } }; return (

Bloquear p/ manutenção

setData(e.target.value)} className="w-full bg-gray-50 border-2 border-gray-100 rounded-xl px-3 py-2.5 text-sm font-bold text-gray-700 outline-none focus:border-teal-400" />
setHIni(e.target.value)} className="w-full bg-gray-50 border-2 border-gray-100 rounded-xl px-3 py-2.5 text-sm font-bold text-gray-700 outline-none focus:border-teal-400" />
setHFim(e.target.value)} className="w-full bg-gray-50 border-2 border-gray-100 rounded-xl px-3 py-2.5 text-sm font-bold text-gray-700 outline-none focus:border-teal-400" />
setMotivo(e.target.value)} placeholder="Ex.: limpeza, conserto do equipo..." className="w-full bg-gray-50 border-2 border-gray-100 rounded-xl px-3 py-2.5 text-sm text-gray-700 outline-none focus:border-teal-400" />
); }; // Painel da SALA: aparece quando o workspace ativo é uma sala (tipo='sala'). export const SalaHomeView: React.FC = () => { const toast = useToast(); const confirm = useConfirm(); const ws: any = HybridBackend.getActiveWorkspace(); // Operador (ex.: recepção): só vê a AGENDA e bloqueia manutenção — sem locação/financeiro. const isOperador = ws?.papel_sala === 'operador'; const [lancamentos, setLancamentos] = useState([]); const [reservas, setReservas] = useState<{ feitas: any[]; recebidas: any[] }>({ feitas: [], recebidas: [] }); const [loading, setLoading] = useState(false); const [bloqueioOpen, setBloqueioOpen] = useState(false); // Relatório por sala (período) const hoje = new Date(); const [inicio, setInicio] = useState(new Date(hoje.getFullYear(), hoje.getMonth(), 1).toISOString().split('T')[0]); const [fim, setFim] = useState(hoje.toISOString().split('T')[0]); const [rel, setRel] = useState(null); const [relLoading, setRelLoading] = useState(false); const carregar = useCallback(async () => { setLoading(true); try { if (isOperador) { // Operador não tem acesso ao financeiro/recebíveis: carrega só a agenda da sala. const rs = await HybridBackend.getSalaReservas(ws.id); setLancamentos([]); setReservas({ feitas: [], recebidas: (Array.isArray(rs) ? rs : []).map((r: any) => ({ ...r, sala_id: ws.id })) }); } else { const [fin, res] = await Promise.all([HybridBackend.getFinanceiro(), HybridBackend.getMinhasReservas()]); setLancamentos(Array.isArray(fin) ? fin : []); setReservas(res && res.recebidas ? res : { feitas: [], recebidas: [] }); } } catch { /* silent */ } finally { setLoading(false); } }, [isOperador, ws?.id]); useEffect(() => { carregar(); }, [carregar]); const carregarRel = useCallback(() => { if (isOperador) { setRel(null); return; } setRelLoading(true); HybridBackend.getSalasRelatorio(inicio, fim).then(setRel).catch(() => setRel(null)).finally(() => setRelLoading(false)); }, [inicio, fim, isOperador]); useEffect(() => { carregarRel(); }, [carregarRel]); const recebido = lancamentos.filter(l => l.status === 'Pago').reduce((s, l) => s + Number(l.valor), 0); const aReceber = lancamentos.filter(l => l.status !== 'Pago').reduce((s, l) => s + Number(l.valor), 0); const reservasDaSala = reservas.recebidas.filter(r => r.sala_id === ws?.id); const pendentes = reservasDaSala.filter(r => r.status === 'pendente').length; const confirmadas = reservasDaSala.filter(r => r.status === 'confirmada').length; const bloquearManutencao = async (inicio: string, fim: string, motivo: string) => { await HybridBackend.criarManutencaoSala(ws.id, { inicio, fim, motivo }); toast.success('SALA BLOQUEADA PARA MANUTENÇÃO.'); setBloqueioOpen(false); carregar(); }; const darBaixa = async (l: any) => { if (!await confirm(`MARCAR como RECEBIDO ${fmtBRL(l.valor)} de "${l.descricao}"?`)) return; try { await HybridBackend.updateFinanceiro({ ...l, status: 'Pago', pago_em: new Date().toISOString().split('T')[0] }); toast.success('RECEBIMENTO CONFIRMADO.'); carregar(); } catch (e: any) { toast.error((e.message || 'ERRO').toUpperCase()); } }; const KPI: React.FC<{ titulo: string; valor: string; cor: string; Icon: any; sub?: string }> = ({ titulo, valor, cor, Icon, sub }) => (

{titulo}

{valor}
{sub &&

{sub}

}
); return (

{ws?.nome || 'Sala'}

{(ws?.cidade || ws?.estado) &&

{ws?.cidade}{ws?.estado ? `/${ws.estado}` : ''}

}
{loading ?
: ( <>
{!isOperador && } {!isOperador && }
setBloqueioOpen(true)} /> {!isOperador && (<>

Recebíveis de locação

{lancamentos.length} lançamento(s)
{lancamentos.length === 0 ? (
Nenhum recebível ainda. Confirme reservas para gerar receita.
) : (
{['Descrição', 'Locatário', 'Venc.', 'Status', 'Valor', ''].map(h => )} {lancamentos.map(l => ( ))}
{h}
{l.descricao} {l.pacientenome || '—'} {dataBR(l.datavencimento)} {l.status} {fmtBRL(l.valor)} {l.status !== 'Pago' && }
)}
{/* Relatório por sala (faturamento + ocupação no período) */}

Relatório por sala

setInicio(e.target.value)} className="bg-gray-50 border border-gray-200 rounded-lg px-2.5 py-1.5 text-xs font-bold text-gray-700 outline-none focus:border-teal-400" />
setFim(e.target.value)} className="bg-gray-50 border border-gray-200 rounded-lg px-2.5 py-1.5 text-xs font-bold text-gray-700 outline-none focus:border-teal-400" />
{relLoading ?
: (
{['Sala', 'Reservas', 'Horas', 'Recebido', 'A receber'].map(h => )} {(rel?.salas || []).map((s: any) => ( ))} {(rel?.salas || []).length === 0 && } {(rel?.salas || []).length > 0 && ( )}
{h}
{s.nome}{s.id === ws?.id && (ATUAL)} {s.reservas} {Number(s.horas).toLocaleString('pt-BR', { maximumFractionDigits: 1 })}h {fmtBRL(s.recebido)} {fmtBRL(s.a_receber)}
Sem dados no período.
Total {rel.total_reservas} {Number(rel.total_horas).toLocaleString('pt-BR', { maximumFractionDigits: 1 })}h {fmtBRL(rel.total_recebido)} {fmtBRL(rel.total_a_receber)}
)}
)} )} {bloqueioOpen && setBloqueioOpen(false)} onConfirm={bloquearManutencao} />}
); };