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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import DoctorLayout from "@/components/doctor-layout"; import DoctorLayout from "@/components/doctor-layout";
import { AvailabilityService } from "@/services/availabilityApi.mjs"; import { AvailabilityService } from "@/services/availabilityApi.mjs";
import { usersService } from "@/services/usersApi.mjs";
import { toast } from "@/hooks/use-toast"; import { toast } from "@/hooks/use-toast";
import { useRouter } from "next/navigation"; 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() { export default function AvailabilityPage() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const router = useRouter(); const router = useRouter();
const [isLoading, setIsLoading] = useState(false); 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>(""); const [modalidadeConsulta, setModalidadeConsulta] = useState<string>("");
useEffect(() => { useEffect(() => {
@ -54,9 +24,6 @@ export default function AvailabilityPage() {
try { try {
const response = await AvailabilityService.list(); const response = await AvailabilityService.list();
console.log(response); console.log(response);
const user = await usersService.getMe();
console.log(user);
setUserData(user);
} catch (e: any) { } catch (e: any) {
alert(`${e?.error} ${e?.message}`); alert(`${e?.error} ${e?.message}`);
} }
@ -73,7 +40,8 @@ export default function AvailabilityPage() {
const formData = new FormData(form); const formData = new FormData(form);
const apiPayload = { const apiPayload = {
doctor_id: userData?.user.id, doctor_id: doctorIdTemp,
created_by: doctorIdTemp,
weekday: (formData.get("weekday") as string) || undefined, weekday: (formData.get("weekday") as string) || undefined,
start_time: (formData.get("horarioEntrada") as string) || undefined, start_time: (formData.get("horarioEntrada") as string) || undefined,
end_time: (formData.get("horarioSaida") 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 { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import {
import { Save, Loader2, Pause } from "lucide-react"; Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Save, Loader2 } from "lucide-react";
import ManagerLayout from "@/components/manager-layout"; import ManagerLayout from "@/components/manager-layout";
import { usersService } from "services/usersApi.mjs"; import { usersService } from "services/usersApi.mjs";
import { login } from "services/api.mjs"; import { login } from "services/api.mjs";
@ -19,7 +25,7 @@ interface UserFormData {
papel: string; papel: string;
senha: string; senha: string;
confirmarSenha: string; confirmarSenha: string;
cpf: string; cpf : string
} }
const defaultFormData: UserFormData = { const defaultFormData: UserFormData = {
@ -29,14 +35,16 @@ const defaultFormData: UserFormData = {
papel: "", papel: "",
senha: "", senha: "",
confirmarSenha: "", confirmarSenha: "",
cpf: "", cpf : ""
}; };
const cleanNumber = (value: string): string => value.replace(/\D/g, ""); const cleanNumber = (value: string): string => value.replace(/\D/g, "");
const formatPhone = (value: string): string => { const formatPhone = (value: string): string => {
const cleaned = cleanNumber(value).substring(0, 11); 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 === 11)
if (cleaned.length === 10) return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, "($1) $2-$3"); 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; return cleaned;
}; };
@ -55,7 +63,13 @@ export default function NovoUsuarioPage() {
e.preventDefault(); e.preventDefault();
setError(null); 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."); setError("Por favor, preencha todos os campos obrigatórios.");
return; return;
} }
@ -68,24 +82,28 @@ export default function NovoUsuarioPage() {
setIsSaving(true); setIsSaving(true);
try { try {
await login();
const payload = { const payload = {
full_name: formData.nomeCompleto, full_name: formData.nomeCompleto,
email: formData.email.trim().toLowerCase(), email: formData.email.trim().toLowerCase(),
phone: formData.telefone || null, phone: formData.telefone || null,
role: formData.papel, role: formData.papel,
password: formData.senha, password: formData.senha,
cpf: formData.cpf, cpf : formData.cpf
}; };
console.log("📤 Enviando payload:"); console.log("📤 Enviando payload:", payload);
console.log(payload);
await usersService.create_user(payload); await usersService.create_user(payload);
router.push("/manager/usuario"); router.push("/manager/usuario");
} catch (e: any) { } catch (e: any) {
console.error("Erro ao criar usuário:", e); 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 { } finally {
setIsSaving(false); setIsSaving(false);
} }
@ -97,15 +115,22 @@ export default function NovoUsuarioPage() {
<div className="w-full max-w-screen-lg space-y-8"> <div className="w-full max-w-screen-lg space-y-8">
<div className="flex items-center justify-between border-b pb-4"> <div className="flex items-center justify-between border-b pb-4">
<div> <div>
<h1 className="text-3xl font-extrabold text-gray-900">Novo Usuário</h1> <h1 className="text-3xl font-extrabold text-gray-900">
<p className="text-md text-gray-500">Preencha os dados para cadastrar um novo usuário no sistema.</p> Novo Usuário
</h1>
<p className="text-md text-gray-500">
Preencha os dados para cadastrar um novo usuário no sistema.
</p>
</div> </div>
<Link href="/manager/usuario"> <Link href="/manager/usuario">
<Button variant="outline">Cancelar</Button> <Button variant="outline">Cancelar</Button>
</Link> </Link>
</div> </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 && ( {error && (
<div className="p-4 bg-red-50 text-red-700 rounded-lg border border-red-300"> <div className="p-4 bg-red-50 text-red-700 rounded-lg border border-red-300">
<p className="font-semibold">Erro no Cadastro:</p> <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="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2 md:col-span-2"> <div className="space-y-2 md:col-span-2">
<Label htmlFor="nomeCompleto">Nome Completo *</Label> <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>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email">E-mail *</Label> <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>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="papel">Papel (Função) *</Label> <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"> <SelectTrigger id="papel">
<SelectValue placeholder="Selecione uma função" /> <SelectValue placeholder="Selecione uma função" />
</SelectTrigger> </SelectTrigger>
@ -135,41 +179,88 @@ export default function NovoUsuarioPage() {
<SelectItem value="gestor">Gestor</SelectItem> <SelectItem value="gestor">Gestor</SelectItem>
<SelectItem value="medico">Médico</SelectItem> <SelectItem value="medico">Médico</SelectItem>
<SelectItem value="secretaria">Secretária</SelectItem> <SelectItem value="secretaria">Secretária</SelectItem>
<SelectItem value="paciente">Usuário</SelectItem> <SelectItem value="user">Usuário</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="senha">Senha *</Label> <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>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="confirmarSenha">Confirmar Senha *</Label> <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 /> <Input
{formData.senha && formData.confirmarSenha && formData.senha !== formData.confirmarSenha && <p className="text-xs text-red-500">As senhas não coincidem.</p>} 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>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="telefone">Telefone</Label> <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> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="cpf">Cpf *</Label> <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>
<div className="flex justify-end gap-4 pt-6 border-t mt-6"> <div className="flex justify-end gap-4 pt-6 border-t mt-6">
<Link href="/manager/usuario"> <Link href="/manager/usuario">
<Button type="button" variant="outline" disabled={isSaving}> <Button type="button" variant="outline" disabled={isSaving}>
Cancelar Cancelar
</Button> </Button>
</Link> </Link>
<Button type="submit" className="bg-green-600 hover:bg-green-700" disabled={isSaving}> <Button
{isSaving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Save className="w-4 h-4 mr-2" />} 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"} {isSaving ? "Salvando..." : "Salvar Usuário"}
</Button> </Button>
</div> </div>

View File

@ -16,45 +16,16 @@ import { toast } from "sonner";
import { appointmentsService } from "@/services/appointmentsApi.mjs"; import { appointmentsService } from "@/services/appointmentsApi.mjs";
import { patientsService } from "@/services/patientsApi.mjs"; import { patientsService } from "@/services/patientsApi.mjs";
import { doctorsService } from "@/services/doctorsApi.mjs"; import { doctorsService } from "@/services/doctorsApi.mjs";
import { usersService } from "@/services/usersApi.mjs";
const APPOINTMENTS_STORAGE_KEY = "clinic-appointments"; const APPOINTMENTS_STORAGE_KEY = "clinic-appointments";
interface UserPermissions { // Simulação do paciente logado
isAdmin: boolean; const LOGGED_PATIENT_ID = "P001";
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 PatientAppointments() { export default function PatientAppointments() {
const [appointments, setAppointments] = useState<any[]>([]); const [appointments, setAppointments] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [selectedAppointment, setSelectedAppointment] = useState<any>(null); const [selectedAppointment, setSelectedAppointment] = useState<any>(null);
const [userData, setUserData] = useState<UserData>();
// Modais // Modais
const [rescheduleModal, setRescheduleModal] = useState(false); const [rescheduleModal, setRescheduleModal] = useState(false);
@ -64,27 +35,40 @@ export default function PatientAppointments() {
const [rescheduleData, setRescheduleData] = useState({ date: "", time: "", reason: "" }); const [rescheduleData, setRescheduleData] = useState({ date: "", time: "", reason: "" });
const [cancelReason, setCancelReason] = useState(""); 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 () => { const fetchData = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const queryParams = "order=scheduled_at.desc"; const [appointmentList, patientList, doctorList] = await Promise.all([
const appointmentList = await appointmentsService.search_appointment(queryParams); appointmentsService.list(),
const patientList = await patientsService.list(); patientsService.list(),
const doctorList = await doctorsService.list(); doctorsService.list(),
]);
const user = await usersService.getMe();
setUserData(user);
const doctorMap = new Map(doctorList.map((d: any) => [d.id, d])); const doctorMap = new Map(doctorList.map((d: any) => [d.id, d]));
const patientMap = new Map(patientList.map((p: any) => [p.id, p])); const patientMap = new Map(patientList.map((p: any) => [p.id, p]));
console.log(appointmentList);
// Filtra apenas as consultas do paciente logado // Filtra apenas as consultas do paciente logado
const patientAppointments = appointmentList const patientAppointments = appointmentList
.filter((apt: any) => apt.patient_id === userData?.user.id) .filter((apt: any) => apt.patient_id === LOGGED_PATIENT_ID)
.map((apt: any) => ({ .map((apt: any) => ({
...apt, ...apt,
doctor: doctorMap.get(apt.doctor_id) || { full_name: "Médico não encontrado", specialty: "N/A" }, 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", 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); setRescheduleModal(false);
toast.success("Consulta reagendada com sucesso!"); toast.success("Consulta reagendada com sucesso!");
@ -167,7 +155,11 @@ export default function PatientAppointments() {
cancel_reason: cancelReason, 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); setCancelModal(false);
toast.success("Consulta cancelada com sucesso!"); toast.success("Consulta cancelada com sucesso!");
@ -234,7 +226,12 @@ export default function PatientAppointments() {
<CalendarDays className="mr-2 h-4 w-4" /> <CalendarDays className="mr-2 h-4 w-4" />
Reagendar Reagendar
</Button> </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" /> <X className="mr-2 h-4 w-4" />
Cancelar Cancelar
</Button> </Button>
@ -255,17 +252,27 @@ export default function PatientAppointments() {
<DialogHeader> <DialogHeader>
<DialogTitle>Reagendar Consulta</DialogTitle> <DialogTitle>Reagendar Consulta</DialogTitle>
<DialogDescription> <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> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="date">Nova Data</Label> <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>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="time">Novo Horário</Label> <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> <SelectTrigger>
<SelectValue placeholder="Selecione um horário" /> <SelectValue placeholder="Selecione um horário" />
</SelectTrigger> </SelectTrigger>
@ -280,7 +287,12 @@ export default function PatientAppointments() {
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="reason">Motivo (opcional)</Label> <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>
</div> </div>
<DialogFooter> <DialogFooter>
@ -298,7 +310,8 @@ export default function PatientAppointments() {
<DialogHeader> <DialogHeader>
<DialogTitle>Cancelar Consulta</DialogTitle> <DialogTitle>Cancelar Consulta</DialogTitle>
<DialogDescription> <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> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
@ -306,7 +319,13 @@ export default function PatientAppointments() {
<Label htmlFor="cancel-reason" className="text-sm font-medium"> <Label htmlFor="cancel-reason" className="text-sm font-medium">
Motivo do Cancelamento <span className="text-red-500">*</span> Motivo do Cancelamento <span className="text-red-500">*</span>
</Label> </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>
</div> </div>
<DialogFooter> <DialogFooter>

View File

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

View File

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

View File

@ -1,52 +1,129 @@
"use client"; 'use client'
import * as React from "react"; import * as React from 'react'
import * as ToastPrimitives from "@radix-ui/react-toast"; import * as ToastPrimitives from '@radix-ui/react-toast'
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from 'class-variance-authority'
import { X } from "lucide-react"; 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} />); const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName; 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: { variants: {
variant: { variant: {
default: "border bg-background text-foreground", default: 'border bg-background text-foreground',
destructive: "destructive group border-destructive bg-destructive text-foreground", destructive:
'destructive group border-destructive bg-destructive text-destructive-foreground',
}, },
}, },
defaultVariants: { 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) => { const Toast = React.forwardRef<
return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />; React.ElementRef<typeof ToastPrimitives.Root>,
}); React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
Toast.displayName = ToastPrimitives.Root.displayName; 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} />); const ToastAction = React.forwardRef<
ToastAction.displayName = ToastPrimitives.Action.displayName; 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) => ( const ToastClose = React.forwardRef<
<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}> 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" /> <X className="h-4 w-4" />
</ToastPrimitives.Close> </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} />); const ToastTitle = React.forwardRef<
ToastTitle.displayName = ToastPrimitives.Title.displayName; 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} />); const ToastDescription = React.forwardRef<
ToastDescription.displayName = ToastPrimitives.Description.displayName; 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. * Função de login que o seu formulário usa.
* Ela continua exatamente como era. * Ela continua exatamente como era.
*/ */
export async function login(email, senha) { export async function login() {
console.log("🔐 Iniciando login..."); console.log("🔐 Iniciando login...");
const res = await fetch(`${BASE_URL}/auth/v1/token?grant_type=password`, { const res = await fetch(`${BASE_URL}/auth/v1/token?grant_type=password`, {
method: "POST", method: "POST",
@ -19,8 +19,8 @@ export async function login(email, senha) {
Prefer: "return=representation", Prefer: "return=representation",
}, },
body: JSON.stringify({ body: JSON.stringify({
email: email, email: "riseup@popcode.com.br",
password: senha, password: "riseup",
}), }),
}); });
@ -51,8 +51,8 @@ async function logout() {
await fetch(`${BASE_URL}/auth/v1/logout`, { await fetch(`${BASE_URL}/auth/v1/logout`, {
method: "POST", method: "POST",
headers: { headers: {
apikey: API_KEY, "apikey": API_KEY,
Authorization: `Bearer ${token}`, "Authorization": `Bearer ${token}`,
}, },
}); });
} catch (error) { } catch (error) {
@ -68,12 +68,12 @@ async function logout() {
* Agora com a correção para respostas vazias. * Agora com a correção para respostas vazias.
*/ */
async function request(endpoint, options = {}) { 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 = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
apikey: API_KEY, "apikey": API_KEY,
...(token && { Authorization: `Bearer ${token}` }), ...(token && { "Authorization": `Bearer ${token}` }),
...options.headers, ...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. // Exportamos o objeto 'api' com os métodos que os componentes vão usar.
export const api = { export const api = {
// --- CORREÇÃO 2: PARA CARREGAR O ID DO USUÁRIO --- // --- 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 }), get: (endpoint, options) => request(endpoint, { method: "GET", ...options }),
post: (endpoint, data, options) => request(endpoint, { method: "POST", body: JSON.stringify(data), ...options }), post: (endpoint, data, options) => request(endpoint, { method: "POST", body: JSON.stringify(data), ...options }),