2025-10-30 17:20:28 -03:00

357 lines
11 KiB
TypeScript

import React, { useEffect, useState, useCallback } from "react";
import { X, Loader2 } from "lucide-react";
import {
appointmentService,
patientService,
doctorService,
type Appointment,
type Patient,
type Doctor,
} from "../../services";
import { useAuth } from "../../hooks/useAuth";
// Type aliases para compatibilidade com código antigo
type Consulta = Appointment & {
pacienteId?: string;
medicoId?: string;
dataHora?: string;
observacoes?: string;
};
type Paciente = Patient;
type Medico = Doctor;
interface ConsultaModalProps {
isOpen: boolean;
onClose: () => void;
onSaved: (c: Consulta) => void;
editing?: Consulta | null;
defaultPacienteId?: string;
defaultMedicoId?: string;
lockPaciente?: boolean; // quando abrir a partir do prontuário
lockMedico?: boolean; // quando médico logado não deve mudar
}
const TIPO_SUGESTOES = [
"Primeira consulta",
"Retorno",
"Acompanhamento",
"Exame",
"Telemedicina",
];
const ConsultaModal: React.FC<ConsultaModalProps> = ({
isOpen,
onClose,
onSaved,
editing,
defaultPacienteId,
defaultMedicoId,
lockPaciente = false,
lockMedico = false,
}) => {
const { user } = useAuth();
const [pacientes, setPacientes] = useState<Paciente[]>([]);
const [medicos, setMedicos] = useState<Medico[]>([]);
const [loadingLists, setLoadingLists] = useState(false);
const [pacienteId, setPacienteId] = useState("");
const [medicoId, setMedicoId] = useState("");
const [dataHora, setDataHora] = useState(""); // value for datetime-local
const [tipo, setTipo] = useState("");
const [motivo, setMotivo] = useState("");
const [observacoes, setObservacoes] = useState("");
const [status, setStatus] = useState<string>("agendada");
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
// Load supporting lists
useEffect(() => {
if (!isOpen) return;
let active = true;
(async () => {
try {
setLoadingLists(true);
const [patients, doctors] = await Promise.all([
patientService.list().catch(() => []),
doctorService.list().catch(() => []),
]);
if (!active) return;
setPacientes(patients);
setMedicos(doctors);
} finally {
if (active) setLoadingLists(false);
}
})();
return () => {
active = false;
};
}, [isOpen]);
// Initialize form when opening / editing changes
useEffect(() => {
if (!isOpen) return;
if (editing) {
setPacienteId(editing.pacienteId);
setMedicoId(editing.medicoId);
// Convert ISO to local datetime-local value
try {
const d = new Date(editing.dataHora);
const local = new Date(d.getTime() - d.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16);
setDataHora(local);
} catch {
setDataHora("");
}
setTipo(editing.tipo || "");
setMotivo(editing.motivo || "");
setObservacoes(editing.observacoes || "");
setStatus(editing.status || "agendada");
} else {
setPacienteId(defaultPacienteId || "");
setMedicoId(defaultMedicoId || "");
setDataHora("");
setTipo("");
setMotivo("");
setObservacoes("");
setStatus("agendada");
}
setError(null);
setSaving(false);
}, [isOpen, editing, defaultPacienteId, defaultMedicoId, user]);
const closeOnEsc = useCallback(
(e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
},
[onClose]
);
useEffect(() => {
if (!isOpen) return;
window.addEventListener("keydown", closeOnEsc);
return () => window.removeEventListener("keydown", closeOnEsc);
}, [isOpen, closeOnEsc]);
if (!isOpen) return null;
const validate = (): boolean => {
if (!pacienteId) {
setError("Selecione um paciente.");
return false;
}
if (!medicoId) {
setError("Selecione um médico.");
return false;
}
if (!dataHora) {
setError("Informe data e hora.");
return false;
}
return true;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validate()) return;
setSaving(true);
setError(null);
try {
// Convert local datetime back to ISO
const iso = new Date(dataHora).toISOString();
if (editing) {
const payload: ConsultaUpdate = {
dataHora: iso,
tipo: tipo || undefined,
motivo: motivo || undefined,
observacoes: observacoes || undefined,
status: status,
};
const resp = await consultasService.atualizar(editing.id, payload);
if (!resp.success || !resp.data) {
throw new Error(resp.error || "Falha ao atualizar consulta");
}
onSaved(resp.data);
} else {
const payload: ConsultaCreate = {
pacienteId,
medicoId,
dataHora: iso,
tipo: tipo || undefined,
motivo: motivo || undefined,
observacoes: observacoes || undefined,
};
const resp = await consultasService.criar(payload);
if (!resp.success || !resp.data) {
throw new Error(resp.error || "Falha ao criar consulta");
}
onSaved(resp.data);
}
onClose();
} catch (err) {
const msg = err instanceof Error ? err.message : "Erro ao salvar";
setError(msg);
} finally {
setSaving(false);
}
};
const title = editing ? "Editar Consulta" : "Nova Consulta";
return (
<div className="fixed inset-0 z-50 flex items-start justify-center bg-black/40 p-4 overflow-y-auto">
<div className="bg-white rounded-lg shadow-xl w-full max-w-xl animate-fade-in mt-10">
<div className="flex items-center justify-between px-4 py-3 border-b">
<h2 className="text-lg font-semibold">{title}</h2>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-4 space-y-4">
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-3 py-2 rounded text-sm">
{error}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Paciente
</label>
<select
className="w-full border rounded px-2 py-2 text-sm"
value={pacienteId}
onChange={(e) => setPacienteId(e.target.value)}
disabled={lockPaciente || !!editing}
>
<option value="">Selecione...</option>
{pacientes.map((p) => (
<option key={p.id} value={p.id}>
{p.nome}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Médico
</label>
<select
className="w-full border rounded px-2 py-2 text-sm"
value={medicoId}
onChange={(e) => setMedicoId(e.target.value)}
disabled={lockMedico || !!editing}
>
<option value="">Selecione...</option>
{medicos.map((m) => (
<option key={m.id} value={m.id}>
{m.nome} - {m.especialidade}
</option>
))}
</select>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Data / Hora
</label>
<input
type="datetime-local"
className="w-full border rounded px-2 py-2 text-sm"
value={dataHora}
onChange={(e) => setDataHora(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tipo
</label>
<input
list="tipos-consulta"
className="w-full border rounded px-2 py-2 text-sm"
value={tipo}
onChange={(e) => setTipo(e.target.value)}
placeholder="Ex: Retorno"
/>
<datalist id="tipos-consulta">
{TIPO_SUGESTOES.map((t) => (
<option key={t} value={t} />
))}
</datalist>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Motivo
</label>
<input
className="w-full border rounded px-2 py-2 text-sm"
value={motivo}
onChange={(e) => setMotivo(e.target.value)}
placeholder="Motivo principal"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Observações
</label>
<textarea
className="w-full border rounded px-2 py-2 text-sm resize-y min-h-[80px]"
value={observacoes}
onChange={(e) => setObservacoes(e.target.value)}
placeholder="Notas internas, preparação, etc"
/>
</div>
{editing && (
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Status
</label>
<select
className="w-full border rounded px-2 py-2 text-sm"
value={status}
onChange={(e) => setStatus(e.target.value)}
>
<option value="agendada">Agendada</option>
<option value="confirmada">Confirmada</option>
<option value="cancelada">Cancelada</option>
<option value="realizada">Realizada</option>
<option value="faltou">Faltou</option>
</select>
</div>
)}
</div>
{loadingLists && (
<p className="text-xs text-gray-500 flex items-center">
<Loader2 className="w-4 h-4 mr-1 animate-spin" /> Carregando
listas...
</p>
)}
<div className="flex justify-end gap-2 pt-2 border-t">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm rounded border border-gray-300 text-gray-700 hover:bg-gray-50"
>
Cancelar
</button>
<button
type="submit"
disabled={saving}
className="px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-60 flex items-center"
>
{saving && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}{" "}
{editing ? "Salvar alterações" : "Criar consulta"}
</button>
</div>
</form>
</div>
</div>
);
};
export default ConsultaModal;