// Configuração de horários de funcionamento (clínica) + feriados/fechamentos e // horários dos dentistas. Consome /api/nw/agenda-config (o backend garante o // ownership: horário/feriado da clínica = dono; horário do dentista = ele ou dono). import React, { useState, useEffect, useCallback } from 'react'; import { X, Clock, CalendarOff, Plus, Trash2, Save, Loader2, User } from 'lucide-react'; const API = (import.meta as any).env?.VITE_API_URL || '/api'; const BASE = `${API}/nw/agenda-config`; const DIAS = ['Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado']; async function req(path: string, opts: RequestInit = {}) { const token = localStorage.getItem('SCOREODONTO_AUTH_TOKEN'); const res = await fetch(`${BASE}${path}`, { ...opts, headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(opts.headers || {}) }, }); const txt = await res.text(); const data = txt ? JSON.parse(txt) : {}; if (!res.ok) throw new Error(data.error || `Erro ${res.status}`); return data; } interface Faixa { dia_semana: number; hora_inicio: string; hora_fim: string } interface Dentista { id: string; nome: string; usuario_id: string | null; eh_voce: boolean } // Editor de grade semanal (7 dias, cada um com 0..n faixas). Reusado p/ clínica e dentista. const GradeSemanal: React.FC<{ faixas: Faixa[]; onChange: (f: Faixa[]) => void; disabled?: boolean }> = ({ faixas, onChange, disabled }) => { const porDia = (d: number) => faixas.filter((f) => f.dia_semana === d); const setDia = (d: number, novas: Faixa[]) => onChange([...faixas.filter((f) => f.dia_semana !== d), ...novas]); return (
{DIAS.map((nome, d) => { const fs = porDia(d); return (
{nome}
{fs.length === 0 && Fechado} {fs.map((f, i) => (
{ const n = [...fs]; n[i] = { ...f, hora_inicio: e.target.value }; setDia(d, n); }} className="bg-gray-50 border-2 border-transparent focus:border-teal-600 rounded-xl px-2 py-1.5 text-sm font-bold text-gray-800 outline-none" /> até { const n = [...fs]; n[i] = { ...f, hora_fim: e.target.value }; setDia(d, n); }} className="bg-gray-50 border-2 border-transparent focus:border-teal-600 rounded-xl px-2 py-1.5 text-sm font-bold text-gray-800 outline-none" /> {!disabled && }
))} {!disabled && ( )}
); })}
); }; export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; clinicaId?: string; initialTab?: 'clinica' | 'dentistas'; soDentista?: boolean }> = ({ isOpen, onClose, clinicaId, initialTab, soDentista }) => { const [aba, setAba] = useState<'clinica' | 'dentistas'>(initialTab || 'clinica'); useEffect(() => { if (isOpen && initialTab) setAba(initialTab); }, [isOpen, initialTab]); const [erro, setErro] = useState(null); const [ok, setOk] = useState(null); const [saving, setSaving] = useState(false); // Clínica const [clinFaixas, setClinFaixas] = useState([]); const [feriados, setFeriados] = useState<{ excecoes: any[]; nacionais: any[] }>({ excecoes: [], nacionais: [] }); const [novo, setNovo] = useState({ data: '', nome: '', fecha: true }); // Dentistas const [dentistas, setDentistas] = useState([]); const [dentSel, setDentSel] = useState(''); const [dentFaixas, setDentFaixas] = useState([]); const [folgas, setFolgas] = useState([]); const [novaFolga, setNovaFolga] = useState({ data_inicio: '', data_fim: '', motivo: '' }); const flash = (setter: (v: string | null) => void, msg: string) => { setter(msg); setTimeout(() => setter(null), 3000); }; const carregarClinica = useCallback(async () => { if (!clinicaId) return; try { const h = await req(`/clinica/${clinicaId}/horarios`); setClinFaixas((h.horarios || []).map((r: any) => ({ dia_semana: r.dia_semana, hora_inicio: r.hora_inicio, hora_fim: r.hora_fim }))); const f = await req(`/clinica/${clinicaId}/feriados?ano=${new Date().getFullYear()}`); setFeriados({ excecoes: f.excecoes || [], nacionais: f.nacionais || [] }); } catch (e: any) { flash(setErro, e.message); } }, [clinicaId]); const carregarDentistas = useCallback(async () => { if (!clinicaId) return; try { const d = await req(`/clinica/${clinicaId}/dentistas`); let list: Dentista[] = d.dentistas || []; if (soDentista) { const meus = list.filter((x) => x.eh_voce); if (meus.length) list = meus; } // dentista só vê o próprio setDentistas(list); const first = list.find((x: Dentista) => x.eh_voce) || list[0]; if (first) setDentSel(first.id); } catch (e: any) { flash(setErro, e.message); } }, [clinicaId, soDentista]); 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([]); 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)); 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); try { await req(`/clinica/${clinicaId}/horarios`, { method: 'PUT', body: JSON.stringify({ horarios: clinFaixas }) }); flash(setOk, 'Horário da clínica salvo!'); } catch (e: any) { flash(setErro, e.message); } finally { setSaving(false); } }; const salvarDentista = async () => { setSaving(true); setErro(null); try { await req(`/dentista/${dentSel}/horarios`, { method: 'PUT', body: JSON.stringify({ horarios: dentFaixas }) }); flash(setOk, 'Horário do dentista salvo!'); } catch (e: any) { flash(setErro, e.message); } finally { setSaving(false); } }; const addFeriado = async () => { if (!novo.data || !novo.nome) { flash(setErro, 'Informe data e nome.'); return; } try { await req(`/clinica/${clinicaId}/feriados`, { method: 'POST', body: JSON.stringify(novo) }); setNovo({ data: '', nome: '', fecha: true }); carregarClinica(); flash(setOk, 'Fechamento adicionado!'); } catch (e: any) { flash(setErro, e.message); } }; const delFeriado = async (id: string) => { try { await req(`/clinica/${clinicaId}/feriados/${id}`, { method: 'DELETE' }); carregarClinica(); } catch (e: any) { flash(setErro, e.message); } }; if (!isOpen) return null; return (
{/* Header */}

Horários de Funcionamento

A Secretária IA usa isto para atender

{/* Tabs */}
{(soDentista ? (['dentistas'] as const) : (['clinica', 'dentistas'] as const)).map((t) => ( ))}
{(erro || ok) && (
{erro || ok}
)}
{aba === 'clinica' ? (

Dias e horários que a clínica funciona

Sem horário configurado, assume-se seg–sex 08h–18h. Só o dono edita.

Feriados e fechamentos

setNovo({ ...novo, data: 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" />
setNovo({ ...novo, nome: 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" />
{feriados.excecoes.length > 0 && (
{feriados.excecoes.map((f) => (
{f.data.split('-').reverse().join('/')} · {f.nome}
))}
)}
Feriados nacionais (automáticos)
{feriados.nacionais.map((f) =>
{f.data.split('-').reverse().join('/')} · {f.nome}
)}
) : (
{dentSel ? :

Nenhum dentista.

}

Sem horário próprio, o dentista atende sempre que a clínica está aberta. Cada dentista (ou o dono) edita os seus.

{dentSel && (

Folgas e férias (não atende nestas datas)

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" />
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" />
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" />
{folgas.length > 0 ? (
{folgas.map((f) => (
{f.data_inicio.split('-').reverse().join('/')}{f.data_fim !== f.data_inicio ? ` até ${f.data_fim.split('-').reverse().join('/')}` : ''}{f.motivo ? ` · ${f.motivo}` : ''}
))}
) :

Nenhuma folga futura cadastrada.

}
)}
)}
); };