Files
scoreodonto.com/frontend/views/plugins/SalasPlugin.tsx
T
VPS 4 Builder 343ca560dc feat(salas): contrato de locação — modelo padrão + aceite na reserva
Backend:
- Nova coluna salas.contrato (TEXT, migration ADD COLUMN IF NOT EXISTS).
- INSERT/UPDATE de sala aceitam `contrato`; marketplace SELECT devolve s.contrato
  (minhas salas já via s.*).

Frontend (SalasPlugin):
- CONTRATO_MODELO: modelo padrão cuja cláusula 2 deixa TODOS os procedimentos sob
  inteira responsabilidade do locatário; o dono edita por sala.
- SalaForm ganha textarea "Contrato de locação" pré-preenchida com o modelo +
  botão "Restaurar modelo".
- Passo Pagamento: botão "Ler contrato de locação" abre modal com o texto
  (contrato da sala, ou o modelo) + checkbox de aceite. "Solicitar Reserva" só
  habilita após aceitar; o aceite é registrado nas observações da reserva.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:10:40 +02:00

1241 lines
76 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
DoorOpen, MapPin, Search, RefreshCw, Plus, Pencil, Trash2, X, Save,
CalendarClock, CheckCircle2, XCircle, Clock, Building2, Briefcase, Home, Inbox, Send, Eraser, ChevronUp, ChevronLeft, ChevronRight, FileText
} from 'lucide-react';
import { PageHeader } from '../../components/PageHeader.tsx';
import { useToast } from '../../contexts/ToastContext.tsx';
import { useConfirm } from '../../contexts/ConfirmContext.tsx';
import { HybridBackend } from '../../services/backend.ts';
import { buscarCep } from '../../services/viacep.ts';
const API_URL = (window as any).__API_URL__ || '';
const UF_LIST = ['', 'AC', 'AL', 'AM', 'AP', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MG', 'MS', 'MT', 'PA', 'PB', 'PE', 'PI', 'PR', 'RJ', 'RN', 'RO', 'RR', 'RS', 'SC', 'SE', 'SP', 'TO'];
const COLOR = '#0d9488'; // teal — identidade do plugin
const apiFetch = (path: string, opts: RequestInit = {}) =>
fetch(`${API_URL}${path}`, {
...opts,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('SCOREODONTO_AUTH_TOKEN')}`,
...(opts.headers || {}),
},
});
// ─── Types ───────────────────────────────────────────────────────────────────
interface Sala {
id: string;
nome: string;
tipo: 'clinica' | 'consultorio' | 'independente';
descricao: string | null;
equipamentos: string | null;
endereco: string | null;
bairro: string | null;
cidade: string;
estado: string;
cep: string | null;
valor_hora: string | null;
valor_turno: string | null;
valor_diaria: string | null;
valor_semanal: string | null;
valor_mensal: string | null;
contrato?: string | null;
hora_consulta?: boolean;
turno_consulta?: boolean;
diaria_consulta?: boolean;
semanal_consulta?: boolean;
mensal_consulta?: boolean;
area?: string;
disponivel?: boolean;
owner_nome?: string;
clinica_id?: string | null;
clinica_nome?: string | null;
reservas_pendentes?: number;
dist_km?: number | string | null;
}
interface Reserva {
id: string;
sala_id: string;
sala_nome?: string;
solicitante_nome?: string;
owner_nome?: string;
cidade?: string;
estado?: string;
inicio: string;
fim: string;
modelo: string;
valor?: number | string | null;
status: 'pendente' | 'confirmada' | 'recusada' | 'cancelada';
observacoes?: string | null;
clinica_nome?: string | null;
profissional_nome?: string | null;
}
const TIPO_META: Record<string, { label: string; icon: any }> = {
clinica: { label: 'Sala Clínica', icon: Building2 },
consultorio: { label: 'Sala Consultório', icon: Briefcase },
independente: { label: 'Sala Independente', icon: Home },
};
const AREA_META: Record<string, { nome: string; cor: string }> = {
odontologia: { nome: 'Odontologia', cor: '#2563eb' },
biomedicina: { nome: 'Biomedicina', cor: '#db2777' },
protese: { nome: 'Prótese', cor: '#7c3aed' },
};
const STATUS_STYLE: Record<string, string> = {
pendente: 'bg-amber-100 text-amber-700',
confirmada: 'bg-emerald-100 text-emerald-700',
recusada: 'bg-red-100 text-red-600',
cancelada: 'bg-gray-100 text-gray-500',
};
const fmtValor = (v: string | null) => (v == null || v === '' ? null : Number(v).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }));
const fmtData = (iso: string) => new Date(iso).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' });
const precos = (s: Sala): Array<[string, string]> => {
const mods: Array<[string, string | null, boolean | undefined]> = [
['HORA', s.valor_hora, s.hora_consulta], ['TURNO', s.valor_turno, s.turno_consulta],
['DIÁRIA', s.valor_diaria, s.diaria_consulta], ['SEMANA', s.valor_semanal, s.semanal_consulta],
['MÊS', s.valor_mensal, s.mensal_consulta],
];
return mods
.filter(([, v, c]) => fmtValor(v as string | null) || c)
.map(([l, v]) => [l, fmtValor(v as string | null) || 'Sob consulta'] as [string, string]);
};
// ── Calendário de reserva ────────────────────────────────────────────────────
const MODELOS: Array<{ id: string; label: string; valor: keyof Sala; consulta: keyof Sala }> = [
{ id: 'hora', label: 'Hora', valor: 'valor_hora', consulta: 'hora_consulta' },
{ id: 'turno', label: 'Período', valor: 'valor_turno', consulta: 'turno_consulta' },
{ id: 'diaria', label: 'Dia', valor: 'valor_diaria', consulta: 'diaria_consulta' },
{ id: 'semanal', label: 'Semana', valor: 'valor_semanal', consulta: 'semanal_consulta' },
{ id: 'mensal', label: 'Mês', valor: 'valor_mensal', consulta: 'mensal_consulta' },
];
const TURNOS: Record<string, { label: string; ini: number; fim: number }> = {
manha: { label: 'Manhã', ini: 8, fim: 12 },
tarde: { label: 'Tarde', ini: 13, fim: 18 },
noite: { label: 'Noite', ini: 18, fim: 22 },
};
const WEEKDAYS = ['D', 'S', 'T', 'Q', 'Q', 'S', 'S'];
const MESES = ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
const pad2 = (n: number) => String(n).padStart(2, '0');
const toLocalInput = (d: Date) => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}T${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
const mesmoDia = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
// Células do mês (com padding p/ alinhar ao dia da semana).
const monthGrid = (ref: Date): (Date | null)[] => {
const first = new Date(ref.getFullYear(), ref.getMonth(), 1);
const dias = new Date(ref.getFullYear(), ref.getMonth() + 1, 0).getDate();
const cells: (Date | null)[] = [];
for (let i = 0; i < first.getDay(); i++) cells.push(null);
for (let d = 1; d <= dias; d++) cells.push(new Date(ref.getFullYear(), ref.getMonth(), d));
return cells;
};
// Início/fim conforme o modelo escolhido.
const computeRange = (dia: Date, modelo: string, hora: string, qtd: number, turno: string): { ini: Date; fim: Date } => {
const b = new Date(dia); b.setHours(0, 0, 0, 0);
const ini = new Date(b), fim = new Date(b);
const n = Math.max(1, qtd);
if (modelo === 'hora') {
const [h, m] = hora.split(':').map(Number); ini.setHours(h || 8, m || 0, 0, 0);
fim.setTime(ini.getTime() + n * 3600_000);
} else if (modelo === 'turno') {
const t = TURNOS[turno] || TURNOS.manha; ini.setHours(t.ini, 0, 0, 0); fim.setHours(t.fim, 0, 0, 0);
} else if (modelo === 'semanal') {
ini.setHours(8, 0, 0, 0); fim.setDate(fim.getDate() + (n * 7 - 1)); fim.setHours(18, 0, 0, 0);
} else if (modelo === 'mensal') {
ini.setHours(8, 0, 0, 0); fim.setMonth(fim.getMonth() + n); fim.setDate(fim.getDate() - 1); fim.setHours(18, 0, 0, 0);
} else { // diária — n dias consecutivos
ini.setHours(8, 0, 0, 0); fim.setDate(fim.getDate() + (n - 1)); fim.setHours(18, 0, 0, 0);
}
return { ini, fim };
};
// Quantidade máxima por modelo + rótulo da unidade.
const UNIDADE: Record<string, { sing: string; plur: string; max: number }> = {
hora: { sing: 'hora', plur: 'horas', max: 12 },
diaria: { sing: 'dia', plur: 'dias', max: 30 },
semanal: { sing: 'semana', plur: 'semanas', max: 8 },
mensal: { sing: 'mês', plur: 'meses', max: 12 },
};
// Modelo padrão de contrato de locação — o dono pode editar por sala; se em branco, vale este.
const CONTRATO_MODELO = `CONTRATO DE LOCAÇÃO DE ESPAÇO / SALA
1. OBJETO
A presente locação tem por objeto a cessão temporária do espaço/sala identificado nesta plataforma, para uso profissional durante o período reservado.
2. RESPONSABILIDADE
Todos os procedimentos, atendimentos e serviços realizados no espaço durante o período locado são de INTEIRA E EXCLUSIVA RESPONSABILIDADE DO LOCATÁRIO (quem aluga), abrangendo as esferas civil, criminal, ética, sanitária e trabalhista, perante pacientes, terceiros e órgãos de classe. O LOCADOR (dono da sala) não responde por qualquer ato praticado pelo locatário.
3. USO E CONSERVAÇÃO
O locatário compromete-se a utilizar o espaço e os equipamentos com zelo, devolvendo-os nas mesmas condições em que os recebeu. Eventuais danos serão integralmente ressarcidos pelo locatário.
4. REGULARIDADE PROFISSIONAL
O locatário declara possuir registro profissional ativo e todas as licenças e autorizações exigidas para o exercício de sua atividade, isentando o locador de qualquer responsabilidade a esse respeito.
5. PAGAMENTO
O valor e a forma de pagamento são os informados no pedido de reserva e combinados diretamente entre as partes.
6. CANCELAMENTO
Cancelamentos devem ser comunicados com antecedência razoável, conforme acordado entre as partes.
Ao aceitar, o LOCATÁRIO declara ter lido e concordado integralmente com os termos acima.`;
const inputCls = 'w-full px-3 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold text-gray-700 outline-none focus:border-teal-400';
const labelCls = 'text-[10px] font-black text-gray-400 uppercase tracking-widest block mb-1';
// Estilo padrão de filtros — fonte única em components/filterStyles.
import { inputNew, labelNew } from '../../components/filterStyles.tsx';
// ─── Modal: cadastrar / editar sala ──────────────────────────────────────────
const SalaFormModal: React.FC<{ sala: Sala | null; onClose: () => void; onSaved: () => void }> = ({ sala, onClose, onSaved }) => {
const toast = useToast();
const ws = HybridBackend.getActiveWorkspace();
const [saving, setSaving] = useState(false);
const [vincularUnidade, setVincularUnidade] = useState(sala ? !!sala.clinica_id : !!ws?.id);
const [areas, setAreas] = useState<{ slug: string; nome: string }[]>([]);
useEffect(() => { HybridBackend.getAreas().then(setAreas); }, []);
const [f, setF] = useState({
nome: sala?.nome || '', tipo: sala?.tipo || 'independente', descricao: sala?.descricao || '',
equipamentos: sala?.equipamentos || '', endereco: sala?.endereco || '', bairro: sala?.bairro || '',
cidade: sala?.cidade || '', estado: sala?.estado || '', cep: sala?.cep || '', area: sala?.area || '',
valorHora: sala?.valor_hora || '', valorTurno: sala?.valor_turno || '', valorDiaria: sala?.valor_diaria || '',
valorSemanal: sala?.valor_semanal || '', valorMensal: sala?.valor_mensal || '',
horaConsulta: sala?.hora_consulta === true, turnoConsulta: sala?.turno_consulta === true,
diariaConsulta: sala?.diaria_consulta === true, semanalConsulta: sala?.semanal_consulta === true,
mensalConsulta: sala?.mensal_consulta === true,
disponivel: sala?.disponivel !== false,
contrato: sala?.contrato ?? CONTRATO_MODELO,
});
const set = (k: string, v: any) => setF(p => ({ ...p, [k]: v }));
const handleSave = async () => {
if (!f.nome || !f.cidade || !f.estado) { toast.error('NOME, CIDADE E ESTADO SÃO OBRIGATÓRIOS.'); return; }
setSaving(true);
try {
const body = JSON.stringify({
...f,
clinicaId: vincularUnidade && ws?.id ? ws.id : null,
valorHora: f.valorHora === '' ? null : +f.valorHora, valorTurno: f.valorTurno === '' ? null : +f.valorTurno,
valorDiaria: f.valorDiaria === '' ? null : +f.valorDiaria, valorSemanal: f.valorSemanal === '' ? null : +f.valorSemanal,
valorMensal: f.valorMensal === '' ? null : +f.valorMensal,
});
const res = sala
? await apiFetch(`/api/salas/${sala.id}`, { method: 'PUT', body })
: await apiFetch('/api/salas', { method: 'POST', body });
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Erro ao salvar.');
toast.success(sala ? 'SALA ATUALIZADA!' : 'SALA CADASTRADA!');
// Sala é workspace próprio: atualiza o seletor. Se for a 1ª (sem workspace ativo), recarrega p/ assumir contexto.
const semWs = !HybridBackend.getActiveWorkspace();
await HybridBackend.refreshWorkspaces();
if (!sala && semWs) { window.location.reload(); return; }
onSaved();
onClose();
} catch (e: any) { toast.error(e.message.toUpperCase()); }
finally { setSaving(false); }
};
return (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center backdrop-blur-sm p-4">
<div className="bg-white rounded-2xl w-full max-w-2xl shadow-2xl overflow-hidden max-h-[92vh] overflow-y-auto">
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between sticky top-0 bg-white">
<h3 className="text-sm font-black text-gray-900 uppercase">{sala ? 'EDITAR SALA' : 'CADASTRAR SALA'}</h3>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-xl transition-colors"><X size={18} /></button>
</div>
<div className="p-6 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="md:col-span-2">
<label className={labelCls}>Nome da sala *</label>
<input className={inputCls} value={f.nome} onChange={e => set('nome', e.target.value)} placeholder="EX: SALA 2 — EQUIPADA P/ HOF" />
</div>
<div>
<label className={labelCls}>Tipo</label>
<select className={inputCls} value={f.tipo} onChange={e => set('tipo', e.target.value)}>
<option value="clinica">SALA CLÍNICA</option>
<option value="consultorio">SALA CONSULTÓRIO</option>
<option value="independente">SALA INDEPENDENTE</option>
</select>
</div>
<div>
<label className={labelCls}>Disponível p/ locação</label>
<select className={inputCls} value={f.disponivel ? '1' : '0'} onChange={e => set('disponivel', e.target.value === '1')}>
<option value="1">SIM</option>
<option value="0">NÃO</option>
</select>
</div>
<div className="md:col-span-2">
<label className={labelCls}>Descrição</label>
<textarea className={inputCls} rows={2} value={f.descricao} onChange={e => set('descricao', e.target.value)} placeholder="Detalhes da sala, regras de uso..." />
</div>
<div className="md:col-span-2">
<label className={labelCls}>Equipamentos</label>
<input className={inputCls} value={f.equipamentos} onChange={e => set('equipamentos', e.target.value)} placeholder="CADEIRA, FOTOPOLIMERIZADOR, RX..." />
</div>
<div className="md:col-span-2">
<div className="flex items-center justify-between mb-1">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest flex items-center gap-1.5"><FileText size={12} /> Contrato de locação</label>
<button type="button" onClick={() => set('contrato', CONTRATO_MODELO)} className="text-[9px] font-black uppercase text-teal-600 hover:text-teal-700">Restaurar modelo</button>
</div>
<textarea className={`${inputCls} font-medium normal-case`} rows={6} value={f.contrato} onChange={e => set('contrato', e.target.value)} placeholder="Termos do contrato que o locatário deverá aceitar ao reservar..." />
<p className="text-[9px] text-gray-400 font-bold uppercase mt-1">Exibido ao locatário no passo de pagamento; ele precisa aceitar para reservar. preenchemos um modelo edite como quiser.</p>
</div>
<div className="md:col-span-2">
<label className={labelCls}>Endereço</label>
<input className={inputCls} value={f.endereco} onChange={e => set('endereco', e.target.value)} placeholder="RUA / NÚMERO / COMPLEMENTO" />
</div>
<div>
<label className={labelCls}>Bairro</label>
<input className={`${inputCls} uppercase`} value={f.bairro} onChange={e => set('bairro', e.target.value.toUpperCase())} />
</div>
<div>
<label className={labelCls}>CEP</label>
<input className={inputCls} value={f.cep} onChange={async e => {
const v = e.target.value;
set('cep', v);
if (v.replace(/\D/g, '').length === 8) {
const r = await buscarCep(v);
if (r) { setF(p => ({ ...p, endereco: r.logradouro, bairro: r.bairro, cidade: r.cidade, estado: r.estado })); toast.success('ENDEREÇO PREENCHIDO PELO CEP!'); }
else toast.error('CEP NÃO ENCONTRADO.');
}
}} placeholder="79000-000" />
</div>
<div>
<label className={labelCls}>Cidade *</label>
<input className={`${inputCls} uppercase`} value={f.cidade} onChange={e => set('cidade', e.target.value.toUpperCase())} />
</div>
<div>
<label className={labelCls}>Estado *</label>
<select className={inputCls} value={f.estado} onChange={e => set('estado', e.target.value)}>
{UF_LIST.map(uf => <option key={uf} value={uf}>{uf || 'SELECIONE'}</option>)}
</select>
</div>
</div>
{ws?.id && (
<label className="flex items-center gap-3 p-3 rounded-xl border border-gray-100 bg-gray-50/60 cursor-pointer">
<input type="checkbox" checked={vincularUnidade} onChange={e => setVincularUnidade(e.target.checked)} className="w-4 h-4 accent-teal-600" />
<span className="text-[11px] font-black text-gray-600 uppercase">Anunciar pela unidade: {ws.nome}</span>
</label>
)}
<div>
<label className={labelCls}>Área da sala</label>
<select className={inputCls} value={f.area} onChange={e => set('area', e.target.value)}>
<option value=""> Área (padrão pelo seu perfil) </option>
{areas.map(a => <option key={a.slug} value={a.slug}>{a.nome}</option>)}
</select>
</div>
<div>
<p className={labelCls}>Valores de locação fixo (R$) ou "sob consulta" por modalidade</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{([['valorHora', 'horaConsulta', 'HORA'], ['valorTurno', 'turnoConsulta', 'TURNO'], ['valorDiaria', 'diariaConsulta', 'DIÁRIA'], ['valorSemanal', 'semanalConsulta', 'SEMANAL'], ['valorMensal', 'mensalConsulta', 'MENSAL']] as const).map(([vk, ck, l]) => (
<div key={vk} className="flex items-center gap-2">
<label className={`${labelCls} w-16 mb-0 shrink-0`}>{l}</label>
<input type="number" min="0" disabled={(f as any)[ck]} value={(f as any)[ck] ? '' : (f as any)[vk]} onChange={e => set(vk, e.target.value)}
className={`${inputCls} flex-1 ${(f as any)[ck] ? 'opacity-40' : ''}`} placeholder={(f as any)[ck] ? 'sob consulta' : '—'} />
<label className="flex items-center gap-1 text-[9px] font-bold text-gray-500 uppercase cursor-pointer whitespace-nowrap">
<input type="checkbox" checked={(f as any)[ck]} onChange={e => set(ck, e.target.checked)} className="w-3.5 h-3.5 accent-teal-600" /> sob consulta
</label>
</div>
))}
</div>
</div>
</div>
<div className="px-6 pb-6 flex gap-3">
<button onClick={onClose} className="flex-1 py-3 border border-gray-200 rounded-xl text-xs font-black uppercase text-gray-500 hover:bg-gray-50 transition-colors">CANCELAR</button>
<button onClick={handleSave} disabled={saving} className="flex-1 py-3 text-white rounded-xl text-xs font-black uppercase transition-colors flex items-center justify-center gap-2 disabled:opacity-60" style={{ backgroundColor: COLOR }}>
{saving ? <RefreshCw size={14} className="animate-spin" /> : <Save size={14} />} SALVAR
</button>
</div>
</div>
</div>
);
};
// ─── Modal: reservar sala ────────────────────────────────────────────────────
const ReservaModal: React.FC<{ sala: Sala; onClose: () => void; onReserved: () => void }> = ({ sala, onClose, onReserved }) => {
const toast = useToast();
const ws = HybridBackend.getActiveWorkspace();
const [saving, setSaving] = useState(false);
const [ocupadas, setOcupadas] = useState<Reserva[]>([]);
const [f, setF] = useState({ inicio: '', fim: '', modelo: 'hora', observacoes: '' });
// Reserva institucional: em nome da unidade ativa, com profissional designado opcional.
const [comoUnidade, setComoUnidade] = useState(false);
const [profissionais, setProfissionais] = useState<Array<{ usuario_id: string; nome: string }>>([]);
const [profissionalId, setProfissionalId] = useState('');
// ── Calendário de reserva ──
const hoje0 = new Date(); hoje0.setHours(0, 0, 0, 0);
const [mesRef, setMesRef] = useState(new Date(hoje0.getFullYear(), hoje0.getMonth(), 1));
const [diaSel, setDiaSel] = useState<Date | null>(null);
const [diasMulti, setDiasMulti] = useState<Date[]>([]); // modelo "Dia": dias avulsos (1, 5, 20…)
const [hora, setHora] = useState('08:00');
const [qtd, setQtd] = useState(1);
const [turno, setTurno] = useState('manha');
const HORAS = Array.from({ length: 29 }, (_, i) => `${pad2(7 + Math.floor(i / 2))}:${i % 2 ? '30' : '00'}`); // 07:0021:00
const modelosDisp = MODELOS.filter(m => fmtValor(sala[m.valor] as any) || sala[m.consulta]);
const diasOcupados = new Set(ocupadas.map(o => new Date(o.inicio).toDateString()));
const ocupadasDoDia = diaSel ? ocupadas.filter(o => new Date(o.inicio).toDateString() === diaSel.toDateString()) : [];
const rangeFim = diaSel ? computeRange(diaSel, f.modelo, hora, qtd, turno).fim : null;
const diasMultiSorted = [...diasMulti].sort((a, b) => a.getTime() - b.getTime());
const temSelecao = f.modelo === 'diaria' ? diasMulti.length > 0 : !!diaSel;
// ── Wizard (4 passos, formato slide) ──
const [step, setStep] = useState(0);
const [formaPagamento, setFormaPagamento] = useState('pix');
const [showContrato, setShowContrato] = useState(false);
const [contratoAceito, setContratoAceito] = useState(false);
const contratoTexto = (sala.contrato && sala.contrato.trim()) || CONTRATO_MODELO;
const STEPS = ['Dados', 'Calendário', 'Resumo', 'Pagamento'];
const FORMAS: Record<string, string> = { pix: 'PIX', dinheiro: 'Dinheiro', transferencia: 'Transferência', cartao: 'Cartão' };
const modeloLabel = MODELOS.find(m => m.id === f.modelo)?.label || '';
const total = (() => {
const m = MODELOS.find(x => x.id === f.modelo); const v = m ? Number(sala[m.valor]) : NaN;
if (!Number.isFinite(v)) return 0;
if (f.modelo === 'turno') return v;
if (f.modelo === 'diaria') return v * diasMulti.length;
return v * Math.max(1, qtd);
})();
// Modelo padrão = 1º disponível.
useEffect(() => { if (modelosDisp.length && !modelosDisp.some(m => m.id === f.modelo)) setF(p => ({ ...p, modelo: modelosDisp[0].id })); /* eslint-disable-next-line */ }, []);
// Recalcula início/fim conforme dia + modelo + horário.
useEffect(() => {
if (!diaSel) return;
const { ini, fim } = computeRange(diaSel, f.modelo, hora, qtd, turno);
setF(p => ({ ...p, inicio: toLocalInput(ini), fim: toLocalInput(fim) }));
}, [diaSel, f.modelo, hora, qtd, turno]);
// Ao trocar de modelo, volta a quantidade para 1 e limpa a multi-seleção de dias.
useEffect(() => { setQtd(1); setDiasMulti([]); }, [f.modelo]);
// Depois de escolher o(s) dia(s), revela os controles (que ficam abaixo do calendário).
const ctrlRef = useRef<HTMLDivElement>(null);
useEffect(() => { if (diaSel || diasMulti.length) ctrlRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }, [diaSel, diasMulti.length, f.modelo]);
useEffect(() => {
apiFetch(`/api/salas/${sala.id}/reservas`)
.then(r => (r.ok ? r.json() : []))
.then(setOcupadas)
.catch(() => {});
}, [sala.id]);
useEffect(() => {
if (!comoUnidade || !ws?.id || profissionais.length) return;
apiFetch(`/api/dentistas?clinicaId=${encodeURIComponent(ws.id)}`)
.then(r => (r.ok ? r.json() : []))
.then((ds: any[]) => setProfissionais(ds.filter(d => d.usuario_id).map(d => ({ usuario_id: d.usuario_id, nome: d.nome }))))
.catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [comoUnidade]);
const handleReserve = async () => {
// Modelo "Dia" → uma reserva por dia avulso selecionado; demais → um único intervalo.
const ranges = f.modelo === 'diaria'
? diasMultiSorted.map(d => computeRange(d, 'diaria', hora, 1, turno))
: (f.inicio && f.fim ? [{ ini: new Date(f.inicio), fim: new Date(f.fim) }] : []);
if (!ranges.length) { toast.error(f.modelo === 'diaria' ? 'SELECIONE AO MENOS UM DIA.' : 'INFORME INÍCIO E FIM.'); return; }
if (!contratoAceito) { toast.error('É PRECISO ACEITAR O CONTRATO DE LOCAÇÃO.'); return; }
setSaving(true);
try {
const obs = [f.observacoes, `Forma de pagamento: ${FORMAS[formaPagamento] || formaPagamento}`, 'Contrato de locação: aceito pelo locatário'].filter(Boolean).join(' · ') || null;
const base = {
modelo: f.modelo, observacoes: obs,
clinicaId: comoUnidade && ws?.id ? ws.id : null,
profissionalUsuarioId: comoUnidade && profissionalId ? profissionalId : null,
};
let ok = 0; const erros: string[] = [];
for (const r of ranges) {
const res = await apiFetch(`/api/salas/${sala.id}/reservas`, {
method: 'POST',
body: JSON.stringify({ ...base, inicio: r.ini.toISOString(), fim: r.fim.toISOString() }),
});
const data = await res.json().catch(() => ({}));
if (res.ok) ok++; else erros.push(data.error || 'erro');
}
if (!ok) throw new Error(erros[0] || 'Erro ao reservar.');
toast.success(ranges.length > 1 ? `${ok} RESERVAS SOLICITADAS! AGUARDE A CONFIRMAÇÃO DO LOCADOR.` : 'RESERVA SOLICITADA! AGUARDE A CONFIRMAÇÃO DO LOCADOR.');
if (erros.length) toast.error(`${erros.length} DIA(S) NÃO PUDERAM SER RESERVADOS (JÁ OCUPADOS?).`);
onReserved();
onClose();
} catch (e: any) { toast.error(e.message.toUpperCase()); }
finally { setSaving(false); }
};
return (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center backdrop-blur-sm p-4">
<div className="bg-white rounded-[1.5rem] w-full max-w-lg shadow-2xl overflow-hidden max-h-[92vh] flex flex-col">
{/* Header + stepper */}
<div className="px-6 pt-5 pb-4 border-b border-gray-100 shrink-0">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-black text-gray-900 uppercase">Reservar {sala.nome}</h3>
<p className="text-[10px] font-bold uppercase tracking-widest" style={{ color: COLOR }}>{[sala.cidade, sala.estado].filter(Boolean).join(' · ')}</p>
</div>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-xl transition-colors"><X size={18} /></button>
</div>
<div className="flex items-center gap-2 mt-4">
{STEPS.map((s, i) => (
<button key={s} type="button" onClick={() => i < step && setStep(i)} className="flex-1 flex flex-col items-center gap-1.5">
<div className={`w-full h-1.5 rounded-full transition-colors ${i <= step ? '' : 'bg-gray-100'}`} style={i <= step ? { backgroundColor: COLOR } : {}} />
<span className={`text-[8px] font-black uppercase tracking-wide ${i === step ? 'text-gray-700' : 'text-gray-300'}`}>{s}</span>
</button>
))}
</div>
</div>
{/* Slides */}
<div className="flex-1 overflow-hidden min-h-[340px]">
<div className="flex h-full transition-transform duration-300 ease-out" style={{ transform: `translateX(-${step * 100}%)` }}>
{/* PASSO 1 — Dados */}
<div className="w-full shrink-0 overflow-y-auto p-6 space-y-4">
{ws?.id && (
<div>
<label className={labelCls}>Reservar como</label>
<div className="flex gap-2">
{[{ v: false, l: 'PESSOAL' }, { v: true, l: ws.nome?.toUpperCase() || 'UNIDADE' }].map(o => (
<button key={String(o.v)} type="button" onClick={() => setComoUnidade(o.v)}
className={`flex-1 py-2.5 rounded-xl text-[10px] font-black uppercase border transition-all truncate px-2 ${comoUnidade === o.v ? 'text-white border-transparent' : 'bg-gray-50 text-gray-500 border-gray-200 hover:bg-gray-100'}`}
style={comoUnidade === o.v ? { backgroundColor: COLOR } : {}}>{o.l}</button>
))}
</div>
</div>
)}
{comoUnidade && (
<div>
<label className={labelCls}>Profissional que vai usar a sala (opcional)</label>
<select className={inputCls} value={profissionalId} onChange={e => setProfissionalId(e.target.value)}>
<option value="">EU MESMO ({HybridBackend.getCurrentUserName()?.toUpperCase() || 'SOLICITANTE'})</option>
{profissionais.map(p => <option key={p.usuario_id} value={p.usuario_id}>{p.nome?.toUpperCase()}</option>)}
</select>
<p className="text-[9px] text-gray-400 font-bold uppercase mt-1">O anti-conflito verifica a agenda do profissional selecionado</p>
</div>
)}
<div>
<label className={labelCls}>Modelo de locação</label>
<div className="grid grid-cols-3 sm:grid-cols-5 gap-2 mt-1">
{(modelosDisp.length ? modelosDisp : [MODELOS[0]]).map(m => {
const ativo = f.modelo === m.id;
const preco = fmtValor(sala[m.valor] as any) || (sala[m.consulta] ? 'sob consulta' : '—');
return (
<button key={m.id} type="button" onClick={() => setF(p => ({ ...p, modelo: m.id }))}
className={`p-2.5 rounded-xl border text-center transition-all ${ativo ? 'text-white border-transparent shadow-sm' : 'bg-gray-50 text-gray-600 border-gray-200 hover:bg-gray-100'}`}
style={ativo ? { backgroundColor: COLOR } : {}}>
<div className="text-[11px] font-black uppercase leading-none">{m.label}</div>
<div className={`text-[9px] font-bold mt-1 leading-none ${ativo ? 'text-white/80' : 'text-gray-400'}`}>{preco}</div>
</button>
);
})}
</div>
</div>
<div>
<label className={labelCls}>Observações</label>
<textarea className={inputCls} rows={4} value={f.observacoes} onChange={e => setF(p => ({ ...p, observacoes: e.target.value }))} placeholder="Alguma informação para o locador…" />
</div>
</div>
{/* PASSO 2 — Calendário */}
<div className="w-full shrink-0 overflow-y-auto p-6 space-y-4">
<div className="rounded-2xl border border-gray-100 p-4">
<div className="flex items-center justify-between mb-3">
<button type="button" onClick={() => setMesRef(m => new Date(m.getFullYear(), m.getMonth() - 1, 1))} className="p-1.5 rounded-lg hover:bg-gray-100 text-gray-500"><ChevronLeft size={18} /></button>
<span className="text-xs font-black text-gray-800 uppercase tracking-wide">{MESES[mesRef.getMonth()]} {mesRef.getFullYear()}</span>
<button type="button" onClick={() => setMesRef(m => new Date(m.getFullYear(), m.getMonth() + 1, 1))} className="p-1.5 rounded-lg hover:bg-gray-100 text-gray-500"><ChevronRight size={18} /></button>
</div>
<div className="grid grid-cols-7 gap-1 mb-1">
{WEEKDAYS.map((w, i) => <div key={i} className="text-center text-[9px] font-black text-gray-300 uppercase py-1">{w}</div>)}
</div>
<div className="grid grid-cols-7 gap-1">
{monthGrid(mesRef).map((dia, i) => {
if (!dia) return <div key={i} />;
const passado = dia < hoje0;
const sel = f.modelo === 'diaria'
? diasMulti.some(d => mesmoDia(d, dia))
: !!(diaSel && mesmoDia(dia, diaSel));
const emRange = !!(diaSel && rangeFim && (f.modelo === 'semanal' || f.modelo === 'mensal') && dia > diaSel && dia <= rangeFim);
const ocupado = diasOcupados.has(dia.toDateString());
const onPick = () => {
if (f.modelo === 'diaria') {
setDiasMulti(prev => prev.some(d => mesmoDia(d, dia)) ? prev.filter(d => !mesmoDia(d, dia)) : [...prev, dia]);
} else {
setDiaSel(dia);
}
};
return (
<button key={i} type="button" disabled={passado} onClick={onPick}
className={`relative h-9 rounded-lg text-xs font-bold flex items-center justify-center transition-all ${passado ? 'text-gray-200 cursor-not-allowed' : sel ? 'text-white shadow' : emRange ? 'text-teal-700' : 'text-gray-600 hover:bg-gray-100'}`}
style={sel ? { backgroundColor: COLOR } : emRange ? { backgroundColor: `${COLOR}22` } : {}}>
{dia.getDate()}
{ocupado && !sel && <span className="absolute bottom-1 w-1 h-1 rounded-full bg-amber-400" />}
</button>
);
})}
</div>
</div>
{temSelecao && (
<div ref={ctrlRef} className="rounded-2xl border-2 p-4 space-y-3" style={{ borderColor: `${COLOR}33`, backgroundColor: `${COLOR}0a` }}>
<div className="flex items-center justify-between">
<div>
<p className="text-[9px] font-black text-gray-400 uppercase tracking-widest">Selecionado</p>
<p className="text-xs font-black text-gray-800">
{f.modelo === 'diaria'
? `${diasMulti.length} ${diasMulti.length === 1 ? 'dia' : 'dias'}`
: `${diaSel?.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' })}${(f.modelo === 'semanal' || f.modelo === 'mensal') && rangeFim ? `${rangeFim.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' })}` : ''}`}
</p>
</div>
<span className="text-[10px] font-black uppercase px-2.5 py-1 rounded-full" style={{ backgroundColor: `${COLOR}18`, color: COLOR }}>{modeloLabel}</span>
</div>
{f.modelo === 'hora' && (
<div className="grid grid-cols-2 gap-3">
<div>
<label className={labelCls}>Início</label>
<select className={inputCls} value={hora} onChange={e => setHora(e.target.value)}>
{HORAS.map(h => <option key={h} value={h}>{h}</option>)}
</select>
</div>
<div>
<label className={labelCls}>Duração</label>
<div className="flex items-center gap-2">
<button type="button" onClick={() => setQtd(q => Math.max(1, q - 1))} className="w-9 h-9 rounded-lg border border-gray-200 bg-white text-gray-500 font-black text-lg leading-none"></button>
<span className="flex-1 text-center text-sm font-black text-gray-800">{qtd}h</span>
<button type="button" onClick={() => setQtd(q => Math.min(12, q + 1))} className="w-9 h-9 rounded-lg border border-gray-200 bg-white text-gray-500 font-black text-lg leading-none">+</button>
</div>
</div>
</div>
)}
{f.modelo === 'turno' && (
<div>
<label className={labelCls}>Período</label>
<div className="grid grid-cols-3 gap-2">
{Object.entries(TURNOS).map(([k, t]) => (
<button key={k} type="button" onClick={() => setTurno(k)}
className={`py-2 rounded-xl text-[11px] font-black uppercase border transition-all ${turno === k ? 'text-white border-transparent' : 'bg-white text-gray-500 border-gray-200 hover:bg-gray-100'}`}
style={turno === k ? { backgroundColor: COLOR } : {}}>
{t.label}<div className="text-[8px] font-bold opacity-70">{pad2(t.ini)}h{pad2(t.fim)}h</div>
</button>
))}
</div>
</div>
)}
{(f.modelo === 'semanal' || f.modelo === 'mensal') && (
<div>
<label className={labelCls}>Quantidade de {UNIDADE[f.modelo]?.plur}</label>
<div className="flex items-center gap-2">
<button type="button" onClick={() => setQtd(q => Math.max(1, q - 1))} className="w-9 h-9 rounded-lg border border-gray-200 bg-white text-gray-500 font-black text-lg leading-none"></button>
<span className="flex-1 text-center text-sm font-black text-gray-800">{qtd} {qtd === 1 ? UNIDADE[f.modelo]?.sing : UNIDADE[f.modelo]?.plur}</span>
<button type="button" onClick={() => setQtd(q => Math.min(UNIDADE[f.modelo]?.max || 12, q + 1))} className="w-9 h-9 rounded-lg border border-gray-200 bg-white text-gray-500 font-black text-lg leading-none">+</button>
</div>
</div>
)}
{f.modelo === 'diaria' && (
<div>
<label className={labelCls}>Dias selecionados ({diasMulti.length})</label>
<div className="flex flex-wrap gap-1.5">
{diasMultiSorted.map((d, i) => {
const ocup = diasOcupados.has(d.toDateString());
return (
<button key={i} type="button" onClick={() => setDiasMulti(prev => prev.filter(x => !mesmoDia(x, d)))}
className="flex items-center gap-1 pl-2.5 pr-1.5 py-1 rounded-full text-[10px] font-black text-white" style={{ backgroundColor: ocup ? '#d97706' : COLOR }}>
{d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' })}
<X size={11} />
</button>
);
})}
</div>
<p className="text-[9px] font-bold text-gray-400 uppercase mt-1.5">Toque no calendário para adicionar/remover dias · cada dia vira uma reserva (08h18h)</p>
</div>
)}
{ocupadasDoDia.length > 0 && (
<div className="p-3 rounded-xl bg-amber-50 border border-amber-100">
<p className="text-[9px] font-black text-amber-600 uppercase tracking-widest mb-1.5 flex items-center gap-1"><Clock size={11} /> ocupado neste dia</p>
<div className="space-y-1 max-h-24 overflow-y-auto">
{ocupadasDoDia.map(o => (
<p key={o.id} className="text-[10px] font-bold text-amber-700">
{new Date(o.inicio).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}{new Date(o.fim).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })} ({o.status})
</p>
))}
</div>
</div>
)}
</div>
)}
</div>
{/* PASSO 3 — Resumo detalhado + históricos */}
<div className="w-full shrink-0 overflow-y-auto p-6 space-y-4">
<div className="rounded-2xl border border-gray-100 overflow-hidden">
{([
['Sala', sala.nome],
['Local', [sala.cidade, sala.estado].filter(Boolean).join(' · ') || '—'],
['Modelo', modeloLabel],
...(f.modelo === 'diaria'
? [
['Quantidade', `${diasMulti.length} ${diasMulti.length === 1 ? 'dia' : 'dias'}`],
['Dias', diasMultiSorted.map(d => d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' })).join(', ') || '—'],
]
: [
...(UNIDADE[f.modelo] ? [['Quantidade', `${qtd} ${qtd === 1 ? UNIDADE[f.modelo].sing : UNIDADE[f.modelo].plur}`]] : []),
['Início', f.inicio ? fmtData(new Date(f.inicio).toISOString()) : '—'],
['Fim', f.fim ? fmtData(new Date(f.fim).toISOString()) : '—'],
]) as Array<[string, string]>,
['Reservar como', comoUnidade ? (ws?.nome || 'Unidade') : 'Pessoal'],
] as Array<[string, string]>).map(([k, v]) => (
<div key={k} className="flex items-center justify-between px-4 py-2.5 border-b border-gray-50 last:border-0">
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">{k}</span>
<span className="text-xs font-bold text-gray-800 text-right max-w-[62%] truncate">{v}</span>
</div>
))}
<div className="flex items-center justify-between px-4 py-3 bg-teal-50/60">
<span className="text-[10px] font-black text-teal-600 uppercase tracking-widest">Total estimado</span>
<span className="text-base font-black text-teal-700">{total > 0 ? fmtValor(String(total)) : 'Sob consulta'}</span>
</div>
</div>
<div>
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 flex items-center gap-1"><Clock size={11} /> Histórico de reservas desta sala</p>
{ocupadas.length === 0 ? (
<p className="text-[11px] text-gray-300 font-bold uppercase py-2">Sem reservas registradas</p>
) : (
<div className="space-y-1.5 max-h-44 overflow-y-auto">
{ocupadas.slice().sort((a, b) => new Date(b.inicio).getTime() - new Date(a.inicio).getTime()).map(o => (
<div key={o.id} className="flex items-center justify-between text-[11px] bg-gray-50 rounded-lg px-3 py-1.5">
<span className="font-bold text-gray-600">{fmtData(o.inicio)} {new Date(o.fim).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}</span>
<span className={`text-[9px] font-black uppercase px-1.5 py-0.5 rounded-full ${o.status === 'confirmada' ? 'bg-emerald-100 text-emerald-700' : o.status === 'pendente' ? 'bg-amber-100 text-amber-700' : 'bg-gray-200 text-gray-500'}`}>{o.status}</span>
</div>
))}
</div>
)}
</div>
</div>
{/* PASSO 4 — Pagamento */}
<div className="w-full shrink-0 overflow-y-auto p-6 space-y-4">
<div className="rounded-2xl bg-gradient-to-br from-teal-50 to-white border border-teal-100 p-5 text-center">
<p className="text-[10px] font-black text-teal-600 uppercase tracking-widest">Total{modeloLabel ? ` · ${modeloLabel}` : ''}</p>
<p className="text-3xl font-black text-teal-700 mt-1">{total > 0 ? fmtValor(String(total)) : 'Sob consulta'}</p>
{total > 0 && f.modelo === 'diaria' && diasMulti.length > 1 && <p className="text-[10px] text-teal-500 font-bold mt-1">{diasMulti.length} dias × {fmtValor(String(Number(sala.valor_diaria)))}</p>}
{total > 0 && f.modelo !== 'diaria' && UNIDADE[f.modelo] && qtd > 1 && <p className="text-[10px] text-teal-500 font-bold mt-1">{qtd} {UNIDADE[f.modelo].plur} × {fmtValor(String(Number(sala[MODELOS.find(m => m.id === f.modelo)!.valor])))}</p>}
</div>
<div>
<label className={labelCls}>Forma de pagamento</label>
<div className="grid grid-cols-2 gap-2 mt-1">
{Object.entries(FORMAS).map(([k, l]) => (
<button key={k} type="button" onClick={() => setFormaPagamento(k)}
className={`py-2.5 rounded-xl text-[11px] font-black uppercase border transition-all ${formaPagamento === k ? 'text-white border-transparent' : 'bg-gray-50 text-gray-500 border-gray-200 hover:bg-gray-100'}`}
style={formaPagamento === k ? { backgroundColor: COLOR } : {}}>{l}</button>
))}
</div>
</div>
<div>
<label className={labelCls}>Contrato de locação</label>
<button type="button" onClick={() => setShowContrato(true)}
className="w-full flex items-center justify-between rounded-xl border p-3.5 transition-colors hover:bg-gray-50"
style={{ borderColor: contratoAceito ? '#16a34a55' : `${COLOR}44`, backgroundColor: contratoAceito ? '#16a34a0d' : `${COLOR}0a` }}>
<span className="flex items-center gap-2 text-xs font-black text-gray-700 uppercase"><FileText size={15} style={{ color: contratoAceito ? '#16a34a' : COLOR }} /> Ler contrato de locação</span>
<span className="text-[10px] font-black uppercase flex items-center gap-1" style={{ color: contratoAceito ? '#16a34a' : COLOR }}>
{contratoAceito ? <><CheckCircle2 size={13} /> Aceito</> : 'Abrir'}
</span>
</button>
<label className="flex items-start gap-2 mt-2 px-1 cursor-pointer">
<input type="checkbox" checked={contratoAceito} onChange={e => setContratoAceito(e.target.checked)} className="mt-0.5 w-4 h-4 accent-teal-600 shrink-0" />
<span className="text-[11px] font-bold text-gray-600 leading-snug">Li e concordo com o contrato de locação os procedimentos realizados são de minha inteira responsabilidade.</span>
</label>
</div>
<div className="rounded-xl bg-gray-50 border border-gray-100 p-3.5 text-[11px] text-gray-500 leading-relaxed">
<span className="font-black text-gray-600 uppercase">Como funciona: </span>ao solicitar, o locador recebe seu pedido e confirma. O pagamento é combinado diretamente com ele após a confirmação sua preferência ({FORMAS[formaPagamento]}) vai junto no pedido.
</div>
</div>
</div>
</div>
{/* Footer nav */}
<div className="px-6 py-4 border-t border-gray-100 flex gap-3 shrink-0">
<button onClick={() => (step === 0 ? onClose() : setStep(s => s - 1))} className="flex-1 py-3 border border-gray-200 rounded-xl text-xs font-black uppercase text-gray-500 hover:bg-gray-50 transition-colors">
{step === 0 ? 'Cancelar' : 'Voltar'}
</button>
{step < 3 ? (
<button onClick={() => setStep(s => s + 1)} disabled={step === 1 && !temSelecao}
className="flex-1 py-3 text-white rounded-xl text-xs font-black uppercase transition-opacity hover:opacity-90 disabled:opacity-40 flex items-center justify-center gap-2" style={{ backgroundColor: COLOR }}>
Próximo <ChevronRight size={14} />
</button>
) : (
<button onClick={handleReserve} disabled={saving || !temSelecao || !contratoAceito}
className="flex-1 py-3 text-white rounded-xl text-xs font-black uppercase transition-opacity hover:opacity-90 disabled:opacity-50 flex items-center justify-center gap-2" style={{ backgroundColor: COLOR }}>
{saving ? <RefreshCw size={14} className="animate-spin" /> : <CalendarClock size={14} />} Solicitar Reserva
</button>
)}
</div>
</div>
{/* Modal: contrato de locação */}
{showContrato && (
<div className="fixed inset-0 bg-black/60 z-[60] flex items-center justify-center backdrop-blur-sm p-4" onClick={() => setShowContrato(false)}>
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl max-h-[85vh] flex flex-col overflow-hidden" onClick={e => e.stopPropagation()}>
<div className="px-5 py-4 border-b border-gray-100 flex items-center justify-between shrink-0">
<h4 className="text-sm font-black text-gray-900 uppercase flex items-center gap-2"><FileText size={16} style={{ color: COLOR }} /> Contrato de locação</h4>
<button onClick={() => setShowContrato(false)} className="p-1.5 hover:bg-gray-100 rounded-lg text-gray-500"><X size={16} /></button>
</div>
<div className="p-5 overflow-y-auto text-[11px] text-gray-600 leading-relaxed whitespace-pre-wrap font-medium">{contratoTexto}</div>
<div className="p-4 border-t border-gray-100 shrink-0 flex gap-2">
<button onClick={() => setShowContrato(false)} className="flex-1 py-3 border border-gray-200 rounded-xl text-xs font-black uppercase text-gray-500 hover:bg-gray-50">Fechar</button>
<button onClick={() => { setContratoAceito(true); setShowContrato(false); }} className="flex-1 py-3 text-white rounded-xl text-xs font-black uppercase hover:opacity-90 flex items-center justify-center gap-2" style={{ backgroundColor: COLOR }}><CheckCircle2 size={14} /> Li e concordo</button>
</div>
</div>
</div>
)}
</div>
);
};
// ─── Card de sala (marketplace + minhas) ────────────────────────────────────
const SalaCard: React.FC<{ sala: Sala; mine?: boolean; onReserve?: () => void; onEdit?: () => void; onDelete?: () => void }> = ({ sala, mine, onReserve, onEdit, onDelete }) => {
const meta = TIPO_META[sala.tipo] || TIPO_META.independente;
const Icon = meta.icon;
return (
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-5 hover:shadow-md transition-all">
<div className="flex items-start gap-3 mb-3">
<div className="w-10 h-10 rounded-xl flex items-center justify-center shrink-0" style={{ backgroundColor: `${COLOR}15` }}>
<Icon size={18} style={{ color: COLOR }} />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-black text-gray-900 text-sm uppercase truncate">{sala.nome}</span>
<span className="text-[9px] font-black px-2 py-0.5 rounded-full uppercase" style={{ backgroundColor: `${COLOR}15`, color: COLOR }}>{meta.label}</span>
{sala.area && AREA_META[sala.area] && <span className="text-[9px] font-black px-2 py-0.5 rounded-full uppercase text-white" style={{ backgroundColor: AREA_META[sala.area].cor }}>{AREA_META[sala.area].nome}</span>}
{mine && sala.disponivel === false && <span className="text-[9px] font-black px-2 py-0.5 rounded-full uppercase bg-gray-100 text-gray-500">OCULTA</span>}
{mine && (sala.reservas_pendentes || 0) > 0 && <span className="text-[9px] font-black px-2 py-0.5 rounded-full uppercase bg-amber-100 text-amber-700">{sala.reservas_pendentes} PENDENTE{sala.reservas_pendentes! > 1 ? 'S' : ''}</span>}
</div>
{!mine && (sala.clinica_nome || sala.owner_nome) && <p className="text-[10px] text-gray-400 font-bold uppercase mt-0.5">POR {sala.clinica_nome || sala.owner_nome}</p>}
{mine && sala.clinica_nome && <p className="text-[10px] text-gray-400 font-bold uppercase mt-0.5">ANUNCIADA POR {sala.clinica_nome}</p>}
</div>
</div>
{sala.descricao && <p className="text-xs text-gray-500 font-medium mb-2 line-clamp-2">{sala.descricao}</p>}
<div className="flex items-center gap-1.5 text-xs text-gray-500 font-medium mb-2">
<MapPin size={12} className="shrink-0 text-gray-400" />
<span>{[sala.endereco, sala.bairro, sala.cidade, sala.estado].filter(Boolean).join(' · ')}</span>
{sala.dist_km != null && <span className="ml-auto shrink-0 text-[9px] font-black px-2 py-0.5 rounded-full" style={{ backgroundColor: `${COLOR}15`, color: COLOR }}>~{sala.dist_km} KM</span>}
</div>
{sala.equipamentos && (
<p className="text-[10px] text-gray-400 font-bold uppercase mb-3">EQUIP.: {sala.equipamentos}</p>
)}
<div className="flex flex-wrap gap-1.5 mb-4">
{precos(sala).map(([l, v]) => (
<span key={l} className="text-[9px] font-black px-2 py-1 rounded-lg bg-gray-50 border border-gray-100 text-gray-600 uppercase">{l}: {fmtValor(v)}</span>
))}
{precos(sala).length === 0 && <span className="text-[9px] font-bold text-gray-300 uppercase">Valores a combinar</span>}
</div>
<div className="flex gap-2 pt-2 border-t border-gray-50">
{mine ? (
<>
<button onClick={onEdit} className="flex-1 py-2.5 rounded-xl border border-gray-200 text-gray-500 hover:bg-gray-50 text-[10px] font-black uppercase flex items-center justify-center gap-1.5 transition-colors"><Pencil size={12} /> EDITAR</button>
<button onClick={onDelete} className="px-4 py-2.5 rounded-xl border border-red-100 text-red-400 hover:bg-red-50 transition-colors"><Trash2 size={14} /></button>
</>
) : (
<button onClick={onReserve} className="flex-1 py-2.5 rounded-xl text-white text-[10px] font-black uppercase flex items-center justify-center gap-1.5 transition-colors hover:opacity-90" style={{ backgroundColor: COLOR }}>
<CalendarClock size={13} /> RESERVAR
</button>
)}
</div>
</div>
);
};
// ─── Lista de reservas ───────────────────────────────────────────────────────
const ReservaRow: React.FC<{ r: Reserva; recebida?: boolean; onAction: (acao: 'confirmar' | 'recusar' | 'cancelar') => void }> = ({ r, recebida, onAction }) => (
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex flex-col md:flex-row md:items-center gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-black text-gray-900 text-xs uppercase">{r.sala_nome || r.sala_id}</span>
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase ${STATUS_STYLE[r.status] || STATUS_STYLE.cancelada}`}>{r.status}</span>
<span className="text-[9px] font-black px-2 py-0.5 rounded-full uppercase bg-gray-50 text-gray-500">{r.modelo}</span>
</div>
<p className="text-[11px] text-gray-500 font-bold mt-1">{fmtData(r.inicio)} {fmtData(r.fim)}</p>
<p className="text-[10px] text-gray-400 font-medium uppercase mt-0.5">
{recebida
? <>SOLICITANTE: {r.clinica_nome ? `${r.clinica_nome} (${r.solicitante_nome || '—'})` : (r.solicitante_nome || '—')}</>
: <>LOCADOR: {r.owner_nome || '—'} · {[r.cidade, r.estado].filter(Boolean).join('/')}{r.clinica_nome ? <> · EM NOME DE {r.clinica_nome}</> : null}</>}
</p>
{r.profissional_nome && <p className="text-[10px] text-gray-400 font-medium uppercase mt-0.5">PROFISSIONAL: {r.profissional_nome}</p>}
{r.observacoes && <p className="text-[10px] text-gray-400 mt-0.5">OBS: {r.observacoes}</p>}
</div>
{['pendente', 'confirmada'].includes(r.status) && (
<div className="flex gap-2 shrink-0">
{recebida && r.status === 'pendente' && (
<>
<button onClick={() => onAction('confirmar')} className="px-3 py-2 rounded-xl bg-emerald-500 text-white text-[10px] font-black uppercase flex items-center gap-1 hover:bg-emerald-600 transition-colors"><CheckCircle2 size={12} /> CONFIRMAR</button>
<button onClick={() => onAction('recusar')} className="px-3 py-2 rounded-xl border border-red-200 text-red-500 text-[10px] font-black uppercase flex items-center gap-1 hover:bg-red-50 transition-colors"><XCircle size={12} /> RECUSAR</button>
</>
)}
<button onClick={() => onAction('cancelar')} className="px-3 py-2 rounded-xl border border-gray-200 text-gray-400 text-[10px] font-black uppercase hover:bg-gray-50 transition-colors">CANCELAR</button>
</div>
)}
</div>
);
// ─── Main view ───────────────────────────────────────────────────────────────
type Tab = 'marketplace' | 'minhas' | 'reservas';
// Bloqueio quando falta endereço/CEP da origem (clínica ou perfil) — a busca é por proximidade.
const GateEndereco: React.FC<{ isClinic: boolean }> = ({ isClinic }) => (
<div className="bg-white rounded-2xl border border-amber-200 shadow-sm p-8 text-center">
<div className="w-14 h-14 rounded-2xl bg-amber-50 flex items-center justify-center mx-auto mb-4">
<MapPin size={26} className="text-amber-500" />
</div>
<h3 className="text-base font-black uppercase tracking-tight text-gray-900">Complete seu endereço</h3>
<p className="text-sm text-gray-500 mt-2 max-w-md mx-auto leading-relaxed">
A busca de salas é por proximidade. Preencha {isClinic
? 'a localização e o CEP da sua clínica/consultório (em Configurações)'
: 'a localização e o CEP do seu perfil (em Profissionais → Meu Perfil)'} para continuar.
</p>
<button onClick={() => window.location.assign(isClinic ? '/configuracoes' : '/marketplace-profissionais')}
className="mt-5 inline-flex items-center gap-2 px-5 py-3 rounded-xl text-white text-xs font-black uppercase tracking-widest hover:opacity-90 transition-opacity"
style={{ backgroundColor: COLOR }}>
{isClinic ? 'Ir para Configurações' : 'Completar meu perfil'}
</button>
</div>
);
export const SalasPlugin: React.FC<{ view?: 'minhas' | 'alugar' }> = ({ view }) => {
const toast = useToast();
const confirm = useConfirm();
const [tab, setTab] = useState<Tab>(view === 'minhas' ? 'minhas' : 'marketplace');
const [loading, setLoading] = useState(false);
// marketplace
const [salas, setSalas] = useState<Sala[]>([]);
const [fTipo, setFTipo] = useState('');
const [fEstado, setFEstado] = useState('');
const [fCidade, setFCidade] = useState('');
const [fCep, setFCep] = useState('');
const [fRaio, setFRaio] = useState('');
const [fArea, setFArea] = useState('');
const [fBusca, setFBusca] = useState('');
const [showFiltros, setShowFiltros] = useState(true);
const [areas, setAreas] = useState<{ slug: string; nome: string }[]>([]);
useEffect(() => { HybridBackend.getAreas().then(setAreas); }, []);
const limparFiltros = () => { setFTipo(''); setFEstado(''); setFCidade(''); setFCep(''); setFRaio(''); setFArea(''); setFBusca(''); };
// Busca por texto (nome/cidade/bairro) aplicada sobre o resultado do backend.
const salasFiltradas = fBusca.trim()
? salas.filter(s => `${s.nome} ${(s as any).cidade || ''} ${(s as any).bairro || ''}`.toLowerCase().includes(fBusca.trim().toLowerCase()))
: salas;
// minhas
const [minhas, setMinhas] = useState<Sala[]>([]);
// reservas
const [reservas, setReservas] = useState<{ feitas: Reserva[]; recebidas: Reserva[] }>({ feitas: [], recebidas: [] });
const [editingSala, setEditingSala] = useState<Sala | null | 'new'>(null);
const [reservingSala, setReservingSala] = useState<Sala | null>(null);
// Endereço da origem (clínica ou perfil) preenchido? null = checando.
const role = HybridBackend.getCurrentRole();
const ws = HybridBackend.getActiveWorkspace();
const isProfissional = ['dentista', 'biomedico', 'protetico'].includes(role);
const [enderecoOk, setEnderecoOk] = useState<boolean | null>(null);
const checkEndereco = useCallback(async () => {
try {
if (isProfissional) {
const res = await apiFetch('/api/profissionais/perfil');
const p = res.ok ? await res.json() : {};
setEnderecoOk(!!(p?.cep && p?.cidade && p?.estado));
} else if (ws?.id) {
const r = await HybridBackend.getClinicaProfile(ws.id);
const c: any = r?.clinica || {};
setEnderecoOk(!!(c.cep && c.cidade && c.estado));
} else {
setEnderecoOk(true);
}
} catch { setEnderecoOk(true); }
}, [isProfissional, ws?.id]);
useEffect(() => { checkEndereco(); }, [checkEndereco]);
const fetchMarketplace = useCallback(async () => {
setLoading(true);
try {
const params = new URLSearchParams();
if (fTipo) params.set('tipo', fTipo);
if (fEstado) params.set('estado', fEstado);
if (fCidade) params.set('cidade', fCidade);
if (fArea) params.set('area', fArea);
// Origem automática: clínica/consultório ativo (backend cai no geo do usuário se faltar).
const wsId = HybridBackend.getActiveWorkspace()?.id;
if (wsId) params.set('clinicaId', wsId);
if (fCep) params.set('cep', fCep); // CEP digitado sobrepõe a origem automática
if (fCep && Number(fRaio) > 0) params.set('raio', fRaio);
const res = await apiFetch(`/api/salas/marketplace?${params}`);
if (res.ok) setSalas(await res.json());
} catch { /* silently fail */ }
finally { setLoading(false); }
}, [fTipo, fEstado, fCidade, fCep, fRaio, fArea]);
const fetchMinhas = useCallback(async () => {
setLoading(true);
try { const res = await apiFetch('/api/salas/minhas'); if (res.ok) setMinhas(await res.json()); }
catch { /* silently fail */ }
finally { setLoading(false); }
}, []);
const fetchReservas = useCallback(async () => {
setLoading(true);
try {
// Inclui as reservas da unidade ativa (feitas por qualquer membro), além das pessoais.
const wsId = HybridBackend.getActiveWorkspace()?.id;
const res = await apiFetch(`/api/sala-reservas/minhas${wsId ? `?clinicaId=${encodeURIComponent(wsId)}` : ''}`);
if (res.ok) setReservas(await res.json());
}
catch { /* silently fail */ }
finally { setLoading(false); }
}, []);
// Trocar de rota (/alugar-salas ↔ /minhas-salas) reaproveita este componente e
// só muda a prop `view` — o tab do useState não reage sozinho. Reseta a aba p/ a
// do contexto novo (senão a URL muda mas o conteúdo continua na aba anterior).
useEffect(() => { setTab(view === 'minhas' ? 'minhas' : 'marketplace'); }, [view]);
useEffect(() => {
if (tab === 'marketplace') fetchMarketplace();
if (tab === 'minhas') fetchMinhas();
if (tab === 'reservas') fetchReservas();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tab]);
const handleDelete = async (sala: Sala) => {
if (!await confirm(`EXCLUIR A SALA "${sala.nome}"?`)) return;
try {
const res = await apiFetch(`/api/salas/${sala.id}`, { method: 'DELETE' });
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Erro ao excluir.');
toast.success('SALA EXCLUÍDA.');
// Se a sala excluída era o workspace ativo, sai do contexto dela.
if (HybridBackend.getActiveWorkspace()?.id === sala.id) { localStorage.removeItem('SCOREODONTO_ACTIVE_WORKSPACE'); await HybridBackend.refreshWorkspaces(); window.location.reload(); return; }
await HybridBackend.refreshWorkspaces();
fetchMinhas();
} catch (e: any) { toast.error(e.message.toUpperCase()); }
};
const handleReservaAction = async (r: Reserva, acao: 'confirmar' | 'recusar' | 'cancelar') => {
if (acao === 'cancelar' && !await confirm('CANCELAR ESTA RESERVA?')) return;
try {
const res = await apiFetch(`/api/sala-reservas/${r.id}`, { method: 'PUT', body: JSON.stringify({ acao }) });
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Erro.');
toast.success(`RESERVA ${data.status?.toUpperCase()}.`);
fetchReservas();
} catch (e: any) { toast.error(e.message.toUpperCase()); }
};
// Dois modos: 'minhas' (administrar/publicar) e 'alugar' (marketplace). Sem prop = tudo (legado).
const ALL_TABS: Array<{ id: Tab; label: string; icon: any }> = [
{ id: 'marketplace', label: 'MARKETPLACE', icon: Search },
{ id: 'minhas', label: 'MINHAS SALAS', icon: DoorOpen },
{ id: 'reservas', label: 'RESERVAS', icon: CalendarClock },
];
const TABS = view === 'minhas' ? ALL_TABS.filter(t => t.id === 'minhas' || t.id === 'reservas')
: view === 'alugar' ? ALL_TABS.filter(t => t.id === 'marketplace' || t.id === 'reservas')
: ALL_TABS;
const pendentesRecebidas = reservas.recebidas.filter(r => r.status === 'pendente').length;
return (
<div className="space-y-8 animate-in fade-in duration-500">
<PageHeader
title={view === 'minhas' ? 'MINHAS SALAS' : view === 'alugar' ? 'ALUGAR SALAS' : 'LOCAÇÃO DE SALAS'}
description={view === 'minhas' ? 'Publique e administre suas salas, preços, disponibilidade e reservas recebidas.'
: view === 'alugar' ? 'Busque e reserve salas equipadas de outros profissionais.'
: 'Alugue salas equipadas ou disponibilize as suas para outros profissionais.'}
/>
{/* Tabs — segmented control: no mobile ocupa a largura toda e distribui igual;
no desktop encolhe ao conteúdo. */}
{TABS.length > 1 && (
<div className="flex gap-1 bg-gray-100/80 p-1 rounded-2xl w-full sm:w-fit">
{TABS.map(t => {
const Icon = t.icon;
const active = tab === t.id;
return (
<button key={t.id} onClick={() => setTab(t.id)}
className={`flex-1 sm:flex-none flex items-center justify-center gap-2 px-3 sm:px-5 py-2.5 rounded-xl text-[11px] font-black uppercase tracking-wider transition-all ${active ? 'bg-white shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
style={active ? { color: COLOR } : {}}>
<Icon size={14} className="shrink-0" /> <span className="truncate">{t.label}</span>
{t.id === 'reservas' && pendentesRecebidas > 0 && (
<span className="text-[9px] font-black px-1.5 py-0.5 rounded-full text-white" style={{ backgroundColor: COLOR }}>{pendentesRecebidas}</span>
)}
</button>
);
})}
</div>
)}
{/* MARKETPLACE */}
{tab === 'marketplace' && enderecoOk === false && (
<GateEndereco isClinic={!isProfissional} />
)}
{tab === 'marketplace' && enderecoOk !== false && (
<>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-5">
{/* Busca por nome/cidade/bairro (sobre o resultado) */}
<div className="relative mb-5">
<Search size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none" />
<input
className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-200 rounded-2xl text-sm font-medium text-gray-700 outline-none focus:border-teal-400 focus:bg-white transition-colors"
value={fBusca} onChange={e => setFBusca(e.target.value)}
placeholder="Buscar sala por nome, cidade ou bairro..." />
</div>
{showFiltros && (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className={labelNew}>Área</label>
<select className={inputNew} value={fArea} onChange={e => setFArea(e.target.value)}>
<option value="">Todas</option>
{areas.map(a => <option key={a.slug} value={a.slug}>{a.nome}</option>)}
</select>
</div>
<div>
<label className={labelNew}>Tipo</label>
<select className={inputNew} value={fTipo} onChange={e => setFTipo(e.target.value)}>
<option value="">Todos</option>
<option value="clinica">Sala Clínica</option>
<option value="consultorio">Sala Consultório</option>
<option value="independente">Sala Independente</option>
</select>
</div>
<div>
<label className={labelNew}>Estado</label>
<select className={inputNew} value={fEstado} onChange={e => setFEstado(e.target.value)}>
{UF_LIST.map(uf => <option key={uf} value={uf}>{uf || 'Todos'}</option>)}
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 md:items-end">
<div>
<label className={labelNew}>Cidade</label>
<input className={`${inputNew} uppercase`} value={fCidade} onChange={e => setFCidade(e.target.value.toUpperCase())} placeholder="CAMPO GRANDE" />
</div>
<div>
<label className={labelNew}>Meu CEP</label>
<input className={inputNew} value={fCep} onChange={e => setFCep(e.target.value)} placeholder="79000-000" />
</div>
<div>
<div className="flex items-center justify-between mb-1.5">
<label className={`${labelNew} mb-0`}>Raio de Busca</label>
<span className="text-[10px] font-black px-2 py-0.5 rounded-full bg-teal-100 text-teal-700">{fRaio || 0} KM</span>
</div>
<input type="range" min="0" max="200" step="5" value={fRaio || 0}
onChange={e => setFRaio(e.target.value === '0' ? '' : e.target.value)}
className="w-full accent-teal-600 cursor-pointer" />
{!fCep && <p className="text-[10px] text-gray-400 mt-1">Informe o CEP para filtrar por raio.</p>}
</div>
</div>
</div>
)}
<div className="flex items-center justify-between mt-5 pt-4 border-t border-gray-50">
<button onClick={() => setShowFiltros(s => !s)}
className="flex items-center gap-1.5 text-xs font-bold text-gray-500 hover:text-gray-700 transition-colors">
<ChevronUp size={14} className={`transition-transform ${showFiltros ? '' : 'rotate-180'}`} />
{showFiltros ? 'Ocultar Filtros' : 'Mostrar Filtros'}
</button>
<div className="flex items-center gap-4">
<button onClick={limparFiltros} className="flex items-center gap-1.5 text-xs font-bold text-gray-500 hover:text-gray-700 transition-colors">
<Eraser size={13} /> Limpar Filtros
</button>
<button onClick={fetchMarketplace} disabled={loading}
className="flex items-center gap-2 px-6 py-3 text-white rounded-xl text-xs font-black uppercase tracking-widest transition-opacity disabled:opacity-60 hover:opacity-90" style={{ backgroundColor: COLOR }}>
{loading ? <RefreshCw size={14} className="animate-spin" /> : <Search size={14} />} Buscar
</button>
</div>
</div>
</div>
{loading ? (
<div className="flex justify-center py-12"><RefreshCw size={24} className="animate-spin" style={{ color: COLOR }} /></div>
) : salasFiltradas.length === 0 ? (
<div className="text-center py-12 text-gray-400">
<DoorOpen size={40} className="mx-auto mb-3 opacity-40" />
<p className="font-bold text-sm uppercase">Nenhuma sala encontrada</p>
<p className="text-xs mt-1">Ajuste os filtros ou aguarde novos anúncios</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{salasFiltradas.map(s => <SalaCard key={s.id} sala={s} onReserve={() => setReservingSala(s)} />)}
</div>
)}
</>
)}
{/* MINHAS SALAS */}
{tab === 'minhas' && (
<>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<h3 className="text-sm font-black text-gray-800 uppercase tracking-tight">
{minhas.length > 0 ? `${minhas.length} sala${minhas.length > 1 ? 's' : ''} cadastrada${minhas.length > 1 ? 's' : ''}` : 'Suas salas'}
</h3>
<button onClick={() => setEditingSala('new')}
className="w-full sm:w-auto flex items-center justify-center gap-2 px-5 py-2.5 text-white rounded-xl text-xs font-black uppercase tracking-widest transition-opacity hover:opacity-90 shadow-sm" style={{ backgroundColor: COLOR }}>
<Plus size={16} /> CADASTRAR SALA
</button>
</div>
{loading ? (
<div className="flex justify-center py-12"><RefreshCw size={24} className="animate-spin" style={{ color: COLOR }} /></div>
) : minhas.length === 0 ? (
<div className="text-center py-12 text-gray-400">
<DoorOpen size={40} className="mx-auto mb-3 opacity-40" />
<p className="font-bold text-sm uppercase">Você ainda não cadastrou salas</p>
<p className="text-xs mt-1">Disponibilize uma sala e receba solicitações de reserva</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{minhas.map(s => <SalaCard key={s.id} sala={s} mine onEdit={() => setEditingSala(s)} onDelete={() => handleDelete(s)} />)}
</div>
)}
</>
)}
{/* RESERVAS */}
{tab === 'reservas' && (
loading ? (
<div className="flex justify-center py-12"><RefreshCw size={24} className="animate-spin" style={{ color: COLOR }} /></div>
) : (
<div className="space-y-8">
{/* Receita de locação (reservas confirmadas viram recebível no financeiro da sala) */}
{(() => {
const conf = reservas.recebidas.filter(r => r.status === 'confirmada' && Number(r.valor) > 0);
const total = conf.reduce((s, r) => s + Number(r.valor), 0);
if (conf.length === 0) return null;
return (
<div className="bg-teal-50 border border-teal-100 rounded-2xl px-5 py-3 flex items-center justify-between">
<span className="text-[11px] font-black text-teal-700 uppercase tracking-wide">Receita de locação (confirmadas)</span>
<span className="text-lg font-black text-teal-700">R$ {total.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}<span className="text-[10px] font-bold text-teal-500 ml-2">{conf.length} reserva(s) financeiro</span></span>
</div>
);
})()}
<div>
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-3 flex items-center gap-1.5"><Inbox size={12} /> RECEBIDAS (MINHAS SALAS)</p>
{reservas.recebidas.length === 0
? <p className="text-xs text-gray-300 font-bold uppercase">Nenhuma reserva recebida</p>
: <div className="space-y-3">{reservas.recebidas.map(r => <ReservaRow key={r.id} r={r} recebida onAction={a => handleReservaAction(r, a)} />)}</div>}
</div>
<div>
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-3 flex items-center gap-1.5"><Send size={12} /> FEITAS POR MIM</p>
{reservas.feitas.length === 0
? <p className="text-xs text-gray-300 font-bold uppercase">Você ainda não solicitou reservas</p>
: <div className="space-y-3">{reservas.feitas.map(r => <ReservaRow key={r.id} r={r} onAction={a => handleReservaAction(r, a)} />)}</div>}
</div>
</div>
)
)}
{editingSala && <SalaFormModal sala={editingSala === 'new' ? null : editingSala} onClose={() => setEditingSala(null)} onSaved={fetchMinhas} />}
{reservingSala && <ReservaModal sala={reservingSala} onClose={() => setReservingSala(null)} onReserved={() => setTab('reservas')} />}
</div>
);
};