Compare commits

..

No commits in common. "50fd9141ce96c9e29ac41638a5dbfc0c03bd4ee5" and "2a015a7f63d25b4fc4d510f769ea414c29a61de0" have entirely different histories.

7 changed files with 1224 additions and 1002 deletions

View File

@ -8,45 +8,15 @@ import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import DoctorLayout from "@/components/doctor-layout";
import { AvailabilityService } from "@/services/availabilityApi.mjs";
import { usersService } from "@/services/usersApi.mjs";
import { toast } from "@/hooks/use-toast";
import { useRouter } from "next/navigation";
interface UserPermissions {
isAdmin: boolean;
isManager: boolean;
isDoctor: boolean;
isSecretary: boolean;
isAdminOrManager: boolean;
}
interface UserData {
user: {
id: string;
email: string;
email_confirmed_at: string | null;
created_at: string | null;
last_sign_in_at: string | null;
};
profile: {
id: string;
full_name: string;
email: string;
phone: string;
avatar_url: string | null;
disabled: boolean;
created_at: string | null;
updated_at: string | null;
};
roles: string[];
permissions: UserPermissions;
}
export default function AvailabilityPage() {
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [userData, setUserData] = useState<UserData>();
const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}");
const doctorIdTemp = "3bb9ee4a-cfdd-4d81-b628-383907dfa225";
const [modalidadeConsulta, setModalidadeConsulta] = useState<string>("");
useEffect(() => {
@ -54,9 +24,6 @@ export default function AvailabilityPage() {
try {
const response = await AvailabilityService.list();
console.log(response);
const user = await usersService.getMe();
console.log(user);
setUserData(user);
} catch (e: any) {
alert(`${e?.error} ${e?.message}`);
}
@ -73,7 +40,8 @@ export default function AvailabilityPage() {
const formData = new FormData(form);
const apiPayload = {
doctor_id: userData?.user.id,
doctor_id: doctorIdTemp,
created_by: doctorIdTemp,
weekday: (formData.get("weekday") as string) || undefined,
start_time: (formData.get("horarioEntrada") as string) || undefined,
end_time: (formData.get("horarioSaida") as string) || undefined,

View File

@ -6,8 +6,14 @@ import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Save, Loader2, Pause } from "lucide-react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Save, Loader2 } from "lucide-react";
import ManagerLayout from "@/components/manager-layout";
import { usersService } from "services/usersApi.mjs";
import { login } from "services/api.mjs";
@ -19,7 +25,7 @@ interface UserFormData {
papel: string;
senha: string;
confirmarSenha: string;
cpf: string;
cpf : string
}
const defaultFormData: UserFormData = {
@ -29,14 +35,16 @@ const defaultFormData: UserFormData = {
papel: "",
senha: "",
confirmarSenha: "",
cpf: "",
cpf : ""
};
const cleanNumber = (value: string): string => value.replace(/\D/g, "");
const formatPhone = (value: string): string => {
const cleaned = cleanNumber(value).substring(0, 11);
if (cleaned.length === 11) return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, "($1) $2-$3");
if (cleaned.length === 10) return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, "($1) $2-$3");
if (cleaned.length === 11)
return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, "($1) $2-$3");
if (cleaned.length === 10)
return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, "($1) $2-$3");
return cleaned;
};
@ -55,7 +63,13 @@ export default function NovoUsuarioPage() {
e.preventDefault();
setError(null);
if (!formData.email || !formData.nomeCompleto || !formData.papel || !formData.senha || !formData.confirmarSenha) {
if (
!formData.email ||
!formData.nomeCompleto ||
!formData.papel ||
!formData.senha ||
!formData.confirmarSenha
) {
setError("Por favor, preencha todos os campos obrigatórios.");
return;
}
@ -68,24 +82,28 @@ export default function NovoUsuarioPage() {
setIsSaving(true);
try {
await login();
const payload = {
full_name: formData.nomeCompleto,
email: formData.email.trim().toLowerCase(),
phone: formData.telefone || null,
role: formData.papel,
password: formData.senha,
cpf: formData.cpf,
cpf : formData.cpf
};
console.log("📤 Enviando payload:");
console.log(payload);
console.log("📤 Enviando payload:", payload);
await usersService.create_user(payload);
router.push("/manager/usuario");
} catch (e: any) {
console.error("Erro ao criar usuário:", e);
setError(e?.message || "Não foi possível criar o usuário. Verifique os dados e tente novamente.");
setError(
e?.message ||
"Não foi possível criar o usuário. Verifique os dados e tente novamente."
);
} finally {
setIsSaving(false);
}
@ -97,15 +115,22 @@ export default function NovoUsuarioPage() {
<div className="w-full max-w-screen-lg space-y-8">
<div className="flex items-center justify-between border-b pb-4">
<div>
<h1 className="text-3xl font-extrabold text-gray-900">Novo Usuário</h1>
<p className="text-md text-gray-500">Preencha os dados para cadastrar um novo usuário no sistema.</p>
<h1 className="text-3xl font-extrabold text-gray-900">
Novo Usuário
</h1>
<p className="text-md text-gray-500">
Preencha os dados para cadastrar um novo usuário no sistema.
</p>
</div>
<Link href="/manager/usuario">
<Button variant="outline">Cancelar</Button>
</Link>
</div>
<form onSubmit={handleSubmit} className="space-y-6 bg-white p-6 md:p-10 border rounded-xl shadow-lg">
<form
onSubmit={handleSubmit}
className="space-y-6 bg-white p-6 md:p-10 border rounded-xl shadow-lg"
>
{error && (
<div className="p-4 bg-red-50 text-red-700 rounded-lg border border-red-300">
<p className="font-semibold">Erro no Cadastro:</p>
@ -116,17 +141,36 @@ export default function NovoUsuarioPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2 md:col-span-2">
<Label htmlFor="nomeCompleto">Nome Completo *</Label>
<Input id="nomeCompleto" value={formData.nomeCompleto} onChange={(e) => handleInputChange("nomeCompleto", e.target.value)} placeholder="Nome e Sobrenome" required />
<Input
id="nomeCompleto"
value={formData.nomeCompleto}
onChange={(e) =>
handleInputChange("nomeCompleto", e.target.value)
}
placeholder="Nome e Sobrenome"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">E-mail *</Label>
<Input id="email" type="email" value={formData.email} onChange={(e) => handleInputChange("email", e.target.value)} placeholder="exemplo@dominio.com" required />
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder="exemplo@dominio.com"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="papel">Papel (Função) *</Label>
<Select value={formData.papel} onValueChange={(v) => handleInputChange("papel", v)} required>
<Select
value={formData.papel}
onValueChange={(v) => handleInputChange("papel", v)}
required
>
<SelectTrigger id="papel">
<SelectValue placeholder="Selecione uma função" />
</SelectTrigger>
@ -135,41 +179,88 @@ export default function NovoUsuarioPage() {
<SelectItem value="gestor">Gestor</SelectItem>
<SelectItem value="medico">Médico</SelectItem>
<SelectItem value="secretaria">Secretária</SelectItem>
<SelectItem value="paciente">Usuário</SelectItem>
<SelectItem value="user">Usuário</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="senha">Senha *</Label>
<Input id="senha" type="password" value={formData.senha} onChange={(e) => handleInputChange("senha", e.target.value)} placeholder="Mínimo 8 caracteres" minLength={8} required />
<Input
id="senha"
type="password"
value={formData.senha}
onChange={(e) => handleInputChange("senha", e.target.value)}
placeholder="Mínimo 8 caracteres"
minLength={8}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmarSenha">Confirmar Senha *</Label>
<Input id="confirmarSenha" type="password" value={formData.confirmarSenha} onChange={(e) => handleInputChange("confirmarSenha", e.target.value)} placeholder="Repita a senha" required />
{formData.senha && formData.confirmarSenha && formData.senha !== formData.confirmarSenha && <p className="text-xs text-red-500">As senhas não coincidem.</p>}
<Input
id="confirmarSenha"
type="password"
value={formData.confirmarSenha}
onChange={(e) =>
handleInputChange("confirmarSenha", e.target.value)
}
placeholder="Repita a senha"
required
/>
{formData.senha &&
formData.confirmarSenha &&
formData.senha !== formData.confirmarSenha && (
<p className="text-xs text-red-500">
As senhas não coincidem.
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="telefone">Telefone</Label>
<Input id="telefone" value={formData.telefone} onChange={(e) => handleInputChange("telefone", e.target.value)} placeholder="(00) 00000-0000" maxLength={15} />
<Input
id="telefone"
value={formData.telefone}
onChange={(e) =>
handleInputChange("telefone", e.target.value)
}
placeholder="(00) 00000-0000"
maxLength={15}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="cpf">Cpf *</Label>
<Input id="cpf" type="cpf" value={formData.cpf} onChange={(e) => handleInputChange("cpf", e.target.value)} placeholder="xxx.xxx.xxx-xx" required />
<Input
id="cpf"
type="cpf"
value={formData.cpf}
onChange={(e) => handleInputChange("cpf", e.target.value)}
placeholder="xxx.xxx.xxx-xx"
required
/>
</div>
<div className="flex justify-end gap-4 pt-6 border-t mt-6">
<Link href="/manager/usuario">
<Button type="button" variant="outline" disabled={isSaving}>
Cancelar
</Button>
</Link>
<Button type="submit" className="bg-green-600 hover:bg-green-700" disabled={isSaving}>
{isSaving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Save className="w-4 h-4 mr-2" />}
<Button
type="submit"
className="bg-green-600 hover:bg-green-700"
disabled={isSaving}
>
{isSaving ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
{isSaving ? "Salvando..." : "Salvar Usuário"}
</Button>
</div>

View File

@ -16,45 +16,16 @@ import { toast } from "sonner";
import { appointmentsService } from "@/services/appointmentsApi.mjs";
import { patientsService } from "@/services/patientsApi.mjs";
import { doctorsService } from "@/services/doctorsApi.mjs";
import { usersService } from "@/services/usersApi.mjs";
const APPOINTMENTS_STORAGE_KEY = "clinic-appointments";
interface UserPermissions {
isAdmin: boolean;
isManager: boolean;
isDoctor: boolean;
isSecretary: boolean;
isAdminOrManager: boolean;
}
interface UserData {
user: {
id: string;
email: string;
email_confirmed_at: string | null;
created_at: string | null;
last_sign_in_at: string | null;
};
profile: {
id: string;
full_name: string;
email: string;
phone: string;
avatar_url: string | null;
disabled: boolean;
created_at: string | null;
updated_at: string | null;
};
roles: string[];
permissions: UserPermissions;
}
// Simulação do paciente logado
const LOGGED_PATIENT_ID = "P001";
export default function PatientAppointments() {
const [appointments, setAppointments] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedAppointment, setSelectedAppointment] = useState<any>(null);
const [userData, setUserData] = useState<UserData>();
// Modais
const [rescheduleModal, setRescheduleModal] = useState(false);
@ -64,27 +35,40 @@ export default function PatientAppointments() {
const [rescheduleData, setRescheduleData] = useState({ date: "", time: "", reason: "" });
const [cancelReason, setCancelReason] = useState("");
const timeSlots = ["08:00", "08:30", "09:00", "09:30", "10:00", "10:30", "11:00", "11:30", "14:00", "14:30", "15:00", "15:30", "16:00", "16:30", "17:00", "17:30"];
const timeSlots = [
"08:00",
"08:30",
"09:00",
"09:30",
"10:00",
"10:30",
"11:00",
"11:30",
"14:00",
"14:30",
"15:00",
"15:30",
"16:00",
"16:30",
"17:00",
"17:30",
];
const fetchData = async () => {
setIsLoading(true);
try {
const queryParams = "order=scheduled_at.desc";
const appointmentList = await appointmentsService.search_appointment(queryParams);
const patientList = await patientsService.list();
const doctorList = await doctorsService.list();
const user = await usersService.getMe();
setUserData(user);
const [appointmentList, patientList, doctorList] = await Promise.all([
appointmentsService.list(),
patientsService.list(),
doctorsService.list(),
]);
const doctorMap = new Map(doctorList.map((d: any) => [d.id, d]));
const patientMap = new Map(patientList.map((p: any) => [p.id, p]));
console.log(appointmentList);
// Filtra apenas as consultas do paciente logado
const patientAppointments = appointmentList
.filter((apt: any) => apt.patient_id === userData?.user.id)
.filter((apt: any) => apt.patient_id === LOGGED_PATIENT_ID)
.map((apt: any) => ({
...apt,
doctor: doctorMap.get(apt.doctor_id) || { full_name: "Médico não encontrado", specialty: "N/A" },
@ -146,7 +130,11 @@ export default function PatientAppointments() {
status: "requested",
});
setAppointments((prev) => prev.map((apt) => (apt.id === selectedAppointment.id ? { ...apt, scheduled_at: newScheduledAt, status: "requested" } : apt)));
setAppointments((prev) =>
prev.map((apt) =>
apt.id === selectedAppointment.id ? { ...apt, scheduled_at: newScheduledAt, status: "requested" } : apt
)
);
setRescheduleModal(false);
toast.success("Consulta reagendada com sucesso!");
@ -167,7 +155,11 @@ export default function PatientAppointments() {
cancel_reason: cancelReason,
});
setAppointments((prev) => prev.map((apt) => (apt.id === selectedAppointment.id ? { ...apt, status: "cancelled" } : apt)));
setAppointments((prev) =>
prev.map((apt) =>
apt.id === selectedAppointment.id ? { ...apt, status: "cancelled" } : apt
)
);
setCancelModal(false);
toast.success("Consulta cancelada com sucesso!");
@ -234,7 +226,12 @@ export default function PatientAppointments() {
<CalendarDays className="mr-2 h-4 w-4" />
Reagendar
</Button>
<Button variant="outline" size="sm" className="text-red-600 hover:text-red-700 hover:bg-red-50" onClick={() => handleCancel(appointment)}>
<Button
variant="outline"
size="sm"
className="text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => handleCancel(appointment)}
>
<X className="mr-2 h-4 w-4" />
Cancelar
</Button>
@ -255,17 +252,27 @@ export default function PatientAppointments() {
<DialogHeader>
<DialogTitle>Reagendar Consulta</DialogTitle>
<DialogDescription>
Escolha uma nova data e horário para sua consulta com <strong>{selectedAppointment?.doctor?.full_name}</strong>.
Escolha uma nova data e horário para sua consulta com{" "}
<strong>{selectedAppointment?.doctor?.full_name}</strong>.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="date">Nova Data</Label>
<Input id="date" type="date" value={rescheduleData.date} onChange={(e) => setRescheduleData((prev) => ({ ...prev, date: e.target.value }))} min={new Date().toISOString().split("T")[0]} />
<Input
id="date"
type="date"
value={rescheduleData.date}
onChange={(e) => setRescheduleData((prev) => ({ ...prev, date: e.target.value }))}
min={new Date().toISOString().split("T")[0]}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="time">Novo Horário</Label>
<Select value={rescheduleData.time} onValueChange={(value) => setRescheduleData((prev) => ({ ...prev, time: value }))}>
<Select
value={rescheduleData.time}
onValueChange={(value) => setRescheduleData((prev) => ({ ...prev, time: value }))}
>
<SelectTrigger>
<SelectValue placeholder="Selecione um horário" />
</SelectTrigger>
@ -280,7 +287,12 @@ export default function PatientAppointments() {
</div>
<div className="grid gap-2">
<Label htmlFor="reason">Motivo (opcional)</Label>
<Textarea id="reason" placeholder="Explique brevemente o motivo do reagendamento..." value={rescheduleData.reason} onChange={(e) => setRescheduleData((prev) => ({ ...prev, reason: e.target.value }))} />
<Textarea
id="reason"
placeholder="Explique brevemente o motivo do reagendamento..."
value={rescheduleData.reason}
onChange={(e) => setRescheduleData((prev) => ({ ...prev, reason: e.target.value }))}
/>
</div>
</div>
<DialogFooter>
@ -298,7 +310,8 @@ export default function PatientAppointments() {
<DialogHeader>
<DialogTitle>Cancelar Consulta</DialogTitle>
<DialogDescription>
Deseja realmente cancelar sua consulta com <strong>{selectedAppointment?.doctor?.full_name}</strong>?
Deseja realmente cancelar sua consulta com{" "}
<strong>{selectedAppointment?.doctor?.full_name}</strong>?
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
@ -306,7 +319,13 @@ export default function PatientAppointments() {
<Label htmlFor="cancel-reason" className="text-sm font-medium">
Motivo do Cancelamento <span className="text-red-500">*</span>
</Label>
<Textarea id="cancel-reason" placeholder="Informe o motivo do cancelamento (mínimo 10 caracteres)" value={cancelReason} onChange={(e) => setCancelReason(e.target.value)} className="min-h-[100px]" />
<Textarea
id="cancel-reason"
placeholder="Informe o motivo do cancelamento (mínimo 10 caracteres)"
value={cancelReason}
onChange={(e) => setCancelReason(e.target.value)}
className="min-h-[100px]"
/>
</div>
</div>
<DialogFooter>

View File

@ -1,79 +1,91 @@
"use client";
"use client"
import { useState, useEffect, useCallback } from "react";
import { Calendar, Clock, User } from "lucide-react";
import PatientLayout from "@/components/patient-layout";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { doctorsService } from "services/doctorsApi.mjs";
import { useState, useEffect, useCallback } from "react"
import { Calendar, Clock, User } from "lucide-react"
import PatientLayout from "@/components/patient-layout"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import { doctorsService } from "services/doctorsApi.mjs"
interface Doctor {
id: string;
full_name: string;
specialty: string;
phone_mobile: string;
id: string
full_name: string
specialty: string
phone_mobile: string
}
const APPOINTMENTS_STORAGE_KEY = "clinic-appointments";
const APPOINTMENTS_STORAGE_KEY = "clinic-appointments"
export default function ScheduleAppointment() {
const [selectedDoctor, setSelectedDoctor] = useState("");
const [selectedDate, setSelectedDate] = useState("");
const [selectedTime, setSelectedTime] = useState("");
const [notes, setNotes] = useState("");
const [selectedDoctor, setSelectedDoctor] = useState("")
const [selectedDate, setSelectedDate] = useState("")
const [selectedTime, setSelectedTime] = useState("")
const [notes, setNotes] = useState("")
// novos campos
const [tipoConsulta, setTipoConsulta] = useState("presencial");
const [duracao, setDuracao] = useState("30");
const [convenio, setConvenio] = useState("");
const [queixa, setQueixa] = useState("");
const [obsPaciente, setObsPaciente] = useState("");
const [obsInternas, setObsInternas] = useState("");
const [tipoConsulta, setTipoConsulta] = useState("presencial")
const [duracao, setDuracao] = useState("30")
const [convenio, setConvenio] = useState("")
const [queixa, setQueixa] = useState("")
const [obsPaciente, setObsPaciente] = useState("")
const [obsInternas, setObsInternas] = useState("")
const [doctors, setDoctors] = useState<Doctor[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [doctors, setDoctors] = useState<Doctor[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const fetchDoctors = useCallback(async () => {
setLoading(true);
setError(null);
setLoading(true)
setError(null)
try {
const data: Doctor[] = await doctorsService.list();
console.log(data);
setDoctors(data || []);
const data: Doctor[] = await doctorsService.list()
setDoctors(data || [])
} catch (e: any) {
console.error("Erro ao carregar lista de médicos:", e);
setError("Não foi possível carregar a lista de médicos. Verifique a conexão com a API.");
setDoctors([]);
console.error("Erro ao carregar lista de médicos:", e)
setError("Não foi possível carregar a lista de médicos. Verifique a conexão com a API.")
setDoctors([])
} finally {
setLoading(false);
setLoading(false)
}
}, []);
}, [])
useEffect(() => {
fetchDoctors();
}, [fetchDoctors]);
fetchDoctors()
}, [fetchDoctors])
const availableTimes = ["08:00", "08:30", "09:00", "09:30", "10:00", "10:30", "14:00", "14:30", "15:00", "15:30", "16:00", "16:30"];
const availableTimes = [
"08:00",
"08:30",
"09:00",
"09:30",
"10:00",
"10:30",
"14:00",
"14:30",
"15:00",
"15:30",
"16:00",
"16:30",
]
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
e.preventDefault()
const doctorDetails = doctors.find((d) => d.id === selectedDoctor);
const doctorDetails = doctors.find((d) => d.id === selectedDoctor)
const patientDetails = {
id: "P001",
full_name: "Paciente Exemplo Único",
location: "Clínica Geral",
phone: "(11) 98765-4321",
};
}
if (!patientDetails || !doctorDetails) {
alert("Erro: Selecione o médico ou dados do paciente indisponíveis.");
return;
alert("Erro: Selecione o médico ou dados do paciente indisponíveis.")
return
}
const newAppointment = {
@ -92,27 +104,27 @@ export default function ScheduleAppointment() {
notes,
status: "agendada",
phone: patientDetails.phone,
};
}
const storedAppointmentsRaw = localStorage.getItem(APPOINTMENTS_STORAGE_KEY);
const currentAppointments = storedAppointmentsRaw ? JSON.parse(storedAppointmentsRaw) : [];
const updatedAppointments = [...currentAppointments, newAppointment];
localStorage.setItem(APPOINTMENTS_STORAGE_KEY, JSON.stringify(updatedAppointments));
const storedAppointmentsRaw = localStorage.getItem(APPOINTMENTS_STORAGE_KEY)
const currentAppointments = storedAppointmentsRaw ? JSON.parse(storedAppointmentsRaw) : []
const updatedAppointments = [...currentAppointments, newAppointment]
localStorage.setItem(APPOINTMENTS_STORAGE_KEY, JSON.stringify(updatedAppointments))
alert(`Consulta com ${doctorDetails.full_name} agendada com sucesso!`);
alert(`Consulta com ${doctorDetails.full_name} agendada com sucesso!`)
// resetar campos
setSelectedDoctor("");
setSelectedDate("");
setSelectedTime("");
setNotes("");
setTipoConsulta("presencial");
setDuracao("30");
setConvenio("");
setQueixa("");
setObsPaciente("");
setObsInternas("");
};
setSelectedDoctor("")
setSelectedDate("")
setSelectedTime("")
setNotes("")
setTipoConsulta("presencial")
setDuracao("30")
setConvenio("")
setQueixa("")
setObsPaciente("")
setObsInternas("")
}
return (
<PatientLayout>
@ -131,6 +143,8 @@ export default function ScheduleAppointment() {
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Médico */}
<div className="space-y-2">
<Label htmlFor="doctor">Médico</Label>
@ -162,7 +176,13 @@ export default function ScheduleAppointment() {
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="date">Data</Label>
<Input id="date" type="date" value={selectedDate} onChange={(e) => setSelectedDate(e.target.value)} min={new Date().toISOString().split("T")[0]} />
<Input
id="date"
type="date"
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
min={new Date().toISOString().split("T")[0]}
/>
</div>
<div className="space-y-2">
@ -198,42 +218,79 @@ export default function ScheduleAppointment() {
<div className="space-y-2">
<Label htmlFor="duracao">Duração (minutos)</Label>
<Input id="duracao" type="number" min={10} max={120} value={duracao} onChange={(e) => setDuracao(e.target.value)} />
<Input
id="duracao"
type="number"
min={10}
max={120}
value={duracao}
onChange={(e) => setDuracao(e.target.value)}
/>
</div>
</div>
{/* Convênio */}
<div className="space-y-2">
<Label htmlFor="convenio">Convênio (opcional)</Label>
<Input id="convenio" placeholder="Nome do convênio do paciente" value={convenio} onChange={(e) => setConvenio(e.target.value)} />
<Input
id="convenio"
placeholder="Nome do convênio do paciente"
value={convenio}
onChange={(e) => setConvenio(e.target.value)}
/>
</div>
{/* Queixa Principal */}
<div className="space-y-2">
<Label htmlFor="queixa">Queixa Principal (opcional)</Label>
<Textarea id="queixa" placeholder="Descreva brevemente o motivo da consulta..." value={queixa} onChange={(e) => setQueixa(e.target.value)} />
<Textarea
id="queixa"
placeholder="Descreva brevemente o motivo da consulta..."
value={queixa}
onChange={(e) => setQueixa(e.target.value)}
/>
</div>
{/* Observações do Paciente */}
<div className="space-y-2">
<Label htmlFor="obsPaciente">Observações do Paciente (opcional)</Label>
<Textarea id="obsPaciente" placeholder="Anotações relevantes informadas pelo paciente..." value={obsPaciente} onChange={(e) => setObsPaciente(e.target.value)} />
<Textarea
id="obsPaciente"
placeholder="Anotações relevantes informadas pelo paciente..."
value={obsPaciente}
onChange={(e) => setObsPaciente(e.target.value)}
/>
</div>
{/* Observações Internas */}
<div className="space-y-2">
<Label htmlFor="obsInternas">Observações Internas (opcional)</Label>
<Textarea id="obsInternas" placeholder="Anotações para a equipe da clínica..." value={obsInternas} onChange={(e) => setObsInternas(e.target.value)} />
<Textarea
id="obsInternas"
placeholder="Anotações para a equipe da clínica..."
value={obsInternas}
onChange={(e) => setObsInternas(e.target.value)}
/>
</div>
{/* Observações gerais */}
<div className="space-y-2">
<Label htmlFor="notes">Observações gerais (opcional)</Label>
<Textarea id="notes" placeholder="Descreva brevemente o motivo da consulta ou observações importantes" value={notes} onChange={(e) => setNotes(e.target.value)} rows={3} />
<Textarea
id="notes"
placeholder="Descreva brevemente o motivo da consulta ou observações importantes"
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={3}
/>
</div>
{/* Botão */}
<Button type="submit" className="w-full" disabled={!selectedDoctor || !selectedDate || !selectedTime}>
<Button
type="submit"
className="w-full"
disabled={!selectedDoctor || !selectedDate || !selectedTime}
>
Agendar Consulta
</Button>
</form>
@ -254,14 +311,18 @@ export default function ScheduleAppointment() {
{selectedDoctor && (
<div className="flex items-center space-x-2">
<User className="h-4 w-4 text-gray-500" />
<span className="text-sm">{doctors.find((d) => d.id === selectedDoctor)?.full_name}</span>
<span className="text-sm">
{doctors.find((d) => d.id === selectedDoctor)?.full_name}
</span>
</div>
)}
{selectedDate && (
<div className="flex items-center space-x-2">
<Calendar className="h-4 w-4 text-gray-500" />
<span className="text-sm">{new Date(selectedDate).toLocaleDateString("pt-BR")}</span>
<span className="text-sm">
{new Date(selectedDate).toLocaleDateString("pt-BR")}
</span>
</div>
)}
@ -289,5 +350,5 @@ export default function ScheduleAppointment() {
</div>
</div>
</PatientLayout>
);
)
}

View File

@ -1,9 +1,9 @@
// Caminho: components/LoginForm.tsx
"use client";
"use client"
import type React from "react";
import { useState } from "react";
import { useRouter } from "next/navigation";
import type React from "react"
import { useState } from "react"
import { useRouter } from "next/navigation"
// Nossos serviços de API centralizados e limpos
import { login, api } from "@/services/api.mjs";
@ -16,20 +16,20 @@ import { useToast } from "@/hooks/use-toast";
import { Eye, EyeOff, Mail, Lock, Loader2 } from "lucide-react";
interface LoginFormProps {
children?: React.ReactNode;
children?: React.ReactNode
}
interface FormState {
email: string;
password: string;
email: string
password: string
}
export function LoginForm({ children }: LoginFormProps) {
const [form, setForm] = useState<FormState>({ email: "", password: "" });
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const { toast } = useToast();
const [form, setForm] = useState<FormState>({ email: "", password: "" })
const [showPassword, setShowPassword] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const { toast } = useToast()
// --- NOVOS ESTADOS PARA CONTROLE DE MÚLTIPLOS PERFIS ---
const [userRoles, setUserRoles] = useState<string[]>([]);
@ -48,25 +48,15 @@ export function LoginForm({ children }: LoginFormProps) {
}
const completeUserInfo = { ...user, user_metadata: { ...user.user_metadata, role: selectedDashboardRole } };
localStorage.setItem("user_info", JSON.stringify(completeUserInfo));
localStorage.setItem('user_info', JSON.stringify(completeUserInfo));
let redirectPath = "";
switch (selectedDashboardRole) {
case "manager":
redirectPath = "/manager/home";
break;
case "doctor":
redirectPath = "/doctor/medicos";
break;
case "secretary":
redirectPath = "/secretary/pacientes";
break;
case "patient":
redirectPath = "/patient/dashboard";
break;
case "finance":
redirectPath = "/finance/home";
break;
case "manager": redirectPath = "/manager/home"; break;
case "doctor": redirectPath = "/doctor/medicos"; break;
case "secretary": redirectPath = "/secretary/pacientes"; break;
case "patient": redirectPath = "/patient/dashboard"; break;
case "finance": redirectPath = "/finance/home"; break;
}
if (redirectPath) {
@ -89,7 +79,7 @@ export function LoginForm({ children }: LoginFormProps) {
try {
// A chamada de login continua a mesma
const authData = await login(form.email, form.password);
const authData = await login();
const user = authData.user;
if (!user || !user.id) {
throw new Error("Resposta de autenticação inválida.");
@ -110,28 +100,28 @@ export function LoginForm({ children }: LoginFormProps) {
// --- AQUI COMEÇA A NOVA LÓGICA DE DECISÃO ---
// Caso 1: Usuário é ADMIN, mostra todos os dashboards possíveis.
if (rolesFromApi.includes("admin")) {
setUserRoles(["manager", "doctor", "secretary", "paciente", "finance"]);
if (rolesFromApi.includes('admin')) {
setUserRoles(["manager", "doctor", "secretary", "patient", "finance"]);
setIsLoading(false); // Para o loading para mostrar a tela de seleção
return;
}
// Mapeia os roles da API para os perfis de dashboard que o usuário pode acessar
const displayRoles = new Set<string>();
rolesFromApi.forEach((role) => {
rolesFromApi.forEach(role => {
switch (role) {
case "gestor":
displayRoles.add("manager");
displayRoles.add("finance");
case 'gestor':
displayRoles.add('manager');
displayRoles.add('finance');
break;
case "medico":
displayRoles.add("doctor");
case 'medico':
displayRoles.add('doctor');
break;
case "secretaria":
displayRoles.add("secretary");
case 'secretaria':
displayRoles.add('secretary');
break;
case "paciente": // Mapeamento de 'patient' (ou outro nome que você use para paciente)
displayRoles.add("patient");
case 'patient': // Mapeamento de 'patient' (ou outro nome que você use para paciente)
displayRoles.add('patient');
break;
}
});
@ -145,8 +135,8 @@ export function LoginForm({ children }: LoginFormProps) {
// Caso 3: Se tem múltiplos perfis (ex: 'gestor'), mostra a tela de seleção.
else {
setUserRoles(finalRoles);
setIsLoading(false);
}
} catch (error) {
localStorage.removeItem("token");
localStorage.removeItem("user_info");
@ -156,9 +146,12 @@ export function LoginForm({ children }: LoginFormProps) {
description: error instanceof Error ? error.message : "Ocorreu um erro inesperado.",
variant: "destructive",
});
}
} finally {
// Apenas para o loading se não houver redirecionamento ou seleção de perfil
if (userRoles.length === 0) {
setIsLoading(false);
}
}
};
// --- JSX ATUALIZADO COM RENDERIZAÇÃO CONDICIONAL ---
@ -172,14 +165,22 @@ export function LoginForm({ children }: LoginFormProps) {
<Label htmlFor="email">E-mail</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground w-5 h-5" />
<Input id="email" type="email" placeholder="seu.email@exemplo.com" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} className="pl-10 h-11" required disabled={isLoading} autoComplete="username" />
<Input
id="email" type="email" placeholder="seu.email@exemplo.com"
value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })}
className="pl-10 h-11" required disabled={isLoading} autoComplete="username"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Senha</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground w-5 h-5" />
<Input id="password" type={showPassword ? "text" : "password"} placeholder="Digite sua senha" value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} className="pl-10 pr-12 h-11" required disabled={isLoading} autoComplete="current-password" />
<Input
id="password" type={showPassword ? "text" : "password"} placeholder="Digite sua senha"
value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })}
className="pl-10 pr-12 h-11" required disabled={isLoading} autoComplete="current-password"
/>
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute right-2 top-1/2 -translate-y-1/2 h-8 w-8 p-0 text-muted-foreground hover:text-foreground" disabled={isLoading}>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
@ -196,7 +197,12 @@ export function LoginForm({ children }: LoginFormProps) {
<p className="text-sm text-muted-foreground text-center">Selecione com qual perfil deseja entrar:</p>
<div className="flex flex-col space-y-3 pt-2">
{userRoles.map((role) => (
<Button key={role} variant="outline" className="h-11 text-base" onClick={() => handleRoleSelection(role)}>
<Button
key={role}
variant="outline"
className="h-11 text-base"
onClick={() => handleRoleSelection(role)}
>
Entrar como: {role.charAt(0).toUpperCase() + role.slice(1)}
</Button>
))}
@ -207,5 +213,5 @@ export function LoginForm({ children }: LoginFormProps) {
{children}
</CardContent>
</Card>
);
)
}

View File

@ -1,52 +1,129 @@
"use client";
'use client'
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from 'react'
import * as ToastPrimitives from '@radix-ui/react-toast'
import { cva, type VariantProps } from 'class-variance-authority'
import { X } from 'lucide-react'
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils'
const ToastProvider = ToastPrimitives.Provider;
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<React.ElementRef<typeof ToastPrimitives.Viewport>, React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>>(({ className, ...props }, ref) => <ToastPrimitives.Viewport ref={ref} className={cn("fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", className)} {...props} />);
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className,
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva("group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", {
const toastVariants = cva(
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive: "destructive group border-destructive bg-destructive text-foreground",
default: 'border bg-background text-foreground',
destructive:
'destructive group border-destructive bg-destructive text-destructive-foreground',
},
},
defaultVariants: {
variant: "default",
variant: 'default',
},
});
},
)
const Toast = React.forwardRef<React.ElementRef<typeof ToastPrimitives.Root>, React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>>(({ className, variant, ...props }, ref) => {
return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />;
});
Toast.displayName = ToastPrimitives.Root.displayName;
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<React.ElementRef<typeof ToastPrimitives.Action>, React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>>(({ className, ...props }, ref) => <ToastPrimitives.Action ref={ref} className={cn("inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive", className)} {...props} />);
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className,
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<React.ElementRef<typeof ToastPrimitives.Close>, React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>>(({ className, ...props }, ref) => (
<ToastPrimitives.Close ref={ref} className={cn("absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", className)} toast-close="" {...props}>
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<React.ElementRef<typeof ToastPrimitives.Title>, React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>>(({ className, ...props }, ref) => <ToastPrimitives.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />);
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn('text-sm font-semibold', className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<React.ElementRef<typeof ToastPrimitives.Description>, React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>>(({ className, ...props }, ref) => <ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />);
ToastDescription.displayName = ToastPrimitives.Description.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn('text-sm opacity-90', className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>;
type ToastActionElement = React.ReactElement<typeof ToastAction>
export { type ToastProps, type ToastActionElement, ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction };
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@ -9,7 +9,7 @@ const API_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
* Função de login que o seu formulário usa.
* Ela continua exatamente como era.
*/
export async function login(email, senha) {
export async function login() {
console.log("🔐 Iniciando login...");
const res = await fetch(`${BASE_URL}/auth/v1/token?grant_type=password`, {
method: "POST",
@ -19,8 +19,8 @@ export async function login(email, senha) {
Prefer: "return=representation",
},
body: JSON.stringify({
email: email,
password: senha,
email: "riseup@popcode.com.br",
password: "riseup",
}),
});
@ -51,8 +51,8 @@ async function logout() {
await fetch(`${BASE_URL}/auth/v1/logout`, {
method: "POST",
headers: {
apikey: API_KEY,
Authorization: `Bearer ${token}`,
"apikey": API_KEY,
"Authorization": `Bearer ${token}`,
},
});
} catch (error) {
@ -68,12 +68,12 @@ async function logout() {
* Agora com a correção para respostas vazias.
*/
async function request(endpoint, options = {}) {
const token = typeof window !== "undefined" ? localStorage.getItem("token") : null;
const token = typeof window !== 'undefined' ? localStorage.getItem("token") : null;
const headers = {
"Content-Type": "application/json",
apikey: API_KEY,
...(token && { Authorization: `Bearer ${token}` }),
"apikey": API_KEY,
...(token && { "Authorization": `Bearer ${token}` }),
...options.headers,
};
@ -97,7 +97,7 @@ async function request(endpoint, options = {}) {
// Exportamos o objeto 'api' com os métodos que os componentes vão usar.
export const api = {
// --- CORREÇÃO 2: PARA CARREGAR O ID DO USUÁRIO ---
getSession: () => request("/auth/v1/user"),
getSession: () => request('/auth/v1/user'),
get: (endpoint, options) => request(endpoint, { method: "GET", ...options }),
post: (endpoint, data, options) => request(endpoint, { method: "POST", body: JSON.stringify(data), ...options }),