feat(salas): reserva vira wizard de 4 passos em slide
Modal de reservar sala reorganizado num wizard (formato slide, translateX) com stepper clicável p/ voltar: 1) Dados — reservar como (pessoal/unidade) + profissional + observações. 2) Calendário — modelo (cartões c/ preço) + calendário mensal + horário/turno. 3) Resumo — resumo detalhado (sala, modelo, período, total) + histórico de reservas da sala. 4) Pagamento — total, forma de pagamento (PIX/Dinheiro/Transferência/Cartão) e como funciona; a forma escolhida vai nas observações do pedido. Navegação Voltar/Próximo (Próximo bloqueado no calendário até escolher o dia). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -337,6 +337,17 @@ const ReservaModal: React.FC<{ sala: Sala; onClose: () => void; onReserved: () =
|
||||
const ocupadasDoDia = diaSel ? ocupadas.filter(o => new Date(o.inicio).toDateString() === diaSel.toDateString()) : [];
|
||||
const rangeFim = diaSel ? (() => { const d = new Date(diaSel); d.setDate(d.getDate() + (f.modelo === 'mensal' ? 29 : 6)); return d; })() : null;
|
||||
|
||||
// ── Wizard (4 passos, formato slide) ──
|
||||
const [step, setStep] = useState(0);
|
||||
const [formaPagamento, setFormaPagamento] = useState('pix');
|
||||
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;
|
||||
return Number.isFinite(v) ? (f.modelo === 'hora' ? v * Math.max(1, qtd) : v) : 0;
|
||||
})();
|
||||
|
||||
// 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.
|
||||
@@ -369,7 +380,8 @@ const ReservaModal: React.FC<{ sala: Sala; onClose: () => void; onReserved: () =
|
||||
const res = await apiFetch(`/api/salas/${sala.id}/reservas`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
inicio: new Date(f.inicio).toISOString(), fim: new Date(f.fim).toISOString(), modelo: f.modelo, observacoes: f.observacoes || null,
|
||||
inicio: new Date(f.inicio).toISOString(), fim: new Date(f.fim).toISOString(), modelo: f.modelo,
|
||||
observacoes: [f.observacoes, `Forma de pagamento: ${FORMAS[formaPagamento] || formaPagamento}`].filter(Boolean).join(' · ') || null,
|
||||
clinicaId: comoUnidade && ws?.id ? ws.id : null,
|
||||
profissionalUsuarioId: comoUnidade && profissionalId ? profissionalId : null,
|
||||
}),
|
||||
@@ -385,152 +397,231 @@ const ReservaModal: React.FC<{ sala: Sala; onClose: () => void; onReserved: () =
|
||||
|
||||
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-lg 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">
|
||||
<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 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>
|
||||
<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">
|
||||
{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>
|
||||
))}
|
||||
|
||||
{/* 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}>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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
{/* Modelo de locação — cartões com preço */}
|
||||
<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>
|
||||
|
||||
{/* Calendário mensal */}
|
||||
<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 = !!(diaSel && mesmoDia(dia, diaSel));
|
||||
const emRange = !!(diaSel && rangeFim && (f.modelo === 'semanal' || f.modelo === 'mensal') && dia > diaSel && dia <= rangeFim);
|
||||
const ocupado = diasOcupados.has(dia.toDateString());
|
||||
return (
|
||||
<button key={i} type="button" disabled={passado} onClick={() => setDiaSel(dia)}
|
||||
className={`relative aspect-square 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>
|
||||
|
||||
{/* Controles de horário (conforme o modelo) */}
|
||||
{diaSel && f.modelo === 'hora' && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* PASSO 2 — Calendário */}
|
||||
<div className="w-full shrink-0 overflow-y-auto p-6 space-y-4">
|
||||
<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 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 text-gray-500 font-black text-lg leading-none">+</button>
|
||||
<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>
|
||||
)}
|
||||
{diaSel && f.modelo === 'turno' && (
|
||||
<div>
|
||||
<label className={labelCls}>Turno</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-gray-50 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 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 = !!(diaSel && mesmoDia(dia, diaSel));
|
||||
const emRange = !!(diaSel && rangeFim && (f.modelo === 'semanal' || f.modelo === 'mensal') && dia > diaSel && dia <= rangeFim);
|
||||
const ocupado = diasOcupados.has(dia.toDateString());
|
||||
return (
|
||||
<button key={i} type="button" disabled={passado} onClick={() => setDiaSel(dia)}
|
||||
className={`relative aspect-square 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>
|
||||
{diaSel && 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 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 text-gray-500 font-black text-lg leading-none">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{diaSel && f.modelo === 'turno' && (
|
||||
<div>
|
||||
<label className={labelCls}>Turno</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-gray-50 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>
|
||||
)}
|
||||
{diaSel && 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} /> Já 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>
|
||||
)}
|
||||
|
||||
{/* Resumo do período */}
|
||||
{f.inicio && f.fim && (
|
||||
<div className="rounded-2xl bg-teal-50 border border-teal-100 p-3.5 flex items-center gap-3">
|
||||
<CalendarClock size={18} className="text-teal-600 shrink-0" />
|
||||
<div className="text-xs font-bold text-teal-800 leading-snug">
|
||||
{fmtData(new Date(f.inicio).toISOString())} <span className="text-teal-400">→</span> {fmtData(new Date(f.fim).toISOString())}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className={labelCls}>Observações</label>
|
||||
<textarea className={inputCls} rows={2} value={f.observacoes} onChange={e => setF(p => ({ ...p, observacoes: e.target.value }))} />
|
||||
</div>
|
||||
{diaSel && 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} /> Já ocupado neste dia</p>
|
||||
<div className="space-y-1 max-h-28 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>
|
||||
{/* 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],
|
||||
['Início', f.inicio ? fmtData(new Date(f.inicio).toISOString()) : '—'],
|
||||
['Fim', f.fim ? fmtData(new Date(f.fim).toISOString()) : '—'],
|
||||
['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>
|
||||
{f.modelo === 'hora' && total > 0 && <p className="text-[10px] text-teal-500 font-bold mt-1">{qtd}h × {fmtValor(String(Number(sala.valor_hora)))}</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 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>
|
||||
<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={handleReserve} 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" /> : <CalendarClock size={14} />} SOLICITAR RESERVA
|
||||
|
||||
{/* 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 && !f.inicio}
|
||||
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 || !f.inicio}
|
||||
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>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user