feature/add-appointments-endpoint #52

Merged
JoaoGustavo-dev merged 9 commits from feature/add-appointments-endpoint into develop 2025-10-19 05:15:11 +00:00
3 changed files with 374 additions and 178 deletions
Showing only changes of commit 83018b8854 - Show all commits

View File

@ -55,7 +55,7 @@ import {
} from "@/components/ui/select";
import { mockProfessionals } from "@/lib/mocks/appointment-mocks";
import { listarAgendamentos, buscarPacientesPorIds, buscarMedicosPorIds } from "@/lib/api";
import { listarAgendamentos, buscarPacientesPorIds, buscarMedicosPorIds, atualizarAgendamento } from "@/lib/api";
import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form";
const formatDate = (date: string | Date) => {
@ -80,15 +80,17 @@ export default function ConsultasPage() {
const [showForm, setShowForm] = useState(false);
const [editingAppointment, setEditingAppointment] = useState<any | null>(null);
const [viewingAppointment, setViewingAppointment] = useState<any | null>(null);
// Local form state used when editing. Keep hook at top-level to avoid Hooks order changes.
const [localForm, setLocalForm] = useState<any | null>(null);
const mapAppointmentToFormData = (appointment: any) => {
const professional = mockProfessionals.find((p) => p.id === appointment.professional);
const appointmentDate = new Date(appointment.time || appointment.scheduled_at || Date.now());
return {
id: appointment.id,
patientName: appointment.patient,
professionalName: professional ? professional.name : "",
patientId: appointment.patient_id || appointment.patientId || null,
professionalName: appointment.professional || "",
appointmentDate: appointmentDate.toISOString().split("T")[0],
startTime: appointmentDate.toTimeString().split(" ")[0].substring(0, 5),
endTime: new Date(appointmentDate.getTime() + (appointment.duration || 30) * 60000)
@ -127,22 +129,72 @@ export default function ConsultasPage() {
const handleCancel = () => {
setEditingAppointment(null);
setShowForm(false);
setLocalForm(null);
};
const handleSave = (formData: any) => {
const updatedAppointment = {
id: formData.id,
patient: formData.patientName,
time: new Date(`${formData.appointmentDate}T${formData.startTime}`).toISOString(),
duration: 30,
type: formData.appointmentType as any,
status: formData.status as any,
professional: appointments.find((a) => a.id === formData.id)?.professional || "",
notes: formData.notes,
};
const handleSave = async (formData: any) => {
try {
// build scheduled_at ISO (formData.startTime is 'HH:MM')
const scheduled_at = new Date(`${formData.appointmentDate}T${formData.startTime}`).toISOString();
setAppointments((prev) => prev.map((a) => (a.id === updatedAppointment.id ? updatedAppointment : a)));
handleCancel();
// compute duration from start/end times when available
let duration_minutes = 30;
try {
if (formData.startTime && formData.endTime) {
const [sh, sm] = String(formData.startTime).split(":").map((n: string) => Number(n));
const [eh, em] = String(formData.endTime).split(":").map((n: string) => Number(n));
const start = (sh || 0) * 60 + (sm || 0);
const end = (eh || 0) * 60 + (em || 0);
if (!Number.isNaN(start) && !Number.isNaN(end) && end > start) duration_minutes = end - start;
}
} catch (e) {
// fallback to default
duration_minutes = 30;
}
const payload: any = {
scheduled_at,
duration_minutes,
status: formData.status || undefined,
notes: formData.notes ?? null,
chief_complaint: formData.chief_complaint ?? null,
patient_notes: formData.patient_notes ?? null,
insurance_provider: formData.insurance_provider ?? null,
// convert local datetime-local inputs (which may be in 'YYYY-MM-DDTHH:MM' format) to proper ISO if present
checked_in_at: formData.checked_in_at ? new Date(formData.checked_in_at).toISOString() : null,
completed_at: formData.completed_at ? new Date(formData.completed_at).toISOString() : null,
cancelled_at: formData.cancelled_at ? new Date(formData.cancelled_at).toISOString() : null,
cancellation_reason: formData.cancellation_reason ?? null,
};
// Call PATCH endpoint
const updated = await atualizarAgendamento(formData.id, payload);
// Build UI-friendly row using server response and existing local fields
const existing = appointments.find((a) => a.id === formData.id) || {};
const mapped = {
id: updated.id,
patient: formData.patientName || existing.patient || '',
time: updated.scheduled_at || updated.created_at || scheduled_at,
duration: updated.duration_minutes || duration_minutes,
type: updated.appointment_type || formData.appointmentType || existing.type || 'presencial',
status: updated.status || formData.status || existing.status,
professional: existing.professional || formData.professionalName || '',
notes: updated.notes || updated.patient_notes || formData.notes || existing.notes || '',
};
setAppointments((prev) => prev.map((a) => (a.id === mapped.id ? mapped : a)));
handleCancel();
} catch (err) {
console.error('[ConsultasPage] Falha ao atualizar agendamento', err);
// Inform the user
try {
const msg = err instanceof Error ? err.message : String(err);
alert('Falha ao salvar alterações: ' + msg);
} catch (e) {
// ignore
}
}
};
useEffect(() => {
@ -190,6 +242,7 @@ export default function ConsultasPage() {
return {
id: a.id,
patient,
patient_id: a.patient_id,
time: a.scheduled_at || a.created_at || "",
duration: a.duration_minutes || 30,
type: a.appointment_type || "presencial",
@ -214,15 +267,23 @@ export default function ConsultasPage() {
};
}, []);
// editing view: render the calendar registration form with controlled data
if (showForm && editingAppointment) {
const [localForm, setLocalForm] = useState<any>(editingAppointment);
const onFormChange = (d: any) => setLocalForm(d);
// Keep localForm synchronized with editingAppointment
useEffect(() => {
if (showForm && editingAppointment) {
setLocalForm(editingAppointment);
}
if (!showForm) setLocalForm(null);
}, [showForm, editingAppointment]);
const saveLocal = () => {
handleSave(localForm);
};
const onFormChange = (d: any) => setLocalForm(d);
const saveLocal = async () => {
if (!localForm) return;
await handleSave(localForm);
};
// If editing, render the edit form as a focused view (keeps hooks stable)
if (showForm && localForm) {
return (
<div className="space-y-6 p-6 bg-background">
<div className="flex items-center gap-4">

View File

@ -1,7 +1,8 @@
"use client";
import { useState } from "react";
import { useState, useEffect, useRef } from "react";
import { buscarPacientePorId } from "@/lib/api";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
@ -27,6 +28,16 @@ interface FormData {
requestingProfessional?: string;
appointmentType?: string;
notes?: string;
// API-editable appointment fields
status?: string;
duration_minutes?: number;
chief_complaint?: string;
patient_notes?: string;
insurance_provider?: string;
checked_in_at?: string; // ISO datetime
completed_at?: string; // ISO datetime
cancelled_at?: string; // ISO datetime
cancellation_reason?: string;
}
interface CalendarRegistrationFormProperties {
@ -47,120 +58,187 @@ const formatValidityDate = (value: string) => {
export function CalendarRegistrationForm({ formData, onFormChange }: CalendarRegistrationFormProperties) {
const [isAdditionalInfoOpen, setIsAdditionalInfoOpen] = useState(false);
const [patientDetails, setPatientDetails] = useState<any | null>(null);
const [loadingPatient, setLoadingPatient] = useState(false);
// Helpers to convert between ISO (server) and input[type=datetime-local] value
const isoToDatetimeLocal = (iso?: string | null) => {
if (!iso) return "";
try {
let s = String(iso).trim();
// normalize common variants: space between date/time -> T
s = s.replace(" ", "T");
// If no timezone info (no Z or +/-), try treating as UTC by appending Z
if (!/[zZ]$/.test(s) && !/[+-]\d{2}:?\d{2}$/.test(s)) {
// try parse first; if invalid, append Z
const tryParse = Date.parse(s);
if (isNaN(tryParse)) {
s = s + "Z";
}
}
const d = new Date(s);
if (isNaN(d.getTime())) return "";
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
const hh = String(d.getHours()).padStart(2, "0");
const min = String(d.getMinutes()).padStart(2, "0");
return `${yyyy}-${mm}-${dd}T${hh}:${min}`;
} catch (e) {
return "";
}
};
const datetimeLocalToIso = (value: string) => {
if (!value) return null;
// value expected: YYYY-MM-DDTHH:MM or with seconds
try {
// If the browser gives a value without seconds, Date constructor will treat as local when we split
const [datePart, timePart] = value.split("T");
if (!datePart || !timePart) return null;
const [y, m, d] = datePart.split("-").map((s) => parseInt(s, 10));
const timeParts = timePart.split(":");
const hh = parseInt(timeParts[0], 10);
const min = parseInt(timeParts[1] || "0", 10);
const sec = parseInt(timeParts[2] || "0", 10);
if ([y, m, d, hh, min, sec].some((n) => Number.isNaN(n))) return null;
const dt = new Date(y, m - 1, d, hh, min, sec, 0);
return dt.toISOString();
} catch (e) {
return null;
}
};
// Automatically fetch patient details when the form receives a patientId
useEffect(() => {
const maybeId = (formData as any).patientId || (formData as any).patient_id || null;
if (!maybeId) {
setPatientDetails(null);
return;
}
let mounted = true;
setLoadingPatient(true);
buscarPacientePorId(maybeId)
.then((p) => {
if (!mounted) return;
setPatientDetails(p);
})
.catch((e) => {
if (!mounted) return;
setPatientDetails({ error: String(e) });
})
.finally(() => {
if (!mounted) return;
setLoadingPatient(false);
});
return () => {
mounted = false;
};
}, [(formData as any).patientId, (formData as any).patient_id]);
const handleChange = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = event.target;
if (name === 'validade') {
const formattedValue = formatValidityDate(value);
onFormChange({ ...formData, [name]: formattedValue });
} else {
onFormChange({ ...formData, [name]: value });
// map datetime-local fields to ISO before sending up
if (name === 'checked_in_at' || name === 'completed_at' || name === 'cancelled_at') {
const iso = datetimeLocalToIso(value as string);
onFormChange({ ...formData, [name]: iso });
return;
}
if (name === 'validade') {
const formattedValue = formatValidityDate(value);
onFormChange({ ...formData, [name]: formattedValue });
return;
}
// ensure duration is stored as a number
if (name === 'duration_minutes') {
const n = Number(value);
onFormChange({ ...formData, duration_minutes: Number.isNaN(n) ? undefined : n });
return;
}
// If user edits endTime manually, accept the value and clear lastAutoEndRef so auto-calc won't overwrite
if (name === 'endTime') {
// store as-is (HH:MM)
try {
// clear auto flag so user edits persist
(lastAutoEndRef as any).current = null;
} catch (e) {}
onFormChange({ ...formData, endTime: value });
return;
}
onFormChange({ ...formData, [name]: value });
};
// Auto-calculate endTime from startTime + duration_minutes
const lastAutoEndRef = useRef<string | null>(null);
useEffect(() => {
const start = (formData as any).startTime;
const dur = (formData as any).duration_minutes;
const date = (formData as any).appointmentDate; // YYYY-MM-DD
if (!start) return;
// if duration is not a finite number, don't compute
const minutes = typeof dur === 'number' && Number.isFinite(dur) ? dur : 0;
// build a Date from appointmentDate + startTime; fall back to today if appointmentDate missing
const datePart = date || new Date().toISOString().slice(0, 10);
const [y, m, d] = String(datePart).split('-').map((s) => parseInt(s, 10));
const [hh, mm] = String(start).split(':').map((s) => parseInt(s, 10));
if ([y, m, d, hh, mm].some((n) => Number.isNaN(n))) return;
const dt = new Date(y, m - 1, d, hh, mm, 0, 0);
const dt2 = new Date(dt.getTime() + minutes * 60000);
const newEnd = `${String(dt2.getHours()).padStart(2, '0')}:${String(dt2.getMinutes()).padStart(2, '0')}`;
const currentEnd = (formData as any).endTime;
// Only overwrite if endTime is empty or it was the previously auto-calculated value
if (!currentEnd || currentEnd === lastAutoEndRef.current) {
lastAutoEndRef.current = newEnd;
onFormChange({ ...formData, endTime: newEnd });
}
}, [(formData as any).startTime, (formData as any).duration_minutes, (formData as any).appointmentDate]);
return (
<form className="space-y-8">
<div className="border border-border rounded-md p-6 space-y-4 bg-card">
<h2 className="font-medium text-foreground">Informações do paciente</h2>
<div className="grid grid-cols-1 md:grid-cols-12 gap-4">
<div className="md:col-span-6 space-y-2">
<Label className="text-[13px]">Nome *</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
name="patientName"
placeholder="Digite o nome do paciente"
className="h-11 pl-8 rounded-md transition-colors hover:bg-muted/30"
value={formData.patientName || ''}
onChange={handleChange}
/>
</div>
</div>
<div className="md:col-span-3 space-y-2">
<Label className="text-[13px]">CPF do paciente</Label>
<Input name="cpf" placeholder="Número do CPF" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.cpf || ''} onChange={handleChange} />
</div>
<div className="md:col-span-3 space-y-2">
<Label className="text-[13px]">RG</Label>
<Input name="rg" placeholder="Número do RG" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.rg || ''} onChange={handleChange} />
</div>
<div className="md:col-span-3 space-y-2">
<Label className="text-[13px]">Data de nascimento *</Label>
<Input name="birthDate" type="date" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.birthDate || ''} onChange={handleChange} />
</div>
<div className="md:col-span-3 space-y-2">
<Label className="text-[13px]">Telefone</Label>
<div className="flex gap-2">
<select name="phoneCode" className="h-11 w-20 rounded-md border border-gray-300 dark:border-input bg-background text-foreground px-2 text-[13px] transition-colors hover:bg-muted/30 hover:border-gray-400" value={formData.phoneCode || '+55'} onChange={handleChange}>
<option value="+55">+55</option>
<option value="+351">+351</option>
<option value="+1">+1</option>
</select>
<Input name="phoneNumber" placeholder="(99) 99999-9999" className="h-11 flex-1 rounded-md transition-colors hover:bg-muted/30" value={formData.phoneNumber || ''} onChange={handleChange} />
</div>
</div>
<div className="md:col-span-6 space-y-2">
<Label className="text-[13px]">E-mail</Label>
<Input name="email" type="email" placeholder="email@exemplo.com" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.email || ''} onChange={handleChange} />
</div>
<div className="md:col-span-6 space-y-2">
<Label className="text-[13px]">Convênio</Label>
<div className="relative">
<select name="convenio" className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400" value={formData.convenio || ''} onChange={handleChange}>
<option value="" disabled>Selecione um convênio</option>
<option value="sulamerica">Sulamérica</option>
<option value="bradesco">Bradesco Saúde</option>
<option value="amil">Amil</option>
<option value="unimed">Unimed</option>
</select>
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
</div>
</div>
<div className="md:col-span-6 space-y-2">
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label className="text-[13px]">Matrícula</Label>
<Input name="matricula" placeholder="000000000" maxLength={9} className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.matricula || ''} onChange={handleChange} />
</div>
<div className="space-y-2">
<Label className="text-[13px]">Validade</Label>
<Input name="validade" placeholder="00/00/0000" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.validade || ''} onChange={handleChange} />
</div>
</div>
</div>
<div className="md:col-span-12 space-y-2">
<div
className="flex items-center justify-between cursor-pointer"
onClick={() => setIsAdditionalInfoOpen(!isAdditionalInfoOpen)}
>
<div className="flex items-center gap-2">
<Label className="text-sm font-medium cursor-pointer text-primary m-0">Informações adicionais</Label>
<ChevronDown className={`h-4 w-4 text-primary transition-transform duration-200 ${isAdditionalInfoOpen ? 'rotate-180' : ''}`} />
</div>
</div>
{isAdditionalInfoOpen && (
<div className="space-y-2">
<div className="md:col-span-6 space-y-2">
<Label className="text-[13px]">Nome</Label>
<div className="relative">
<select
name="documentos"
className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400"
value={formData.documentos || ''}
onChange={handleChange}
>
<option value="" disabled>
Documentos e anexos
</option>
<option value="identidade">Identidade / CPF</option>
<option value="comprovante_residencia">Comprovante de residência</option>
<option value="guias">Guias / Encaminhamentos</option>
<option value="outros">Outros</option>
</select>
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-primary" />
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
name="patientName"
placeholder="Nome do paciente"
className="h-11 pl-8 rounded-md transition-colors bg-muted/10"
value={formData.patientName || ''}
disabled
/>
</div>
</div>
)}
<div className="md:col-span-6 flex items-start justify-end">
<div className="text-right text-sm">
{loadingPatient ? (
<div>Carregando dados do paciente...</div>
) : patientDetails ? (
patientDetails.error ? (
<div className="text-red-500">Erro ao carregar paciente: {String(patientDetails.error)}</div>
) : (
<div className="text-sm text-muted-foreground space-y-1">
<div><strong>CPF:</strong> {patientDetails.cpf || '-'}</div>
<div><strong>Telefone:</strong> {patientDetails.phone_mobile || patientDetails.telefone || '-'}</div>
<div><strong>E-mail:</strong> {patientDetails.email || '-'}</div>
<div><strong>Data de nascimento:</strong> {patientDetails.birth_date || '-'}</div>
</div>
)
) : (
<div className="text-xs text-muted-foreground">Paciente não vinculado</div>
)}
<div className="mt-1 text-xs text-muted-foreground">Para editar os dados do paciente, acesse a ficha do paciente.</div>
</div>
</div>
</div>
</div>
@ -170,27 +248,18 @@ export function CalendarRegistrationForm({ formData, onFormChange }: CalendarReg
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-[13px]">Nome do profissional *</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input name="professionalName" className="h-11 w-full rounded-md pl-8 pr-12 text-[13px] transition-colors hover:bg-muted/30" value={formData.professionalName || ''} onChange={handleChange} />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label className="text-[13px]">Unidade *</Label>
<select name="unit" className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400" value={formData.unit || 'nei'} onChange={handleChange}>
<option value="nei">Núcleo de Especialidades Integradas</option>
<option value="cc">Clínica Central</option>
</select>
</div>
<div className="space-y-2">
<Label className="text-[13px]">Data *</Label>
<div className="relative">
<Calendar className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input name="appointmentDate" type="date" className="h-11 w-full rounded-md pl-8 pr-3 text-[13px] transition-colors hover:bg-muted/30" value={formData.appointmentDate || ''} onChange={handleChange} />
</div>
</div>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input name="professionalName" className="h-11 w-full rounded-md pl-8 pr-12 text-[13px] transition-colors hover:bg-muted/30" value={formData.professionalName || ''} onChange={handleChange} disabled />
</div>
</div>
<div className="space-y-2">
<Label className="text-[13px]">Data *</Label>
<div className="relative">
<Calendar className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input name="appointmentDate" type="date" className="h-11 w-full rounded-md pl-8 pr-3 text-[13px] transition-colors hover:bg-muted/30" value={formData.appointmentDate || ''} onChange={handleChange} />
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="space-y-2">
<Label className="text-[13px]">Início *</Label>
@ -200,48 +269,84 @@ export function CalendarRegistrationForm({ formData, onFormChange }: CalendarReg
<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} />
</div>
<div className="space-y-2">
<Label className="text-[13px]">Profissional solicitante</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<select name="requestingProfessional" className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-8 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400" value={formData.requestingProfessional || ''} onChange={handleChange}>
<option value="" disabled>Selecione solicitante</option>
<option value="dr-a">Dr. A</option>
<option value="dr-b">Dr. B</option>
</select>
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
</div>
</div>
{/* Profissional solicitante removed per user request */}
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-[13px]">Tipo de atendimento *</Label>
<div className="flex items-center space-x-2">
<Input type="checkbox" id="reembolso" className="h-4 w-4" />
<Label htmlFor="reembolso" className="text-[13px] font-medium">Pagamento via Reembolso</Label>
</div>
</div>
<div className="relative mt-1">
<Search className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input name="appointmentType" placeholder="Pesquisar" className="h-11 w-full rounded-md pl-8 pr-8 text-[13px] transition-colors hover:bg-muted/30" value={formData.appointmentType || ''} onChange={handleChange} />
</div>
<div className="flex items-center justify-between">
<Label className="text-[13px]">Tipo de atendimento *</Label>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-[13px]">Observações</Label>
<div className="flex items-center space-x-2">
<Input type="checkbox" id="imprimir" className="h-4 w-4" />
<Label htmlFor="imprimir" className="text-[13px] font-medium">Imprimir na Etiqueta / Pulseira</Label>
</div>
</div>
<Textarea name="notes" rows={6} className="text-[13px] min-h-[120px] resize-none rounded-md transition-colors hover:bg-muted/30" value={formData.notes || ''} onChange={handleChange} />
<div className="relative mt-1">
<Search className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input name="appointmentType" placeholder="Pesquisar" className="h-11 w-full rounded-md pl-8 pr-8 text-[13px] transition-colors hover:bg-muted/30" value={formData.appointmentType || ''} onChange={handleChange} disabled />
</div>
<div className="grid grid-cols-3 gap-3 mt-3">
<div>
<Label className="text-[13px]">Status</Label>
<select name="status" className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-3 text-[13px]" value={formData.status || ''} onChange={handleChange}>
<option value="">Selecione</option>
<option value="requested">Solicitado</option>
<option value="confirmed">Confirmado</option>
<option value="checked_in">Check-in</option>
<option value="in_progress">Em andamento</option>
<option value="completed">Concluído</option>
<option value="cancelled">Cancelado</option>
<option value="no_show">Não compareceu</option>
</select>
</div>
<div>
<Label className="text-[13px]">Duração (min)</Label>
<Input name="duration_minutes" type="number" min={1} className="h-11 w-full rounded-md" value={formData.duration_minutes ?? ''} onChange={handleChange} />
</div>
<div>
<Label className="text-[13px]">Convênio</Label>
<Input name="insurance_provider" placeholder="Operadora" className="h-11 w-full rounded-md" value={formData.insurance_provider || ''} onChange={handleChange} />
</div>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-[13px]">Observações</Label>
</div>
<Textarea name="notes" rows={4} className="text-[13px] min-h-[80px] resize-none rounded-md transition-colors hover:bg-muted/30" value={formData.notes || ''} onChange={handleChange} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3">
<div>
<Label className="text-[13px]">Queixa principal</Label>
<Textarea name="chief_complaint" rows={3} className="text-[13px] rounded-md" value={formData.chief_complaint || ''} onChange={handleChange} />
</div>
<div>
<Label className="text-[13px]">Notas do paciente</Label>
<Textarea name="patient_notes" rows={3} className="text-[13px] rounded-md" value={formData.patient_notes || ''} onChange={handleChange} />
</div>
</div>
<div className="grid grid-cols-3 gap-3 mt-3">
<div>
<Label className="text-[13px]">Horário de check-in</Label>
<Input name="checked_in_at" type="datetime-local" className="h-11 w-full rounded-md" value={isoToDatetimeLocal(formData.checked_in_at as any)} onChange={handleChange} />
</div>
<div>
<Label className="text-[13px]">Concluído em</Label>
<Input name="completed_at" type="datetime-local" className="h-11 w-full rounded-md" value={isoToDatetimeLocal(formData.completed_at as any)} onChange={handleChange} />
</div>
<div>
<Label className="text-[13px]">Cancelado em</Label>
<Input name="cancelled_at" type="datetime-local" className="h-11 w-full rounded-md" value={isoToDatetimeLocal(formData.cancelled_at as any)} onChange={handleChange} />
</div>
</div>
<div className="mt-3">
<Label className="text-[13px]">Motivo do cancelamento</Label>
<Input name="cancellation_reason" className="h-11 w-full rounded-md" value={formData.cancellation_reason || ''} onChange={handleChange} />
</div>
</div>
</div>
</div>
</div>
</div>
</form>
);
}
</form>
);
}

View File

@ -968,6 +968,36 @@ export type Appointment = {
updated_by?: string | null;
};
// Payload for updating an appointment (PATCH /rest/v1/appointments/{id})
export type AppointmentUpdate = Partial<{
scheduled_at: string;
duration_minutes: number;
status: 'requested' | 'confirmed' | 'checked_in' | 'in_progress' | 'completed' | 'cancelled' | 'no_show' | string;
chief_complaint: string | null;
notes: string | null;
patient_notes: string | null;
insurance_provider: string | null;
checked_in_at: string | null;
completed_at: string | null;
cancelled_at: string | null;
cancellation_reason: string | null;
}>;
/**
* Atualiza um agendamento existente (PATCH /rest/v1/appointments?id=eq.<id>)
*/
export async function atualizarAgendamento(id: string | number, input: AppointmentUpdate): Promise<Appointment> {
if (!id) throw new Error('ID do agendamento é obrigatório');
const url = `${REST}/appointments?id=eq.${encodeURIComponent(String(id))}`;
const res = await fetch(url, {
method: 'PATCH',
headers: withPrefer({ ...baseHeaders(), 'Content-Type': 'application/json' }, 'return=representation'),
body: JSON.stringify(input),
});
const arr = await parse<Appointment[] | Appointment>(res);
return Array.isArray(arr) ? arr[0] : (arr as Appointment);
}
/**
* Lista agendamentos via REST (GET /rest/v1/appointments)
* Aceita query string completa (ex: `?select=*&limit=100&order=scheduled_at.desc`)