Descrição curta do que foi alterado
This commit is contained in:
parent
591d8681ac
commit
6c0c7d75b8
@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { Search, Plus, Eye, Edit, Trash2 } from "lucide-react";
|
import { Search, Plus, Eye, Edit, Trash2, X } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
appointmentService,
|
appointmentService,
|
||||||
type Appointment,
|
type Appointment,
|
||||||
@ -25,9 +25,14 @@ export function SecretaryAppointmentList() {
|
|||||||
const [statusFilter, setStatusFilter] = useState("Todos");
|
const [statusFilter, setStatusFilter] = useState("Todos");
|
||||||
const [typeFilter, setTypeFilter] = useState("Todos");
|
const [typeFilter, setTypeFilter] = useState("Todos");
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const [modalMode, setModalMode] = useState<"create" | "edit">("create");
|
||||||
|
const [selectedAppointment, setSelectedAppointment] = useState<
|
||||||
|
AppointmentWithDetails | null
|
||||||
|
>(null);
|
||||||
const [patients, setPatients] = useState<Patient[]>([]);
|
const [patients, setPatients] = useState<Patient[]>([]);
|
||||||
const [doctors, setDoctors] = useState<Doctor[]>([]);
|
const [doctors, setDoctors] = useState<Doctor[]>([]);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState<any>({
|
||||||
|
id: undefined,
|
||||||
patient_id: "",
|
patient_id: "",
|
||||||
doctor_id: "",
|
doctor_id: "",
|
||||||
scheduled_at: "",
|
scheduled_at: "",
|
||||||
@ -81,6 +86,28 @@ export function SecretaryAppointmentList() {
|
|||||||
loadDoctorsAndPatients();
|
loadDoctorsAndPatients();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Se outro componente pediu para abrir o modal de criação com paciente pré-selecionado
|
||||||
|
useEffect(() => {
|
||||||
|
const openFromSession = () => {
|
||||||
|
const patientId = sessionStorage.getItem("selectedPatientForAppointment");
|
||||||
|
if (patientId) {
|
||||||
|
setFormData((prev: any) => ({ ...prev, patient_id: patientId }));
|
||||||
|
setModalMode("create");
|
||||||
|
setShowCreateModal(true);
|
||||||
|
sessionStorage.removeItem("selectedPatientForAppointment");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try on mount
|
||||||
|
openFromSession();
|
||||||
|
|
||||||
|
// Listen for explicit events
|
||||||
|
const handler = () => openFromSession();
|
||||||
|
window.addEventListener("open-create-appointment", handler);
|
||||||
|
return () => window.removeEventListener("open-create-appointment", handler);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Função de filtro
|
// Função de filtro
|
||||||
const filteredAppointments = appointments.filter((appointment) => {
|
const filteredAppointments = appointments.filter((appointment) => {
|
||||||
// Filtro de busca por nome do paciente ou médico
|
// Filtro de busca por nome do paciente ou médico
|
||||||
@ -91,13 +118,36 @@ export function SecretaryAppointmentList() {
|
|||||||
appointment.doctor?.full_name?.toLowerCase().includes(searchLower) ||
|
appointment.doctor?.full_name?.toLowerCase().includes(searchLower) ||
|
||||||
appointment.order_number?.toString().includes(searchTerm);
|
appointment.order_number?.toString().includes(searchTerm);
|
||||||
|
|
||||||
|
// Mapeia o valor selecionado no select para o valor real usado na API/data
|
||||||
|
const mapStatusFilterToValue = (label: string) => {
|
||||||
|
if (label === "Todos") return null;
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
Confirmada: "confirmed",
|
||||||
|
Agendada: "requested",
|
||||||
|
Cancelada: "cancelled",
|
||||||
|
"Concluída": "completed",
|
||||||
|
Concluida: "completed",
|
||||||
|
};
|
||||||
|
return map[label] || label.toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapTypeFilterToValue = (label: string) => {
|
||||||
|
if (label === "Todos") return null;
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
Presencial: "presencial",
|
||||||
|
Telemedicina: "telemedicina",
|
||||||
|
};
|
||||||
|
return map[label] || label.toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusValue = mapStatusFilterToValue(statusFilter);
|
||||||
|
const typeValue = mapTypeFilterToValue(typeFilter);
|
||||||
|
|
||||||
// Filtro de status
|
// Filtro de status
|
||||||
const matchesStatus =
|
const matchesStatus = statusValue === null || appointment.status === statusValue;
|
||||||
statusFilter === "Todos" || appointment.status === statusFilter;
|
|
||||||
|
|
||||||
// Filtro de tipo
|
// Filtro de tipo
|
||||||
const matchesType =
|
const matchesType = typeValue === null || appointment.appointment_type === typeValue;
|
||||||
typeFilter === "Todos" || appointment.appointment_type === typeFilter;
|
|
||||||
|
|
||||||
return matchesSearch && matchesStatus && matchesType;
|
return matchesSearch && matchesStatus && matchesType;
|
||||||
});
|
});
|
||||||
@ -135,16 +185,25 @@ export function SecretaryAppointmentList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await appointmentService.create({
|
if (modalMode === "edit" && formData.id) {
|
||||||
patient_id: formData.patient_id,
|
// Update only allowed fields per API types
|
||||||
doctor_id: formData.doctor_id,
|
const updatePayload: any = {};
|
||||||
scheduled_at: new Date(formData.scheduled_at).toISOString(),
|
if (formData.scheduled_at) updatePayload.scheduled_at = new Date(formData.scheduled_at).toISOString();
|
||||||
appointment_type: formData.appointment_type as
|
if (formData.notes) updatePayload.notes = formData.notes;
|
||||||
| "presencial"
|
await appointmentService.update(formData.id, updatePayload);
|
||||||
| "telemedicina",
|
toast.success("Consulta atualizada com sucesso!");
|
||||||
});
|
} else {
|
||||||
|
await appointmentService.create({
|
||||||
toast.success("Consulta agendada com sucesso!");
|
patient_id: formData.patient_id,
|
||||||
|
doctor_id: formData.doctor_id,
|
||||||
|
scheduled_at: new Date(formData.scheduled_at).toISOString(),
|
||||||
|
appointment_type: formData.appointment_type as
|
||||||
|
| "presencial"
|
||||||
|
| "telemedicina",
|
||||||
|
patient_notes: formData.notes,
|
||||||
|
});
|
||||||
|
toast.success("Consulta agendada com sucesso!");
|
||||||
|
}
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
loadAppointments();
|
loadAppointments();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -157,6 +216,23 @@ export function SecretaryAppointmentList() {
|
|||||||
loadAppointments();
|
loadAppointments();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleViewAppointment = (appointment: AppointmentWithDetails) => {
|
||||||
|
setSelectedAppointment(appointment);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditAppointment = (appointment: AppointmentWithDetails) => {
|
||||||
|
setModalMode("edit");
|
||||||
|
setFormData({
|
||||||
|
id: appointment.id,
|
||||||
|
patient_id: appointment.patient_id || "",
|
||||||
|
doctor_id: appointment.doctor_id || "",
|
||||||
|
scheduled_at: appointment.scheduled_at || "",
|
||||||
|
appointment_type: appointment.appointment_type || "presencial",
|
||||||
|
notes: appointment.notes || "",
|
||||||
|
});
|
||||||
|
setShowCreateModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
setStatusFilter("Todos");
|
setStatusFilter("Todos");
|
||||||
@ -411,12 +487,14 @@ export function SecretaryAppointmentList() {
|
|||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
|
onClick={() => handleViewAppointment(appointment)}
|
||||||
title="Visualizar"
|
title="Visualizar"
|
||||||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
onClick={() => handleEditAppointment(appointment)}
|
||||||
title="Editar"
|
title="Editar"
|
||||||
className="p-2 text-orange-600 hover:bg-orange-50 rounded-lg transition-colors"
|
className="p-2 text-orange-600 hover:bg-orange-50 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
@ -443,7 +521,7 @@ export function SecretaryAppointmentList() {
|
|||||||
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
<div className="p-6 border-b border-gray-200">
|
<div className="p-6 border-b border-gray-200">
|
||||||
<h2 className="text-2xl font-bold text-gray-900">
|
<h2 className="text-2xl font-bold text-gray-900">
|
||||||
Nova Consulta
|
{modalMode === "edit" ? "Editar Consulta" : "Nova Consulta"}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -527,6 +605,22 @@ export function SecretaryAppointmentList() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Observações
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={formData.notes || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, notes: e.target.value })
|
||||||
|
}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 h-24"
|
||||||
|
placeholder="Observações da consulta"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 pt-4">
|
<div className="flex justify-end gap-3 pt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -539,13 +633,78 @@ export function SecretaryAppointmentList() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||||
>
|
>
|
||||||
Agendar Consulta
|
{modalMode === "edit" ? "Salvar Alterações" : "Agendar Consulta"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Modal de Visualizar Consulta */}
|
||||||
|
{selectedAppointment && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<div className="p-6 border-b border-gray-200 flex items-center justify-between">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900">Visualizar Consulta</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedAppointment(null)}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-500 mb-1">Paciente</label>
|
||||||
|
<p className="text-gray-900 font-medium">{selectedAppointment.patient?.full_name || '—'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-500 mb-1">Médico</label>
|
||||||
|
<p className="text-gray-900">{selectedAppointment.doctor?.full_name || '—'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-500 mb-1">Data</label>
|
||||||
|
<p className="text-gray-900">{selectedAppointment.scheduled_at ? formatDate(selectedAppointment.scheduled_at) : '—'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-500 mb-1">Hora</label>
|
||||||
|
<p className="text-gray-900">{selectedAppointment.scheduled_at ? formatTime(selectedAppointment.scheduled_at) : '—'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-500 mb-1">Tipo</label>
|
||||||
|
<p className="text-gray-900">{selectedAppointment.appointment_type === 'telemedicina' ? 'Telemedicina' : 'Presencial'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-500 mb-1">Status</label>
|
||||||
|
<div>{getStatusBadge(selectedAppointment.status || 'agendada')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-500 mb-1">Observações</label>
|
||||||
|
<p className="text-gray-900 whitespace-pre-wrap">{selectedAppointment.notes || '—'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedAppointment(null)}
|
||||||
|
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
Fechar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,7 +61,11 @@ const formatDoctorName = (fullName: string): string => {
|
|||||||
return `Dr. ${name}`;
|
return `Dr. ${name}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SecretaryDoctorList() {
|
export function SecretaryDoctorList({
|
||||||
|
onOpenSchedule,
|
||||||
|
}: {
|
||||||
|
onOpenSchedule?: (doctorId: string) => void;
|
||||||
|
}) {
|
||||||
const [doctors, setDoctors] = useState<Doctor[]>([]);
|
const [doctors, setDoctors] = useState<Doctor[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
@ -79,6 +83,8 @@ export function SecretaryDoctorList() {
|
|||||||
crm_uf: "",
|
crm_uf: "",
|
||||||
specialty: "",
|
specialty: "",
|
||||||
});
|
});
|
||||||
|
const [showViewModal, setShowViewModal] = useState(false);
|
||||||
|
const [selectedDoctor, setSelectedDoctor] = useState<Doctor | null>(null);
|
||||||
|
|
||||||
const loadDoctors = async () => {
|
const loadDoctors = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -378,12 +384,26 @@ export function SecretaryDoctorList() {
|
|||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedDoctor(doctor);
|
||||||
|
setShowViewModal(true);
|
||||||
|
}}
|
||||||
title="Visualizar"
|
title="Visualizar"
|
||||||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
// Prefer callback from parent to switch tab; fallback to sessionStorage
|
||||||
|
if (onOpenSchedule) {
|
||||||
|
onOpenSchedule(doctor.id);
|
||||||
|
} else {
|
||||||
|
sessionStorage.setItem("selectedDoctorForSchedule", doctor.id);
|
||||||
|
// dispatch a custom event to inform parent (optional)
|
||||||
|
window.dispatchEvent(new CustomEvent("open-doctor-schedule"));
|
||||||
|
}
|
||||||
|
}}
|
||||||
title="Gerenciar agenda"
|
title="Gerenciar agenda"
|
||||||
className="p-2 text-green-600 hover:bg-green-50 rounded-lg transition-colors"
|
className="p-2 text-green-600 hover:bg-green-50 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
@ -597,6 +617,51 @@ export function SecretaryDoctorList() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Modal de Visualizar Médico */}
|
||||||
|
{showViewModal && selectedDoctor && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900">Visualizar Médico</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowViewModal(false)}
|
||||||
|
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">Nome</p>
|
||||||
|
<p className="text-gray-900 font-medium">{selectedDoctor.full_name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">Especialidade</p>
|
||||||
|
<p className="text-gray-900">{selectedDoctor.specialty || '—'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">CRM</p>
|
||||||
|
<p className="text-gray-900">{selectedDoctor.crm || '—'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">Email</p>
|
||||||
|
<p className="text-gray-900">{selectedDoctor.email || '—'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 border-t border-gray-200 flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowViewModal(false)}
|
||||||
|
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
Fechar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,6 +92,15 @@ export function SecretaryDoctorSchedule() {
|
|||||||
loadDoctors();
|
loadDoctors();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// If a doctor id was requested by other components (via sessionStorage), select it
|
||||||
|
useEffect(() => {
|
||||||
|
const requested = sessionStorage.getItem("selectedDoctorForSchedule");
|
||||||
|
if (requested) {
|
||||||
|
setSelectedDoctorId(requested);
|
||||||
|
sessionStorage.removeItem("selectedDoctorForSchedule");
|
||||||
|
}
|
||||||
|
}, [doctors]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[SecretaryDoctorSchedule] Estado availabilities atualizado:", {
|
console.log("[SecretaryDoctorSchedule] Estado availabilities atualizado:", {
|
||||||
count: availabilities.length,
|
count: availabilities.length,
|
||||||
|
|||||||
@ -40,7 +40,11 @@ const buscarEnderecoViaCEP = async (cep: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SecretaryPatientList() {
|
export function SecretaryPatientList({
|
||||||
|
onOpenAppointment,
|
||||||
|
}: {
|
||||||
|
onOpenAppointment?: (patientId: string) => void;
|
||||||
|
}) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [patients, setPatients] = useState<Patient[]>([]);
|
const [patients, setPatients] = useState<Patient[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -54,6 +58,8 @@ export function SecretaryPatientList() {
|
|||||||
const [modalMode, setModalMode] = useState<"create" | "edit">("create");
|
const [modalMode, setModalMode] = useState<"create" | "edit">("create");
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
const [patientToDelete, setPatientToDelete] = useState<Patient | null>(null);
|
const [patientToDelete, setPatientToDelete] = useState<Patient | null>(null);
|
||||||
|
const [showViewModal, setShowViewModal] = useState(false);
|
||||||
|
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
||||||
const [formData, setFormData] = useState<PacienteFormData>({
|
const [formData, setFormData] = useState<PacienteFormData>({
|
||||||
nome: "",
|
nome: "",
|
||||||
social_name: "",
|
social_name: "",
|
||||||
@ -136,7 +142,15 @@ export function SecretaryPatientList() {
|
|||||||
return currentMonth === birthMonth;
|
return currentMonth === birthMonth;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return matchesSearch && matchesBirthday;
|
// Filtro de convênio
|
||||||
|
const matchesInsurance =
|
||||||
|
insuranceFilter === "Todos" ||
|
||||||
|
((patient as any).convenio || "Particular") === insuranceFilter;
|
||||||
|
|
||||||
|
// Filtro VIP (se o backend fornecer uma flag 'is_vip' ou 'vip')
|
||||||
|
const matchesVIP = !showVIP || ((patient as any).is_vip === true || (patient as any).vip === true);
|
||||||
|
|
||||||
|
return matchesSearch && matchesBirthday && matchesInsurance && matchesVIP;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
@ -333,6 +347,21 @@ export function SecretaryPatientList() {
|
|||||||
setShowDeleteDialog(true);
|
setShowDeleteDialog(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleViewPatient = (patient: Patient) => {
|
||||||
|
setSelectedPatient(patient);
|
||||||
|
setShowViewModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSchedulePatient = (patient: Patient) => {
|
||||||
|
if (onOpenAppointment) {
|
||||||
|
onOpenAppointment(patient.id as string);
|
||||||
|
} else {
|
||||||
|
// fallback: store in sessionStorage and dispatch event
|
||||||
|
sessionStorage.setItem("selectedPatientForAppointment", patient.id as string);
|
||||||
|
window.dispatchEvent(new CustomEvent("open-create-appointment"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleConfirmDelete = async () => {
|
const handleConfirmDelete = async () => {
|
||||||
if (!patientToDelete?.id) return;
|
if (!patientToDelete?.id) return;
|
||||||
|
|
||||||
@ -518,17 +547,19 @@ export function SecretaryPatientList() {
|
|||||||
{/* TODO: Buscar próximo agendamento */}—
|
{/* TODO: Buscar próximo agendamento */}—
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 text-sm text-gray-700">
|
<td className="px-6 py-4 text-sm text-gray-700">
|
||||||
Particular
|
{(patient as any).convenio || "Particular"}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
|
onClick={() => handleViewPatient(patient)}
|
||||||
title="Visualizar"
|
title="Visualizar"
|
||||||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
onClick={() => handleSchedulePatient(patient)}
|
||||||
title="Agendar consulta"
|
title="Agendar consulta"
|
||||||
className="p-2 text-green-600 hover:bg-green-50 rounded-lg transition-colors"
|
className="p-2 text-green-600 hover:bg-green-50 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
@ -596,6 +627,50 @@ export function SecretaryPatientList() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Modal de Visualizar Paciente */}
|
||||||
|
{showViewModal && selectedPatient && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-xl max-w-3xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<div className="p-6 border-b border-gray-200 flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900">Visualizar Paciente</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowViewModal(false)}
|
||||||
|
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">Nome</p>
|
||||||
|
<p className="text-gray-900 font-medium">{selectedPatient.full_name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">Email</p>
|
||||||
|
<p className="text-gray-900">{selectedPatient.email || '—'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">Telefone</p>
|
||||||
|
<p className="text-gray-900">{selectedPatient.phone_mobile || '—'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">Convênio</p>
|
||||||
|
<p className="text-gray-900">{(selectedPatient as any).convenio || 'Particular'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowViewModal(false)}
|
||||||
|
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
Fechar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Delete Confirmation Dialog */}
|
{/* Delete Confirmation Dialog */}
|
||||||
{showDeleteDialog && patientToDelete && (
|
{showDeleteDialog && patientToDelete && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
|||||||
@ -35,6 +35,14 @@ export function SecretaryReportList() {
|
|||||||
loadPatients();
|
loadPatients();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Recarrega automaticamente quando o filtro de status muda
|
||||||
|
// (evita depender do clique em Buscar)
|
||||||
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
loadReports();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [statusFilter]);
|
||||||
|
|
||||||
const loadPatients = async () => {
|
const loadPatients = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await patientService.list();
|
const data = await patientService.list();
|
||||||
@ -267,9 +275,24 @@ export function SecretaryReportList() {
|
|||||||
const loadReports = async () => {
|
const loadReports = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await reportService.list();
|
// Se um filtro de status estiver aplicado, encaminhar para o serviço
|
||||||
|
// Cast explícito para o tipo esperado pelo serviço (ReportStatus)
|
||||||
|
const filters = statusFilter ? { status: statusFilter as any } : undefined;
|
||||||
|
console.log("[SecretaryReportList] loadReports filters:", filters);
|
||||||
|
const data = await reportService.list(filters);
|
||||||
console.log("✅ Relatórios carregados:", data);
|
console.log("✅ Relatórios carregados:", data);
|
||||||
setReports(Array.isArray(data) ? data : []);
|
// Garantir filtro por status no cliente como fallback caso o backend
|
||||||
|
// não aplique corretamente o filtro. Normaliza para evitar problemas
|
||||||
|
// de case/whitespace.
|
||||||
|
let reportsList = Array.isArray(data) ? data : [];
|
||||||
|
if (statusFilter) {
|
||||||
|
const normalized = statusFilter.toString().toLowerCase().trim();
|
||||||
|
reportsList = reportsList.filter((r) => {
|
||||||
|
const rs = (r.status || "").toString().toLowerCase().trim();
|
||||||
|
return rs === normalized;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setReports(reportsList);
|
||||||
if (Array.isArray(data) && data.length === 0) {
|
if (Array.isArray(data) && data.length === 0) {
|
||||||
console.warn("⚠️ Nenhum relatório encontrado na API");
|
console.warn("⚠️ Nenhum relatório encontrado na API");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,8 +90,24 @@ export default function PainelSecretaria() {
|
|||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="max-w-[1400px] mx-auto px-6 py-8">
|
<main className="max-w-[1400px] mx-auto px-6 py-8">
|
||||||
{activeTab === "pacientes" && <SecretaryPatientList />}
|
{activeTab === "pacientes" && (
|
||||||
{activeTab === "medicos" && <SecretaryDoctorList />}
|
<SecretaryPatientList
|
||||||
|
onOpenAppointment={(patientId: string) => {
|
||||||
|
// store selected patient for appointment and switch to consultas tab
|
||||||
|
sessionStorage.setItem("selectedPatientForAppointment", patientId);
|
||||||
|
setActiveTab("consultas");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeTab === "medicos" && (
|
||||||
|
<SecretaryDoctorList
|
||||||
|
onOpenSchedule={(doctorId: string) => {
|
||||||
|
// store selected doctor for schedule and switch to agenda tab
|
||||||
|
sessionStorage.setItem("selectedDoctorForSchedule", doctorId);
|
||||||
|
setActiveTab("agenda");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{activeTab === "consultas" && <SecretaryAppointmentList />}
|
{activeTab === "consultas" && <SecretaryAppointmentList />}
|
||||||
{activeTab === "agenda" && <SecretaryDoctorSchedule />}
|
{activeTab === "agenda" && <SecretaryDoctorSchedule />}
|
||||||
{activeTab === "relatorios" && <SecretaryReportList />}
|
{activeTab === "relatorios" && <SecretaryReportList />}
|
||||||
|
|||||||
@ -27,6 +27,9 @@ class ReportService {
|
|||||||
params["status"] = `eq.${filters.status}`;
|
params["status"] = `eq.${filters.status}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log para depuração: mostra quais params serão enviados
|
||||||
|
console.log("[ReportService] list() params:", params);
|
||||||
|
|
||||||
const response = await apiClient.get<Report[]>(this.basePath, { params });
|
const response = await apiClient.get<Report[]>(this.basePath, { params });
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user