diff --git a/susconecta/app/profissional/page.tsx b/susconecta/app/profissional/page.tsx index 00a9662..0138a6a 100644 --- a/susconecta/app/profissional/page.tsx +++ b/susconecta/app/profissional/page.tsx @@ -9,7 +9,7 @@ import { useAuth } from "@/hooks/useAuth"; import { useToast } from "@/hooks/use-toast"; import { useAvatarUrl } from "@/hooks/useAvatarUrl"; import { UploadAvatar } from '@/components/ui/upload-avatar'; -import { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, buscarMedicos, listarAgendamentos, type Paciente, buscarRelatorioPorId, atualizarMedico } from "@/lib/api"; +import { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, buscarMedicos, listarAgendamentos, type Paciente, buscarRelatorioPorId, atualizarMedico, listarDisponibilidades, DoctorAvailability, deletarDisponibilidade, listarExcecoes, DoctorException, deletarExcecao } from "@/lib/api"; import { ENV_CONFIG } from '@/lib/env-config'; import { useReports } from "@/hooks/useReports"; import { CreateReportData } from "@/types/report-types"; @@ -19,6 +19,8 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select"; +import AvailabilityForm from '@/components/features/forms/availability-form'; +import ExceptionForm from '@/components/features/forms/exception-form'; import { SimpleThemeToggle } from "@/components/ui/simple-theme-toggle"; import { Table, @@ -65,6 +67,29 @@ const colorsByType = { Oftalmologia: "#2ecc71" }; + // Função para traduzir dias da semana + function translateWeekday(w?: string) { + if (!w) return ''; + const key = w.toString().toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '').replace(/[^a-z0-9]/g, ''); + const map: Record = { + 'segunda': 'Segunda', + 'terca': 'Terça', + 'quarta': 'Quarta', + 'quinta': 'Quinta', + 'sexta': 'Sexta', + 'sabado': 'Sábado', + 'domingo': 'Domingo', + 'monday': 'Segunda', + 'tuesday': 'Terça', + 'wednesday': 'Quarta', + 'thursday': 'Quinta', + 'friday': 'Sexta', + 'saturday': 'Sábado', + 'sunday': 'Domingo', + }; + return map[key] ?? w; + } + // Helpers para normalizar dados de paciente (suporta schema antigo e novo) const getPatientName = (p: any) => p?.full_name ?? p?.nome ?? ''; const getPatientCpf = (p: any) => p?.cpf ?? ''; @@ -132,6 +157,16 @@ const ProfissionalPage = () => { const [isEditingProfile, setIsEditingProfile] = useState(false); const [doctorId, setDoctorId] = useState(null); + // Estados para disponibilidades e exceções do médico logado + const [availabilities, setAvailabilities] = useState([]); + const [exceptions, setExceptions] = useState([]); + const [availLoading, setAvailLoading] = useState(false); + const [exceptLoading, setExceptLoading] = useState(false); + const [editingAvailability, setEditingAvailability] = useState(null); + const [editingException, setEditingException] = useState(null); + const [showAvailabilityForm, setShowAvailabilityForm] = useState(false); + const [showExceptionForm, setShowExceptionForm] = useState(false); + // Hook para carregar automaticamente o avatar do médico const { avatarUrl: retrievedAvatarUrl } = useAvatarUrl(doctorId); // Removemos o placeholder extenso — inicializamos com valores minimalistas e vazios. @@ -286,6 +321,48 @@ const ProfissionalPage = () => { } }, [retrievedAvatarUrl]); + // Carregar disponibilidades e exceções do médico logado + const reloadAvailabilities = async (medId?: string) => { + const id = medId || doctorId; + if (!id) return; + try { + setAvailLoading(true); + const avails = await listarDisponibilidades({ doctorId: id, active: true }); + setAvailabilities(Array.isArray(avails) ? avails : []); + } catch (e) { + console.warn('[ProfissionalPage] Erro ao carregar disponibilidades:', e); + setAvailabilities([]); + } finally { + setAvailLoading(false); + } + }; + + const reloadExceptions = async (medId?: string) => { + const id = medId || doctorId; + if (!id) return; + try { + setExceptLoading(true); + console.log('[ProfissionalPage] Recarregando exceções para médico:', id); + const excepts = await listarExcecoes({ doctorId: id }); + console.log('[ProfissionalPage] Exceções carregadas:', excepts); + setExceptions(Array.isArray(excepts) ? excepts : []); + } catch (e) { + console.warn('[ProfissionalPage] Erro ao carregar exceções:', e); + setExceptions([]); + } finally { + setExceptLoading(false); + } + }; + + // Carrega disponibilidades quando doctorId muda + useEffect(() => { + if (doctorId) { + reloadAvailabilities(doctorId); + reloadExceptions(doctorId); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [doctorId]); + // Estados para campos principais da consulta const [consultaAtual, setConsultaAtual] = useState({ @@ -2746,7 +2823,129 @@ const ProfissionalPage = () => { ); - + const renderDisponibilidadesSection = () => { + // Filtrar apenas a primeira disponibilidade de cada dia da semana + const availabilityByDay = new Map(); + (availabilities || []).forEach((a) => { + const day = String(a.weekday ?? '').toLowerCase(); + if (!availabilityByDay.has(day)) { + availabilityByDay.set(day, a); + } + }); + const filteredAvailabilities = Array.from(availabilityByDay.values()); + + // Filtrar apenas a primeira exceção de cada data + const exceptionByDate = new Map(); + (exceptions || []).forEach((ex) => { + const date = String(ex.exception_date ?? ex.date ?? ''); + if (!exceptionByDate.has(date)) { + exceptionByDate.set(date, ex); + } + }); + const filteredExceptions = Array.from(exceptionByDate.values()); + + return ( +
+
+

Minhas Disponibilidades

+
+ +
+
+ + {/* Disponibilidades */} + {availLoading ? ( +
Carregando disponibilidades…
+ ) : filteredAvailabilities && filteredAvailabilities.length > 0 ? ( +
+ {filteredAvailabilities.map((a) => ( +
+
+
{translateWeekday(a.weekday)} • {a.start_time} — {a.end_time}
+
Duração: {a.slot_minutes} min • Tipo: {a.appointment_type || '—'} • {a.active ? 'Ativa' : 'Inativa'}
+
+
+ + +
+
+ ))} +
+ ) : ( +
+ Nenhuma disponibilidade cadastrada. +
+ )} + + {/* Exceções */} +
+

Exceções (Bloqueios/Liberações)

+ {exceptLoading ? ( +
Carregando exceções…
+ ) : filteredExceptions && filteredExceptions.length > 0 ? ( +
+ {filteredExceptions.map((ex) => ( +
+
+
+ {new Date(ex.exception_date ?? ex.date).toLocaleDateString('pt-BR')} +
+
+ Tipo: bloqueio • Motivo: {ex.reason || '—'} +
+
+
+ {/* Sem ações para exceções */} +
+
+ ))} +
+ ) : ( +
+ Nenhuma exceção cadastrada. +
+ )} +
+
+ ); + }; + const renderPerfilSection = () => (
{/* Header com Título e Botão */} @@ -3022,6 +3221,8 @@ const ProfissionalPage = () => { ); case 'laudos': return renderLaudosSection(); + case 'disponibilidades': + return renderDisponibilidadesSection(); case 'comunicacao': return renderComunicacaoSection(); case 'perfil': @@ -3158,6 +3359,17 @@ const ProfissionalPage = () => { Laudos +
- {} + {/* AvailabilityForm para criar/editar disponibilidades */} + {showAvailabilityForm && ( + { + if (!open) { + setShowAvailabilityForm(false); + setEditingAvailability(null); + } + }} + doctorId={editingAvailability?.doctor_id ?? doctorId} + availability={editingAvailability} + mode={editingAvailability ? "edit" : "create"} + onSaved={(saved) => { + console.log('Disponibilidade salva', saved); + setEditingAvailability(null); + setShowAvailabilityForm(false); + reloadAvailabilities(); + }} + /> + )} + + {/* Popup antigo (manter para compatibilidade) */} {showPopup && (
diff --git a/susconecta/components/features/forms/exception-form.tsx b/susconecta/components/features/forms/exception-form.tsx index 2b81e1d..063363d 100644 --- a/susconecta/components/features/forms/exception-form.tsx +++ b/susconecta/components/features/forms/exception-form.tsx @@ -28,6 +28,19 @@ export default function ExceptionForm({ open, onOpenChange, doctorId = null, onS const [showDatePicker, setShowDatePicker] = useState(false) const { toast } = useToast() + // Resetar form quando dialog fecha + const handleOpenChange = (newOpen: boolean) => { + if (!newOpen) { + setDate('') + setStartTime('') + setEndTime('') + setKind('bloqueio') + setReason('') + setShowDatePicker(false) + } + onOpenChange(newOpen) + } + async function handleSubmit(e?: React.FormEvent) { e?.preventDefault() if (!doctorId) { @@ -53,7 +66,7 @@ export default function ExceptionForm({ open, onOpenChange, doctorId = null, onS const saved = await criarExcecao(payload) toast({ title: 'Exceção criada', description: `${payload.date} • ${kind}`, variant: 'default' }) onSaved?.(saved) - onOpenChange(false) + handleOpenChange(false) } catch (err: any) { console.error('Erro ao criar exceção:', err) toast({ title: 'Erro', description: err?.message || String(err), variant: 'destructive' }) @@ -63,7 +76,7 @@ export default function ExceptionForm({ open, onOpenChange, doctorId = null, onS } return ( - + Criar exceção @@ -103,6 +116,7 @@ export default function ExceptionForm({ open, onOpenChange, doctorId = null, onS mode="single" selected={date ? (() => { try { + // Parse como local date para compatibilidade com Calendar const [y, m, d] = String(date).split('-').map(Number); return new Date(y, m - 1, d); } catch (e) { @@ -111,10 +125,12 @@ export default function ExceptionForm({ open, onOpenChange, doctorId = null, onS })() : undefined} onSelect={(selectedDate) => { if (selectedDate) { + // Extrair data como local para evitar problemas de timezone const y = selectedDate.getFullYear(); const m = String(selectedDate.getMonth() + 1).padStart(2, '0'); const d = String(selectedDate.getDate()).padStart(2, '0'); const dateStr = `${y}-${m}-${d}`; + console.log('[ExceptionForm] Data selecionada:', dateStr, 'de', selectedDate); setDate(dateStr); setShowDatePicker(false); } @@ -160,7 +176,7 @@ export default function ExceptionForm({ open, onOpenChange, doctorId = null, onS
- + diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts index 7846058..349c5f6 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -488,11 +488,10 @@ export async function deletarDisponibilidade(id: string): Promise { headers: withPrefer({ ...baseHeaders() }, 'return=minimal'), }); - if (res.status === 204) return; - // Some deployments may return 200 with a representation — accept that too - if (res.status === 200) return; - // Otherwise surface a friendly error using parse() - await parse(res as Response); + if (res.status === 204 || res.status === 200) return; + + // Se chegou aqui e não foi sucesso, lance erro + throw new Error(`Erro ao deletar disponibilidade: ${res.status}`); } // ===== EXCEÇÕES (Doctor Exceptions) ===== @@ -580,14 +579,21 @@ export async function listarExcecoes(params?: { doctorId?: string; date?: string export async function deletarExcecao(id: string): Promise { if (!id) throw new Error('ID da exceção é obrigatório'); const url = `${REST}/doctor_exceptions?id=eq.${encodeURIComponent(String(id))}`; + console.log('[deletarExcecao] Deletando exceção:', id, 'URL:', url); const res = await fetch(url, { method: 'DELETE', headers: withPrefer({ ...baseHeaders() }, 'return=minimal'), }); - if (res.status === 204) return; - if (res.status === 200) return; - await parse(res as Response); + console.log('[deletarExcecao] Status da resposta:', res.status); + + if (res.status === 204 || res.status === 200) { + console.log('[deletarExcecao] Exceção deletada com sucesso'); + return; + } + + // Se chegou aqui e não foi sucesso, lance erro + throw new Error(`Erro ao deletar exceção: ${res.status}`); }