develop #83

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

View File

@ -11,10 +11,7 @@ import { EventInput } from "@fullcalendar/core/index.js";
import { Sidebar } from "@/components/dashboard/sidebar";
import { PagesHeader } from "@/components/dashboard/header";
import { Button } from "@/components/ui/button";
import {
mockAppointments,
mockWaitingList,
} from "@/lib/mocks/appointment-mocks";
import { mockWaitingList } from "@/lib/mocks/appointment-mocks";
import "./index.css";
import Link from "next/link";
import {
@ -30,7 +27,7 @@ const ListaEspera = dynamic(
);
export default function AgendamentoPage() {
const [appointments, setAppointments] = useState(mockAppointments);
const [appointments, setAppointments] = useState<any[]>([]);
const [waitingList, setWaitingList] = useState(mockWaitingList);
const [activeTab, setActiveTab] = useState<"calendar" | "espera">("calendar");
const [requestsList, setRequestsList] = useState<EventInput[]>();
@ -47,23 +44,51 @@ export default function AgendamentoPage() {
}, []);
useEffect(() => {
let events: EventInput[] = [];
appointments.forEach((obj) => {
const event: EventInput = {
title: `${obj.patient}: ${obj.type}`,
start: new Date(obj.time),
end: new Date(new Date(obj.time).getTime() + obj.duration * 60 * 1000),
color:
obj.status === "confirmed"
? "#68d68a"
: obj.status === "pending"
? "#ffe55f"
: "#ff5f5fff",
};
events.push(event);
// Fetch real appointments and map to calendar events
let mounted = true;
(async () => {
try {
// listarAgendamentos accepts a query string; request a reasonable limit and order
const arr = await (await import('@/lib/api')).listarAgendamentos('select=*&order=scheduled_at.desc&limit=500').catch(() => []);
if (!mounted) return;
if (!arr || !arr.length) {
setAppointments([]);
setRequestsList([]);
return;
}
// 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);
}, [appointments]);
setRequestsList(events || []);
} 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
const handleSaveAppointment = (appointment: any) => {

View File

@ -508,7 +508,7 @@ export default function ConsultasPage() {
{capitalize(appointment.status)}
</Badge>
</TableCell>
<TableCell>{formatDate(appointment.time)}</TableCell>
<TableCell>{formatDate(appointment.scheduled_at ?? appointment.time)}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -560,7 +560,7 @@ export default function ConsultasPage() {
</div>
<div className="grid grid-cols-4 items-center gap-4">
<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 className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Status</Label>

View File

@ -4,7 +4,7 @@ import SignatureCanvas from "react-signature-canvas";
import Link from "next/link";
import ProtectedRoute from "@/components/ProtectedRoute";
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 { CreateReportData } from "@/types/report-types";
import { Button } from "@/components/ui/button";
@ -239,36 +239,127 @@ const ProfissionalPage = () => {
const [events, setEvents] = useState<any[]>([
{
id: 1,
title: "Ana Souza",
type: "Cardiologia",
time: "09:00",
date: new Date().toISOString().split('T')[0],
pacienteId: "123.456.789-00",
color: colorsByType.Cardiologia
},
{
id: 2,
title: "Bruno Lima",
type: "Cardiologia",
time: "10:30",
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
const [events, setEvents] = useState<any[]>([]);
// Load real appointments for the logged in doctor and map to calendar events
useEffect(() => {
let mounted = true;
(async () => {
try {
// If we already have a doctorId (set earlier), use it. Otherwise try to resolve from the logged user
let docId = doctorId;
if (!docId && user && user.email) {
// buscarMedicos may return the doctor's record including id
try {
const docs = await buscarMedicos(user.email).catch(() => []);
if (Array.isArray(docs) && docs.length > 0) {
const chosen = docs.find(d => String((d as any).user_id) === String(user.id)) || docs[0];
docId = (chosen as any)?.id ?? null;
if (mounted && !doctorId) setDoctorId(docId);
}
]);
} 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 [showPopup, setShowPopup] = useState(false);
const [showActionModal, setShowActionModal] = useState(false);

View File

@ -766,7 +766,53 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode =
</div>
<div className="space-y-2">
<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>
{/* Profissional solicitante removed per user request */}
</div>