forked from RiseUP/riseup-squad20
add-edit-appointments-endpoints
This commit is contained in:
parent
23e0765c5b
commit
83018b8854
@ -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();
|
||||
|
||||
// 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,
|
||||
};
|
||||
|
||||
setAppointments((prev) => prev.map((a) => (a.id === updatedAppointment.id ? updatedAppointment : a)));
|
||||
// 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
|
||||
// Keep localForm synchronized with editingAppointment
|
||||
useEffect(() => {
|
||||
if (showForm && editingAppointment) {
|
||||
const [localForm, setLocalForm] = useState<any>(editingAppointment);
|
||||
setLocalForm(editingAppointment);
|
||||
}
|
||||
if (!showForm) setLocalForm(null);
|
||||
}, [showForm, editingAppointment]);
|
||||
|
||||
const onFormChange = (d: any) => setLocalForm(d);
|
||||
|
||||
const saveLocal = () => {
|
||||
handleSave(localForm);
|
||||
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">
|
||||
|
||||
@ -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,119 +58,186 @@ 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;
|
||||
|
||||
// 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 });
|
||||
} else {
|
||||
onFormChange({ ...formData, [name]: value });
|
||||
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>
|
||||
<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"
|
||||
placeholder="Nome do paciente"
|
||||
className="h-11 pl-8 rounded-md transition-colors bg-muted/10"
|
||||
value={formData.patientName || ''}
|
||||
onChange={handleChange}
|
||||
disabled
|
||||
/>
|
||||
</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="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" />
|
||||
</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>
|
||||
@ -172,17 +250,9 @@ export function CalendarRegistrationForm({ formData, onFormChange }: CalendarReg
|
||||
<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} />
|
||||
<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="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">
|
||||
@ -190,7 +260,6 @@ export function CalendarRegistrationForm({ formData, onFormChange }: CalendarReg
|
||||
<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>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Início *</Label>
|
||||
@ -200,43 +269,79 @@ 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} />
|
||||
<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 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>
|
||||
<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>
|
||||
<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="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>
|
||||
|
||||
@ -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`)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user