feat(secretaria): folga do dentista por data (A) + salas alugadas na agenda (B)
A — Folga/férias do dentista por DATA: tabela dentistas_folgas; o dentista (ou o dono) marca dias em que não atende; o /slots pula o dentista nessas datas. Endpoints /dentista/:id/folgas (GET/POST/DELETE) + UI na aba Dentistas. B — Salas alugadas: o dentista com sala_reserva aprovada passa a ter horários oferecidos pela Secretária no quarto (disponibilidade = janela da reserva, casada pelo usuario_id). /slots ganha passada de salas (slot com sala_id + local); /book grava agendamentos.sala_id (coluna aditiva). Geração passa a usar CANDIDATE_CAP e ordena por horário (clínica e salas competem). Testado em dev: folga pula os dias do dentista; reserva de sala gera slots com o nome da sala. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,8 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
|
||||
const [dentistas, setDentistas] = useState<Dentista[]>([]);
|
||||
const [dentSel, setDentSel] = useState<string>('');
|
||||
const [dentFaixas, setDentFaixas] = useState<Faixa[]>([]);
|
||||
const [folgas, setFolgas] = useState<any[]>([]);
|
||||
const [novaFolga, setNovaFolga] = useState({ data_inicio: '', data_fim: '', motivo: '' });
|
||||
|
||||
const flash = (setter: (v: string | null) => void, msg: string) => { setter(msg); setTimeout(() => setter(null), 3000); };
|
||||
|
||||
@@ -101,12 +103,25 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
|
||||
}, [clinicaId]);
|
||||
|
||||
useEffect(() => { if (isOpen) { carregarClinica(); carregarDentistas(); } }, [isOpen, carregarClinica, carregarDentistas]);
|
||||
const carregarFolgas = useCallback((id: string) => {
|
||||
req(`/dentista/${id}/folgas`).then((f) => setFolgas(f.folgas || [])).catch(() => setFolgas([]));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!dentSel) { setDentFaixas([]); return; }
|
||||
if (!dentSel) { setDentFaixas([]); setFolgas([]); return; }
|
||||
req(`/dentista/${dentSel}/horarios`).then((h) =>
|
||||
setDentFaixas((h.horarios || []).map((r: any) => ({ dia_semana: r.dia_semana, hora_inicio: r.hora_inicio, hora_fim: r.hora_fim })))
|
||||
).catch((e) => flash(setErro, e.message));
|
||||
}, [dentSel]);
|
||||
carregarFolgas(dentSel);
|
||||
}, [dentSel, carregarFolgas]);
|
||||
|
||||
const addFolga = async () => {
|
||||
if (!novaFolga.data_inicio) { flash(setErro, 'Informe a data.'); return; }
|
||||
try { await req(`/dentista/${dentSel}/folgas`, { method: 'POST', body: JSON.stringify(novaFolga) }); setNovaFolga({ data_inicio: '', data_fim: '', motivo: '' }); carregarFolgas(dentSel); flash(setOk, 'Folga adicionada!'); }
|
||||
catch (e: any) { flash(setErro, e.message); }
|
||||
};
|
||||
const delFolga = async (id: string) => {
|
||||
try { await req(`/dentista/${dentSel}/folgas/${id}`, { method: 'DELETE' }); carregarFolgas(dentSel); } catch (e: any) { flash(setErro, e.message); }
|
||||
};
|
||||
|
||||
const salvarClinica = async () => {
|
||||
setSaving(true); setErro(null);
|
||||
@@ -215,6 +230,33 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
|
||||
</div>
|
||||
{dentSel ? <GradeSemanal faixas={dentFaixas} onChange={setDentFaixas} /> : <p className="text-xs text-gray-400">Nenhum dentista.</p>}
|
||||
<p className="text-[11px] text-gray-400 italic">Sem horário próprio, o dentista atende sempre que a clínica está aberta. Cada dentista (ou o dono) edita os seus.</p>
|
||||
|
||||
{dentSel && (
|
||||
<section className="pt-4 border-t border-gray-100">
|
||||
<h3 className="text-sm font-black text-gray-800 uppercase tracking-tight flex items-center gap-2 mb-3"><CalendarOff size={16} className="text-teal-600" /> Folgas e férias (não atende nestas datas)</h3>
|
||||
<div className="flex flex-wrap items-end gap-2 mb-3 bg-gray-50 p-4 rounded-2xl">
|
||||
<div><label className="text-[9px] font-black text-gray-400 uppercase block mb-1">De</label>
|
||||
<input type="date" value={novaFolga.data_inicio} onChange={(e) => setNovaFolga({ ...novaFolga, data_inicio: e.target.value })} className="bg-white border-2 border-transparent focus:border-teal-600 rounded-xl px-3 py-2 text-sm font-bold outline-none" /></div>
|
||||
<div><label className="text-[9px] font-black text-gray-400 uppercase block mb-1">Até (opcional)</label>
|
||||
<input type="date" value={novaFolga.data_fim} onChange={(e) => setNovaFolga({ ...novaFolga, data_fim: e.target.value })} className="bg-white border-2 border-transparent focus:border-teal-600 rounded-xl px-3 py-2 text-sm font-bold outline-none" /></div>
|
||||
<div className="flex-1 min-w-[140px]"><label className="text-[9px] font-black text-gray-400 uppercase block mb-1">Motivo</label>
|
||||
<input type="text" placeholder="Ex: Férias, congresso" value={novaFolga.motivo} onChange={(e) => setNovaFolga({ ...novaFolga, motivo: e.target.value })} className="w-full bg-white border-2 border-transparent focus:border-teal-600 rounded-xl px-3 py-2 text-sm font-bold outline-none" /></div>
|
||||
<button onClick={addFolga} className="bg-gray-900 hover:bg-black text-white text-xs font-black uppercase px-4 py-2.5 rounded-xl flex items-center gap-1"><Plus size={14} /> Adicionar</button>
|
||||
</div>
|
||||
{folgas.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{folgas.map((f) => (
|
||||
<div key={f.id} className="flex items-center justify-between bg-amber-50 rounded-xl px-3 py-2">
|
||||
<span className="text-xs font-bold text-amber-700">
|
||||
{f.data_inicio.split('-').reverse().join('/')}{f.data_fim !== f.data_inicio ? ` até ${f.data_fim.split('-').reverse().join('/')}` : ''}{f.motivo ? ` · ${f.motivo}` : ''}
|
||||
</span>
|
||||
<button onClick={() => delFolga(f.id)} className="text-amber-300 hover:text-amber-600"><Trash2 size={15} /></button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : <p className="text-[11px] text-gray-400 italic">Nenhuma folga futura cadastrada.</p>}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user