Merge pull request 'add-exceptions-endpoints' (#49) from feature/add-exceptions-endpoints into develop
Reviewed-on: #49
This commit is contained in:
commit
eaf5b3f983
@ -11,7 +11,8 @@ import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye, Users } fro
|
||||
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 ExceptionForm from '@/components/forms/exception-form'
|
||||
import { listarDisponibilidades, DoctorAvailability, deletarDisponibilidade, listarExcecoes, DoctorException, deletarExcecao } from '@/lib/api'
|
||||
|
||||
|
||||
import { listarMedicos, excluirMedico, buscarMedicos, buscarMedicoPorId, buscarPacientesPorIds, Medico } from "@/lib/api";
|
||||
@ -98,6 +99,10 @@ export default function DoutoresPage() {
|
||||
const [availabilities, setAvailabilities] = useState<DoctorAvailability[]>([]);
|
||||
const [availLoading, setAvailLoading] = useState(false);
|
||||
const [editingAvailability, setEditingAvailability] = useState<DoctorAvailability | null>(null);
|
||||
const [exceptions, setExceptions] = useState<DoctorException[]>([]);
|
||||
const [exceptionsLoading, setExceptionsLoading] = useState(false);
|
||||
const [exceptionViewingFor, setExceptionViewingFor] = useState<Medico | null>(null);
|
||||
const [exceptionOpenFor, setExceptionOpenFor] = useState<Medico | null>(null);
|
||||
const [searchResults, setSearchResults] = useState<Medico[]>([]);
|
||||
const [searchMode, setSearchMode] = useState(false);
|
||||
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||
@ -480,6 +485,11 @@ export default function DoutoresPage() {
|
||||
Criar disponibilidade
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={() => setExceptionOpenFor(doctor)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Criar exceção
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={async () => {
|
||||
setAvailLoading(true);
|
||||
try {
|
||||
@ -496,6 +506,22 @@ export default function DoutoresPage() {
|
||||
Ver disponibilidades
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={async () => {
|
||||
setExceptionsLoading(true);
|
||||
try {
|
||||
const list = await listarExcecoes({ doctorId: doctor.id });
|
||||
setExceptions(list || []);
|
||||
setExceptionViewingFor(doctor);
|
||||
} catch (e) {
|
||||
console.warn('Erro ao listar exceções:', e);
|
||||
} finally {
|
||||
setExceptionsLoading(false);
|
||||
}
|
||||
}}>
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Ver exceções
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={() => handleEdit(String(doctor.id))}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
@ -570,6 +596,15 @@ export default function DoutoresPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{exceptionOpenFor && (
|
||||
<ExceptionForm
|
||||
open={!!exceptionOpenFor}
|
||||
onOpenChange={(open) => { if (!open) setExceptionOpenFor(null); }}
|
||||
doctorId={exceptionOpenFor?.id}
|
||||
onSaved={(saved) => { console.log('Exceção criada', saved); setExceptionOpenFor(null); /* reload availabilities in case a full-day block affects listing */ reloadAvailabilities(exceptionOpenFor?.id); }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Edit availability modal */}
|
||||
{editingAvailability && (
|
||||
<AvailabilityForm
|
||||
@ -633,6 +668,56 @@ export default function DoutoresPage() {
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{/* Ver exceções dialog */}
|
||||
{exceptionViewingFor && (
|
||||
<Dialog open={!!exceptionViewingFor} onOpenChange={(open) => { if (!open) { setExceptionViewingFor(null); setExceptions([]); } }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Exceções - {exceptionViewingFor.full_name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Lista de exceções (bloqueios/liberações) do médico selecionado.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">
|
||||
{exceptionsLoading ? (
|
||||
<div>Carregando exceções…</div>
|
||||
) : exceptions && exceptions.length ? (
|
||||
<div className="space-y-2">
|
||||
{exceptions.map((ex) => (
|
||||
<div key={String(ex.id)} className="p-2 border rounded flex justify-between items-start">
|
||||
<div>
|
||||
<div className="font-medium">{ex.date} {ex.start_time ? `• ${ex.start_time}` : ''} {ex.end_time ? `— ${ex.end_time}` : ''}</div>
|
||||
<div className="text-xs text-muted-foreground">Tipo: {ex.kind} • Motivo: {ex.reason || '—'}</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="destructive" onClick={async () => {
|
||||
if (!confirm('Excluir esta exceção?')) return;
|
||||
try {
|
||||
await deletarExcecao(String(ex.id));
|
||||
const list = await listarExcecoes({ doctorId: exceptionViewingFor?.id });
|
||||
setExceptions(list || []);
|
||||
} catch (e) {
|
||||
console.warn('Erro ao deletar exceção:', e);
|
||||
alert((e as any)?.message || 'Erro ao deletar exceção');
|
||||
}
|
||||
}}>Excluir</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div>Nenhuma exceção encontrada.</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={() => { setExceptionViewingFor(null); setExceptions([]); }}>Fechar</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Mostrando {displayedDoctors.length} {searchMode ? 'resultado(s) da busca' : `de ${doctors.length}`}
|
||||
</div>
|
||||
|
||||
@ -2,11 +2,12 @@
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogFooter, AlertDialogAction, AlertDialogCancel } from '@/components/ui/alert-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 { criarDisponibilidade, atualizarDisponibilidade, listarExcecoes, DoctorAvailabilityCreate, DoctorAvailability, DoctorAvailabilityUpdate, DoctorException } from '@/lib/api'
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
|
||||
export interface AvailabilityFormProps {
|
||||
@ -28,6 +29,7 @@ export function AvailabilityForm({ open, onOpenChange, doctorId = null, onSaved,
|
||||
const [active, setActive] = useState<boolean>(true)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const { toast } = useToast()
|
||||
const [blockedException, setBlockedException] = useState<null | { date: string; reason?: string; times?: string }>(null)
|
||||
|
||||
// When editing, populate state from availability prop
|
||||
useEffect(() => {
|
||||
@ -54,6 +56,73 @@ export function AvailabilityForm({ open, onOpenChange, doctorId = null, onSaved,
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
// Pre-check exceptions for this doctor to avoid creating an availability
|
||||
// that is blocked by an existing exception. If a blocking exception is
|
||||
// found we show a specific toast and abort the creation request.
|
||||
try {
|
||||
const exceptions: DoctorException[] = await listarExcecoes({ doctorId: String(doctorId) });
|
||||
const today = new Date();
|
||||
const oneYearAhead = new Date();
|
||||
oneYearAhead.setFullYear(oneYearAhead.getFullYear() + 1);
|
||||
|
||||
const parseTimeToMinutes = (t?: string | null) => {
|
||||
if (!t) return null;
|
||||
const parts = String(t).split(':').map((p) => Number(p));
|
||||
if (parts.length >= 2 && !Number.isNaN(parts[0]) && !Number.isNaN(parts[1])) {
|
||||
return parts[0] * 60 + parts[1];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const reqStart = parseTimeToMinutes(`${startTime}:00`);
|
||||
const reqEnd = parseTimeToMinutes(`${endTime}:00`);
|
||||
|
||||
const normalizeWeekday = (w?: string) => {
|
||||
if (!w) return w;
|
||||
const k = String(w).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[k] ?? k;
|
||||
};
|
||||
|
||||
const reqWeekday = normalizeWeekday(weekday);
|
||||
|
||||
for (const ex of exceptions || []) {
|
||||
if (!ex || !ex.date) continue;
|
||||
const exDate = new Date(ex.date + 'T00:00:00');
|
||||
if (isNaN(exDate.getTime())) continue;
|
||||
if (exDate < today || exDate > oneYearAhead) continue;
|
||||
if (ex.kind !== 'bloqueio') continue;
|
||||
|
||||
const exWeekday = normalizeWeekday(exDate.toLocaleDateString('en-US', { weekday: 'long' }));
|
||||
if (exWeekday !== reqWeekday) continue;
|
||||
|
||||
// whole-day block
|
||||
if (!ex.start_time && !ex.end_time) {
|
||||
setBlockedException({ date: ex.date, reason: ex.reason ?? undefined, times: undefined })
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const exStart = parseTimeToMinutes(ex.start_time ?? undefined);
|
||||
const exEnd = parseTimeToMinutes(ex.end_time ?? undefined);
|
||||
if (reqStart != null && reqEnd != null && exStart != null && exEnd != null) {
|
||||
if (reqStart < exEnd && exStart < reqEnd) {
|
||||
setBlockedException({ date: ex.date, reason: ex.reason ?? undefined, times: `${ex.start_time}–${ex.end_time}` })
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// If checking exceptions fails, continue and let the API handle it. We
|
||||
// intentionally do not block the flow here because failure to fetch
|
||||
// exceptions shouldn't completely prevent admins from creating slots.
|
||||
console.warn('Falha ao verificar exceções antes da criação:', e);
|
||||
}
|
||||
|
||||
if (mode === 'create') {
|
||||
const payload: DoctorAvailabilityCreate = {
|
||||
doctor_id: String(doctorId),
|
||||
@ -105,7 +174,10 @@ export function AvailabilityForm({ open, onOpenChange, doctorId = null, onSaved,
|
||||
}
|
||||
}
|
||||
|
||||
const be = blockedException
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@ -175,6 +247,27 @@ export function AvailabilityForm({ open, onOpenChange, doctorId = null, onSaved,
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog open={!!be} onOpenChange={(open) => { if (!open) setBlockedException(null) }}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Data bloqueada</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<div className="px-6 pb-6 pt-2">
|
||||
{be ? (
|
||||
<div className="space-y-2">
|
||||
<p>Não é possível criar disponibilidade para o dia <strong>{be!.date}</strong>.</p>
|
||||
{be!.times ? <p>Horário bloqueado: <strong>{be!.times}</strong></p> : null}
|
||||
{be!.reason ? <p>Motivo: <strong>{be!.reason}</strong></p> : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction onClick={() => setBlockedException(null)}>OK</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
112
susconecta/components/forms/exception-form.tsx
Normal file
112
susconecta/components/forms/exception-form.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
"use client"
|
||||
|
||||
import { useState } 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 { criarExcecao, DoctorExceptionCreate } from '@/lib/api'
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
|
||||
export interface ExceptionFormProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
doctorId?: string | null
|
||||
onSaved?: (saved: any) => void
|
||||
}
|
||||
|
||||
export default function ExceptionForm({ open, onOpenChange, doctorId = null, onSaved }: ExceptionFormProps) {
|
||||
const [date, setDate] = useState<string>('')
|
||||
const [startTime, setStartTime] = useState<string>('')
|
||||
const [endTime, setEndTime] = useState<string>('')
|
||||
const [kind, setKind] = useState<'bloqueio'|'liberacao'>('bloqueio')
|
||||
const [reason, setReason] = useState<string>('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const { toast } = useToast()
|
||||
|
||||
async function handleSubmit(e?: React.FormEvent) {
|
||||
e?.preventDefault()
|
||||
if (!doctorId) {
|
||||
toast({ title: 'Erro', description: 'ID do médico não informado', variant: 'destructive' })
|
||||
return
|
||||
}
|
||||
if (!date) {
|
||||
toast({ title: 'Erro', description: 'Data obrigatória', variant: 'destructive' })
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const payload: DoctorExceptionCreate = {
|
||||
doctor_id: String(doctorId),
|
||||
date: String(date),
|
||||
start_time: startTime ? `${startTime}:00` : undefined,
|
||||
end_time: endTime ? `${endTime}:00` : undefined,
|
||||
kind,
|
||||
reason: reason || undefined,
|
||||
}
|
||||
|
||||
const saved = await criarExcecao(payload)
|
||||
toast({ title: 'Exceção criada', description: `${payload.date} • ${kind}`, variant: 'default' })
|
||||
onSaved?.(saved)
|
||||
onOpenChange(false)
|
||||
} catch (err: any) {
|
||||
console.error('Erro ao criar exceção:', err)
|
||||
toast({ title: 'Erro', description: err?.message || String(err), variant: 'destructive' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Criar exceção</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4 py-4">
|
||||
<div>
|
||||
<Label>Data</Label>
|
||||
<Input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Início (opcional)</Label>
|
||||
<Input type="time" value={startTime} onChange={(e) => setStartTime(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Fim (opcional)</Label>
|
||||
<Input type="time" value={endTime} onChange={(e) => setEndTime(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Tipo</Label>
|
||||
<Select value={kind} onValueChange={(v) => setKind(v as any)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="bloqueio">Bloqueio</SelectItem>
|
||||
<SelectItem value="liberacao">Liberação</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Motivo (opcional)</Label>
|
||||
<Input value={reason} onChange={(e) => setReason(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={submitting}>Cancelar</Button>
|
||||
<Button type="submit" disabled={submitting}>{submitting ? 'Salvando...' : 'Criar exceção'}</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@ -234,6 +234,83 @@ export async function criarDisponibilidade(input: DoctorAvailabilityCreate): Pro
|
||||
throw new Error('Não foi possível determinar o usuário atual (created_by). Faça login novamente antes de criar uma disponibilidade.');
|
||||
}
|
||||
|
||||
// --- Prevent creating an availability if a blocking exception exists ---
|
||||
// We fetch exceptions for this doctor and check upcoming exceptions (from today)
|
||||
// that either block the whole day (start_time/end_time null) or overlap the
|
||||
// requested time window on any matching future date within the next year.
|
||||
try {
|
||||
const exceptions = await listarExcecoes({ doctorId: input.doctor_id });
|
||||
const today = new Date();
|
||||
const oneYearAhead = new Date();
|
||||
oneYearAhead.setFullYear(oneYearAhead.getFullYear() + 1);
|
||||
|
||||
const parseTimeToMinutes = (t?: string | null) => {
|
||||
if (!t) return null;
|
||||
const parts = String(t).split(':').map((p) => Number(p));
|
||||
if (parts.length >= 2 && !Number.isNaN(parts[0]) && !Number.isNaN(parts[1])) {
|
||||
return parts[0] * 60 + parts[1];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// requested availability interval in minutes (relative to a day)
|
||||
const reqStart = parseTimeToMinutes(input.start_time);
|
||||
const reqEnd = parseTimeToMinutes(input.end_time);
|
||||
|
||||
const weekdayKey = (w?: string) => {
|
||||
if (!w) return w;
|
||||
const k = String(w).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[k] ?? k;
|
||||
};
|
||||
|
||||
for (const ex of exceptions || []) {
|
||||
try {
|
||||
if (!ex || !ex.date) continue;
|
||||
const exDate = new Date(ex.date + 'T00:00:00');
|
||||
if (isNaN(exDate.getTime())) continue;
|
||||
// only consider future exceptions within one year
|
||||
if (exDate < today || exDate > oneYearAhead) continue;
|
||||
|
||||
// if the exception is of kind 'bloqueio' it blocks times
|
||||
if (ex.kind !== 'bloqueio') continue;
|
||||
|
||||
// map exDate weekday to server weekday mapping
|
||||
const exWeekday = weekdayKey(exDate.toLocaleDateString('en-US', { weekday: 'long' }));
|
||||
const reqWeekday = weekdayKey(input.weekday);
|
||||
|
||||
// We only consider exceptions that fall on the same weekday as the requested availability
|
||||
if (exWeekday !== reqWeekday) continue;
|
||||
|
||||
// If exception has no start_time/end_time -> blocks whole day
|
||||
if (!ex.start_time && !ex.end_time) {
|
||||
throw new Error(`Existe uma exceção de bloqueio no dia ${ex.date}. Não é possível criar disponibilidade para este dia.`);
|
||||
}
|
||||
|
||||
// otherwise check time overlap
|
||||
const exStart = parseTimeToMinutes(ex.start_time ?? undefined);
|
||||
const exEnd = parseTimeToMinutes(ex.end_time ?? undefined);
|
||||
|
||||
if (reqStart != null && reqEnd != null && exStart != null && exEnd != null) {
|
||||
// overlap if reqStart < exEnd && exStart < reqEnd
|
||||
if (reqStart < exEnd && exStart < reqEnd) {
|
||||
throw new Error(`A disponibilidade conflita com uma exceção de bloqueio em ${ex.date} (${ex.start_time}–${ex.end_time}).`);
|
||||
}
|
||||
}
|
||||
} catch (inner) {
|
||||
// rethrow to be handled below
|
||||
throw inner;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// If listarExcecoes failed (network etc), surface that error; it's safer
|
||||
// to prevent creation if we cannot verify exceptions? We'll rethrow.
|
||||
if (e instanceof Error) throw e;
|
||||
}
|
||||
|
||||
const payload: any = {
|
||||
slot_minutes: input.slot_minutes ?? 30,
|
||||
appointment_type: input.appointment_type ?? 'presencial',
|
||||
@ -424,6 +501,101 @@ export async function deletarDisponibilidade(id: string): Promise<void> {
|
||||
await parse(res as Response);
|
||||
}
|
||||
|
||||
// ===== EXCEÇÕES (Doctor Exceptions) =====
|
||||
export type DoctorExceptionCreate = {
|
||||
doctor_id: string;
|
||||
date: string; // YYYY-MM-DD
|
||||
start_time?: string | null; // HH:MM:SS (optional)
|
||||
end_time?: string | null; // HH:MM:SS (optional)
|
||||
kind: 'bloqueio' | 'liberacao';
|
||||
reason?: string | null;
|
||||
};
|
||||
|
||||
export type DoctorException = DoctorExceptionCreate & {
|
||||
id: string;
|
||||
created_at?: string;
|
||||
created_by?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cria uma exceção para um médico (POST /rest/v1/doctor_exceptions)
|
||||
*/
|
||||
export async function criarExcecao(input: DoctorExceptionCreate): Promise<DoctorException> {
|
||||
// populate created_by as other functions do
|
||||
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
|
||||
}
|
||||
}
|
||||
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 exceção.');
|
||||
}
|
||||
|
||||
const payload: any = {
|
||||
doctor_id: input.doctor_id,
|
||||
date: input.date,
|
||||
start_time: input.start_time ?? null,
|
||||
end_time: input.end_time ?? null,
|
||||
kind: input.kind,
|
||||
reason: input.reason ?? null,
|
||||
created_by: createdBy,
|
||||
};
|
||||
|
||||
const url = `${REST}/doctor_exceptions`;
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: withPrefer({ ...baseHeaders(), 'Content-Type': 'application/json' }, 'return=representation'),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const arr = await parse<DoctorException[] | DoctorException>(res);
|
||||
return Array.isArray(arr) ? arr[0] : (arr as DoctorException);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista exceções. Se doctorId for passado, filtra por médico; se date for passado, filtra por data.
|
||||
*/
|
||||
export async function listarExcecoes(params?: { doctorId?: string; date?: string }): Promise<DoctorException[]> {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.doctorId) qs.set('doctor_id', `eq.${encodeURIComponent(String(params.doctorId))}`);
|
||||
if (params?.date) qs.set('date', `eq.${encodeURIComponent(String(params.date))}`);
|
||||
const url = `${REST}/doctor_exceptions${qs.toString() ? `?${qs.toString()}` : ''}`;
|
||||
const res = await fetch(url, { method: 'GET', headers: baseHeaders() });
|
||||
return await parse<DoctorException[]>(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleta uma exceção por ID (DELETE /rest/v1/doctor_exceptions?id=eq.<id>)
|
||||
*/
|
||||
export async function deletarExcecao(id: string): Promise<void> {
|
||||
if (!id) throw new Error('ID da exceção é obrigatório');
|
||||
const url = `${REST}/doctor_exceptions?id=eq.${encodeURIComponent(String(id))}`;
|
||||
const res = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: withPrefer({ ...baseHeaders() }, 'return=minimal'),
|
||||
});
|
||||
|
||||
if (res.status === 204) return;
|
||||
if (res.status === 200) return;
|
||||
await parse(res as Response);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user