forked from RiseUP/riseup-squad20
add-availability-endpoints
This commit is contained in:
parent
bf926cd9b7
commit
a9bbd2f872
@ -10,6 +10,8 @@ import { Label } from "@/components/ui/label";
|
||||
import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye, Users } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-form";
|
||||
import AvailabilityForm from '@/components/forms/availability-form'
|
||||
import { listarDisponibilidades, DoctorAvailability, deletarDisponibilidade } from '@/lib/api'
|
||||
|
||||
|
||||
import { listarMedicos, excluirMedico, buscarMedicos, buscarMedicoPorId, buscarPacientesPorIds, Medico } from "@/lib/api";
|
||||
@ -57,6 +59,28 @@ function normalizeMedico(m: any): Medico {
|
||||
};
|
||||
}
|
||||
|
||||
function translateWeekday(w?: string) {
|
||||
if (!w) return '';
|
||||
const key = w.toString().toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '').replace(/[^a-z0-9]/g, '');
|
||||
const map: Record<string, string> = {
|
||||
'segunda': 'Segunda',
|
||||
'terca': 'Terça',
|
||||
'quarta': 'Quarta',
|
||||
'quinta': 'Quinta',
|
||||
'sexta': 'Sexta',
|
||||
'sabado': 'Sábado',
|
||||
'domingo': 'Domingo',
|
||||
'monday': 'Segunda',
|
||||
'tuesday': 'Terça',
|
||||
'wednesday': 'Quarta',
|
||||
'thursday': 'Quinta',
|
||||
'friday': 'Sexta',
|
||||
'saturday': 'Sábado',
|
||||
'sunday': 'Domingo',
|
||||
};
|
||||
return map[key] ?? w;
|
||||
}
|
||||
|
||||
|
||||
export default function DoutoresPage() {
|
||||
const [doctors, setDoctors] = useState<Medico[]>([]);
|
||||
@ -69,6 +93,11 @@ export default function DoutoresPage() {
|
||||
const [assignedPatients, setAssignedPatients] = useState<any[]>([]);
|
||||
const [assignedLoading, setAssignedLoading] = useState(false);
|
||||
const [assignedDoctor, setAssignedDoctor] = useState<Medico | null>(null);
|
||||
const [availabilityOpenFor, setAvailabilityOpenFor] = useState<Medico | null>(null);
|
||||
const [availabilityViewingFor, setAvailabilityViewingFor] = useState<Medico | null>(null);
|
||||
const [availabilities, setAvailabilities] = useState<DoctorAvailability[]>([]);
|
||||
const [availLoading, setAvailLoading] = useState(false);
|
||||
const [editingAvailability, setEditingAvailability] = useState<DoctorAvailability | null>(null);
|
||||
const [searchResults, setSearchResults] = useState<Medico[]>([]);
|
||||
const [searchMode, setSearchMode] = useState(false);
|
||||
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||
@ -280,6 +309,19 @@ export default function DoutoresPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadAvailabilities(doctorId?: string) {
|
||||
if (!doctorId) return;
|
||||
setAvailLoading(true);
|
||||
try {
|
||||
const list = await listarDisponibilidades({ doctorId, active: true });
|
||||
setAvailabilities(list || []);
|
||||
} catch (e) {
|
||||
console.warn('Erro ao recarregar disponibilidades:', e);
|
||||
} finally {
|
||||
setAvailLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!confirm("Excluir este médico?")) return;
|
||||
@ -433,6 +475,27 @@ export default function DoutoresPage() {
|
||||
Ver pacientes atribuídos
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={() => setAvailabilityOpenFor(doctor)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Criar disponibilidade
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={async () => {
|
||||
setAvailLoading(true);
|
||||
try {
|
||||
const list = await listarDisponibilidades({ doctorId: doctor.id, active: true });
|
||||
setAvailabilities(list || []);
|
||||
setAvailabilityViewingFor(doctor);
|
||||
} catch (e) {
|
||||
console.warn('Erro ao listar disponibilidades:', e);
|
||||
} finally {
|
||||
setAvailLoading(false);
|
||||
}
|
||||
}}>
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Ver disponibilidades
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={() => handleEdit(String(doctor.id))}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
@ -497,6 +560,79 @@ export default function DoutoresPage() {
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{/* Availability modal */}
|
||||
{availabilityOpenFor && (
|
||||
<AvailabilityForm
|
||||
open={!!availabilityOpenFor}
|
||||
onOpenChange={(open) => { if (!open) setAvailabilityOpenFor(null); }}
|
||||
doctorId={availabilityOpenFor?.id}
|
||||
onSaved={(saved) => { console.log('Disponibilidade salva', saved); setAvailabilityOpenFor(null); /* optionally reload list */ reloadAvailabilities(availabilityOpenFor?.id); }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Edit availability modal */}
|
||||
{editingAvailability && (
|
||||
<AvailabilityForm
|
||||
open={!!editingAvailability}
|
||||
onOpenChange={(open) => { if (!open) setEditingAvailability(null); }}
|
||||
doctorId={editingAvailability?.doctor_id ?? availabilityViewingFor?.id}
|
||||
availability={editingAvailability}
|
||||
mode="edit"
|
||||
onSaved={(saved) => { console.log('Disponibilidade atualizada', saved); setEditingAvailability(null); reloadAvailabilities(editingAvailability?.doctor_id ?? availabilityViewingFor?.id); }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Ver disponibilidades dialog */}
|
||||
{availabilityViewingFor && (
|
||||
<Dialog open={!!availabilityViewingFor} onOpenChange={(open) => { if (!open) { setAvailabilityViewingFor(null); setAvailabilities([]); } }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Disponibilidades - {availabilityViewingFor.full_name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Lista de disponibilidades públicas do médico selecionado.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">
|
||||
{availLoading ? (
|
||||
<div>Carregando disponibilidades…</div>
|
||||
) : availabilities && availabilities.length ? (
|
||||
<div className="space-y-2">
|
||||
{availabilities.map((a) => (
|
||||
<div key={String(a.id)} className="p-2 border rounded flex justify-between items-start">
|
||||
<div>
|
||||
<div className="font-medium">{translateWeekday(a.weekday)} • {a.start_time} — {a.end_time}</div>
|
||||
<div className="text-xs text-muted-foreground">Duração: {a.slot_minutes} min • Tipo: {a.appointment_type || '—'} • {a.active ? 'Ativa' : 'Inativa'}</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline" onClick={() => setEditingAvailability(a)}>Editar</Button>
|
||||
<Button size="sm" variant="destructive" onClick={async () => {
|
||||
if (!confirm('Excluir esta disponibilidade?')) return;
|
||||
try {
|
||||
await deletarDisponibilidade(String(a.id));
|
||||
// reload
|
||||
reloadAvailabilities(availabilityViewingFor?.id ?? a.doctor_id);
|
||||
} catch (e) {
|
||||
console.warn('Erro ao deletar disponibilidade:', e);
|
||||
alert((e as any)?.message || 'Erro ao deletar disponibilidade');
|
||||
}
|
||||
}}>Excluir</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div>Nenhuma disponibilidade encontrada.</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={() => { setAvailabilityViewingFor(null); setAvailabilities([]); }}>Fechar</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Mostrando {displayedDoctors.length} {searchMode ? 'resultado(s) da busca' : `de ${doctors.length}`}
|
||||
</div>
|
||||
|
||||
181
susconecta/components/forms/availability-form.tsx
Normal file
181
susconecta/components/forms/availability-form.tsx
Normal file
@ -0,0 +1,181 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { criarDisponibilidade, atualizarDisponibilidade, DoctorAvailabilityCreate, DoctorAvailability, DoctorAvailabilityUpdate } from '@/lib/api'
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
|
||||
export interface AvailabilityFormProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
doctorId?: string | null
|
||||
onSaved?: (saved: any) => void
|
||||
// when editing, pass the existing availability and set mode to 'edit'
|
||||
availability?: DoctorAvailability | null
|
||||
mode?: 'create' | 'edit'
|
||||
}
|
||||
|
||||
export function AvailabilityForm({ open, onOpenChange, doctorId = null, onSaved, availability = null, mode = 'create' }: AvailabilityFormProps) {
|
||||
const [weekday, setWeekday] = useState<string>('segunda')
|
||||
const [startTime, setStartTime] = useState<string>('09:00')
|
||||
const [endTime, setEndTime] = useState<string>('17:00')
|
||||
const [slotMinutes, setSlotMinutes] = useState<number>(30)
|
||||
const [appointmentType, setAppointmentType] = useState<'presencial'|'telemedicina'>('presencial')
|
||||
const [active, setActive] = useState<boolean>(true)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const { toast } = useToast()
|
||||
|
||||
// When editing, populate state from availability prop
|
||||
useEffect(() => {
|
||||
if (mode === 'edit' && availability) {
|
||||
// weekday may be 'monday' or 'segunda' — keep original string
|
||||
setWeekday(String(availability.weekday ?? 'segunda'))
|
||||
// strip seconds for time inputs (HH:MM)
|
||||
const st = String(availability.start_time ?? '09:00:00').replace(/:00$/,'')
|
||||
const et = String(availability.end_time ?? '17:00:00').replace(/:00$/,'')
|
||||
setStartTime(st)
|
||||
setEndTime(et)
|
||||
setSlotMinutes(Number(availability.slot_minutes ?? 30))
|
||||
setAppointmentType((availability.appointment_type ?? 'presencial') as any)
|
||||
setActive(!!availability.active)
|
||||
}
|
||||
}, [mode, availability])
|
||||
|
||||
async function handleSubmit(e?: React.FormEvent) {
|
||||
e?.preventDefault()
|
||||
if (!doctorId) {
|
||||
toast({ title: 'Erro', description: 'ID do médico não informado', variant: 'destructive' })
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
if (mode === 'create') {
|
||||
const payload: DoctorAvailabilityCreate = {
|
||||
doctor_id: String(doctorId),
|
||||
weekday: weekday as any,
|
||||
start_time: `${startTime}:00`,
|
||||
end_time: `${endTime}:00`,
|
||||
slot_minutes: slotMinutes,
|
||||
appointment_type: appointmentType,
|
||||
active,
|
||||
}
|
||||
|
||||
const saved = await criarDisponibilidade(payload)
|
||||
const labelMap: Record<string,string> = {
|
||||
'segunda':'Segunda','terca':'Terça','quarta':'Quarta','quinta':'Quinta','sexta':'Sexta','sabado':'Sábado','domingo':'Domingo',
|
||||
'monday':'Segunda','tuesday':'Terça','wednesday':'Quarta','thursday':'Quinta','friday':'Sexta','saturday':'Sábado','sunday':'Domingo'
|
||||
}
|
||||
const label = labelMap[weekday as string] ?? String(weekday)
|
||||
toast({ title: 'Disponibilidade criada', description: `${label} ${startTime}–${endTime}`, variant: 'default' })
|
||||
onSaved?.(saved)
|
||||
onOpenChange(false)
|
||||
} else {
|
||||
// edit mode: update existing availability
|
||||
if (!availability || !availability.id) {
|
||||
throw new Error('Disponibilidade inválida para edição')
|
||||
}
|
||||
const payload: DoctorAvailabilityUpdate = {
|
||||
weekday: weekday as any,
|
||||
start_time: `${startTime}:00`,
|
||||
end_time: `${endTime}:00`,
|
||||
slot_minutes: slotMinutes,
|
||||
appointment_type: appointmentType,
|
||||
active,
|
||||
}
|
||||
const updated = await atualizarDisponibilidade(String(availability.id), payload)
|
||||
const labelMap: Record<string,string> = {
|
||||
'segunda':'Segunda','terca':'Terça','quarta':'Quarta','quinta':'Quinta','sexta':'Sexta','sabado':'Sábado','domingo':'Domingo',
|
||||
'monday':'Segunda','tuesday':'Terça','wednesday':'Quarta','thursday':'Quinta','friday':'Sexta','saturday':'Sábado','sunday':'Domingo'
|
||||
}
|
||||
const label = labelMap[weekday as string] ?? String(weekday)
|
||||
toast({ title: 'Disponibilidade atualizada', description: `${label} ${startTime}–${endTime}`, variant: 'default' })
|
||||
onSaved?.(updated)
|
||||
onOpenChange(false)
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Erro ao criar disponibilidade:', err)
|
||||
toast({ title: 'Erro', description: err?.message || String(err), variant: 'destructive' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Criar disponibilidade</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Dia da semana</Label>
|
||||
<Select value={weekday} onValueChange={(v) => setWeekday(v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="segunda">Segunda</SelectItem>
|
||||
<SelectItem value="terca">Terça</SelectItem>
|
||||
<SelectItem value="quarta">Quarta</SelectItem>
|
||||
<SelectItem value="quinta">Quinta</SelectItem>
|
||||
<SelectItem value="sexta">Sexta</SelectItem>
|
||||
<SelectItem value="sabado">Sábado</SelectItem>
|
||||
<SelectItem value="domingo">Domingo</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Tipo</Label>
|
||||
<Select value={appointmentType} onValueChange={(v) => setAppointmentType(v as any)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="presencial">Presencial</SelectItem>
|
||||
<SelectItem value="telemedicina">Telemedicina</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label>Início</Label>
|
||||
<Input type="time" value={startTime} onChange={(e) => setStartTime(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Fim</Label>
|
||||
<Input type="time" value={endTime} onChange={(e) => setEndTime(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Minutos por slot</Label>
|
||||
<Input type="number" value={String(slotMinutes)} onChange={(e) => setSlotMinutes(Number(e.target.value || 30))} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="flex items-center gap-2">
|
||||
<input type="checkbox" checked={active} onChange={(e) => setActive(e.target.checked)} />
|
||||
<span>Ativo</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={submitting}>Cancelar</Button>
|
||||
<Button type="submit" disabled={submitting}>{submitting ? 'Salvando...' : 'Criar disponibilidade'}</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default AvailabilityForm
|
||||
@ -1,6 +1,7 @@
|
||||
// lib/api.ts
|
||||
|
||||
import { ENV_CONFIG } from '@/lib/env-config';
|
||||
import { AUTH_STORAGE_KEYS } from '@/types/auth'
|
||||
// Use ENV_CONFIG for SUPABASE URL and anon key in frontend
|
||||
|
||||
export type ApiOk<T = any> = {
|
||||
@ -146,6 +147,284 @@ export type MedicoInput = {
|
||||
updated_by?: string | null;
|
||||
};
|
||||
|
||||
// ===== DISPONIBILIDADE (Doctor Availability) =====
|
||||
export type DoctorAvailabilityCreate = {
|
||||
doctor_id: string;
|
||||
weekday: 'segunda' | 'terca' | 'quarta' | 'quinta' | 'sexta' | 'sabado' | 'domingo';
|
||||
start_time: string; // 'HH:MM:SS'
|
||||
end_time: string; // 'HH:MM:SS'
|
||||
slot_minutes?: number;
|
||||
appointment_type?: 'presencial' | 'telemedicina';
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
export type DoctorAvailability = DoctorAvailabilityCreate & {
|
||||
id: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
created_by?: string;
|
||||
updated_by?: string | null;
|
||||
};
|
||||
|
||||
export type DoctorAvailabilityUpdate = Partial<{
|
||||
weekday: 'segunda' | 'terca' | 'quarta' | 'quinta' | 'sexta' | 'sabado' | 'domingo' | string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
slot_minutes: number;
|
||||
appointment_type: 'presencial' | 'telemedicina' | string;
|
||||
active: boolean;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Cria uma disponibilidade de médico (POST /rest/v1/doctor_availability)
|
||||
*/
|
||||
export async function criarDisponibilidade(input: DoctorAvailabilityCreate): Promise<DoctorAvailability> {
|
||||
// Apply sensible defaults
|
||||
// Map weekday to the server-expected enum value if necessary. Some deployments use
|
||||
// English weekday names (monday..sunday) as the enum values; the UI uses
|
||||
// Portuguese values (segunda..domingo). Normalize and convert here so POST
|
||||
// doesn't fail with Postgres `invalid input value for enum weekday`.
|
||||
const mapWeekdayForServer = (w?: string) => {
|
||||
if (!w) return w;
|
||||
const key = w.toString().toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '').replace(/[^a-z0-9]/g, '');
|
||||
const map: Record<string, string> = {
|
||||
'segunda': 'monday',
|
||||
'terca': 'tuesday',
|
||||
'quarta': 'wednesday',
|
||||
'quinta': 'thursday',
|
||||
'sexta': 'friday',
|
||||
'sabado': 'saturday',
|
||||
'domingo': 'sunday',
|
||||
// allow common english names through
|
||||
'monday': 'monday',
|
||||
'tuesday': 'tuesday',
|
||||
'wednesday': 'wednesday',
|
||||
'thursday': 'thursday',
|
||||
'friday': 'friday',
|
||||
'saturday': 'saturday',
|
||||
'sunday': 'sunday',
|
||||
};
|
||||
return map[key] ?? w;
|
||||
};
|
||||
|
||||
// Determine created_by: try localStorage first, then fall back to calling user-info
|
||||
let createdBy: string | null = null;
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const raw = localStorage.getItem(AUTH_STORAGE_KEYS.USER);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
createdBy = parsed?.id ?? parsed?.user?.id ?? null;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
if (!createdBy) {
|
||||
try {
|
||||
const info = await getUserInfo();
|
||||
createdBy = info?.user?.id ?? null;
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (!createdBy) {
|
||||
throw new Error('Não foi possível determinar o usuário atual (created_by). Faça login novamente antes de criar uma disponibilidade.');
|
||||
}
|
||||
|
||||
const payload: any = {
|
||||
slot_minutes: input.slot_minutes ?? 30,
|
||||
appointment_type: input.appointment_type ?? 'presencial',
|
||||
active: typeof input.active === 'undefined' ? true : input.active,
|
||||
doctor_id: input.doctor_id,
|
||||
weekday: mapWeekdayForServer(input.weekday),
|
||||
start_time: input.start_time,
|
||||
end_time: input.end_time,
|
||||
};
|
||||
|
||||
const url = `${REST}/doctor_availability`;
|
||||
// Try several payload permutations to tolerate different server enum/time formats.
|
||||
const attempts = [] as Array<{ weekdayVal: string | undefined; withSeconds: boolean }>;
|
||||
const mappedWeekday = mapWeekdayForServer(input.weekday);
|
||||
const originalWeekday = input.weekday;
|
||||
attempts.push({ weekdayVal: mappedWeekday, withSeconds: true });
|
||||
attempts.push({ weekdayVal: originalWeekday, withSeconds: true });
|
||||
attempts.push({ weekdayVal: mappedWeekday, withSeconds: false });
|
||||
attempts.push({ weekdayVal: originalWeekday, withSeconds: false });
|
||||
|
||||
let lastRes: Response | null = null;
|
||||
for (const at of attempts) {
|
||||
const start = at.withSeconds ? input.start_time : String(input.start_time).replace(/:00$/,'');
|
||||
const end = at.withSeconds ? input.end_time : String(input.end_time).replace(/:00$/,'');
|
||||
const tryPayload: any = {
|
||||
slot_minutes: input.slot_minutes ?? 30,
|
||||
appointment_type: input.appointment_type ?? 'presencial',
|
||||
active: typeof input.active === 'undefined' ? true : input.active,
|
||||
doctor_id: input.doctor_id,
|
||||
weekday: at.weekdayVal,
|
||||
start_time: start,
|
||||
end_time: end,
|
||||
created_by: createdBy,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: withPrefer({ ...baseHeaders(), 'Content-Type': 'application/json' }, 'return=representation'),
|
||||
body: JSON.stringify(tryPayload),
|
||||
});
|
||||
lastRes = res;
|
||||
if (res.ok) {
|
||||
const arr = await parse<DoctorAvailability[] | DoctorAvailability>(res);
|
||||
return Array.isArray(arr) ? arr[0] : (arr as DoctorAvailability);
|
||||
}
|
||||
|
||||
// If server returned a 4xx, try next permutation; for 5xx, bail out and throw the error from parse()
|
||||
if (res.status >= 500) {
|
||||
// Let parse produce the error with friendly messaging
|
||||
return await parse<DoctorAvailability>(res);
|
||||
}
|
||||
|
||||
// Log a warning and continue to next attempt
|
||||
const raw = await res.clone().text().catch(() => '');
|
||||
console.warn('[criarDisponibilidade] tentativa falhou', { status: res.status, weekday: at.weekdayVal, withSeconds: at.withSeconds, raw });
|
||||
// continue to next attempt
|
||||
} catch (e) {
|
||||
console.warn('[criarDisponibilidade] fetch erro na tentativa', e);
|
||||
// continue to next attempt
|
||||
}
|
||||
}
|
||||
|
||||
// All attempts failed — throw using the last response to get friendly message from parse()
|
||||
if (lastRes) {
|
||||
return await parse<DoctorAvailability>(lastRes);
|
||||
}
|
||||
throw new Error('Falha ao criar disponibilidade: nenhuma resposta do servidor.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista disponibilidades. Se doctorId for passado, filtra por médico.
|
||||
*/
|
||||
export async function listarDisponibilidades(params?: { doctorId?: string; active?: boolean }): Promise<DoctorAvailability[]> {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.doctorId) qs.set('doctor_id', `eq.${encodeURIComponent(String(params.doctorId))}`);
|
||||
if (typeof params?.active !== 'undefined') qs.set('active', `eq.${params.active ? 'true' : 'false'}`);
|
||||
|
||||
const url = `${REST}/doctor_availability${qs.toString() ? `?${qs.toString()}` : ''}`;
|
||||
const res = await fetch(url, { method: 'GET', headers: baseHeaders() });
|
||||
return await parse<DoctorAvailability[]>(res);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Atualiza uma disponibilidade existente (PATCH /rest/v1/doctor_availability?id=eq.<id>)
|
||||
*/
|
||||
export async function atualizarDisponibilidade(id: string, input: DoctorAvailabilityUpdate): Promise<DoctorAvailability> {
|
||||
if (!id) throw new Error('ID da disponibilidade é obrigatório');
|
||||
|
||||
const mapWeekdayForServer = (w?: string) => {
|
||||
if (!w) return w;
|
||||
const key = w.toString().toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '').replace(/[^a-z0-9]/g, '');
|
||||
const map: Record<string, string> = {
|
||||
'segunda': 'monday',
|
||||
'terca': 'tuesday',
|
||||
'quarta': 'wednesday',
|
||||
'quinta': 'thursday',
|
||||
'sexta': 'friday',
|
||||
'sabado': 'saturday',
|
||||
'domingo': 'sunday',
|
||||
'monday': 'monday',
|
||||
'tuesday': 'tuesday',
|
||||
'wednesday': 'wednesday',
|
||||
'thursday': 'thursday',
|
||||
'friday': 'friday',
|
||||
'saturday': 'saturday',
|
||||
'sunday': 'sunday',
|
||||
};
|
||||
return map[key] ?? w;
|
||||
};
|
||||
|
||||
// determine updated_by
|
||||
let updatedBy: string | null = null;
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const raw = localStorage.getItem(AUTH_STORAGE_KEYS.USER);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
updatedBy = parsed?.id ?? parsed?.user?.id ?? null;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!updatedBy) {
|
||||
try {
|
||||
const info = await getUserInfo();
|
||||
updatedBy = info?.user?.id ?? null;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const restUrl = `${REST}/doctor_availability?id=eq.${encodeURIComponent(String(id))}`;
|
||||
|
||||
// Build candidate payloads (weekday mapped/original, with/without seconds)
|
||||
const candidates: Array<DoctorAvailabilityUpdate> = [];
|
||||
const wk = input.weekday ? mapWeekdayForServer(String(input.weekday)) : undefined;
|
||||
// preferred candidate
|
||||
candidates.push({ ...input, weekday: wk });
|
||||
// original weekday if different
|
||||
if (input.weekday && String(input.weekday) !== wk) candidates.push({ ...input, weekday: input.weekday });
|
||||
// times without seconds
|
||||
const stripSeconds = (t?: string) => t ? String(t).replace(/:00$/,'') : t;
|
||||
candidates.push({ ...input, start_time: stripSeconds(input.start_time), end_time: stripSeconds(input.end_time), weekday: wk });
|
||||
|
||||
let lastRes: Response | null = null;
|
||||
for (const cand of candidates) {
|
||||
const payload: any = { ...cand };
|
||||
if (updatedBy) payload.updated_by = updatedBy;
|
||||
|
||||
try {
|
||||
const res = await fetch(restUrl, {
|
||||
method: 'PATCH',
|
||||
headers: withPrefer({ ...baseHeaders(), 'Content-Type': 'application/json' }, 'return=representation'),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
lastRes = res;
|
||||
if (res.ok) {
|
||||
const arr = await parse<DoctorAvailability[] | DoctorAvailability>(res);
|
||||
return Array.isArray(arr) ? arr[0] : (arr as DoctorAvailability);
|
||||
}
|
||||
if (res.status >= 500) return await parse<DoctorAvailability>(res);
|
||||
const raw = await res.clone().text().catch(() => '');
|
||||
console.warn('[atualizarDisponibilidade] tentativa falhou', { status: res.status, payload, raw });
|
||||
} catch (e) {
|
||||
console.warn('[atualizarDisponibilidade] erro fetch', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (lastRes) return await parse<DoctorAvailability>(lastRes);
|
||||
throw new Error('Falha ao atualizar disponibilidade: sem resposta do servidor');
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleta uma disponibilidade por ID (DELETE /rest/v1/doctor_availability?id=eq.<id>)
|
||||
*/
|
||||
export async function deletarDisponibilidade(id: string): Promise<void> {
|
||||
if (!id) throw new Error('ID da disponibilidade é obrigatório');
|
||||
const url = `${REST}/doctor_availability?id=eq.${encodeURIComponent(String(id))}`;
|
||||
// Request minimal return to get a 204 No Content when the delete succeeds.
|
||||
const res = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: withPrefer({ ...baseHeaders() }, 'return=minimal'),
|
||||
});
|
||||
|
||||
if (res.status === 204) return;
|
||||
// Some deployments may return 200 with a representation — accept that too
|
||||
if (res.status === 200) return;
|
||||
// Otherwise surface a friendly error using parse()
|
||||
await parse(res as Response);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ===== CONFIG =====
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user