develop #83
@ -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<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)
|
||||
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<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
|
||||
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 = () => {
|
||||
</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 = () => (
|
||||
<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 */}
|
||||
@ -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 = () => {
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
Laudos
|
||||
</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
|
||||
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"
|
||||
@ -3194,7 +3406,29 @@ const ProfissionalPage = () => {
|
||||
</main>
|
||||
</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 && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex justify-center items-center z-50">
|
||||
|
||||
|
||||
@ -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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Criar exceção</DialogTitle>
|
||||
@ -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
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@ -488,11 +488,10 @@ export async function deletarDisponibilidade(id: string): Promise<void> {
|
||||
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<void> {
|
||||
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}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user