develop #83

Merged
M-Gabrielly merged 426 commits from develop into main 2025-12-04 04:13:15 +00:00
3 changed files with 270 additions and 14 deletions
Showing only changes of commit 4b9f0695f2 - Show all commits

View File

@ -9,7 +9,7 @@ import { useAuth } from "@/hooks/useAuth";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useAvatarUrl } from "@/hooks/useAvatarUrl"; import { useAvatarUrl } from "@/hooks/useAvatarUrl";
import { UploadAvatar } from '@/components/ui/upload-avatar'; 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 { ENV_CONFIG } from '@/lib/env-config';
import { useReports } from "@/hooks/useReports"; import { useReports } from "@/hooks/useReports";
import { CreateReportData } from "@/types/report-types"; import { CreateReportData } from "@/types/report-types";
@ -19,6 +19,8 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select"; 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 { SimpleThemeToggle } from "@/components/ui/simple-theme-toggle";
import { import {
Table, Table,
@ -65,6 +67,29 @@ const colorsByType = {
Oftalmologia: "#2ecc71" 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<string, string> = {
'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) // Helpers para normalizar dados de paciente (suporta schema antigo e novo)
const getPatientName = (p: any) => p?.full_name ?? p?.nome ?? ''; const getPatientName = (p: any) => p?.full_name ?? p?.nome ?? '';
const getPatientCpf = (p: any) => p?.cpf ?? ''; const getPatientCpf = (p: any) => p?.cpf ?? '';
@ -132,6 +157,16 @@ const ProfissionalPage = () => {
const [isEditingProfile, setIsEditingProfile] = useState(false); const [isEditingProfile, setIsEditingProfile] = useState(false);
const [doctorId, setDoctorId] = useState<string | null>(null); const [doctorId, setDoctorId] = useState<string | null>(null);
// Estados para disponibilidades e exceções do médico logado
const [availabilities, setAvailabilities] = useState<DoctorAvailability[]>([]);
const [exceptions, setExceptions] = useState<DoctorException[]>([]);
const [availLoading, setAvailLoading] = useState(false);
const [exceptLoading, setExceptLoading] = useState(false);
const [editingAvailability, setEditingAvailability] = useState<DoctorAvailability | null>(null);
const [editingException, setEditingException] = useState<DoctorException | null>(null);
const [showAvailabilityForm, setShowAvailabilityForm] = useState(false);
const [showExceptionForm, setShowExceptionForm] = useState(false);
// Hook para carregar automaticamente o avatar do médico // Hook para carregar automaticamente o avatar do médico
const { avatarUrl: retrievedAvatarUrl } = useAvatarUrl(doctorId); const { avatarUrl: retrievedAvatarUrl } = useAvatarUrl(doctorId);
// Removemos o placeholder extenso — inicializamos com valores minimalistas e vazios. // Removemos o placeholder extenso — inicializamos com valores minimalistas e vazios.
@ -286,6 +321,48 @@ const ProfissionalPage = () => {
} }
}, [retrievedAvatarUrl]); }, [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 // Estados para campos principais da consulta
const [consultaAtual, setConsultaAtual] = useState({ const [consultaAtual, setConsultaAtual] = useState({
@ -2746,7 +2823,129 @@ const ProfissionalPage = () => {
</div> </div>
); );
const renderDisponibilidadesSection = () => {
// Filtrar apenas a primeira disponibilidade de cada dia da semana
const availabilityByDay = new Map<string, DoctorAvailability>();
(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<string, DoctorException>();
(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 (
<section className="bg-card shadow-md rounded-lg border border-border p-3 sm:p-4 md:p-6 w-full">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mb-6">
<h2 className="text-xl sm:text-2xl font-bold">Minhas Disponibilidades</h2>
<div className="flex gap-2 w-full sm:w-auto">
<Button
size="sm"
className="flex-1 sm:flex-initial bg-blue-600 hover:bg-blue-700 text-xs sm:text-sm"
onClick={() => {
setEditingAvailability(null);
setShowAvailabilityForm(true);
}}
>
+ Disponibilidade
</Button>
</div>
</div>
{/* Disponibilidades */}
{availLoading ? (
<div className="text-sm text-muted-foreground p-4">Carregando disponibilidades</div>
) : filteredAvailabilities && filteredAvailabilities.length > 0 ? (
<div className="space-y-2">
{filteredAvailabilities.map((a) => (
<div key={String(a.id)} className="p-3 border rounded flex justify-between items-start">
<div className="flex-1">
<div className="font-medium text-sm sm:text-base">{translateWeekday(a.weekday)} {a.start_time} {a.end_time}</div>
<div className="text-xs text-muted-foreground">Duração: {a.slot_minutes} min Tipo: {a.appointment_type || '—'} {a.active ? 'Ativa' : 'Inativa'}</div>
</div>
<div className="flex gap-2 ml-2">
<Button
size="sm"
variant="outline"
className="text-xs"
onClick={() => {
setEditingAvailability(a);
setShowAvailabilityForm(true);
}}
>
Editar
</Button>
<Button
size="sm"
variant="destructive"
className="text-xs"
onClick={async () => {
if (!confirm('Excluir esta disponibilidade?')) return;
try {
await deletarDisponibilidade(String(a.id));
reloadAvailabilities();
toast({ title: 'Disponibilidade excluída', variant: 'default' });
} catch (e) {
console.warn('Erro ao deletar disponibilidade:', e);
toast({ title: 'Erro ao excluir', description: (e as any)?.message || String(e), variant: 'destructive' });
}
}}
>
Excluir
</Button>
</div>
</div>
))}
</div>
) : (
<div className="text-sm text-muted-foreground p-4 border rounded-lg bg-muted/50">
Nenhuma disponibilidade cadastrada.
</div>
)}
{/* Exceções */}
<div className="mt-8">
<h3 className="text-lg sm:text-xl font-bold mb-4">Exceções (Bloqueios/Liberações)</h3>
{exceptLoading ? (
<div className="text-sm text-muted-foreground p-4">Carregando exceções</div>
) : filteredExceptions && filteredExceptions.length > 0 ? (
<div className="space-y-2">
{filteredExceptions.map((ex) => (
<div key={String(ex.id)} className="p-3 border rounded flex justify-between items-start">
<div className="flex-1">
<div className="font-medium text-sm sm:text-base">
{new Date(ex.exception_date ?? ex.date).toLocaleDateString('pt-BR')}
</div>
<div className="text-xs text-muted-foreground">
Tipo: bloqueio Motivo: {ex.reason || '—'}
</div>
</div>
<div className="flex gap-2 ml-2">
{/* Sem ações para exceções */}
</div>
</div>
))}
</div>
) : (
<div className="text-sm text-muted-foreground p-4 border rounded-lg bg-muted/50">
Nenhuma exceção cadastrada.
</div>
)}
</div>
</section>
);
};
const renderPerfilSection = () => ( const renderPerfilSection = () => (
<div className="mx-auto flex w-full max-w-6xl flex-col gap-4 sm:gap-6 px-0 py-4 sm:py-8 md:px-4"> <div className="mx-auto flex w-full max-w-6xl flex-col gap-4 sm:gap-6 px-0 py-4 sm:py-8 md:px-4">
{/* Header com Título e Botão */} {/* Header com Título e Botão */}
@ -3022,6 +3221,8 @@ const ProfissionalPage = () => {
); );
case 'laudos': case 'laudos':
return renderLaudosSection(); return renderLaudosSection();
case 'disponibilidades':
return renderDisponibilidadesSection();
case 'comunicacao': case 'comunicacao':
return renderComunicacaoSection(); return renderComunicacaoSection();
case 'perfil': case 'perfil':
@ -3158,6 +3359,17 @@ const ProfissionalPage = () => {
<FileText className="mr-2 h-4 w-4" /> <FileText className="mr-2 h-4 w-4" />
Laudos Laudos
</Button> </Button>
<Button
variant={activeSection === 'disponibilidades' ? 'default' : 'ghost'}
className="w-full justify-start text-sm md:text-base transition-colors hover:bg-primary! hover:text-white! cursor-pointer"
onClick={() => {
setActiveSection('disponibilidades');
setSidebarOpen(false);
}}
>
<Clock className="mr-2 h-4 w-4" />
Disponibilidades
</Button>
<Button <Button
variant={activeSection === 'comunicacao' ? 'default' : 'ghost'} variant={activeSection === 'comunicacao' ? 'default' : 'ghost'}
className="w-full justify-start text-sm md:text-base transition-colors hover:bg-primary! hover:text-white! cursor-pointer" className="w-full justify-start text-sm md:text-base transition-colors hover:bg-primary! hover:text-white! cursor-pointer"
@ -3194,7 +3406,29 @@ const ProfissionalPage = () => {
</main> </main>
</div> </div>
{} {/* AvailabilityForm para criar/editar disponibilidades */}
{showAvailabilityForm && (
<AvailabilityForm
open={showAvailabilityForm}
onOpenChange={(open) => {
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 && ( {showPopup && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex justify-center items-center z-50"> <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex justify-center items-center z-50">

View File

@ -28,6 +28,19 @@ export default function ExceptionForm({ open, onOpenChange, doctorId = null, onS
const [showDatePicker, setShowDatePicker] = useState(false) const [showDatePicker, setShowDatePicker] = useState(false)
const { toast } = useToast() 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) { async function handleSubmit(e?: React.FormEvent) {
e?.preventDefault() e?.preventDefault()
if (!doctorId) { if (!doctorId) {
@ -53,7 +66,7 @@ export default function ExceptionForm({ open, onOpenChange, doctorId = null, onS
const saved = await criarExcecao(payload) const saved = await criarExcecao(payload)
toast({ title: 'Exceção criada', description: `${payload.date}${kind}`, variant: 'default' }) toast({ title: 'Exceção criada', description: `${payload.date}${kind}`, variant: 'default' })
onSaved?.(saved) onSaved?.(saved)
onOpenChange(false) handleOpenChange(false)
} catch (err: any) { } catch (err: any) {
console.error('Erro ao criar exceção:', err) console.error('Erro ao criar exceção:', err)
toast({ title: 'Erro', description: err?.message || String(err), variant: 'destructive' }) toast({ title: 'Erro', description: err?.message || String(err), variant: 'destructive' })
@ -63,7 +76,7 @@ export default function ExceptionForm({ open, onOpenChange, doctorId = null, onS
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Criar exceção</DialogTitle> <DialogTitle>Criar exceção</DialogTitle>
@ -103,6 +116,7 @@ export default function ExceptionForm({ open, onOpenChange, doctorId = null, onS
mode="single" mode="single"
selected={date ? (() => { selected={date ? (() => {
try { try {
// Parse como local date para compatibilidade com Calendar
const [y, m, d] = String(date).split('-').map(Number); const [y, m, d] = String(date).split('-').map(Number);
return new Date(y, m - 1, d); return new Date(y, m - 1, d);
} catch (e) { } catch (e) {
@ -111,10 +125,12 @@ export default function ExceptionForm({ open, onOpenChange, doctorId = null, onS
})() : undefined} })() : undefined}
onSelect={(selectedDate) => { onSelect={(selectedDate) => {
if (selectedDate) { if (selectedDate) {
// Extrair data como local para evitar problemas de timezone
const y = selectedDate.getFullYear(); const y = selectedDate.getFullYear();
const m = String(selectedDate.getMonth() + 1).padStart(2, '0'); const m = String(selectedDate.getMonth() + 1).padStart(2, '0');
const d = String(selectedDate.getDate()).padStart(2, '0'); const d = String(selectedDate.getDate()).padStart(2, '0');
const dateStr = `${y}-${m}-${d}`; const dateStr = `${y}-${m}-${d}`;
console.log('[ExceptionForm] Data selecionada:', dateStr, 'de', selectedDate);
setDate(dateStr); setDate(dateStr);
setShowDatePicker(false); setShowDatePicker(false);
} }
@ -160,7 +176,7 @@ export default function ExceptionForm({ open, onOpenChange, doctorId = null, onS
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={submitting}>Cancelar</Button> <Button variant="ghost" onClick={() => handleOpenChange(false)} disabled={submitting}>Cancelar</Button>
<Button type="submit" disabled={submitting}>{submitting ? 'Salvando...' : 'Criar exceção'}</Button> <Button type="submit" disabled={submitting}>{submitting ? 'Salvando...' : 'Criar exceção'}</Button>
</DialogFooter> </DialogFooter>
</form> </form>

View File

@ -488,11 +488,10 @@ export async function deletarDisponibilidade(id: string): Promise<void> {
headers: withPrefer({ ...baseHeaders() }, 'return=minimal'), headers: withPrefer({ ...baseHeaders() }, 'return=minimal'),
}); });
if (res.status === 204) return; if (res.status === 204 || res.status === 200) return;
// Some deployments may return 200 with a representation — accept that too
if (res.status === 200) return; // Se chegou aqui e não foi sucesso, lance erro
// Otherwise surface a friendly error using parse() throw new Error(`Erro ao deletar disponibilidade: ${res.status}`);
await parse(res as Response);
} }
// ===== EXCEÇÕES (Doctor Exceptions) ===== // ===== EXCEÇÕES (Doctor Exceptions) =====
@ -580,14 +579,21 @@ export async function listarExcecoes(params?: { doctorId?: string; date?: string
export async function deletarExcecao(id: string): Promise<void> { export async function deletarExcecao(id: string): Promise<void> {
if (!id) throw new Error('ID da exceção é obrigatório'); if (!id) throw new Error('ID da exceção é obrigatório');
const url = `${REST}/doctor_exceptions?id=eq.${encodeURIComponent(String(id))}`; const url = `${REST}/doctor_exceptions?id=eq.${encodeURIComponent(String(id))}`;
console.log('[deletarExcecao] Deletando exceção:', id, 'URL:', url);
const res = await fetch(url, { const res = await fetch(url, {
method: 'DELETE', method: 'DELETE',
headers: withPrefer({ ...baseHeaders() }, 'return=minimal'), headers: withPrefer({ ...baseHeaders() }, 'return=minimal'),
}); });
if (res.status === 204) return; console.log('[deletarExcecao] Status da resposta:', res.status);
if (res.status === 200) return;
await parse(res as Response); 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}`);
} }