develop #83
@ -11,10 +11,7 @@ import { EventInput } from "@fullcalendar/core/index.js";
|
|||||||
import { Sidebar } from "@/components/dashboard/sidebar";
|
import { Sidebar } from "@/components/dashboard/sidebar";
|
||||||
import { PagesHeader } from "@/components/dashboard/header";
|
import { PagesHeader } from "@/components/dashboard/header";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import { mockWaitingList } from "@/lib/mocks/appointment-mocks";
|
||||||
mockAppointments,
|
|
||||||
mockWaitingList,
|
|
||||||
} from "@/lib/mocks/appointment-mocks";
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
@ -30,7 +27,7 @@ const ListaEspera = dynamic(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export default function AgendamentoPage() {
|
export default function AgendamentoPage() {
|
||||||
const [appointments, setAppointments] = useState(mockAppointments);
|
const [appointments, setAppointments] = useState<any[]>([]);
|
||||||
const [waitingList, setWaitingList] = useState(mockWaitingList);
|
const [waitingList, setWaitingList] = useState(mockWaitingList);
|
||||||
const [activeTab, setActiveTab] = useState<"calendar" | "espera">("calendar");
|
const [activeTab, setActiveTab] = useState<"calendar" | "espera">("calendar");
|
||||||
const [requestsList, setRequestsList] = useState<EventInput[]>();
|
const [requestsList, setRequestsList] = useState<EventInput[]>();
|
||||||
@ -47,23 +44,51 @@ export default function AgendamentoPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let events: EventInput[] = [];
|
// Fetch real appointments and map to calendar events
|
||||||
appointments.forEach((obj) => {
|
let mounted = true;
|
||||||
const event: EventInput = {
|
(async () => {
|
||||||
title: `${obj.patient}: ${obj.type}`,
|
try {
|
||||||
start: new Date(obj.time),
|
// listarAgendamentos accepts a query string; request a reasonable limit and order
|
||||||
end: new Date(new Date(obj.time).getTime() + obj.duration * 60 * 1000),
|
const arr = await (await import('@/lib/api')).listarAgendamentos('select=*&order=scheduled_at.desc&limit=500').catch(() => []);
|
||||||
color:
|
if (!mounted) return;
|
||||||
obj.status === "confirmed"
|
if (!arr || !arr.length) {
|
||||||
? "#68d68a"
|
setAppointments([]);
|
||||||
: obj.status === "pending"
|
setRequestsList([]);
|
||||||
? "#ffe55f"
|
return;
|
||||||
: "#ff5f5fff",
|
}
|
||||||
};
|
|
||||||
events.push(event);
|
// Batch-fetch patient names for display
|
||||||
|
const patientIds = Array.from(new Set(arr.map((a: any) => a.patient_id).filter(Boolean)));
|
||||||
|
const patients = (patientIds && patientIds.length) ? await (await import('@/lib/api')).buscarPacientesPorIds(patientIds) : [];
|
||||||
|
const patientsById: Record<string, any> = {};
|
||||||
|
(patients || []).forEach((p: any) => { if (p && p.id) patientsById[String(p.id)] = p; });
|
||||||
|
|
||||||
|
setAppointments(arr || []);
|
||||||
|
|
||||||
|
const events: EventInput[] = (arr || []).map((obj: any) => {
|
||||||
|
const scheduled = obj.scheduled_at || obj.scheduledAt || obj.time || null;
|
||||||
|
const start = scheduled ? new Date(scheduled) : null;
|
||||||
|
const duration = Number(obj.duration_minutes ?? obj.duration ?? 30) || 30;
|
||||||
|
const patient = (patientsById[String(obj.patient_id)]?.full_name) || obj.patient_name || obj.patient_full_name || obj.patient || 'Paciente';
|
||||||
|
const title = `${patient}: ${obj.appointment_type ?? obj.type ?? ''}`.trim();
|
||||||
|
const color = obj.status === 'confirmed' ? '#68d68a' : obj.status === 'pending' ? '#ffe55f' : '#ff5f5fff';
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
start: start || new Date(),
|
||||||
|
end: start ? new Date(start.getTime() + duration * 60 * 1000) : undefined,
|
||||||
|
color,
|
||||||
|
extendedProps: { raw: obj },
|
||||||
|
} as EventInput;
|
||||||
});
|
});
|
||||||
setRequestsList(events);
|
setRequestsList(events || []);
|
||||||
}, [appointments]);
|
} catch (err) {
|
||||||
|
console.warn('[AgendamentoPage] falha ao carregar agendamentos', err);
|
||||||
|
setAppointments([]);
|
||||||
|
setRequestsList([]);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => { mounted = false; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
// mantive para caso a lógica de salvar consulta passe a funcionar
|
// mantive para caso a lógica de salvar consulta passe a funcionar
|
||||||
const handleSaveAppointment = (appointment: any) => {
|
const handleSaveAppointment = (appointment: any) => {
|
||||||
|
|||||||
@ -508,7 +508,7 @@ export default function ConsultasPage() {
|
|||||||
{capitalize(appointment.status)}
|
{capitalize(appointment.status)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{formatDate(appointment.time)}</TableCell>
|
<TableCell>{formatDate(appointment.scheduled_at ?? appointment.time)}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@ -560,7 +560,7 @@ export default function ConsultasPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
<Label className="text-right">Data e Hora</Label>
|
<Label className="text-right">Data e Hora</Label>
|
||||||
<span className="col-span-3">{viewingAppointment?.time ? formatDate(viewingAppointment.time) : ''}</span>
|
<span className="col-span-3">{(viewingAppointment?.scheduled_at ?? viewingAppointment?.time) ? formatDate(viewingAppointment?.scheduled_at ?? viewingAppointment?.time) : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
<Label className="text-right">Status</Label>
|
<Label className="text-right">Status</Label>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import SignatureCanvas from "react-signature-canvas";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import ProtectedRoute from "@/components/ProtectedRoute";
|
import ProtectedRoute from "@/components/ProtectedRoute";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, buscarMedicos, type Paciente, buscarRelatorioPorId, atualizarMedico } from "@/lib/api";
|
import { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, buscarMedicos, listarAgendamentos, type Paciente, buscarRelatorioPorId, atualizarMedico } from "@/lib/api";
|
||||||
import { useReports } from "@/hooks/useReports";
|
import { useReports } from "@/hooks/useReports";
|
||||||
import { CreateReportData } from "@/types/report-types";
|
import { CreateReportData } from "@/types/report-types";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -239,36 +239,127 @@ const ProfissionalPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const [events, setEvents] = useState<any[]>([
|
const [events, setEvents] = useState<any[]>([]);
|
||||||
|
// Load real appointments for the logged in doctor and map to calendar events
|
||||||
{
|
useEffect(() => {
|
||||||
id: 1,
|
let mounted = true;
|
||||||
title: "Ana Souza",
|
(async () => {
|
||||||
type: "Cardiologia",
|
try {
|
||||||
time: "09:00",
|
// If we already have a doctorId (set earlier), use it. Otherwise try to resolve from the logged user
|
||||||
date: new Date().toISOString().split('T')[0],
|
let docId = doctorId;
|
||||||
pacienteId: "123.456.789-00",
|
if (!docId && user && user.email) {
|
||||||
color: colorsByType.Cardiologia
|
// buscarMedicos may return the doctor's record including id
|
||||||
},
|
try {
|
||||||
{
|
const docs = await buscarMedicos(user.email).catch(() => []);
|
||||||
id: 2,
|
if (Array.isArray(docs) && docs.length > 0) {
|
||||||
title: "Bruno Lima",
|
const chosen = docs.find(d => String((d as any).user_id) === String(user.id)) || docs[0];
|
||||||
type: "Cardiologia",
|
docId = (chosen as any)?.id ?? null;
|
||||||
time: "10:30",
|
if (mounted && !doctorId) setDoctorId(docId);
|
||||||
date: new Date().toISOString().split('T')[0],
|
|
||||||
pacienteId: "987.654.321-00",
|
|
||||||
color: colorsByType.Cardiologia
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "Carla Menezes",
|
|
||||||
type: "Dermatologia",
|
|
||||||
time: "14:00",
|
|
||||||
date: new Date().toISOString().split('T')[0],
|
|
||||||
pacienteId: "111.222.333-44",
|
|
||||||
color: colorsByType.Dermatologia
|
|
||||||
}
|
}
|
||||||
]);
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!docId) {
|
||||||
|
// nothing to fetch yet
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch appointments for this doctor. We'll ask for future and recent past appointments
|
||||||
|
// using a simple filter: doctor_id=eq.<docId>&order=scheduled_at.asc&limit=200
|
||||||
|
const qs = `?select=*&doctor_id=eq.${encodeURIComponent(String(docId))}&order=scheduled_at.asc&limit=200`;
|
||||||
|
const appts = await listarAgendamentos(qs).catch(() => []);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
// Enrich appointments with patient names (batch fetch) and map to UI events
|
||||||
|
const patientIds = Array.from(new Set((appts || []).map((x:any) => String(x.patient_id || x.patient_id_raw || '').trim()).filter(Boolean)));
|
||||||
|
let patientMap = new Map<string, any>();
|
||||||
|
if (patientIds.length) {
|
||||||
|
try {
|
||||||
|
const patients = await buscarPacientesPorIds(patientIds).catch(() => []);
|
||||||
|
for (const p of patients || []) {
|
||||||
|
if (p && p.id) patientMap.set(String(p.id), p);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[ProfissionalPage] falha ao buscar pacientes para eventos:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapped = (appts || []).map((a: any, idx: number) => {
|
||||||
|
const scheduled = a.scheduled_at || a.time || a.created_at || null;
|
||||||
|
// Use local date components to avoid UTC shift when showing the appointment day/time
|
||||||
|
let datePart = new Date().toISOString().split('T')[0];
|
||||||
|
let timePart = '';
|
||||||
|
if (scheduled) {
|
||||||
|
try {
|
||||||
|
const d = new Date(scheduled);
|
||||||
|
// build local date string YYYY-MM-DD using local getters
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
datePart = `${y}-${m}-${day}`;
|
||||||
|
timePart = `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pid = a.patient_id || a.patient || a.patient_id_raw || a.patientId || null;
|
||||||
|
const patientObj = pid ? patientMap.get(String(pid)) : null;
|
||||||
|
const patientName = patientObj?.full_name || a.patient || a.patient_name || String(pid) || 'Paciente';
|
||||||
|
const patientIdVal = pid || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: a.id ?? `srv-${idx}-${String(a.scheduled_at || a.created_at || idx)}`,
|
||||||
|
title: patientName || 'Paciente',
|
||||||
|
type: a.appointment_type || 'Consulta',
|
||||||
|
time: timePart || '',
|
||||||
|
date: datePart,
|
||||||
|
pacienteId: patientIdVal,
|
||||||
|
color: colorsByType[a.specialty as keyof typeof colorsByType] || '#4dabf7',
|
||||||
|
raw: a,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setEvents(mapped);
|
||||||
|
|
||||||
|
// Helper: parse 'YYYY-MM-DD' into a local Date to avoid UTC parsing which can shift day
|
||||||
|
const parseYMDToLocal = (ymd?: string) => {
|
||||||
|
if (!ymd || typeof ymd !== 'string') return new Date();
|
||||||
|
const parts = ymd.split('-').map((p) => Number(p));
|
||||||
|
if (parts.length < 3 || parts.some((n) => Number.isNaN(n))) return new Date(ymd);
|
||||||
|
const [y, m, d] = parts;
|
||||||
|
return new Date(y, (m || 1) - 1, d || 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set calendar view to nearest upcoming appointment (or today)
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
const upcoming = mapped.find((m:any) => {
|
||||||
|
if (!m.raw) return false;
|
||||||
|
const s = m.raw.scheduled_at || m.raw.time || m.raw.created_at;
|
||||||
|
if (!s) return false;
|
||||||
|
const t = new Date(s).getTime();
|
||||||
|
return !isNaN(t) && t >= now;
|
||||||
|
});
|
||||||
|
if (upcoming) {
|
||||||
|
setCurrentCalendarDate(parseYMDToLocal(upcoming.date));
|
||||||
|
} else if (mapped.length) {
|
||||||
|
// fallback: show the date of the first appointment
|
||||||
|
setCurrentCalendarDate(parseYMDToLocal(mapped[0].date));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[ProfissionalPage] falha ao carregar agendamentos do servidor:', err);
|
||||||
|
// Keep mocked/empty events if fetch fails
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => { mounted = false; };
|
||||||
|
}, [doctorId, user?.id, user?.email]);
|
||||||
const [editingEvent, setEditingEvent] = useState<any>(null);
|
const [editingEvent, setEditingEvent] = useState<any>(null);
|
||||||
const [showPopup, setShowPopup] = useState(false);
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
const [showActionModal, setShowActionModal] = useState(false);
|
const [showActionModal, setShowActionModal] = useState(false);
|
||||||
|
|||||||
@ -766,7 +766,53 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode =
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-[13px]">Término *</Label>
|
<Label className="text-[13px]">Término *</Label>
|
||||||
<Input name="endTime" type="time" className="h-11 w-full rounded-md px-3 text-[13px] transition-colors hover:bg-muted/30" value={formData.endTime || ''} onChange={handleChange} />
|
{/*
|
||||||
|
When creating a new appointment from a predefined slot, the end time
|
||||||
|
is derived from the slot's start + duration and therefore cannot be
|
||||||
|
edited. We disable/readOnly the input in create mode when either a
|
||||||
|
slot is selected (startTime corresponds to an availableSlots entry)
|
||||||
|
or the duration was locked from a slot (lockedDurationFromSlot).
|
||||||
|
*/}
|
||||||
|
<Input
|
||||||
|
name="endTime"
|
||||||
|
type="time"
|
||||||
|
className="h-11 w-full rounded-md px-3 text-[13px] transition-colors hover:bg-muted/30"
|
||||||
|
value={formData.endTime || ''}
|
||||||
|
onChange={handleChange}
|
||||||
|
readOnly={createMode && (lockedDurationFromSlot || Boolean(((): boolean => {
|
||||||
|
try {
|
||||||
|
const date = (formData as any).appointmentDate || '';
|
||||||
|
const time = (formData as any).startTime || '';
|
||||||
|
if (!date || !time) return false;
|
||||||
|
// Check if startTime matches one of the availableSlots (meaning slot-driven)
|
||||||
|
return (availableSlots || []).some((s) => {
|
||||||
|
try {
|
||||||
|
const d = new Date(s.datetime);
|
||||||
|
const hh = String(d.getHours()).padStart(2, '0');
|
||||||
|
const mm = String(d.getMinutes()).padStart(2, '0');
|
||||||
|
const dateOnly = d.toISOString().split('T')[0];
|
||||||
|
return dateOnly === date && `${hh}:${mm}` === time;
|
||||||
|
} catch (e) { return false; }
|
||||||
|
});
|
||||||
|
} catch (e) { return false; }
|
||||||
|
})()))}
|
||||||
|
disabled={createMode && (lockedDurationFromSlot || Boolean(((): boolean => {
|
||||||
|
try {
|
||||||
|
const date = (formData as any).appointmentDate || '';
|
||||||
|
const time = (formData as any).startTime || '';
|
||||||
|
if (!date || !time) return false;
|
||||||
|
return (availableSlots || []).some((s) => {
|
||||||
|
try {
|
||||||
|
const d = new Date(s.datetime);
|
||||||
|
const hh = String(d.getHours()).padStart(2, '0');
|
||||||
|
const mm = String(d.getMinutes()).padStart(2, '0');
|
||||||
|
const dateOnly = d.toISOString().split('T')[0];
|
||||||
|
return dateOnly === date && `${hh}:${mm}` === time;
|
||||||
|
} catch (e) { return false; }
|
||||||
|
});
|
||||||
|
} catch (e) { return false; }
|
||||||
|
})()))}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Profissional solicitante removed per user request */}
|
{/* Profissional solicitante removed per user request */}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user