983582f066
Funcionário/membro com vínculo na clínica dona da sala passa a ter a sala como workspace de acesso OPERACIONAL (papel_sala='operador'): vê a agenda da sala e bloqueia horário para manutenção, sem locação nem financeiro. O Painel da Sala detecta o modo operador e esconde recebíveis/relatório/baixa, mostrando só a agenda + ação de bloqueio. Bloqueio de manutenção: reserva tipo='manutencao' (coluna nova, default 'locacao'), valor 0, nasce 'confirmada' (sem aprovação) e NÃO gera recebível. Reaproveita o anti-conflito de horário da sala (409 em sobreposição) e o cancelamento por solicitante/unidade. A agenda renderiza o bloqueio em cinza. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
336 lines
23 KiB
TypeScript
336 lines
23 KiB
TypeScript
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 (
|
||
<div key={r.id} className={`absolute left-0.5 right-0.5 rounded-md px-1 py-0.5 overflow-hidden border ${cls}`}
|
||
style={{ top, height }} title={`${legenda} ${ini.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}–${fim.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}`}>
|
||
<p className="text-[9px] font-black leading-tight truncate">{ini.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}</p>
|
||
<p className="text-[9px] font-bold leading-tight truncate uppercase">{legenda}</p>
|
||
</div>
|
||
);
|
||
});
|
||
|
||
return (
|
||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
||
<div className="px-6 py-4 border-b border-gray-100 bg-gray-50/50 flex items-center justify-between gap-3">
|
||
<h3 className="text-sm font-black text-gray-800 uppercase flex items-center gap-2"><CalendarDays size={16} className="text-teal-600" /> Agenda da sala</h3>
|
||
<div className="flex items-center gap-2">
|
||
{onBloquear && (
|
||
<button onClick={onBloquear} className="flex items-center gap-1.5 bg-gray-700 text-white px-3 py-1.5 rounded-lg font-black text-[10px] uppercase hover:bg-gray-800 transition-colors">
|
||
<Wrench size={12} /> Bloquear manutenção
|
||
</button>
|
||
)}
|
||
<span className="text-[11px] font-black text-gray-500 uppercase">{label}</span>
|
||
<button onClick={() => setWeekStart(d => { const x = new Date(d); x.setDate(x.getDate() - 7); return x; })} className="p-1.5 hover:bg-gray-100 rounded-lg text-gray-500"><ChevronLeft size={16} /></button>
|
||
<button onClick={() => setWeekStart(segOf(new Date()))} className="text-[10px] font-black uppercase text-teal-600 hover:bg-teal-50 px-2 py-1.5 rounded-lg">Hoje</button>
|
||
<button onClick={() => setWeekStart(d => { const x = new Date(d); x.setDate(x.getDate() + 7); return x; })} className="p-1.5 hover:bg-gray-100 rounded-lg text-gray-500"><ChevronRight size={16} /></button>
|
||
</div>
|
||
</div>
|
||
<div className="overflow-x-auto">
|
||
<div className="min-w-[680px]">
|
||
{/* Cabeçalho dos dias */}
|
||
<div className="grid" style={{ gridTemplateColumns: '44px repeat(7, 1fr)' }}>
|
||
<div className="border-b border-gray-100" />
|
||
{dias.map((d, i) => {
|
||
const hoje = sameDay(d, new Date());
|
||
return (
|
||
<div key={i} className={`text-center py-2 border-b border-l border-gray-100 ${hoje ? 'bg-teal-50' : ''}`}>
|
||
<p className="text-[10px] font-black text-gray-400 uppercase">{DIAS[i]}</p>
|
||
<p className={`text-sm font-black ${hoje ? 'text-teal-700' : 'text-gray-700'}`}>{d.getDate()}</p>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
{/* Grade de horas */}
|
||
<div className="grid" style={{ gridTemplateColumns: '44px repeat(7, 1fr)' }}>
|
||
{/* coluna de horas */}
|
||
<div className="relative" style={{ height: (H_FIM - H_INI) * ROW_H }}>
|
||
{Array.from({ length: H_FIM - H_INI }, (_, h) => (
|
||
<div key={h} className="absolute right-1 text-[9px] font-bold text-gray-300" style={{ top: h * ROW_H - 6 }}>{String(H_INI + h).padStart(2, '0')}h</div>
|
||
))}
|
||
</div>
|
||
{dias.map((d, i) => (
|
||
<div key={i} className="relative border-l border-gray-100" style={{ height: (H_FIM - H_INI) * ROW_H }}>
|
||
{Array.from({ length: H_FIM - H_INI }, (_, h) => <div key={h} className="absolute left-0 right-0 border-b border-gray-50" style={{ top: (h + 1) * ROW_H }} />)}
|
||
{blocosDoDia(d)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="px-6 py-2 flex gap-4 text-[10px] font-bold text-gray-400 uppercase border-t border-gray-50">
|
||
<span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded bg-teal-200 border border-teal-300" /> Confirmada</span>
|
||
<span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded bg-amber-200 border border-amber-300" /> Pendente</span>
|
||
<span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded bg-gray-200 border border-gray-400" /> Manutenção</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 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<void> }> = ({ 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 (
|
||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm">
|
||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-100">
|
||
<h3 className="font-black text-sm uppercase text-gray-800 flex items-center gap-2"><Wrench size={16} className="text-gray-600" /> Bloquear p/ manutenção</h3>
|
||
<button onClick={onClose} className="p-1.5 hover:bg-gray-100 rounded-lg"><X size={18} /></button>
|
||
</div>
|
||
<div className="p-6 space-y-4">
|
||
<div>
|
||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest block mb-1.5">Data</label>
|
||
<input type="date" value={data} onChange={e => 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" />
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest block mb-1.5">Início</label>
|
||
<input type="time" value={hIni} onChange={e => 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" />
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest block mb-1.5">Fim</label>
|
||
<input type="time" value={hFim} onChange={e => 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" />
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest block mb-1.5">Motivo (opcional)</label>
|
||
<input value={motivo} onChange={e => 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" />
|
||
</div>
|
||
<button onClick={salvar} disabled={saving} className="w-full bg-gray-700 text-white py-3 rounded-xl font-black text-sm uppercase hover:bg-gray-800 disabled:opacity-50 flex items-center justify-center gap-2">
|
||
{saving ? <RefreshCw size={16} className="animate-spin" /> : <Wrench size={16} />} Bloquear sala
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 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<any[]>([]);
|
||
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<any | null>(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 }) => (
|
||
<div className="bg-white p-5 rounded-2xl border border-gray-100 shadow-sm relative overflow-hidden">
|
||
<div className="absolute top-0 right-0 p-3 opacity-10"><Icon size={56} className={cor} /></div>
|
||
<h4 className="text-[10px] font-black text-gray-400 uppercase tracking-widest">{titulo}</h4>
|
||
<div className="mt-2 text-xl font-black text-gray-900">{valor}</div>
|
||
{sub && <p className="mt-2 text-[10px] font-bold text-gray-400 uppercase">{sub}</p>}
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<div className="space-y-6 pb-10">
|
||
<PageHeader title="PAINEL DA SALA">
|
||
<button onClick={carregar} className="bg-white border border-gray-200 text-gray-600 p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2 text-xs font-bold transition-all shadow-sm">
|
||
<RefreshCw size={16} /> <span className="hidden sm:inline">ATUALIZAR</span>
|
||
</button>
|
||
</PageHeader>
|
||
|
||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm px-6 py-4 flex items-center gap-3">
|
||
<div className="w-10 h-10 rounded-xl flex items-center justify-center text-white shrink-0" style={{ backgroundColor: '#0d9488' }}><DoorOpen size={20} /></div>
|
||
<div className="min-w-0">
|
||
<p className="font-black text-gray-900 uppercase truncate">{ws?.nome || 'Sala'}</p>
|
||
{(ws?.cidade || ws?.estado) && <p className="text-[11px] text-gray-400 font-bold uppercase flex items-center gap-1"><MapPin size={11} /> {ws?.cidade}{ws?.estado ? `/${ws.estado}` : ''}</p>}
|
||
</div>
|
||
</div>
|
||
|
||
{loading ? <div className="flex justify-center py-16"><RefreshCw className="animate-spin text-teal-600" size={28} /></div> : (
|
||
<>
|
||
<div className={`grid grid-cols-2 ${isOperador ? 'lg:grid-cols-2' : 'lg:grid-cols-4'} gap-4`}>
|
||
{!isOperador && <KPI titulo="Recebido" valor={fmtBRL(recebido)} cor="text-green-600" Icon={Wallet} sub="Locações pagas" />}
|
||
{!isOperador && <KPI titulo="A receber" valor={fmtBRL(aReceber)} cor="text-amber-500" Icon={Clock} sub="Pendentes" />}
|
||
<KPI titulo="Reservas pendentes" valor={String(pendentes)} cor="text-red-500" Icon={Inbox} sub="Aguardando resposta" />
|
||
<KPI titulo="Reservas confirmadas" valor={String(confirmadas)} cor="text-teal-600" Icon={CalendarClock} sub="Nesta sala" />
|
||
</div>
|
||
|
||
<AgendaSala reservas={reservasDaSala} onBloquear={() => setBloqueioOpen(true)} />
|
||
|
||
{!isOperador && (<>
|
||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
||
<div className="px-6 py-4 border-b border-gray-100 bg-gray-50/50 flex justify-between items-center">
|
||
<h3 className="text-sm font-black text-gray-800 uppercase">Recebíveis de locação</h3>
|
||
<span className="text-[10px] font-bold text-gray-400 uppercase">{lancamentos.length} lançamento(s)</span>
|
||
</div>
|
||
{lancamentos.length === 0 ? (
|
||
<div className="py-12 text-center text-gray-400 text-sm font-bold uppercase">Nenhum recebível ainda. Confirme reservas para gerar receita.</div>
|
||
) : (
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-left text-xs">
|
||
<thead className="bg-gray-50/60"><tr>{['Descrição', 'Locatário', 'Venc.', 'Status', 'Valor', ''].map(h => <th key={h} className="px-4 py-3 text-[10px] font-black text-gray-400 uppercase tracking-widest">{h}</th>)}</tr></thead>
|
||
<tbody className="divide-y divide-gray-100">
|
||
{lancamentos.map(l => (
|
||
<tr key={l.id} className="hover:bg-gray-50/50">
|
||
<td className="px-4 py-3 font-bold text-gray-700">{l.descricao}</td>
|
||
<td className="px-4 py-3 text-gray-500 uppercase">{l.pacientenome || '—'}</td>
|
||
<td className="px-4 py-3 text-gray-500 whitespace-nowrap">{dataBR(l.datavencimento)}</td>
|
||
<td className="px-4 py-3"><span className={`px-2 py-0.5 rounded-full text-[9px] font-black uppercase ${l.status === 'Pago' ? 'bg-green-100 text-green-700' : l.status === 'Atrasado' ? 'bg-red-100 text-red-600' : 'bg-amber-100 text-amber-700'}`}>{l.status}</span></td>
|
||
<td className="px-4 py-3 font-black text-gray-800">{fmtBRL(l.valor)}</td>
|
||
<td className="px-4 py-3 text-right">
|
||
{l.status !== 'Pago' && <button onClick={() => darBaixa(l)} title="MARCAR RECEBIDO" className="p-1.5 text-green-600 hover:bg-green-50 rounded-lg transition-colors"><CheckCircle size={16} /></button>}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Relatório por sala (faturamento + ocupação no período) */}
|
||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
||
<div className="px-6 py-4 border-b border-gray-100 bg-gray-50/50 flex flex-wrap items-end justify-between gap-3">
|
||
<h3 className="text-sm font-black text-gray-800 uppercase flex items-center gap-2"><BarChart3 size={16} className="text-teal-600" /> Relatório por sala</h3>
|
||
<div className="flex items-end gap-2">
|
||
<div><label className="text-[9px] font-black text-gray-400 uppercase block mb-1">Início</label><input type="date" value={inicio} onChange={e => 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" /></div>
|
||
<div><label className="text-[9px] font-black text-gray-400 uppercase block mb-1">Fim</label><input type="date" value={fim} onChange={e => 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" /></div>
|
||
<button onClick={carregarRel} className="bg-gray-100 text-gray-600 px-3 py-1.5 rounded-lg font-black text-[10px] uppercase hover:bg-gray-200 flex items-center gap-1.5"><Filter size={12} /> Filtrar</button>
|
||
</div>
|
||
</div>
|
||
{relLoading ? <div className="flex justify-center py-10"><RefreshCw className="animate-spin text-teal-500" size={22} /></div> : (
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-left text-xs">
|
||
<thead className="bg-gray-50/60"><tr>{['Sala', 'Reservas', 'Horas', 'Recebido', 'A receber'].map(h => <th key={h} className="px-4 py-3 text-[10px] font-black text-gray-400 uppercase tracking-widest">{h}</th>)}</tr></thead>
|
||
<tbody className="divide-y divide-gray-100">
|
||
{(rel?.salas || []).map((s: any) => (
|
||
<tr key={s.id} className={`hover:bg-gray-50/50 ${s.id === ws?.id ? 'bg-teal-50/40' : ''}`}>
|
||
<td className="px-4 py-3 font-black text-gray-700 uppercase">{s.nome}{s.id === ws?.id && <span className="ml-2 text-[8px] font-black text-teal-600">(ATUAL)</span>}</td>
|
||
<td className="px-4 py-3 text-gray-600">{s.reservas}</td>
|
||
<td className="px-4 py-3 text-gray-600">{Number(s.horas).toLocaleString('pt-BR', { maximumFractionDigits: 1 })}h</td>
|
||
<td className="px-4 py-3 font-bold text-green-600">{fmtBRL(s.recebido)}</td>
|
||
<td className="px-4 py-3 font-bold text-amber-600">{fmtBRL(s.a_receber)}</td>
|
||
</tr>
|
||
))}
|
||
{(rel?.salas || []).length === 0 && <tr><td colSpan={5} className="px-4 py-8 text-center text-gray-400 text-sm">Sem dados no período.</td></tr>}
|
||
</tbody>
|
||
{(rel?.salas || []).length > 0 && (
|
||
<tfoot className="bg-gray-50/60 border-t-2 border-gray-100">
|
||
<tr className="font-black text-gray-800">
|
||
<td className="px-4 py-3 uppercase text-[10px]">Total</td>
|
||
<td className="px-4 py-3">{rel.total_reservas}</td>
|
||
<td className="px-4 py-3">{Number(rel.total_horas).toLocaleString('pt-BR', { maximumFractionDigits: 1 })}h</td>
|
||
<td className="px-4 py-3 text-green-700">{fmtBRL(rel.total_recebido)}</td>
|
||
<td className="px-4 py-3 text-amber-700">{fmtBRL(rel.total_a_receber)}</td>
|
||
</tr>
|
||
</tfoot>
|
||
)}
|
||
</table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</>)}
|
||
</>
|
||
)}
|
||
{bloqueioOpen && <BloqueioModal onClose={() => setBloqueioOpen(false)} onConfirm={bloquearManutencao} />}
|
||
</div>
|
||
);
|
||
};
|