feature(api-assignments): replicate authorization flow for doctors and patients (search/create user + update roles)
This commit is contained in:
parent
2dd9526e45
commit
01cb0bf7ac
@ -26,7 +26,7 @@ import {
|
||||
|
||||
const ListaEspera = dynamic(
|
||||
() => import("@/components/agendamento/ListaEspera"),
|
||||
{ ssr: false }
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
export default function AgendamentoPage() {
|
||||
@ -48,17 +48,19 @@ export default function AgendamentoPage() {
|
||||
|
||||
useEffect(() => {
|
||||
let events: EventInput[] = [];
|
||||
appointments.forEach((obj) => {
|
||||
appointments.forEach((object) => {
|
||||
const event: EventInput = {
|
||||
title: `${obj.patient}: ${obj.type}`,
|
||||
start: new Date(obj.time),
|
||||
end: new Date(new Date(obj.time).getTime() + obj.duration * 60 * 1000),
|
||||
title: `${object.patient}: ${object.type}`,
|
||||
start: new Date(object.time),
|
||||
end: new Date(
|
||||
new Date(object.time).getTime() + object.duration * 60 * 1000,
|
||||
),
|
||||
color:
|
||||
obj.status === "confirmed"
|
||||
object.status === "confirmed"
|
||||
? "#68d68a"
|
||||
: obj.status === "pending"
|
||||
? "#ffe55f"
|
||||
: "#ff5f5fff",
|
||||
: object.status === "pending"
|
||||
? "#ffe55f"
|
||||
: "#ff5f5fff",
|
||||
};
|
||||
events.push(event);
|
||||
});
|
||||
@ -68,15 +70,15 @@ export default function AgendamentoPage() {
|
||||
// mantive para caso a lógica de salvar consulta passe a funcionar
|
||||
const handleSaveAppointment = (appointment: any) => {
|
||||
if (appointment.id) {
|
||||
setAppointments((prev) =>
|
||||
prev.map((a) => (a.id === appointment.id ? appointment : a))
|
||||
setAppointments((previous) =>
|
||||
previous.map((a) => (a.id === appointment.id ? appointment : a)),
|
||||
);
|
||||
} else {
|
||||
const newAppointment = {
|
||||
...appointment,
|
||||
id: Date.now().toString(),
|
||||
};
|
||||
setAppointments((prev) => [...prev, newAppointment]);
|
||||
setAppointments((previous) => [...previous, newAppointment]);
|
||||
}
|
||||
};
|
||||
|
||||
@ -90,7 +92,9 @@ export default function AgendamentoPage() {
|
||||
<div className="flex w-full flex-col gap-10 p-6">
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">{activeTab === "calendar" ? "Calendário" : "Lista de Espera"}</h1>
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{activeTab === "calendar" ? "Calendário" : "Lista de Espera"}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Navegue através dos atalhos: Calendário (C) ou Fila de espera
|
||||
(F).
|
||||
|
||||
@ -53,10 +53,12 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
import { mockAppointments, mockProfessionals } from "@/lib/mocks/appointment-mocks";
|
||||
import {
|
||||
mockAppointments,
|
||||
mockProfessionals,
|
||||
} from "@/lib/mocks/appointment-mocks";
|
||||
import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form";
|
||||
|
||||
|
||||
const formatDate = (date: string | Date) => {
|
||||
if (!date) return "";
|
||||
return new Date(date).toLocaleDateString("pt-BR", {
|
||||
@ -69,43 +71,56 @@ const formatDate = (date: string | Date) => {
|
||||
};
|
||||
|
||||
const capitalize = (s: string) => {
|
||||
if (typeof s !== 'string' || s.length === 0) return '';
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
if (typeof s !== "string" || s.length === 0) return "";
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
};
|
||||
|
||||
export default function ConsultasPage() {
|
||||
const [appointments, setAppointments] = useState(mockAppointments);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingAppointment, setEditingAppointment] = useState<any | null>(null);
|
||||
const [viewingAppointment, setViewingAppointment] = useState<any | null>(null);
|
||||
const [editingAppointment, setEditingAppointment] = useState<any | null>(
|
||||
null,
|
||||
);
|
||||
const [viewingAppointment, setViewingAppointment] = useState<any | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const mapAppointmentToFormData = (appointment: any) => {
|
||||
const professional = mockProfessionals.find(p => p.id === appointment.professional);
|
||||
const professional = mockProfessionals.find(
|
||||
(p) => p.id === appointment.professional,
|
||||
);
|
||||
const appointmentDate = new Date(appointment.time);
|
||||
|
||||
|
||||
return {
|
||||
id: appointment.id,
|
||||
patientName: appointment.patient,
|
||||
professionalName: professional ? professional.name : '',
|
||||
appointmentDate: appointmentDate.toISOString().split('T')[0],
|
||||
startTime: appointmentDate.toTimeString().split(' ')[0].substring(0, 5),
|
||||
endTime: new Date(appointmentDate.getTime() + appointment.duration * 60000).toTimeString().split(' ')[0].substring(0, 5),
|
||||
status: appointment.status,
|
||||
appointmentType: appointment.type,
|
||||
notes: appointment.notes,
|
||||
cpf: '',
|
||||
rg: '',
|
||||
birthDate: '',
|
||||
phoneCode: '+55',
|
||||
phoneNumber: '',
|
||||
email: '',
|
||||
unit: 'nei',
|
||||
id: appointment.id,
|
||||
patientName: appointment.patient,
|
||||
professionalName: professional ? professional.name : "",
|
||||
appointmentDate: appointmentDate.toISOString().split("T")[0],
|
||||
startTime: appointmentDate.toTimeString().split(" ")[0].substring(0, 5),
|
||||
endTime: new Date(
|
||||
appointmentDate.getTime() + appointment.duration * 60000,
|
||||
)
|
||||
.toTimeString()
|
||||
.split(" ")[0]
|
||||
.substring(0, 5),
|
||||
status: appointment.status,
|
||||
appointmentType: appointment.type,
|
||||
notes: appointment.notes,
|
||||
cpf: "",
|
||||
rg: "",
|
||||
birthDate: "",
|
||||
phoneCode: "+55",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
unit: "nei",
|
||||
};
|
||||
};
|
||||
|
||||
const handleDelete = (appointmentId: string) => {
|
||||
if (window.confirm("Tem certeza que deseja excluir esta consulta?")) {
|
||||
setAppointments((prev) => prev.filter((a) => a.id !== appointmentId));
|
||||
setAppointments((previous) =>
|
||||
previous.filter((a) => a.id !== appointmentId),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -114,7 +129,7 @@ export default function ConsultasPage() {
|
||||
setEditingAppointment(formData);
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
|
||||
const handleView = (appointment: any) => {
|
||||
setViewingAppointment(appointment);
|
||||
};
|
||||
@ -125,40 +140,49 @@ export default function ConsultasPage() {
|
||||
};
|
||||
|
||||
const handleSave = (formData: any) => {
|
||||
|
||||
const updatedAppointment = {
|
||||
id: formData.id,
|
||||
patient: formData.patientName,
|
||||
time: new Date(`${formData.appointmentDate}T${formData.startTime}`).toISOString(),
|
||||
duration: 30,
|
||||
type: formData.appointmentType as any,
|
||||
status: formData.status as any,
|
||||
professional: appointments.find(a => a.id === formData.id)?.professional || '',
|
||||
notes: formData.notes,
|
||||
id: formData.id,
|
||||
patient: formData.patientName,
|
||||
time: new Date(
|
||||
`${formData.appointmentDate}T${formData.startTime}`,
|
||||
).toISOString(),
|
||||
duration: 30,
|
||||
type: formData.appointmentType as any,
|
||||
status: formData.status as any,
|
||||
professional:
|
||||
appointments.find((a) => a.id === formData.id)?.professional || "",
|
||||
notes: formData.notes,
|
||||
};
|
||||
|
||||
setAppointments(prev =>
|
||||
prev.map(a => a.id === updatedAppointment.id ? updatedAppointment : a)
|
||||
setAppointments((previous) =>
|
||||
previous.map((a) =>
|
||||
a.id === updatedAppointment.id ? updatedAppointment : a,
|
||||
),
|
||||
);
|
||||
handleCancel();
|
||||
handleCancel();
|
||||
};
|
||||
|
||||
if (showForm && editingAppointment) {
|
||||
return (
|
||||
<div className="space-y-6 p-6 bg-background">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button type="button" variant="ghost" size="icon" onClick={handleCancel}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-semibold md:text-2xl">Editar Consulta</h1>
|
||||
</div>
|
||||
<CalendarRegistrationForm
|
||||
initialData={editingAppointment}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
<div className="space-y-6 p-6 bg-background">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-semibold md:text-2xl">Editar Consulta</h1>
|
||||
</div>
|
||||
)
|
||||
<CalendarRegistrationForm
|
||||
initialData={editingAppointment}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -166,7 +190,9 @@ export default function ConsultasPage() {
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Gerenciamento de Consultas</h1>
|
||||
<p className="text-muted-foreground">Visualize, filtre e gerencie todas as consultas da clínica.</p>
|
||||
<p className="text-muted-foreground">
|
||||
Visualize, filtre e gerencie todas as consultas da clínica.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/agenda">
|
||||
@ -223,7 +249,7 @@ export default function ConsultasPage() {
|
||||
<TableBody>
|
||||
{appointments.map((appointment) => {
|
||||
const professional = mockProfessionals.find(
|
||||
(p) => p.id === appointment.professional
|
||||
(p) => p.id === appointment.professional,
|
||||
);
|
||||
return (
|
||||
<TableRow key={appointment.id}>
|
||||
@ -239,11 +265,13 @@ export default function ConsultasPage() {
|
||||
appointment.status === "confirmed"
|
||||
? "default"
|
||||
: appointment.status === "pending"
|
||||
? "secondary"
|
||||
: "destructive"
|
||||
? "secondary"
|
||||
: "destructive"
|
||||
}
|
||||
className={
|
||||
appointment.status === "confirmed" ? "bg-green-600" : ""
|
||||
appointment.status === "confirmed"
|
||||
? "bg-green-600"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{capitalize(appointment.status)}
|
||||
@ -265,7 +293,9 @@ export default function ConsultasPage() {
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
Ver
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEdit(appointment)}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleEdit(appointment)}
|
||||
>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenuItem>
|
||||
@ -288,12 +318,16 @@ export default function ConsultasPage() {
|
||||
</Card>
|
||||
|
||||
{viewingAppointment && (
|
||||
<Dialog open={!!viewingAppointment} onOpenChange={() => setViewingAppointment(null)}>
|
||||
<Dialog
|
||||
open={!!viewingAppointment}
|
||||
onOpenChange={() => setViewingAppointment(null)}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Detalhes da Consulta</DialogTitle>
|
||||
<DialogDescription>
|
||||
Informações detalhadas da consulta de {viewingAppointment?.patient}.
|
||||
Informações detalhadas da consulta de{" "}
|
||||
{viewingAppointment?.patient}.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
@ -301,62 +335,68 @@ export default function ConsultasPage() {
|
||||
<Label htmlFor="name" className="text-right">
|
||||
Paciente
|
||||
</Label>
|
||||
<span className="col-span-3">{viewingAppointment?.patient}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">
|
||||
Médico
|
||||
</Label>
|
||||
<span className="col-span-3">
|
||||
{mockProfessionals.find(p => p.id === viewingAppointment?.professional)?.name || "Não encontrado"}
|
||||
{viewingAppointment?.patient}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">
|
||||
Data e Hora
|
||||
</Label>
|
||||
<span className="col-span-3">{viewingAppointment?.time ? formatDate(viewingAppointment.time) : ''}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">
|
||||
Status
|
||||
</Label>
|
||||
<Label className="text-right">Médico</Label>
|
||||
<span className="col-span-3">
|
||||
<Badge
|
||||
variant={
|
||||
viewingAppointment?.status === "confirmed"
|
||||
? "default"
|
||||
: viewingAppointment?.status === "pending"
|
||||
? "secondary"
|
||||
: "destructive"
|
||||
}
|
||||
className={
|
||||
viewingAppointment?.status === "confirmed" ? "bg-green-600" : ""
|
||||
}
|
||||
>
|
||||
{capitalize(viewingAppointment?.status || '')}
|
||||
</Badge>
|
||||
{mockProfessionals.find(
|
||||
(p) => p.id === viewingAppointment?.professional,
|
||||
)?.name || "Não encontrado"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">
|
||||
Tipo
|
||||
</Label>
|
||||
<span className="col-span-3">{capitalize(viewingAppointment?.type || '')}</span>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">Data e Hora</Label>
|
||||
<span className="col-span-3">
|
||||
{viewingAppointment?.time
|
||||
? formatDate(viewingAppointment.time)
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">
|
||||
Observações
|
||||
</Label>
|
||||
<span className="col-span-3">{viewingAppointment?.notes || "Nenhuma"}</span>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">Status</Label>
|
||||
<span className="col-span-3">
|
||||
<Badge
|
||||
variant={
|
||||
viewingAppointment?.status === "confirmed"
|
||||
? "default"
|
||||
: viewingAppointment?.status === "pending"
|
||||
? "secondary"
|
||||
: "destructive"
|
||||
}
|
||||
className={
|
||||
viewingAppointment?.status === "confirmed"
|
||||
? "bg-green-600"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{capitalize(viewingAppointment?.status || "")}
|
||||
</Badge>
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">Tipo</Label>
|
||||
<span className="col-span-3">
|
||||
{capitalize(viewingAppointment?.type || "")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">Observações</Label>
|
||||
<span className="col-span-3">
|
||||
{viewingAppointment?.notes || "Nenhuma"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setViewingAppointment(null)}>Fechar</Button>
|
||||
<Button onClick={() => setViewingAppointment(null)}>
|
||||
Fechar
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,62 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FileDown, BarChart2, Users, DollarSign, TrendingUp, UserCheck, CalendarCheck, ThumbsUp, User, Briefcase } from "lucide-react";
|
||||
import {
|
||||
FileDown,
|
||||
BarChart2,
|
||||
Users,
|
||||
DollarSign,
|
||||
TrendingUp,
|
||||
UserCheck,
|
||||
CalendarCheck,
|
||||
ThumbsUp,
|
||||
User,
|
||||
Briefcase,
|
||||
} from "lucide-react";
|
||||
import jsPDF from "jspdf";
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell } from "recharts";
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
LineChart,
|
||||
Line,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
} from "recharts";
|
||||
|
||||
// Dados fictícios para demonstração
|
||||
const metricas = [
|
||||
{ label: "Atendimentos", value: 1240, icon: <CalendarCheck className="w-6 h-6 text-blue-500" /> },
|
||||
{ label: "Absenteísmo", value: "7,2%", icon: <UserCheck className="w-6 h-6 text-red-500" /> },
|
||||
{ label: "Satisfação", value: "92%", icon: <ThumbsUp className="w-6 h-6 text-green-500" /> },
|
||||
{ label: "Faturamento (Mês)", value: "R$ 45.000", icon: <DollarSign className="w-6 h-6 text-emerald-500" /> },
|
||||
{ label: "No-show", value: "5,1%", icon: <User className="w-6 h-6 text-yellow-500" /> },
|
||||
{
|
||||
label: "Atendimentos",
|
||||
value: 1240,
|
||||
icon: <CalendarCheck className="w-6 h-6 text-blue-500" />,
|
||||
},
|
||||
{
|
||||
label: "Absenteísmo",
|
||||
value: "7,2%",
|
||||
icon: <UserCheck className="w-6 h-6 text-red-500" />,
|
||||
},
|
||||
{
|
||||
label: "Satisfação",
|
||||
value: "92%",
|
||||
icon: <ThumbsUp className="w-6 h-6 text-green-500" />,
|
||||
},
|
||||
{
|
||||
label: "Faturamento (Mês)",
|
||||
value: "R$ 45.000",
|
||||
icon: <DollarSign className="w-6 h-6 text-emerald-500" />,
|
||||
},
|
||||
{
|
||||
label: "No-show",
|
||||
value: "5,1%",
|
||||
icon: <User className="w-6 h-6 text-yellow-500" />,
|
||||
},
|
||||
];
|
||||
|
||||
const consultasPorPeriodo = [
|
||||
@ -74,24 +118,33 @@ const performancePorMedico = [
|
||||
const COLORS = ["#10b981", "#6366f1", "#f59e42", "#ef4444"];
|
||||
|
||||
function exportPDF(title: string, content: string) {
|
||||
const doc = new jsPDF();
|
||||
doc.text(title, 10, 10);
|
||||
doc.text(content, 10, 20);
|
||||
doc.save(`${title.toLowerCase().replace(/ /g, '-')}.pdf`);
|
||||
const document_ = new jsPDF();
|
||||
document_.text(title, 10, 10);
|
||||
document_.text(content, 10, 20);
|
||||
document_.save(`${title.toLowerCase().replace(/ /g, "-")}.pdf`);
|
||||
}
|
||||
|
||||
export default function RelatoriosPage() {
|
||||
return (
|
||||
<div className="p-6 bg-background min-h-screen">
|
||||
<h1 className="text-2xl font-bold mb-6 text-foreground">Dashboard Executivo de Relatórios</h1>
|
||||
<h1 className="text-2xl font-bold mb-6 text-foreground">
|
||||
Dashboard Executivo de Relatórios
|
||||
</h1>
|
||||
|
||||
{/* Métricas principais */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6 mb-8">
|
||||
{metricas.map((m) => (
|
||||
<div key={m.label} className="p-4 bg-card border border-border rounded-lg shadow flex flex-col items-center justify-center">
|
||||
<div
|
||||
key={m.label}
|
||||
className="p-4 bg-card border border-border rounded-lg shadow flex flex-col items-center justify-center"
|
||||
>
|
||||
{m.icon}
|
||||
<span className="text-2xl font-bold mt-2 text-foreground">{m.value}</span>
|
||||
<span className="text-sm text-muted-foreground mt-1 text-center">{m.label}</span>
|
||||
<span className="text-2xl font-bold mt-2 text-foreground">
|
||||
{m.value}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground mt-1 text-center">
|
||||
{m.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -101,8 +154,22 @@ export default function RelatoriosPage() {
|
||||
{/* Consultas realizadas por período */}
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><BarChart2 className="w-5 h-5" /> Consultas por Período</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Consultas por Período", "Resumo das consultas realizadas por período.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
|
||||
<BarChart2 className="w-5 h-5" /> Consultas por Período
|
||||
</h2>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
exportPDF(
|
||||
"Consultas por Período",
|
||||
"Resumo das consultas realizadas por período.",
|
||||
)
|
||||
}
|
||||
>
|
||||
{" "}
|
||||
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
|
||||
</Button>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<BarChart data={consultasPorPeriodo}>
|
||||
@ -118,8 +185,19 @@ export default function RelatoriosPage() {
|
||||
{/* Faturamento mensal/anual */}
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><DollarSign className="w-5 h-5" /> Faturamento Mensal</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Faturamento Mensal", "Resumo do faturamento mensal.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
|
||||
<DollarSign className="w-5 h-5" /> Faturamento Mensal
|
||||
</h2>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
exportPDF("Faturamento Mensal", "Resumo do faturamento mensal.")
|
||||
}
|
||||
>
|
||||
{" "}
|
||||
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
|
||||
</Button>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<LineChart data={faturamentoMensal}>
|
||||
@ -127,7 +205,13 @@ export default function RelatoriosPage() {
|
||||
<XAxis dataKey="mes" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="valor" stroke="#10b981" name="Faturamento" strokeWidth={3} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="valor"
|
||||
stroke="#10b981"
|
||||
name="Faturamento"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
@ -137,8 +221,19 @@ export default function RelatoriosPage() {
|
||||
{/* Taxa de no-show */}
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><UserCheck className="w-5 h-5" /> Taxa de No-show</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Taxa de No-show", "Resumo da taxa de no-show.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
|
||||
<UserCheck className="w-5 h-5" /> Taxa de No-show
|
||||
</h2>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
exportPDF("Taxa de No-show", "Resumo da taxa de no-show.")
|
||||
}
|
||||
>
|
||||
{" "}
|
||||
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
|
||||
</Button>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<LineChart data={taxaNoShow}>
|
||||
@ -146,7 +241,13 @@ export default function RelatoriosPage() {
|
||||
<XAxis dataKey="mes" />
|
||||
<YAxis unit="%" />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="noShow" stroke="#ef4444" name="No-show (%)" strokeWidth={3} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="noShow"
|
||||
stroke="#ef4444"
|
||||
name="No-show (%)"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
@ -154,12 +255,28 @@ export default function RelatoriosPage() {
|
||||
{/* Indicadores de satisfação */}
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><ThumbsUp className="w-5 h-5" /> Satisfação dos Pacientes</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Satisfação dos Pacientes", "Resumo dos indicadores de satisfação.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
|
||||
<ThumbsUp className="w-5 h-5" /> Satisfação dos Pacientes
|
||||
</h2>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
exportPDF(
|
||||
"Satisfação dos Pacientes",
|
||||
"Resumo dos indicadores de satisfação.",
|
||||
)
|
||||
}
|
||||
>
|
||||
{" "}
|
||||
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center h-[220px]">
|
||||
<span className="text-5xl font-bold text-green-500">92%</span>
|
||||
<span className="text-muted-foreground mt-2">Índice de satisfação geral</span>
|
||||
<span className="text-muted-foreground mt-2">
|
||||
Índice de satisfação geral
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -168,8 +285,22 @@ export default function RelatoriosPage() {
|
||||
{/* Pacientes mais atendidos */}
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Users className="w-5 h-5" /> Pacientes Mais Atendidos</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Pacientes Mais Atendidos", "Lista dos pacientes mais atendidos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
|
||||
<Users className="w-5 h-5" /> Pacientes Mais Atendidos
|
||||
</h2>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
exportPDF(
|
||||
"Pacientes Mais Atendidos",
|
||||
"Lista dos pacientes mais atendidos.",
|
||||
)
|
||||
}
|
||||
>
|
||||
{" "}
|
||||
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
|
||||
</Button>
|
||||
</div>
|
||||
<table className="w-full text-sm mt-4">
|
||||
<thead>
|
||||
@ -192,8 +323,22 @@ export default function RelatoriosPage() {
|
||||
{/* Médicos mais produtivos */}
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Briefcase className="w-5 h-5" /> Médicos Mais Produtivos</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Médicos Mais Produtivos", "Lista dos médicos mais produtivos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
|
||||
<Briefcase className="w-5 h-5" /> Médicos Mais Produtivos
|
||||
</h2>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
exportPDF(
|
||||
"Médicos Mais Produtivos",
|
||||
"Lista dos médicos mais produtivos.",
|
||||
)
|
||||
}
|
||||
>
|
||||
{" "}
|
||||
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
|
||||
</Button>
|
||||
</div>
|
||||
<table className="w-full text-sm mt-4">
|
||||
<thead>
|
||||
@ -218,14 +363,39 @@ export default function RelatoriosPage() {
|
||||
{/* Análise de convênios */}
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><DollarSign className="w-5 h-5" /> Análise de Convênios</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Análise de Convênios", "Resumo da análise de convênios.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
|
||||
<DollarSign className="w-5 h-5" /> Análise de Convênios
|
||||
</h2>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
exportPDF(
|
||||
"Análise de Convênios",
|
||||
"Resumo da análise de convênios.",
|
||||
)
|
||||
}
|
||||
>
|
||||
{" "}
|
||||
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
|
||||
</Button>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<PieChart>
|
||||
<Pie data={convenios} dataKey="valor" nameKey="nome" cx="50%" cy="50%" outerRadius={80} label>
|
||||
<Pie
|
||||
data={convenios}
|
||||
dataKey="valor"
|
||||
nameKey="nome"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={80}
|
||||
label
|
||||
>
|
||||
{convenios.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={COLORS[index % COLORS.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
@ -237,8 +407,22 @@ export default function RelatoriosPage() {
|
||||
{/* Performance por médico */}
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><TrendingUp className="w-5 h-5" /> Performance por Médico</h2>
|
||||
<Button size="sm" variant="outline" onClick={() => exportPDF("Performance por Médico", "Resumo da performance por médico.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
|
||||
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" /> Performance por Médico
|
||||
</h2>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
exportPDF(
|
||||
"Performance por Médico",
|
||||
"Resumo da performance por médico.",
|
||||
)
|
||||
}
|
||||
>
|
||||
{" "}
|
||||
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF
|
||||
</Button>
|
||||
</div>
|
||||
<table className="w-full text-sm mt-4">
|
||||
<thead>
|
||||
|
||||
@ -3,21 +3,64 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye } from "lucide-react";
|
||||
import {
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
Search,
|
||||
Edit,
|
||||
Trash2,
|
||||
ArrowLeft,
|
||||
Eye,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-form";
|
||||
|
||||
|
||||
import { listarMedicos, excluirMedico, buscarMedicos, buscarMedicoPorId, Medico } from "@/lib/api";
|
||||
import {
|
||||
listarMedicos,
|
||||
excluirMedico,
|
||||
buscarMedicos,
|
||||
buscarMedicoPorId,
|
||||
Medico,
|
||||
listarAutorizacoesUsuario,
|
||||
atualizarAutorizacoesUsuario,
|
||||
buscarUsuarioPorEmail,
|
||||
criarUsuarioMedico,
|
||||
type AuthorizationRole,
|
||||
} from "@/lib/api";
|
||||
import {
|
||||
UpdateAuthorizationsDialog,
|
||||
type AuthorizationState,
|
||||
} from "@/components/dialogs/update-authorizations-dialog";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
function normalizeMedico(m: any): Medico {
|
||||
return {
|
||||
id: String(m.id ?? m.uuid ?? ""),
|
||||
full_name: m.full_name ?? m.nome ?? "", // 👈 Correção: usar full_name como padrão
|
||||
full_name: m.full_name ?? m.nome ?? "", // 👈 Correção: usar full_name como padrão
|
||||
nome_social: m.nome_social ?? m.social_name ?? null,
|
||||
cpf: m.cpf ?? "",
|
||||
rg: m.rg ?? m.document_number ?? null,
|
||||
@ -56,7 +99,6 @@ function normalizeMedico(m: any): Medico {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default function DoutoresPage() {
|
||||
const [doctors, setDoctors] = useState<Medico[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -66,65 +108,78 @@ export default function DoutoresPage() {
|
||||
const [viewingDoctor, setViewingDoctor] = useState<Medico | null>(null);
|
||||
const [searchResults, setSearchResults] = useState<Medico[]>([]);
|
||||
const [searchMode, setSearchMode] = useState(false);
|
||||
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(
|
||||
null,
|
||||
);
|
||||
const [authDialogOpen, setAuthDialogOpen] = useState(false);
|
||||
const [authTargetDoctor, setAuthTargetDoctor] = useState<Medico | null>(null);
|
||||
const [authInitialRoles, setAuthInitialRoles] =
|
||||
useState<AuthorizationState | null>(null);
|
||||
const [authorizationsLoading, setAuthorizationsLoading] = useState(false);
|
||||
const [authorizationsError, setAuthorizationsError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [authorizationsSubmitDisabled, setAuthorizationsSubmitDisabled] =
|
||||
useState(false);
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const list = await listarMedicos({ limit: 50 });
|
||||
const normalized = (list ?? []).map(normalizeMedico);
|
||||
console.log('🏥 Médicos carregados:', normalized);
|
||||
setDoctors(normalized);
|
||||
|
||||
const list = await listarMedicos({ limit: 50 });
|
||||
const normalized = (list ?? []).map(normalizeMedico);
|
||||
console.log("🏥 Médicos carregados:", normalized);
|
||||
setDoctors(normalized);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Função para detectar se é um UUID válido
|
||||
function isValidUUID(str: string): boolean {
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(str);
|
||||
function isValidUUID(string_: string): boolean {
|
||||
const uuidRegex =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(string_);
|
||||
}
|
||||
|
||||
// Função para buscar médicos no servidor
|
||||
async function handleBuscarServidor(termoBusca?: string) {
|
||||
const termo = (termoBusca || search).trim();
|
||||
|
||||
|
||||
if (!termo) {
|
||||
setSearchMode(false);
|
||||
setSearchResults([]);
|
||||
return;
|
||||
}
|
||||
console.log('🔍 Buscando médico por:', termo);
|
||||
|
||||
console.log("🔍 Buscando médico por:", termo);
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// Se parece com UUID, tenta busca direta por ID
|
||||
if (isValidUUID(termo)) {
|
||||
console.log('📋 Detectado UUID, buscando por ID...');
|
||||
console.log("📋 Detectado UUID, buscando por ID...");
|
||||
try {
|
||||
const medico = await buscarMedicoPorId(termo);
|
||||
const normalizado = normalizeMedico(medico);
|
||||
console.log('✅ Médico encontrado por ID:', normalizado);
|
||||
console.log("✅ Médico encontrado por ID:", normalizado);
|
||||
setSearchResults([normalizado]);
|
||||
setSearchMode(true);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.log('❌ Não encontrado por ID, tentando busca geral...');
|
||||
console.log("❌ Não encontrado por ID, tentando busca geral...");
|
||||
}
|
||||
}
|
||||
|
||||
// Busca geral
|
||||
const resultados = await buscarMedicos(termo);
|
||||
const normalizados = resultados.map(normalizeMedico);
|
||||
console.log('📋 Resultados da busca geral:', normalizados);
|
||||
|
||||
console.log("📋 Resultados da busca geral:", normalizados);
|
||||
|
||||
setSearchResults(normalizados);
|
||||
setSearchMode(true);
|
||||
} catch (error) {
|
||||
console.error('❌ Erro na busca:', error);
|
||||
console.error("❌ Erro na busca:", error);
|
||||
setSearchResults([]);
|
||||
setSearchMode(true);
|
||||
} finally {
|
||||
@ -136,31 +191,31 @@ export default function DoutoresPage() {
|
||||
function handleSearchChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const valor = e.target.value;
|
||||
setSearch(valor);
|
||||
|
||||
|
||||
// Limpa o timeout anterior se existir
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
|
||||
|
||||
// Se limpar a busca, volta ao modo normal
|
||||
if (!valor.trim()) {
|
||||
setSearchMode(false);
|
||||
setSearchResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Busca automática com debounce ajustável
|
||||
// Para IDs (UUID) longos, faz busca no servidor
|
||||
// Para busca parcial, usa apenas filtro local
|
||||
const isLikeUUID = valor.includes('-') && valor.length > 10;
|
||||
const isLikeUUID = valor.includes("-") && valor.length > 10;
|
||||
const shouldSearchServer = isLikeUUID || valor.length >= 3;
|
||||
|
||||
|
||||
if (shouldSearchServer) {
|
||||
const debounceTime = isLikeUUID ? 300 : 500;
|
||||
const newTimeout = setTimeout(() => {
|
||||
handleBuscarServidor(valor);
|
||||
}, debounceTime);
|
||||
|
||||
|
||||
setSearchTimeout(newTimeout);
|
||||
} else {
|
||||
// Para termos curtos, apenas usa filtro local
|
||||
@ -171,7 +226,7 @@ export default function DoutoresPage() {
|
||||
|
||||
// Handler para Enter no campo de busca
|
||||
function handleSearchKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleBuscarServidor();
|
||||
}
|
||||
@ -197,43 +252,65 @@ export default function DoutoresPage() {
|
||||
|
||||
// Lista de médicos a exibir (busca ou filtro local)
|
||||
const displayedDoctors = useMemo(() => {
|
||||
console.log('🔍 Filtro - search:', search, 'searchMode:', searchMode, 'doctors:', doctors.length, 'searchResults:', searchResults.length);
|
||||
|
||||
console.log(
|
||||
"🔍 Filtro - search:",
|
||||
search,
|
||||
"searchMode:",
|
||||
searchMode,
|
||||
"doctors:",
|
||||
doctors.length,
|
||||
"searchResults:",
|
||||
searchResults.length,
|
||||
);
|
||||
|
||||
// Se não tem busca, mostra todos os médicos
|
||||
if (!search.trim()) return doctors;
|
||||
|
||||
|
||||
const q = search.toLowerCase().trim();
|
||||
const qDigits = q.replace(/\D/g, "");
|
||||
|
||||
|
||||
// Se estamos em modo de busca (servidor), filtra os resultados da busca
|
||||
const sourceList = searchMode ? searchResults : doctors;
|
||||
console.log('🔍 Usando sourceList:', searchMode ? 'searchResults' : 'doctors', '- tamanho:', sourceList.length);
|
||||
|
||||
console.log(
|
||||
"🔍 Usando sourceList:",
|
||||
searchMode ? "searchResults" : "doctors",
|
||||
"- tamanho:",
|
||||
sourceList.length,
|
||||
);
|
||||
|
||||
const filtered = sourceList.filter((d) => {
|
||||
// Busca por nome
|
||||
const byName = (d.full_name || "").toLowerCase().includes(q);
|
||||
|
||||
|
||||
// Busca por CRM (remove formatação se necessário)
|
||||
const byCrm = qDigits.length >= 3 && (d.crm || "").replace(/\D/g, "").includes(qDigits);
|
||||
|
||||
const byCrm =
|
||||
qDigits.length >= 3 &&
|
||||
(d.crm || "").replace(/\D/g, "").includes(qDigits);
|
||||
|
||||
// Busca por ID (UUID completo ou parcial)
|
||||
const byId = (d.id || "").toLowerCase().includes(q);
|
||||
|
||||
|
||||
// Busca por email
|
||||
const byEmail = (d.email || "").toLowerCase().includes(q);
|
||||
|
||||
|
||||
// Busca por especialidade
|
||||
const byEspecialidade = (d.especialidade || "").toLowerCase().includes(q);
|
||||
|
||||
|
||||
const match = byName || byCrm || byId || byEmail || byEspecialidade;
|
||||
if (match) {
|
||||
console.log('✅ Match encontrado:', d.full_name, d.id, 'por:', { byName, byCrm, byId, byEmail, byEspecialidade });
|
||||
console.log("✅ Match encontrado:", d.full_name, d.id, "por:", {
|
||||
byName,
|
||||
byCrm,
|
||||
byId,
|
||||
byEmail,
|
||||
byEspecialidade,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
console.log('🔍 Resultados filtrados:', filtered.length);
|
||||
|
||||
console.log("🔍 Resultados filtrados:", filtered.length);
|
||||
return filtered;
|
||||
}, [doctors, search, searchMode, searchResults]);
|
||||
|
||||
@ -242,8 +319,6 @@ export default function DoutoresPage() {
|
||||
setShowForm(true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function handleEdit(id: string) {
|
||||
setEditingId(id);
|
||||
setShowForm(true);
|
||||
@ -253,46 +328,178 @@ export default function DoutoresPage() {
|
||||
setViewingDoctor(doctor);
|
||||
}
|
||||
|
||||
|
||||
async function handleOpenAuthorizations(doctor: Medico) {
|
||||
setAuthTargetDoctor(doctor);
|
||||
setAuthDialogOpen(true);
|
||||
setAuthorizationsLoading(!!doctor.user_id);
|
||||
setAuthorizationsError(null);
|
||||
setAuthInitialRoles(null);
|
||||
setAuthorizationsSubmitDisabled(false);
|
||||
|
||||
if (!doctor.user_id) {
|
||||
setAuthorizationsError(
|
||||
"Este profissional ainda não possui um usuário vinculado. Cadastre ou vincule um usuário para gerenciar autorizações.",
|
||||
);
|
||||
setAuthInitialRoles({ paciente: false, medico: true });
|
||||
setAuthorizationsSubmitDisabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const roles = await listarAutorizacoesUsuario(doctor.user_id);
|
||||
if (!roles.length) {
|
||||
setAuthInitialRoles({ paciente: false, medico: true });
|
||||
} else {
|
||||
setAuthInitialRoles({
|
||||
paciente: roles.includes("paciente"),
|
||||
medico: roles.includes("medico"),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
setAuthorizationsError(
|
||||
error?.message || "Erro ao carregar autorizações.",
|
||||
);
|
||||
} finally {
|
||||
setAuthorizationsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleAuthDialogOpenChange(open: boolean) {
|
||||
if (!open) {
|
||||
setAuthDialogOpen(false);
|
||||
setAuthTargetDoctor(null);
|
||||
setAuthInitialRoles(null);
|
||||
setAuthorizationsError(null);
|
||||
setAuthorizationsLoading(false);
|
||||
setAuthorizationsSubmitDisabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirmAuthorizations(selection: AuthorizationState) {
|
||||
console.log("[Auth] handleConfirmAuthorizations CHAMADA!", selection, "authTargetDoctor=", authTargetDoctor);
|
||||
|
||||
// Verifica se o médico tem email
|
||||
if (!authTargetDoctor?.email) {
|
||||
toast({
|
||||
title: "Email obrigatório",
|
||||
description: "O médico precisa ter um email cadastrado para receber autorizações.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setAuthorizationsLoading(true);
|
||||
setAuthorizationsError(null);
|
||||
|
||||
try {
|
||||
// PASSO 1: Buscar ou criar usuário no sistema de autenticação
|
||||
console.log("[Auth] Buscando user_id para email:", authTargetDoctor.email);
|
||||
|
||||
let userId = await buscarUsuarioPorEmail(authTargetDoctor.email);
|
||||
|
||||
// Se não encontrou, cria um novo usuário
|
||||
if (!userId) {
|
||||
console.log("[Auth] Usuário não existe. Criando novo usuário...");
|
||||
|
||||
const newUserResponse = await criarUsuarioMedico({
|
||||
email: authTargetDoctor.email,
|
||||
full_name: authTargetDoctor.full_name,
|
||||
phone_mobile: authTargetDoctor.telefone || authTargetDoctor.celular || "",
|
||||
});
|
||||
|
||||
userId = newUserResponse.user.id;
|
||||
console.log("[Auth] Novo usuário criado! user_id:", userId);
|
||||
|
||||
// Mostra credenciais ao admin
|
||||
toast({
|
||||
title: "Usuário criado com sucesso!",
|
||||
description: `Email: ${newUserResponse.email}\nSenha: ${newUserResponse.password}`,
|
||||
duration: 10000,
|
||||
});
|
||||
} else {
|
||||
console.log("[Auth] Usuário já existe. user_id:", userId);
|
||||
}
|
||||
|
||||
// PASSO 2: Atualizar autorizações via patient_assignments
|
||||
const selectedRoles: AuthorizationRole[] = [];
|
||||
if (selection.paciente) selectedRoles.push("paciente");
|
||||
if (selection.medico) selectedRoles.push("medico");
|
||||
|
||||
console.log("[Auth] Atualizando roles:", selectedRoles, "para user_id:", userId, "doctor_id:", authTargetDoctor.id);
|
||||
const result = await atualizarAutorizacoesUsuario(
|
||||
userId,
|
||||
authTargetDoctor.id, // doctor_id (usamos como patient_id na tabela)
|
||||
selectedRoles
|
||||
);
|
||||
console.log("[Auth] Resultado:", result);
|
||||
|
||||
toast({
|
||||
title: "Autorizações atualizadas",
|
||||
description: "As permissões deste profissional foram atualizadas com sucesso.",
|
||||
});
|
||||
|
||||
setAuthDialogOpen(false);
|
||||
setAuthTargetDoctor(null);
|
||||
setAuthInitialRoles(null);
|
||||
await load();
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[Auth] Erro:", error);
|
||||
toast({
|
||||
title: "Erro ao atualizar autorizações",
|
||||
description: error?.message || "Não foi possível atualizar as autorizações.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setAuthorizationsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!confirm("Excluir este médico?")) return;
|
||||
await excluirMedico(id);
|
||||
await load();
|
||||
}
|
||||
|
||||
|
||||
function handleSaved(savedDoctor?: Medico) {
|
||||
setShowForm(false);
|
||||
setShowForm(false);
|
||||
|
||||
if (savedDoctor) {
|
||||
const normalized = normalizeMedico(savedDoctor);
|
||||
setDoctors((prev) => {
|
||||
const i = prev.findIndex((d) => String(d.id) === String(normalized.id));
|
||||
if (i < 0) {
|
||||
// Novo médico → adiciona no topo
|
||||
return [normalized, ...prev];
|
||||
} else {
|
||||
// Médico editado → substitui na lista
|
||||
const clone = [...prev];
|
||||
clone[i] = normalized;
|
||||
return clone;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// fallback → recarrega tudo
|
||||
load();
|
||||
if (savedDoctor) {
|
||||
const normalized = normalizeMedico(savedDoctor);
|
||||
setDoctors((previous) => {
|
||||
const index = previous.findIndex(
|
||||
(d) => String(d.id) === String(normalized.id),
|
||||
);
|
||||
if (index < 0) {
|
||||
// Novo médico → adiciona no topo
|
||||
return [normalized, ...previous];
|
||||
} else {
|
||||
// Médico editado → substitui na lista
|
||||
const clone = [...previous];
|
||||
clone[index] = normalized;
|
||||
return clone;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// fallback → recarrega tudo
|
||||
load();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (showForm) {
|
||||
return (
|
||||
<div className="space-y-6 p-6 bg-background">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => setShowForm(false)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowForm(false)}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold">{editingId ? "Editar Médico" : "Novo Médico"}</h1>
|
||||
<h1 className="text-2xl font-bold">
|
||||
{editingId ? "Editar Médico" : "Novo Médico"}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<DoctorRegistrationForm
|
||||
@ -311,7 +518,9 @@ export default function DoutoresPage() {
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Médicos</h1>
|
||||
<p className="text-muted-foreground">Gerencie os médicos da sua clínica</p>
|
||||
<p className="text-muted-foreground">
|
||||
Gerencie os médicos da sua clínica
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
@ -328,15 +537,15 @@ export default function DoutoresPage() {
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleBuscarServidor}
|
||||
onClick={handleClickBuscar}
|
||||
disabled={loading}
|
||||
className="hover:bg-primary hover:text-white"
|
||||
>
|
||||
Buscar
|
||||
</Button>
|
||||
{searchMode && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setSearch("");
|
||||
setSearchMode(false);
|
||||
@ -368,14 +577,19 @@ export default function DoutoresPage() {
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center text-muted-foreground">
|
||||
<TableCell
|
||||
colSpan={5}
|
||||
className="text-center text-muted-foreground"
|
||||
>
|
||||
Carregando…
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : displayedDoctors.length > 0 ? (
|
||||
displayedDoctors.map((doctor) => (
|
||||
<TableRow key={doctor.id}>
|
||||
<TableCell className="font-medium">{doctor.full_name}</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{doctor.full_name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{doctor.especialidade}</Badge>
|
||||
</TableCell>
|
||||
@ -383,7 +597,9 @@ export default function DoutoresPage() {
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span>{doctor.email}</span>
|
||||
<span className="text-sm text-muted-foreground">{doctor.telefone}</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{doctor.telefone}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@ -399,11 +615,22 @@ export default function DoutoresPage() {
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
Ver
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEdit(String(doctor.id))}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleEdit(String(doctor.id))}
|
||||
>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDelete(String(doctor.id))} className="text-destructive">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleOpenAuthorizations(doctor)}
|
||||
>
|
||||
<ShieldCheck className="mr-2 h-4 w-4" />
|
||||
Atualizar autorizações
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(String(doctor.id))}
|
||||
className="text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Excluir
|
||||
</DropdownMenuItem>
|
||||
@ -414,7 +641,10 @@ export default function DoutoresPage() {
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center text-muted-foreground">
|
||||
<TableCell
|
||||
colSpan={5}
|
||||
className="text-center text-muted-foreground"
|
||||
>
|
||||
Nenhum médico encontrado
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@ -424,7 +654,10 @@ export default function DoutoresPage() {
|
||||
</div>
|
||||
|
||||
{viewingDoctor && (
|
||||
<Dialog open={!!viewingDoctor} onOpenChange={() => setViewingDoctor(null)}>
|
||||
<Dialog
|
||||
open={!!viewingDoctor}
|
||||
onOpenChange={() => setViewingDoctor(null)}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Detalhes do Médico</DialogTitle>
|
||||
@ -435,12 +668,16 @@ export default function DoutoresPage() {
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">Nome</Label>
|
||||
<span className="col-span-3 font-medium">{viewingDoctor?.full_name}</span>
|
||||
<span className="col-span-3 font-medium">
|
||||
{viewingDoctor?.full_name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">Especialidade</Label>
|
||||
<span className="col-span-3">
|
||||
<Badge variant="outline">{viewingDoctor?.especialidade}</Badge>
|
||||
<Badge variant="outline">
|
||||
{viewingDoctor?.especialidade}
|
||||
</Badge>
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
@ -464,8 +701,21 @@ export default function DoutoresPage() {
|
||||
)}
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Mostrando {displayedDoctors.length} {searchMode ? 'resultado(s) da busca' : `de ${doctors.length}`}
|
||||
Mostrando {displayedDoctors.length}{" "}
|
||||
{searchMode ? "resultado(s) da busca" : `de ${doctors.length}`}
|
||||
</div>
|
||||
|
||||
<UpdateAuthorizationsDialog
|
||||
open={authDialogOpen}
|
||||
entityType="medico"
|
||||
entityName={authTargetDoctor?.full_name}
|
||||
initialRoles={authInitialRoles ?? undefined}
|
||||
loading={authorizationsLoading}
|
||||
error={authorizationsError}
|
||||
disableSubmit={authorizationsSubmitDisabled}
|
||||
onOpenChange={handleAuthDialogOpenChange}
|
||||
onConfirm={handleConfirmAuthorizations}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,8 +9,8 @@ export default function MainRoutesLayout({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
console.log('[MAIN-ROUTES-LAYOUT] Layout do administrador carregado')
|
||||
|
||||
console.log("[MAIN-ROUTES-LAYOUT] Layout do administrador carregado");
|
||||
|
||||
return (
|
||||
<ProtectedRoute requiredUserType={["administrador"]}>
|
||||
<div className="min-h-screen bg-background flex">
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@ import {
|
||||
excluirPaciente,
|
||||
listarAutorizacoesUsuario,
|
||||
atualizarAutorizacoesUsuario,
|
||||
buscarUsuarioPorEmail,
|
||||
criarUsuarioPaciente,
|
||||
type AuthorizationRole,
|
||||
} from "@/lib/api";
|
||||
import { PatientRegistrationForm } from "@/components/forms/patient-registration-form";
|
||||
@ -75,8 +77,13 @@ export default function PacientesPage() {
|
||||
setLoading(true);
|
||||
const data = await listarPacientes({ page: 1, limit: 20 });
|
||||
|
||||
console.log("[loadAll] Dados brutos da API:", data);
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
setPatients(data.map(normalizePaciente));
|
||||
const normalized = data.map(normalizePaciente);
|
||||
console.log("[loadAll] Pacientes normalizados:", normalized);
|
||||
console.log("[loadAll] user_ids dos pacientes:", normalized.map(p => ({ nome: p.full_name, user_id: p.user_id })));
|
||||
setPatients(normalized);
|
||||
} else {
|
||||
setPatients([]);
|
||||
}
|
||||
@ -175,43 +182,78 @@ export default function PacientesPage() {
|
||||
}
|
||||
|
||||
async function handleConfirmAuthorizations(selection: AuthorizationState) {
|
||||
if (!authTargetPatient?.user_id) {
|
||||
console.log("[Auth] handleConfirmAuthorizations CHAMADA!", selection, "authTargetPatient=", authTargetPatient);
|
||||
|
||||
// Verifica se o paciente tem email
|
||||
if (!authTargetPatient?.email) {
|
||||
toast({
|
||||
title: "Usuário não vinculado",
|
||||
description: "Não foi possível atualizar as autorizações porque o usuário não está vinculado.",
|
||||
title: "Email obrigatório",
|
||||
description: "O paciente precisa ter um email cadastrado para receber autorizações.",
|
||||
variant: "destructive",
|
||||
});
|
||||
setAuthorizationsError(
|
||||
"Vincule este paciente a um usuário antes de ajustar as autorizações.",
|
||||
);
|
||||
setAuthorizationsSubmitDisabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[Auth] Confirm clicked", selection, "targetUserId=", authTargetPatient?.user_id);
|
||||
setAuthorizationsLoading(true);
|
||||
|
||||
setAuthorizationsLoading(true);
|
||||
setAuthorizationsError(null);
|
||||
|
||||
const selectedRoles: AuthorizationRole[] = [];
|
||||
if (selection.paciente) selectedRoles.push("paciente");
|
||||
if (selection.medico) selectedRoles.push("medico");
|
||||
|
||||
|
||||
try {
|
||||
console.log("[Auth] Updating roles to server:", selectedRoles);
|
||||
const result = await atualizarAutorizacoesUsuario(authTargetPatient.user_id, selectedRoles);
|
||||
console.log("[Auth] Update result:", result);
|
||||
// PASSO 1: Buscar ou criar usuário no sistema de autenticação
|
||||
console.log("[Auth] Buscando user_id para email:", authTargetPatient.email);
|
||||
|
||||
let userId = await buscarUsuarioPorEmail(authTargetPatient.email);
|
||||
|
||||
// Se não encontrou, cria um novo usuário
|
||||
if (!userId) {
|
||||
console.log("[Auth] Usuário não existe. Criando novo usuário...");
|
||||
|
||||
const newUserResponse = await criarUsuarioPaciente({
|
||||
email: authTargetPatient.email,
|
||||
full_name: authTargetPatient.full_name,
|
||||
phone_mobile: authTargetPatient.phone_mobile || "",
|
||||
});
|
||||
|
||||
userId = newUserResponse.user.id;
|
||||
console.log("[Auth] Novo usuário criado! user_id:", userId);
|
||||
|
||||
// Mostra credenciais ao admin
|
||||
toast({
|
||||
title: "Usuário criado com sucesso!",
|
||||
description: `Email: ${newUserResponse.email}\nSenha: ${newUserResponse.password}`,
|
||||
duration: 10000,
|
||||
});
|
||||
} else {
|
||||
console.log("[Auth] Usuário já existe. user_id:", userId);
|
||||
}
|
||||
|
||||
// PASSO 2: Atualizar autorizações via patient_assignments
|
||||
const selectedRoles: AuthorizationRole[] = [];
|
||||
if (selection.paciente) selectedRoles.push("paciente");
|
||||
if (selection.medico) selectedRoles.push("medico");
|
||||
|
||||
console.log("[Auth] Atualizando roles:", selectedRoles, "para user_id:", userId, "patient_id:", authTargetPatient.id);
|
||||
const result = await atualizarAutorizacoesUsuario(
|
||||
userId,
|
||||
authTargetPatient.id, // patient_id é obrigatório!
|
||||
selectedRoles
|
||||
);
|
||||
console.log("[Auth] Resultado:", result);
|
||||
|
||||
toast({
|
||||
title: "Autorizações atualizadas",
|
||||
description: "As permissões deste paciente foram atualizadas com sucesso.",
|
||||
});
|
||||
|
||||
setAuthDialogOpen(false);
|
||||
setAuthTargetPatient(null);
|
||||
setAuthInitialRoles(null);
|
||||
await loadAll();
|
||||
} catch (e: any) {
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[Auth] Erro:", error);
|
||||
toast({
|
||||
title: "Erro ao atualizar autorizações",
|
||||
description: e?.message || "Não foi possível atualizar as autorizações.",
|
||||
description: error?.message || "Não foi possível atualizar as autorizações.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
@ -231,20 +273,12 @@ export default function PacientesPage() {
|
||||
|
||||
async function handleSaved(p: Paciente) {
|
||||
// Normaliza e atualiza localmente
|
||||
let saved = normalizePaciente(p);
|
||||
// Vincula o registro de paciente ao usuário autenticado
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
// Preparar payload apenas com campos de PacienteInput e user_id
|
||||
const { id: _id, user_id: _oldUserId, ...rest } = saved;
|
||||
const payload = { ...rest, user_id: user.id };
|
||||
const linked = await atualizarPaciente(saved.id, payload);
|
||||
saved = normalizePaciente(linked);
|
||||
} catch (e) {
|
||||
// Se falhar, mantém saved original
|
||||
console.warn("Falha ao vincular usuário ao paciente:", e);
|
||||
}
|
||||
// Atualiza lista com o registro vinculado
|
||||
const saved = normalizePaciente(p);
|
||||
|
||||
console.log("[handleSaved] Paciente salvo:", saved);
|
||||
console.log("[handleSaved] user_id do paciente:", saved.user_id);
|
||||
|
||||
// Atualiza lista com o registro salvo (que já deve ter user_id do formulário)
|
||||
setPatients((prev) => {
|
||||
const i = prev.findIndex((x) => String(x.id) === String(saved.id));
|
||||
if (i < 0) return [saved, ...prev];
|
||||
|
||||
@ -39,7 +39,7 @@ export default function NovoAgendamentoPage() {
|
||||
const handleSave = () => {
|
||||
console.log("Salvando novo agendamento...", formData);
|
||||
alert("Novo agendamento salvo (simulado)!");
|
||||
router.push("/consultas");
|
||||
router.push("/consultas");
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
@ -50,12 +50,12 @@ export default function NovoAgendamentoPage() {
|
||||
<div className="min-h-screen flex flex-col bg-background">
|
||||
<HeaderAgenda />
|
||||
<main className="flex-1 mx-auto w-full max-w-7xl px-8 py-8">
|
||||
<CalendarRegistrationForm
|
||||
formData={formData}
|
||||
onFormChange={handleFormChange}
|
||||
<CalendarRegistrationForm
|
||||
formData={formData}
|
||||
onFormChange={handleFormChange}
|
||||
/>
|
||||
</main>
|
||||
<FooterAgenda onSave={handleSave} onCancel={handleCancel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +57,9 @@ export default function FinanceiroPage() {
|
||||
</Label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">Valor Particular</Label>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Valor Particular
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<DollarSign className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
@ -67,7 +69,9 @@ export default function FinanceiroPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">Valor Convênio</Label>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Valor Convênio
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<DollarSign className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
@ -99,7 +103,9 @@ export default function FinanceiroPage() {
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">Parcelas</Label>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Parcelas
|
||||
</Label>
|
||||
<select className="h-10 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400">
|
||||
<option value="1">1x</option>
|
||||
<option value="2">2x</option>
|
||||
@ -110,7 +116,9 @@ export default function FinanceiroPage() {
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">Desconto</Label>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Desconto
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Calculator className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
@ -133,16 +141,24 @@ export default function FinanceiroPage() {
|
||||
<div className="bg-muted/30 rounded-lg p-4 space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Subtotal:</span>
|
||||
<span className="text-sm font-medium text-foreground">R$ 0,00</span>
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
R$ 0,00
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Desconto:</span>
|
||||
<span className="text-sm font-medium text-foreground">- R$ 0,00</span>
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
- R$ 0,00
|
||||
</span>
|
||||
</div>
|
||||
<div className="border-t border-border pt-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-base font-medium text-foreground">Total:</span>
|
||||
<span className="text-lg font-bold text-primary">R$ 0,00</span>
|
||||
<span className="text-base font-medium text-foreground">
|
||||
Total:
|
||||
</span>
|
||||
<span className="text-lg font-bold text-primary">
|
||||
R$ 0,00
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -154,4 +170,4 @@ export default function FinanceiroPage() {
|
||||
<FooterAgenda onSave={handleSave} onCancel={handleCancel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,31 +1,33 @@
|
||||
import type React from "react"
|
||||
import type { Metadata } from "next"
|
||||
import { AuthProvider } from "@/hooks/useAuth"
|
||||
import { ThemeProvider } from "@/components/theme-provider"
|
||||
import "./globals.css"
|
||||
import type React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { AuthProvider } from "@/hooks/useAuth";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "MediConnect - Conectando Pacientes e Profissionais de Saúde",
|
||||
description:
|
||||
"Plataforma inovadora que conecta pacientes, clínicas, e médicos de forma prática, segura e humanizada. Experimente o futuro dos agendamentos médicos.",
|
||||
keywords: "saúde, médicos, pacientes, agendamento, telemedicina, SUS",
|
||||
generator: 'v0.app'
|
||||
}
|
||||
generator: "v0.app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="pt-BR" className="antialiased" suppressHydrationWarning>
|
||||
<body style={{ fontFamily: "var(--font-geist-sans)" }}>
|
||||
<ThemeProvider attribute="class" defaultTheme="light" enableSystem={false}>
|
||||
<AuthProvider>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="light"
|
||||
enableSystem={false}
|
||||
>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,48 +1,52 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { AuthenticationError } from '@/lib/auth'
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { AuthenticationError } from "@/lib/auth";
|
||||
|
||||
export default function LoginAdminPage() {
|
||||
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
const { login } = useAuth()
|
||||
const [credentials, setCredentials] = useState({ email: "", password: "" });
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
const { login } = useAuth();
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
// Tentar fazer login usando o contexto com tipo administrador
|
||||
const success = await login(credentials.email, credentials.password, 'administrador')
|
||||
|
||||
const success = await login(
|
||||
credentials.email,
|
||||
credentials.password,
|
||||
"administrador",
|
||||
);
|
||||
|
||||
if (success) {
|
||||
console.log('[LOGIN-ADMIN] Login bem-sucedido, redirecionando...')
|
||||
|
||||
console.log("[LOGIN-ADMIN] Login bem-sucedido, redirecionando...");
|
||||
|
||||
// Redirecionamento direto - solução que funcionou
|
||||
window.location.href = '/dashboard'
|
||||
window.location.href = "/dashboard";
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[LOGIN-ADMIN] Erro no login:', err)
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
setError(err.message)
|
||||
} catch (error_) {
|
||||
console.error("[LOGIN-ADMIN] Erro no login:", error_);
|
||||
|
||||
if (error_ instanceof AuthenticationError) {
|
||||
setError(error_.message);
|
||||
} else {
|
||||
setError('Erro inesperado. Tente novamente.')
|
||||
setError("Erro inesperado. Tente novamente.");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background py-12 px-4 sm:px-6 lg:px-8">
|
||||
@ -55,7 +59,7 @@ export default function LoginAdminPage() {
|
||||
Entre com suas credenciais para acessar o sistema administrativo
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center">Acesso Administrativo</CardTitle>
|
||||
@ -63,7 +67,10 @@ export default function LoginAdminPage() {
|
||||
<CardContent>
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-foreground">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
@ -71,15 +78,20 @@ export default function LoginAdminPage() {
|
||||
type="email"
|
||||
placeholder="Digite seu email"
|
||||
value={credentials.email}
|
||||
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
|
||||
onChange={(e) =>
|
||||
setCredentials({ ...credentials, email: e.target.value })
|
||||
}
|
||||
required
|
||||
className="mt-1"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-foreground">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Senha
|
||||
</label>
|
||||
<Input
|
||||
@ -87,7 +99,9 @@ export default function LoginAdminPage() {
|
||||
type="password"
|
||||
placeholder="Digite sua senha"
|
||||
value={credentials.password}
|
||||
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
|
||||
onChange={(e) =>
|
||||
setCredentials({ ...credentials, password: e.target.value })
|
||||
}
|
||||
required
|
||||
className="mt-1"
|
||||
disabled={loading}
|
||||
@ -100,25 +114,27 @@ export default function LoginAdminPage() {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full cursor-pointer"
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full cursor-pointer"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Entrando...' : 'Entrar no Sistema Administrativo'}
|
||||
{loading ? "Entrando..." : "Entrar no Sistema Administrativo"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<Button variant="outline" asChild className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200">
|
||||
<Link href="/">
|
||||
Voltar ao Início
|
||||
</Link>
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200"
|
||||
>
|
||||
<Link href="/">Voltar ao Início</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,55 +1,63 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { AuthenticationError } from '@/lib/auth'
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { AuthenticationError } from "@/lib/auth";
|
||||
|
||||
export default function LoginPacientePage() {
|
||||
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
const { login } = useAuth()
|
||||
const [credentials, setCredentials] = useState({ email: "", password: "" });
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
const { login } = useAuth();
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
// Tentar fazer login usando o contexto com tipo paciente
|
||||
const success = await login(credentials.email, credentials.password, 'paciente')
|
||||
|
||||
const success = await login(
|
||||
credentials.email,
|
||||
credentials.password,
|
||||
"paciente",
|
||||
);
|
||||
|
||||
if (success) {
|
||||
// Redirecionar para a página do paciente
|
||||
router.push('/paciente')
|
||||
router.push("/paciente");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[LOGIN-PACIENTE] Erro no login:', err)
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
} catch (error_) {
|
||||
console.error("[LOGIN-PACIENTE] Erro no login:", error_);
|
||||
|
||||
if (error_ instanceof AuthenticationError) {
|
||||
// Verificar se é erro de credenciais inválidas (pode ser email não confirmado)
|
||||
if (err.code === '400' || err.details?.error_code === 'invalid_credentials') {
|
||||
if (
|
||||
error_.code === "400" ||
|
||||
error_.details?.error_code === "invalid_credentials"
|
||||
) {
|
||||
setError(
|
||||
'⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, ' +
|
||||
'verifique sua caixa de entrada e clique no link de confirmação ' +
|
||||
'que foi enviado para ' + credentials.email
|
||||
)
|
||||
"⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, " +
|
||||
"verifique sua caixa de entrada e clique no link de confirmação " +
|
||||
"que foi enviado para " +
|
||||
credentials.email,
|
||||
);
|
||||
} else {
|
||||
setError(err.message)
|
||||
setError(error_.message);
|
||||
}
|
||||
} else {
|
||||
setError('Erro inesperado. Tente novamente.')
|
||||
setError("Erro inesperado. Tente novamente.");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background py-12 px-4 sm:px-6 lg:px-8">
|
||||
@ -62,7 +70,7 @@ export default function LoginPacientePage() {
|
||||
Acesse sua área pessoal e gerencie suas consultas
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center">Entrar como Paciente</CardTitle>
|
||||
@ -70,7 +78,10 @@ export default function LoginPacientePage() {
|
||||
<CardContent>
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-foreground">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
@ -78,15 +89,20 @@ export default function LoginPacientePage() {
|
||||
type="email"
|
||||
placeholder="Digite seu email"
|
||||
value={credentials.email}
|
||||
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
|
||||
onChange={(e) =>
|
||||
setCredentials({ ...credentials, email: e.target.value })
|
||||
}
|
||||
required
|
||||
className="mt-1"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-foreground">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Senha
|
||||
</label>
|
||||
<Input
|
||||
@ -94,7 +110,9 @@ export default function LoginPacientePage() {
|
||||
type="password"
|
||||
placeholder="Digite sua senha"
|
||||
value={credentials.password}
|
||||
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
|
||||
onChange={(e) =>
|
||||
setCredentials({ ...credentials, password: e.target.value })
|
||||
}
|
||||
required
|
||||
className="mt-1"
|
||||
disabled={loading}
|
||||
@ -107,25 +125,27 @@ export default function LoginPacientePage() {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full cursor-pointer"
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full cursor-pointer"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Entrando...' : 'Entrar na Minha Área'}
|
||||
{loading ? "Entrando..." : "Entrar na Minha Área"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<Button variant="outline" asChild className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200">
|
||||
<Link href="/">
|
||||
Voltar ao Início
|
||||
</Link>
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200"
|
||||
>
|
||||
<Link href="/">Voltar ao Início</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,57 +1,67 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { AuthenticationError } from '@/lib/auth'
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { AuthenticationError } from "@/lib/auth";
|
||||
|
||||
export default function LoginPage() {
|
||||
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
const { login } = useAuth()
|
||||
const [credentials, setCredentials] = useState({ email: "", password: "" });
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
const { login } = useAuth();
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
// Tentar fazer login usando o contexto com tipo profissional
|
||||
const success = await login(credentials.email, credentials.password, 'profissional')
|
||||
|
||||
const success = await login(
|
||||
credentials.email,
|
||||
credentials.password,
|
||||
"profissional",
|
||||
);
|
||||
|
||||
if (success) {
|
||||
console.log('[LOGIN-PROFISSIONAL] Login bem-sucedido, redirecionando...')
|
||||
|
||||
console.log(
|
||||
"[LOGIN-PROFISSIONAL] Login bem-sucedido, redirecionando...",
|
||||
);
|
||||
|
||||
// Redirecionamento direto - solução que funcionou
|
||||
window.location.href = '/profissional'
|
||||
window.location.href = "/profissional";
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[LOGIN-PROFISSIONAL] Erro no login:', err)
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
} catch (error_) {
|
||||
console.error("[LOGIN-PROFISSIONAL] Erro no login:", error_);
|
||||
|
||||
if (error_ instanceof AuthenticationError) {
|
||||
// Verificar se é erro de credenciais inválidas (pode ser email não confirmado)
|
||||
if (err.code === '400' || err.details?.error_code === 'invalid_credentials') {
|
||||
if (
|
||||
error_.code === "400" ||
|
||||
error_.details?.error_code === "invalid_credentials"
|
||||
) {
|
||||
setError(
|
||||
'⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, ' +
|
||||
'verifique sua caixa de entrada e clique no link de confirmação ' +
|
||||
'que foi enviado para ' + credentials.email
|
||||
)
|
||||
"⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, " +
|
||||
"verifique sua caixa de entrada e clique no link de confirmação " +
|
||||
"que foi enviado para " +
|
||||
credentials.email,
|
||||
);
|
||||
} else {
|
||||
setError(err.message)
|
||||
setError(error_.message);
|
||||
}
|
||||
} else {
|
||||
setError('Erro inesperado. Tente novamente.')
|
||||
setError("Erro inesperado. Tente novamente.");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background py-12 px-4 sm:px-6 lg:px-8">
|
||||
@ -64,7 +74,7 @@ export default function LoginPage() {
|
||||
Entre com suas credenciais para acessar o sistema
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center">Acesso ao Sistema</CardTitle>
|
||||
@ -72,7 +82,10 @@ export default function LoginPage() {
|
||||
<CardContent>
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-foreground">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
@ -80,15 +93,20 @@ export default function LoginPage() {
|
||||
type="email"
|
||||
placeholder="Digite seu email"
|
||||
value={credentials.email}
|
||||
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
|
||||
onChange={(e) =>
|
||||
setCredentials({ ...credentials, email: e.target.value })
|
||||
}
|
||||
required
|
||||
className="mt-1"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-foreground">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Senha
|
||||
</label>
|
||||
<Input
|
||||
@ -96,7 +114,9 @@ export default function LoginPage() {
|
||||
type="password"
|
||||
placeholder="Digite sua senha"
|
||||
value={credentials.password}
|
||||
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
|
||||
onChange={(e) =>
|
||||
setCredentials({ ...credentials, password: e.target.value })
|
||||
}
|
||||
required
|
||||
className="mt-1"
|
||||
disabled={loading}
|
||||
@ -109,25 +129,27 @@ export default function LoginPage() {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full cursor-pointer"
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full cursor-pointer"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Entrando...' : 'Entrar'}
|
||||
{loading ? "Entrando..." : "Entrar"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<Button variant="outline" asChild className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200">
|
||||
<Link href="/">
|
||||
Voltar ao Início
|
||||
</Link>
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200"
|
||||
>
|
||||
<Link href="/">Voltar ao Início</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,76 +1,102 @@
|
||||
'use client'
|
||||
"use client";
|
||||
// import { useAuth } from '@/hooks/useAuth' // removido duplicado
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { User, LogOut, Calendar, FileText, MessageCircle, UserCog, Home, Clock, FolderOpen, ChevronLeft, ChevronRight, MapPin, Stethoscope } from 'lucide-react'
|
||||
import { SimpleThemeToggle } from '@/components/simple-theme-toggle'
|
||||
import Link from 'next/link'
|
||||
import ProtectedRoute from '@/components/ProtectedRoute'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import {
|
||||
User,
|
||||
LogOut,
|
||||
Calendar,
|
||||
FileText,
|
||||
MessageCircle,
|
||||
UserCog,
|
||||
Home,
|
||||
Clock,
|
||||
FolderOpen,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
MapPin,
|
||||
Stethoscope,
|
||||
} from "lucide-react";
|
||||
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
|
||||
import Link from "next/link";
|
||||
import ProtectedRoute from "@/components/ProtectedRoute";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
// Simulação de internacionalização básica
|
||||
const strings = {
|
||||
dashboard: 'Dashboard',
|
||||
consultas: 'Consultas',
|
||||
exames: 'Exames & Laudos',
|
||||
mensagens: 'Mensagens',
|
||||
perfil: 'Perfil',
|
||||
sair: 'Sair',
|
||||
proximaConsulta: 'Próxima Consulta',
|
||||
ultimosExames: 'Últimos Exames',
|
||||
mensagensNaoLidas: 'Mensagens Não Lidas',
|
||||
agendar: 'Agendar',
|
||||
reagendar: 'Reagendar',
|
||||
cancelar: 'Cancelar',
|
||||
detalhes: 'Detalhes',
|
||||
adicionarCalendario: 'Adicionar ao calendário',
|
||||
visualizarLaudo: 'Visualizar Laudo',
|
||||
download: 'Download',
|
||||
compartilhar: 'Compartilhar',
|
||||
inbox: 'Caixa de Entrada',
|
||||
enviarMensagem: 'Enviar Mensagem',
|
||||
salvar: 'Salvar',
|
||||
editarPerfil: 'Editar Perfil',
|
||||
consentimentos: 'Consentimentos',
|
||||
notificacoes: 'Preferências de Notificação',
|
||||
vazio: 'Nenhum dado encontrado.',
|
||||
erro: 'Ocorreu um erro. Tente novamente.',
|
||||
carregando: 'Carregando...',
|
||||
sucesso: 'Salvo com sucesso!',
|
||||
erroSalvar: 'Erro ao salvar.',
|
||||
}
|
||||
dashboard: "Dashboard",
|
||||
consultas: "Consultas",
|
||||
exames: "Exames & Laudos",
|
||||
mensagens: "Mensagens",
|
||||
perfil: "Perfil",
|
||||
sair: "Sair",
|
||||
proximaConsulta: "Próxima Consulta",
|
||||
ultimosExames: "Últimos Exames",
|
||||
mensagensNaoLidas: "Mensagens Não Lidas",
|
||||
agendar: "Agendar",
|
||||
reagendar: "Reagendar",
|
||||
cancelar: "Cancelar",
|
||||
detalhes: "Detalhes",
|
||||
adicionarCalendario: "Adicionar ao calendário",
|
||||
visualizarLaudo: "Visualizar Laudo",
|
||||
download: "Download",
|
||||
compartilhar: "Compartilhar",
|
||||
inbox: "Caixa de Entrada",
|
||||
enviarMensagem: "Enviar Mensagem",
|
||||
salvar: "Salvar",
|
||||
editarPerfil: "Editar Perfil",
|
||||
consentimentos: "Consentimentos",
|
||||
notificacoes: "Preferências de Notificação",
|
||||
vazio: "Nenhum dado encontrado.",
|
||||
erro: "Ocorreu um erro. Tente novamente.",
|
||||
carregando: "Carregando...",
|
||||
sucesso: "Salvo com sucesso!",
|
||||
erroSalvar: "Erro ao salvar.",
|
||||
};
|
||||
|
||||
export default function PacientePage() {
|
||||
const { logout, user } = useAuth()
|
||||
const [tab, setTab] = useState<'dashboard'|'consultas'|'exames'|'mensagens'|'perfil'>('dashboard')
|
||||
const { logout, user } = useAuth();
|
||||
const [tab, setTab] = useState<
|
||||
"dashboard" | "consultas" | "exames" | "mensagens" | "perfil"
|
||||
>("dashboard");
|
||||
|
||||
// Simulação de loaders, empty states e erro
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [toast, setToast] = useState<{type: 'success'|'error', msg: string}|null>(null)
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [toast, setToast] = useState<{
|
||||
type: "success" | "error";
|
||||
msg: string;
|
||||
} | null>(null);
|
||||
|
||||
// Acessibilidade: foco visível e ordem de tabulação garantidos por padrão nos botões e inputs
|
||||
|
||||
const handleLogout = async () => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
await logout()
|
||||
await logout();
|
||||
} catch {
|
||||
setError(strings.erro)
|
||||
setError(strings.erro);
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Estado para edição do perfil
|
||||
const [isEditingProfile, setIsEditingProfile] = useState(false)
|
||||
const [isEditingProfile, setIsEditingProfile] = useState(false);
|
||||
const [profileData, setProfileData] = useState({
|
||||
nome: "Maria Silva Santos",
|
||||
email: user?.email || "paciente@example.com",
|
||||
@ -78,19 +104,20 @@ export default function PacientePage() {
|
||||
endereco: "Rua das Flores, 123",
|
||||
cidade: "São Paulo",
|
||||
cep: "01234-567",
|
||||
biografia: "Paciente desde 2020. Histórico de consultas e exames regulares.",
|
||||
})
|
||||
biografia:
|
||||
"Paciente desde 2020. Histórico de consultas e exames regulares.",
|
||||
});
|
||||
|
||||
const handleProfileChange = (field: string, value: string) => {
|
||||
setProfileData(prev => ({ ...prev, [field]: value }))
|
||||
}
|
||||
setProfileData((previous) => ({ ...previous, [field]: value }));
|
||||
};
|
||||
const handleSaveProfile = () => {
|
||||
setIsEditingProfile(false)
|
||||
setToast({ type: 'success', msg: strings.sucesso })
|
||||
}
|
||||
setIsEditingProfile(false);
|
||||
setToast({ type: "success", msg: strings.sucesso });
|
||||
};
|
||||
const handleCancelEdit = () => {
|
||||
setIsEditingProfile(false)
|
||||
}
|
||||
setIsEditingProfile(false);
|
||||
};
|
||||
function DashboardCards() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
@ -109,58 +136,69 @@ export default function PacientePage() {
|
||||
<span className="font-semibold">{strings.mensagensNaoLidas}</span>
|
||||
<span className="text-2xl">1</span>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Consultas fictícias
|
||||
const [currentDate, setCurrentDate] = useState(new Date())
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const consultasFicticias = [
|
||||
{
|
||||
id: 1,
|
||||
medico: "Dr. Carlos Andrade",
|
||||
especialidade: "Cardiologia",
|
||||
local: "Clínica Coração Feliz",
|
||||
data: new Date().toISOString().split('T')[0],
|
||||
data: new Date().toISOString().split("T")[0],
|
||||
hora: "09:00",
|
||||
status: "Confirmada"
|
||||
status: "Confirmada",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
medico: "Dra. Fernanda Lima",
|
||||
especialidade: "Dermatologia",
|
||||
local: "Clínica Pele Viva",
|
||||
data: new Date().toISOString().split('T')[0],
|
||||
data: new Date().toISOString().split("T")[0],
|
||||
hora: "14:30",
|
||||
status: "Pendente"
|
||||
status: "Pendente",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
medico: "Dr. João Silva",
|
||||
especialidade: "Ortopedia",
|
||||
local: "Hospital Ortopédico",
|
||||
data: (() => { let d = new Date(); d.setDate(d.getDate()+1); return d.toISOString().split('T')[0] })(),
|
||||
data: (() => {
|
||||
let d = new Date();
|
||||
d.setDate(d.getDate() + 1);
|
||||
return d.toISOString().split("T")[0];
|
||||
})(),
|
||||
hora: "11:00",
|
||||
status: "Cancelada"
|
||||
status: "Cancelada",
|
||||
},
|
||||
];
|
||||
|
||||
function formatDatePt(dateStr: string) {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
|
||||
function formatDatePt(dateString: string) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("pt-BR", {
|
||||
weekday: "long",
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function navigateDate(direction: 'prev' | 'next') {
|
||||
function navigateDate(direction: "prev" | "next") {
|
||||
const newDate = new Date(currentDate);
|
||||
newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1));
|
||||
newDate.setDate(newDate.getDate() + (direction === "next" ? 1 : -1));
|
||||
setCurrentDate(newDate);
|
||||
}
|
||||
function goToToday() {
|
||||
setCurrentDate(new Date());
|
||||
}
|
||||
|
||||
const todayStr = currentDate.toISOString().split('T')[0];
|
||||
const consultasDoDia = consultasFicticias.filter(c => c.data === todayStr);
|
||||
const todayString = currentDate.toISOString().split("T")[0];
|
||||
const consultasDoDia = consultasFicticias.filter(
|
||||
(c) => c.data === todayString,
|
||||
);
|
||||
|
||||
function Consultas() {
|
||||
return (
|
||||
@ -171,13 +209,38 @@ export default function PacientePage() {
|
||||
{/* Navegação de Data */}
|
||||
<div className="flex items-center justify-between mb-6 p-4 bg-blue-50 rounded-lg dark:bg-muted">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button variant="outline" size="sm" onClick={() => navigateDate('prev')} className="p-2"><ChevronLeft className="h-4 w-4" /></Button>
|
||||
<h3 className="text-lg font-medium text-foreground">{formatDatePt(todayStr)}</h3>
|
||||
<Button variant="outline" size="sm" onClick={() => navigateDate('next')} className="p-2"><ChevronRight className="h-4 w-4" /></Button>
|
||||
<Button variant="outline" size="sm" onClick={goToToday} className="ml-4 px-3 py-1 text-sm">Hoje</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigateDate("prev")}
|
||||
className="p-2"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<h3 className="text-lg font-medium text-foreground">
|
||||
{formatDatePt(todayString)}
|
||||
</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigateDate("next")}
|
||||
className="p-2"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={goToToday}
|
||||
className="ml-4 px-3 py-1 text-sm"
|
||||
>
|
||||
Hoje
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-muted-foreground">
|
||||
{consultasDoDia.length} consulta{consultasDoDia.length !== 1 ? 's' : ''} agendada{consultasDoDia.length !== 1 ? 's' : ''}
|
||||
{consultasDoDia.length} consulta
|
||||
{consultasDoDia.length !== 1 ? "s" : ""} agendada
|
||||
{consultasDoDia.length !== 1 ? "s" : ""}
|
||||
</div>
|
||||
</div>
|
||||
{/* Lista de Consultas do Dia */}
|
||||
@ -185,16 +248,33 @@ export default function PacientePage() {
|
||||
{consultasDoDia.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-600 dark:text-muted-foreground">
|
||||
<Calendar className="h-12 w-12 mx-auto mb-4 text-gray-400 dark:text-muted-foreground/50" />
|
||||
<p className="text-lg mb-2">Nenhuma consulta agendada para este dia</p>
|
||||
<p className="text-lg mb-2">
|
||||
Nenhuma consulta agendada para este dia
|
||||
</p>
|
||||
<p className="text-sm">Você pode agendar uma nova consulta</p>
|
||||
<Button variant="default" className="mt-4">Agendar Consulta</Button>
|
||||
<Button variant="default" className="mt-4">
|
||||
Agendar Consulta
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
consultasDoDia.map(consulta => (
|
||||
<div key={consulta.id} className="border-l-4 border-t border-r border-b p-4 rounded-lg shadow-sm bg-card border-border">
|
||||
consultasDoDia.map((consulta) => (
|
||||
<div
|
||||
key={consulta.id}
|
||||
className="border-l-4 border-t border-r border-b p-4 rounded-lg shadow-sm bg-card border-border"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-center">
|
||||
<div className="flex items-center">
|
||||
<div className="w-3 h-3 rounded-full mr-3" style={{ backgroundColor: consulta.status === 'Confirmada' ? '#22c55e' : consulta.status === 'Pendente' ? '#fbbf24' : '#ef4444' }}></div>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full mr-3"
|
||||
style={{
|
||||
backgroundColor:
|
||||
consulta.status === "Confirmada"
|
||||
? "#22c55e"
|
||||
: consulta.status === "Pendente"
|
||||
? "#fbbf24"
|
||||
: "#ef4444",
|
||||
}}
|
||||
></div>
|
||||
<div>
|
||||
<div className="font-medium flex items-center">
|
||||
<Stethoscope className="h-4 w-4 mr-2 text-gray-500 dark:text-muted-foreground" />
|
||||
@ -210,12 +290,26 @@ export default function PacientePage() {
|
||||
<span className="font-medium">{consulta.hora}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className={`px-3 py-1 rounded-full text-sm font-medium text-white ${consulta.status === 'Confirmada' ? 'bg-green-600' : consulta.status === 'Pendente' ? 'bg-yellow-500' : 'bg-red-600'}`}>{consulta.status}</div>
|
||||
<div
|
||||
className={`px-3 py-1 rounded-full text-sm font-medium text-white ${consulta.status === "Confirmada" ? "bg-green-600" : consulta.status === "Pendente" ? "bg-yellow-500" : "bg-red-600"}`}
|
||||
>
|
||||
{consulta.status}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<Button variant="outline" size="sm">Detalhes</Button>
|
||||
{consulta.status !== 'Cancelada' && <Button variant="secondary" size="sm">Reagendar</Button>}
|
||||
{consulta.status !== 'Cancelada' && <Button variant="destructive" size="sm">Cancelar</Button>}
|
||||
<Button variant="outline" size="sm">
|
||||
Detalhes
|
||||
</Button>
|
||||
{consulta.status !== "Cancelada" && (
|
||||
<Button variant="secondary" size="sm">
|
||||
Reagendar
|
||||
</Button>
|
||||
)}
|
||||
{consulta.status !== "Cancelada" && (
|
||||
<Button variant="destructive" size="sm">
|
||||
Cancelar
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -223,7 +317,7 @@ export default function PacientePage() {
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Exames e laudos fictícios
|
||||
@ -233,14 +327,16 @@ export default function PacientePage() {
|
||||
nome: "Hemograma Completo",
|
||||
data: "2025-09-20",
|
||||
status: "Disponível",
|
||||
prontuario: "Paciente apresenta hemograma dentro dos padrões de normalidade. Sem alterações significativas.",
|
||||
prontuario:
|
||||
"Paciente apresenta hemograma dentro dos padrões de normalidade. Sem alterações significativas.",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
nome: "Raio-X de Tórax",
|
||||
data: "2025-08-10",
|
||||
status: "Disponível",
|
||||
prontuario: "Exame radiológico sem evidências de lesões pulmonares. Estruturas cardíacas normais.",
|
||||
prontuario:
|
||||
"Exame radiológico sem evidências de lesões pulmonares. Estruturas cardíacas normais.",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@ -257,14 +353,16 @@ export default function PacientePage() {
|
||||
nome: "Laudo Hemograma Completo",
|
||||
data: "2025-09-21",
|
||||
status: "Assinado",
|
||||
laudo: "Hemoglobina, hematócrito, leucócitos e plaquetas dentro dos valores de referência. Sem anemias ou infecções detectadas.",
|
||||
laudo:
|
||||
"Hemoglobina, hematócrito, leucócitos e plaquetas dentro dos valores de referência. Sem anemias ou infecções detectadas.",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
nome: "Laudo Raio-X de Tórax",
|
||||
data: "2025-08-11",
|
||||
status: "Assinado",
|
||||
laudo: "Radiografia sem alterações. Parênquima pulmonar preservado. Ausência de derrame pleural.",
|
||||
laudo:
|
||||
"Radiografia sem alterações. Parênquima pulmonar preservado. Ausência de derrame pleural.",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@ -275,8 +373,12 @@ export default function PacientePage() {
|
||||
},
|
||||
];
|
||||
|
||||
const [exameSelecionado, setExameSelecionado] = useState<null | typeof examesFicticios[0]>(null)
|
||||
const [laudoSelecionado, setLaudoSelecionado] = useState<null | typeof laudosFicticios[0]>(null)
|
||||
const [exameSelecionado, setExameSelecionado] = useState<
|
||||
null | (typeof examesFicticios)[0]
|
||||
>(null);
|
||||
const [laudoSelecionado, setLaudoSelecionado] = useState<
|
||||
null | (typeof laudosFicticios)[0]
|
||||
>(null);
|
||||
|
||||
function ExamesLaudos() {
|
||||
return (
|
||||
@ -285,14 +387,26 @@ export default function PacientePage() {
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-semibold mb-2">Meus Exames</h3>
|
||||
<div className="space-y-3">
|
||||
{examesFicticios.map(exame => (
|
||||
<div key={exame.id} className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4">
|
||||
{examesFicticios.map((exame) => (
|
||||
<div
|
||||
key={exame.id}
|
||||
className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4"
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{exame.nome}</div>
|
||||
<div className="text-sm text-muted-foreground">Data: {new Date(exame.data).toLocaleDateString('pt-BR')}</div>
|
||||
<div className="font-medium text-foreground">
|
||||
{exame.nome}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Data: {new Date(exame.data).toLocaleDateString("pt-BR")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-2 md:mt-0">
|
||||
<Button variant="outline" onClick={() => setExameSelecionado(exame)}>Ver Prontuário</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setExameSelecionado(exame)}
|
||||
>
|
||||
Ver Prontuário
|
||||
</Button>
|
||||
<Button variant="secondary">Download</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -303,14 +417,26 @@ export default function PacientePage() {
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">Meus Laudos</h3>
|
||||
<div className="space-y-3">
|
||||
{laudosFicticios.map(laudo => (
|
||||
<div key={laudo.id} className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4">
|
||||
{laudosFicticios.map((laudo) => (
|
||||
<div
|
||||
key={laudo.id}
|
||||
className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4"
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{laudo.nome}</div>
|
||||
<div className="text-sm text-muted-foreground">Data: {new Date(laudo.data).toLocaleDateString('pt-BR')}</div>
|
||||
<div className="font-medium text-foreground">
|
||||
{laudo.nome}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Data: {new Date(laudo.data).toLocaleDateString("pt-BR")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-2 md:mt-0">
|
||||
<Button variant="outline" onClick={() => setLaudoSelecionado(laudo)}>Visualizar</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setLaudoSelecionado(laudo)}
|
||||
>
|
||||
Visualizar
|
||||
</Button>
|
||||
<Button variant="secondary">Compartilhar</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -319,48 +445,82 @@ export default function PacientePage() {
|
||||
</div>
|
||||
|
||||
{/* Modal Prontuário Exame */}
|
||||
<Dialog open={!!exameSelecionado} onOpenChange={open => !open && setExameSelecionado(null)}>
|
||||
<Dialog
|
||||
open={!!exameSelecionado}
|
||||
onOpenChange={(open) => !open && setExameSelecionado(null)}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Prontuário do Exame</DialogTitle>
|
||||
<DialogDescription>
|
||||
{exameSelecionado && (
|
||||
<>
|
||||
<div className="font-semibold mb-2">{exameSelecionado.nome}</div>
|
||||
<div className="text-sm text-muted-foreground mb-4">Data: {new Date(exameSelecionado.data).toLocaleDateString('pt-BR')}</div>
|
||||
<div className="mb-4 whitespace-pre-line">{exameSelecionado.prontuario}</div>
|
||||
<div className="font-semibold mb-2">
|
||||
{exameSelecionado.nome}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mb-4">
|
||||
Data:{" "}
|
||||
{new Date(exameSelecionado.data).toLocaleDateString(
|
||||
"pt-BR",
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-4 whitespace-pre-line">
|
||||
{exameSelecionado.prontuario}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setExameSelecionado(null)}>Fechar</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setExameSelecionado(null)}
|
||||
>
|
||||
Fechar
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Modal Visualizar Laudo */}
|
||||
<Dialog open={!!laudoSelecionado} onOpenChange={open => !open && setLaudoSelecionado(null)}>
|
||||
<Dialog
|
||||
open={!!laudoSelecionado}
|
||||
onOpenChange={(open) => !open && setLaudoSelecionado(null)}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Laudo Médico</DialogTitle>
|
||||
<DialogDescription>
|
||||
{laudoSelecionado && (
|
||||
<>
|
||||
<div className="font-semibold mb-2">{laudoSelecionado.nome}</div>
|
||||
<div className="text-sm text-muted-foreground mb-4">Data: {new Date(laudoSelecionado.data).toLocaleDateString('pt-BR')}</div>
|
||||
<div className="mb-4 whitespace-pre-line">{laudoSelecionado.laudo}</div>
|
||||
<div className="font-semibold mb-2">
|
||||
{laudoSelecionado.nome}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mb-4">
|
||||
Data:{" "}
|
||||
{new Date(laudoSelecionado.data).toLocaleDateString(
|
||||
"pt-BR",
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-4 whitespace-pre-line">
|
||||
{laudoSelecionado.laudo}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setLaudoSelecionado(null)}>Fechar</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setLaudoSelecionado(null)}
|
||||
>
|
||||
Fechar
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Mensagens fictícias recebidas do médico
|
||||
@ -369,22 +529,25 @@ export default function PacientePage() {
|
||||
id: 1,
|
||||
medico: "Dr. Carlos Andrade",
|
||||
data: "2025-10-06T15:30:00",
|
||||
conteudo: "Olá Maria, seu exame de hemograma está normal. Parabéns por manter seus exames em dia!",
|
||||
lida: false
|
||||
conteudo:
|
||||
"Olá Maria, seu exame de hemograma está normal. Parabéns por manter seus exames em dia!",
|
||||
lida: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
medico: "Dra. Fernanda Lima",
|
||||
data: "2025-09-21T10:15:00",
|
||||
conteudo: "Maria, seu laudo de Raio-X já está disponível no sistema. Qualquer dúvida, estou à disposição.",
|
||||
lida: true
|
||||
conteudo:
|
||||
"Maria, seu laudo de Raio-X já está disponível no sistema. Qualquer dúvida, estou à disposição.",
|
||||
lida: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
medico: "Dr. João Silva",
|
||||
data: "2025-08-12T09:00:00",
|
||||
conteudo: "Bom dia! Lembre-se de agendar seu retorno para acompanhamento da ortopedia.",
|
||||
lida: true
|
||||
conteudo:
|
||||
"Bom dia! Lembre-se de agendar seu retorno para acompanhamento da ortopedia.",
|
||||
lida: true,
|
||||
},
|
||||
];
|
||||
|
||||
@ -397,26 +560,39 @@ export default function PacientePage() {
|
||||
<div className="text-center py-8 text-gray-600 dark:text-muted-foreground">
|
||||
<MessageCircle className="h-12 w-12 mx-auto mb-4 text-gray-400 dark:text-muted-foreground/50" />
|
||||
<p className="text-lg mb-2">Nenhuma mensagem recebida</p>
|
||||
<p className="text-sm">Você ainda não recebeu mensagens dos seus médicos.</p>
|
||||
<p className="text-sm">
|
||||
Você ainda não recebeu mensagens dos seus médicos.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
mensagensFicticias.map(msg => (
|
||||
<div key={msg.id} className={`flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4 border ${!msg.lida ? 'border-primary' : 'border-transparent'}`}>
|
||||
mensagensFicticias.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4 border ${!message.lida ? "border-primary" : "border-transparent"}`}
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium text-foreground flex items-center gap-2">
|
||||
<User className="h-4 w-4 text-primary" />
|
||||
{msg.medico}
|
||||
{!msg.lida && <span className="ml-2 px-2 py-0.5 rounded-full text-xs bg-primary text-white">Nova</span>}
|
||||
{message.medico}
|
||||
{!message.lida && (
|
||||
<span className="ml-2 px-2 py-0.5 rounded-full text-xs bg-primary text-white">
|
||||
Nova
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mb-2">
|
||||
{new Date(message.data).toLocaleString("pt-BR")}
|
||||
</div>
|
||||
<div className="text-foreground whitespace-pre-line">
|
||||
{message.conteudo}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mb-2">{new Date(msg.data).toLocaleString('pt-BR')}</div>
|
||||
<div className="text-foreground whitespace-pre-line">{msg.conteudo}</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function Perfil() {
|
||||
@ -425,98 +601,173 @@ export default function PacientePage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold text-foreground">Meu Perfil</h2>
|
||||
{!isEditingProfile ? (
|
||||
<Button onClick={() => setIsEditingProfile(true)} className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => setIsEditingProfile(true)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
Editar Perfil
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleSaveProfile} className="flex items-center gap-2">Salvar</Button>
|
||||
<Button variant="outline" onClick={handleCancelEdit}>Cancelar</Button>
|
||||
<Button
|
||||
onClick={handleSaveProfile}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
Salvar
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleCancelEdit}>
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Informações Pessoais */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold border-b border-border text-foreground pb-2">Informações Pessoais</h3>
|
||||
<h3 className="text-lg font-semibold border-b border-border text-foreground pb-2">
|
||||
Informações Pessoais
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="nome">Nome Completo</Label>
|
||||
<p className="p-2 bg-muted rounded text-muted-foreground">{profileData.nome}</p>
|
||||
<span className="text-xs text-muted-foreground">Este campo não pode ser alterado</span>
|
||||
<p className="p-2 bg-muted rounded text-muted-foreground">
|
||||
{profileData.nome}
|
||||
</p>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Este campo não pode ser alterado
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
{isEditingProfile ? (
|
||||
<Input id="email" type="email" value={profileData.email} onChange={e => handleProfileChange('email', e.target.value)} />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={profileData.email}
|
||||
onChange={(e) => handleProfileChange("email", e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.email}</p>
|
||||
<p className="p-2 bg-muted/50 rounded text-foreground">
|
||||
{profileData.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="telefone">Telefone</Label>
|
||||
{isEditingProfile ? (
|
||||
<Input id="telefone" value={profileData.telefone} onChange={e => handleProfileChange('telefone', e.target.value)} />
|
||||
<Input
|
||||
id="telefone"
|
||||
value={profileData.telefone}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("telefone", e.target.value)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.telefone}</p>
|
||||
<p className="p-2 bg-muted/50 rounded text-foreground">
|
||||
{profileData.telefone}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Endereço e Contato */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold border-b border-border text-foreground pb-2">Endereço</h3>
|
||||
<h3 className="text-lg font-semibold border-b border-border text-foreground pb-2">
|
||||
Endereço
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="endereco">Endereço</Label>
|
||||
{isEditingProfile ? (
|
||||
<Input id="endereco" value={profileData.endereco} onChange={e => handleProfileChange('endereco', e.target.value)} />
|
||||
<Input
|
||||
id="endereco"
|
||||
value={profileData.endereco}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("endereco", e.target.value)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.endereco}</p>
|
||||
<p className="p-2 bg-muted/50 rounded text-foreground">
|
||||
{profileData.endereco}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cidade">Cidade</Label>
|
||||
{isEditingProfile ? (
|
||||
<Input id="cidade" value={profileData.cidade} onChange={e => handleProfileChange('cidade', e.target.value)} />
|
||||
<Input
|
||||
id="cidade"
|
||||
value={profileData.cidade}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("cidade", e.target.value)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.cidade}</p>
|
||||
<p className="p-2 bg-muted/50 rounded text-foreground">
|
||||
{profileData.cidade}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cep">CEP</Label>
|
||||
{isEditingProfile ? (
|
||||
<Input id="cep" value={profileData.cep} onChange={e => handleProfileChange('cep', e.target.value)} />
|
||||
<Input
|
||||
id="cep"
|
||||
value={profileData.cep}
|
||||
onChange={(e) => handleProfileChange("cep", e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.cep}</p>
|
||||
<p className="p-2 bg-muted/50 rounded text-foreground">
|
||||
{profileData.cep}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="biografia">Biografia</Label>
|
||||
{isEditingProfile ? (
|
||||
<Textarea id="biografia" value={profileData.biografia} onChange={e => handleProfileChange('biografia', e.target.value)} rows={4} placeholder="Conte um pouco sobre você..." />
|
||||
<Textarea
|
||||
id="biografia"
|
||||
value={profileData.biografia}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("biografia", e.target.value)
|
||||
}
|
||||
rows={4}
|
||||
placeholder="Conte um pouco sobre você..."
|
||||
/>
|
||||
) : (
|
||||
<p className="p-2 bg-muted/50 rounded min-h-[100px] text-foreground">{profileData.biografia}</p>
|
||||
<p className="p-2 bg-muted/50 rounded min-h-[100px] text-foreground">
|
||||
{profileData.biografia}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Foto do Perfil */}
|
||||
<div className="border-t border-border pt-6">
|
||||
<h3 className="text-lg font-semibold mb-4 text-foreground">Foto do Perfil</h3>
|
||||
<h3 className="text-lg font-semibold mb-4 text-foreground">
|
||||
Foto do Perfil
|
||||
</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="h-20 w-20">
|
||||
<AvatarFallback className="text-lg">
|
||||
{profileData.nome.split(' ').map(n => n[0]).join('').toUpperCase()}
|
||||
{profileData.nome
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")
|
||||
.toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{isEditingProfile && (
|
||||
<div className="space-y-2">
|
||||
<Button variant="outline" size="sm">Alterar Foto</Button>
|
||||
<p className="text-xs text-muted-foreground">Formatos aceitos: JPG, PNG (máx. 2MB)</p>
|
||||
<Button variant="outline" size="sm">
|
||||
Alterar Foto
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Formatos aceitos: JPG, PNG (máx. 2MB)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Renderização principal
|
||||
@ -536,43 +787,107 @@ export default function PacientePage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SimpleThemeToggle />
|
||||
<Button onClick={handleLogout} variant="destructive" aria-label={strings.sair} disabled={loading} className="ml-2"><LogOut className="h-4 w-4" /> {strings.sair}</Button>
|
||||
<Button
|
||||
onClick={handleLogout}
|
||||
variant="destructive"
|
||||
aria-label={strings.sair}
|
||||
disabled={loading}
|
||||
className="ml-2"
|
||||
>
|
||||
<LogOut className="h-4 w-4" /> {strings.sair}
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{/* Sidebar vertical */}
|
||||
<nav aria-label="Navegação do dashboard" className="w-56 bg-card border-r flex flex-col py-6 px-2 gap-2">
|
||||
<Button variant={tab==='dashboard'?'secondary':'ghost'} aria-current={tab==='dashboard'} onClick={()=>setTab('dashboard')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.dashboard}</Button>
|
||||
<Button variant={tab==='consultas'?'secondary':'ghost'} aria-current={tab==='consultas'} onClick={()=>setTab('consultas')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.consultas}</Button>
|
||||
<Button variant={tab==='exames'?'secondary':'ghost'} aria-current={tab==='exames'} onClick={()=>setTab('exames')} className="justify-start"><FileText className="mr-2 h-5 w-5" />{strings.exames}</Button>
|
||||
<Button variant={tab==='mensagens'?'secondary':'ghost'} aria-current={tab==='mensagens'} onClick={()=>setTab('mensagens')} className="justify-start"><MessageCircle className="mr-2 h-5 w-5" />{strings.mensagens}</Button>
|
||||
<Button variant={tab==='perfil'?'secondary':'ghost'} aria-current={tab==='perfil'} onClick={()=>setTab('perfil')} className="justify-start"><UserCog className="mr-2 h-5 w-5" />{strings.perfil}</Button>
|
||||
<nav
|
||||
aria-label="Navegação do dashboard"
|
||||
className="w-56 bg-card border-r flex flex-col py-6 px-2 gap-2"
|
||||
>
|
||||
<Button
|
||||
variant={tab === "dashboard" ? "secondary" : "ghost"}
|
||||
aria-current={tab === "dashboard"}
|
||||
onClick={() => setTab("dashboard")}
|
||||
className="justify-start"
|
||||
>
|
||||
<Calendar className="mr-2 h-5 w-5" />
|
||||
{strings.dashboard}
|
||||
</Button>
|
||||
<Button
|
||||
variant={tab === "consultas" ? "secondary" : "ghost"}
|
||||
aria-current={tab === "consultas"}
|
||||
onClick={() => setTab("consultas")}
|
||||
className="justify-start"
|
||||
>
|
||||
<Calendar className="mr-2 h-5 w-5" />
|
||||
{strings.consultas}
|
||||
</Button>
|
||||
<Button
|
||||
variant={tab === "exames" ? "secondary" : "ghost"}
|
||||
aria-current={tab === "exames"}
|
||||
onClick={() => setTab("exames")}
|
||||
className="justify-start"
|
||||
>
|
||||
<FileText className="mr-2 h-5 w-5" />
|
||||
{strings.exames}
|
||||
</Button>
|
||||
<Button
|
||||
variant={tab === "mensagens" ? "secondary" : "ghost"}
|
||||
aria-current={tab === "mensagens"}
|
||||
onClick={() => setTab("mensagens")}
|
||||
className="justify-start"
|
||||
>
|
||||
<MessageCircle className="mr-2 h-5 w-5" />
|
||||
{strings.mensagens}
|
||||
</Button>
|
||||
<Button
|
||||
variant={tab === "perfil" ? "secondary" : "ghost"}
|
||||
aria-current={tab === "perfil"}
|
||||
onClick={() => setTab("perfil")}
|
||||
className="justify-start"
|
||||
>
|
||||
<UserCog className="mr-2 h-5 w-5" />
|
||||
{strings.perfil}
|
||||
</Button>
|
||||
</nav>
|
||||
{/* Conteúdo principal */}
|
||||
<div className="flex-1 min-w-0 p-4 max-w-4xl mx-auto w-full">
|
||||
{/* Toasts de feedback */}
|
||||
{toast && (
|
||||
<div className={`fixed top-4 right-4 z-50 px-4 py-2 rounded shadow-lg ${toast.type==='success'?'bg-green-600 text-white':'bg-red-600 text-white'}`} role="alert">{toast.msg}</div>
|
||||
<div
|
||||
className={`fixed top-4 right-4 z-50 px-4 py-2 rounded shadow-lg ${toast.type === "success" ? "bg-green-600 text-white" : "bg-red-600 text-white"}`}
|
||||
role="alert"
|
||||
>
|
||||
{toast.msg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loader global */}
|
||||
{loading && <div className="flex-1 flex items-center justify-center"><span>{strings.carregando}</span></div>}
|
||||
{error && <div className="flex-1 flex items-center justify-center text-red-600"><span>{error}</span></div>}
|
||||
{loading && (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<span>{strings.carregando}</span>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="flex-1 flex items-center justify-center text-red-600">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Conteúdo principal */}
|
||||
{!loading && !error && (
|
||||
<main className="flex-1">
|
||||
{tab==='dashboard' && <DashboardCards />}
|
||||
{tab==='consultas' && <Consultas />}
|
||||
{tab==='exames' && <ExamesLaudos />}
|
||||
{tab==='mensagens' && <Mensagens />}
|
||||
{tab==='perfil' && <Perfil />}
|
||||
{tab === "dashboard" && <DashboardCards />}
|
||||
{tab === "consultas" && <Consultas />}
|
||||
{tab === "exames" && <ExamesLaudos />}
|
||||
{tab === "mensagens" && <Mensagens />}
|
||||
{tab === "perfil" && <Perfil />}
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Header } from "@/components/header"
|
||||
import { HeroSection } from "@/components/hero-section"
|
||||
import { Footer } from "@/components/footer"
|
||||
import { Header } from "@/components/header";
|
||||
import { HeroSection } from "@/components/hero-section";
|
||||
import { Footer } from "@/components/footer";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
@ -11,5 +11,5 @@ export default function HomePage() {
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ export default function ProcedimentoPage() {
|
||||
const isAg = pathname?.startsWith("/agendamento");
|
||||
const isPr = pathname?.startsWith("/procedimento");
|
||||
const isFi = pathname?.startsWith("/financeiro");
|
||||
|
||||
|
||||
const handleSave = () => {
|
||||
// Lógica de salvar será implementada
|
||||
console.log("Salvando procedimentos...");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
import { Header } from "@/components/header"
|
||||
import { AboutSection } from "@/components/about-section"
|
||||
import { Footer } from "@/components/footer"
|
||||
import { Header } from "@/components/header";
|
||||
import { AboutSection } from "@/components/about-section";
|
||||
import { Footer } from "@/components/footer";
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
@ -11,5 +11,5 @@ export default function AboutPage() {
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,90 +1,111 @@
|
||||
'use client'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import type { UserType } from '@/types/auth'
|
||||
import { USER_TYPE_ROUTES, LOGIN_ROUTES, AUTH_STORAGE_KEYS } from '@/types/auth'
|
||||
"use client";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import type { UserType } from "@/types/auth";
|
||||
import {
|
||||
USER_TYPE_ROUTES,
|
||||
LOGIN_ROUTES,
|
||||
AUTH_STORAGE_KEYS,
|
||||
} from "@/types/auth";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode
|
||||
requiredUserType?: UserType[]
|
||||
interface ProtectedRouteProperties {
|
||||
children: React.ReactNode;
|
||||
requiredUserType?: UserType[];
|
||||
}
|
||||
|
||||
export default function ProtectedRoute({
|
||||
children,
|
||||
requiredUserType
|
||||
}: ProtectedRouteProps) {
|
||||
const { authStatus, user } = useAuth()
|
||||
const router = useRouter()
|
||||
const isRedirecting = useRef(false)
|
||||
export default function ProtectedRoute({
|
||||
children,
|
||||
requiredUserType,
|
||||
}: ProtectedRouteProperties) {
|
||||
const { authStatus, user } = useAuth();
|
||||
const router = useRouter();
|
||||
const isRedirecting = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Evitar múltiplos redirects
|
||||
if (isRedirecting.current) return
|
||||
if (isRedirecting.current) return;
|
||||
|
||||
// Durante loading, não fazer nada
|
||||
if (authStatus === 'loading') return
|
||||
if (authStatus === "loading") return;
|
||||
|
||||
// Se não autenticado, redirecionar para login
|
||||
if (authStatus === 'unauthenticated') {
|
||||
isRedirecting.current = true
|
||||
|
||||
console.log('[PROTECTED-ROUTE] Usuário NÃO autenticado - redirecionando...')
|
||||
|
||||
if (authStatus === "unauthenticated") {
|
||||
isRedirecting.current = true;
|
||||
|
||||
console.log(
|
||||
"[PROTECTED-ROUTE] Usuário NÃO autenticado - redirecionando...",
|
||||
);
|
||||
|
||||
// Determinar página de login baseada no histórico
|
||||
let userType: UserType = 'profissional'
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
let userType: UserType = "profissional";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
const storedUserType = localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE)
|
||||
if (storedUserType && ['profissional', 'paciente', 'administrador'].includes(storedUserType)) {
|
||||
userType = storedUserType as UserType
|
||||
const storedUserType = localStorage.getItem(
|
||||
AUTH_STORAGE_KEYS.USER_TYPE,
|
||||
);
|
||||
if (
|
||||
storedUserType &&
|
||||
["profissional", "paciente", "administrador"].includes(
|
||||
storedUserType,
|
||||
)
|
||||
) {
|
||||
userType = storedUserType as UserType;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[PROTECTED-ROUTE] Erro ao ler localStorage:', error)
|
||||
console.warn("[PROTECTED-ROUTE] Erro ao ler localStorage:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const loginRoute = LOGIN_ROUTES[userType]
|
||||
console.log('[PROTECTED-ROUTE] Redirecionando para login:', {
|
||||
|
||||
const loginRoute = LOGIN_ROUTES[userType];
|
||||
console.log("[PROTECTED-ROUTE] Redirecionando para login:", {
|
||||
userType,
|
||||
loginRoute,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
})
|
||||
|
||||
router.push(loginRoute)
|
||||
return
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
});
|
||||
|
||||
router.push(loginRoute);
|
||||
return;
|
||||
}
|
||||
|
||||
// Se autenticado mas não tem permissão para esta página
|
||||
if (authStatus === 'authenticated' && user && requiredUserType && !requiredUserType.includes(user.userType)) {
|
||||
isRedirecting.current = true
|
||||
|
||||
console.log('[PROTECTED-ROUTE] Usuário SEM permissão para esta página', {
|
||||
if (
|
||||
authStatus === "authenticated" &&
|
||||
user &&
|
||||
requiredUserType &&
|
||||
!requiredUserType.includes(user.userType)
|
||||
) {
|
||||
isRedirecting.current = true;
|
||||
|
||||
console.log("[PROTECTED-ROUTE] Usuário SEM permissão para esta página", {
|
||||
userType: user.userType,
|
||||
requiredTypes: requiredUserType
|
||||
})
|
||||
|
||||
const correctRoute = USER_TYPE_ROUTES[user.userType]
|
||||
console.log('[PROTECTED-ROUTE] Redirecionando para área correta:', correctRoute)
|
||||
|
||||
router.push(correctRoute)
|
||||
return
|
||||
requiredTypes: requiredUserType,
|
||||
});
|
||||
|
||||
const correctRoute = USER_TYPE_ROUTES[user.userType];
|
||||
console.log(
|
||||
"[PROTECTED-ROUTE] Redirecionando para área correta:",
|
||||
correctRoute,
|
||||
);
|
||||
|
||||
router.push(correctRoute);
|
||||
return;
|
||||
}
|
||||
|
||||
// Se chegou aqui, acesso está autorizado
|
||||
if (authStatus === 'authenticated') {
|
||||
console.log('[PROTECTED-ROUTE] ACESSO AUTORIZADO!', {
|
||||
if (authStatus === "authenticated") {
|
||||
console.log("[PROTECTED-ROUTE] ACESSO AUTORIZADO!", {
|
||||
userType: user?.userType,
|
||||
email: user?.email,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
})
|
||||
isRedirecting.current = false
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
});
|
||||
isRedirecting.current = false;
|
||||
}
|
||||
}, [authStatus, user, requiredUserType, router])
|
||||
}, [authStatus, user, requiredUserType, router]);
|
||||
|
||||
// Durante loading, mostrar spinner
|
||||
if (authStatus === 'loading') {
|
||||
if (authStatus === "loading") {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
@ -92,11 +113,11 @@ export default function ProtectedRoute({
|
||||
<p className="mt-4 text-gray-600">Verificando autenticação...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Se não autenticado ou redirecionando, mostrar spinner
|
||||
if (authStatus === 'unauthenticated' || isRedirecting.current) {
|
||||
if (authStatus === "unauthenticated" || isRedirecting.current) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
@ -104,7 +125,7 @@ export default function ProtectedRoute({
|
||||
<p className="mt-4 text-gray-600">Redirecionando...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Se usuário não tem permissão, mostrar fallback (não deveria chegar aqui devido ao useEffect)
|
||||
@ -112,12 +133,14 @@ export default function ProtectedRoute({
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Acesso Negado</h2>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
Acesso Negado
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Você não tem permissão para acessar esta página.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mb-6">
|
||||
Tipo de acesso necessário: {requiredUserType.join(' ou ')}
|
||||
Tipo de acesso necessário: {requiredUserType.join(" ou ")}
|
||||
<br />
|
||||
Seu tipo de acesso: {user.userType}
|
||||
</p>
|
||||
@ -129,9 +152,9 @@ export default function ProtectedRoute({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Finalmente, renderizar conteúdo protegido
|
||||
return <>{children}</>
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Lightbulb, CheckCircle } from "lucide-react"
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Lightbulb, CheckCircle } from "lucide-react";
|
||||
|
||||
export function AboutSection() {
|
||||
const values = ["Inovação", "Segurança", "Discrição", "Transparência", "Agilidade"]
|
||||
const values = [
|
||||
"Inovação",
|
||||
"Segurança",
|
||||
"Discrição",
|
||||
"Transparência",
|
||||
"Agilidade",
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-16 lg:py-24 bg-muted/30">
|
||||
@ -26,10 +32,13 @@ export function AboutSection() {
|
||||
<Lightbulb className="w-6 h-6 text-primary-foreground" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-semibold uppercase tracking-wide opacity-90">NOSSO OBJETIVO</h3>
|
||||
<h3 className="text-sm font-semibold uppercase tracking-wide opacity-90">
|
||||
NOSSO OBJETIVO
|
||||
</h3>
|
||||
<p className="text-lg leading-relaxed">
|
||||
Nosso compromisso é garantir qualidade, segurança e sigilo em cada atendimento, unindo tecnologia à
|
||||
responsabilidade médica.
|
||||
Nosso compromisso é garantir qualidade, segurança e sigilo
|
||||
em cada atendimento, unindo tecnologia à responsabilidade
|
||||
médica.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -43,25 +52,30 @@ export function AboutSection() {
|
||||
SOBRE NÓS
|
||||
</div>
|
||||
<h2 className="text-3xl lg:text-4xl font-bold text-foreground leading-tight text-balance">
|
||||
Experimente o futuro do gerenciamento dos seus atendimentos médicos
|
||||
Experimente o futuro do gerenciamento dos seus atendimentos
|
||||
médicos
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 text-muted-foreground leading-relaxed">
|
||||
<p>
|
||||
Somos uma plataforma inovadora que conecta pacientes e médicos de forma prática, segura e humanizada.
|
||||
Nosso objetivo é simplificar o processo de emissão e acompanhamento de laudos médicos, oferecendo um
|
||||
ambiente online confiável e acessível.
|
||||
Somos uma plataforma inovadora que conecta pacientes e médicos
|
||||
de forma prática, segura e humanizada. Nosso objetivo é
|
||||
simplificar o processo de emissão e acompanhamento de laudos
|
||||
médicos, oferecendo um ambiente online confiável e acessível.
|
||||
</p>
|
||||
<p>
|
||||
Aqui, os pacientes podem registrar suas informações de saúde e solicitar laudos de forma rápida,
|
||||
enquanto os médicos têm acesso a ferramentas que facilitam a análise, validação e emissão dos
|
||||
Aqui, os pacientes podem registrar suas informações de saúde e
|
||||
solicitar laudos de forma rápida, enquanto os médicos têm acesso
|
||||
a ferramentas que facilitam a análise, validação e emissão dos
|
||||
documentos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xl font-semibold text-foreground">Nossos valores</h3>
|
||||
<h3 className="text-xl font-semibold text-foreground">
|
||||
Nossos valores
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{values.map((value, index) => (
|
||||
<div key={index} className="flex items-center space-x-2">
|
||||
@ -75,5 +89,5 @@ export function AboutSection() {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,14 +6,17 @@ import { Label } from "../ui/label";
|
||||
import { Switch } from "../ui/switch";
|
||||
import { useState } from "react";
|
||||
|
||||
interface FooterAgendaProps {
|
||||
interface FooterAgendaProperties {
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export default function FooterAgenda({ onSave, onCancel }: FooterAgendaProps) {
|
||||
export default function FooterAgenda({
|
||||
onSave,
|
||||
onCancel,
|
||||
}: FooterAgendaProperties) {
|
||||
const [bloqueio, setBloqueio] = useState(false);
|
||||
|
||||
|
||||
return (
|
||||
<div className="sticky bottom-0 left-0 right-0 border-t border-border bg-background">
|
||||
<div className="mx-auto w-full max-w-7xl px-8 py-3 flex items-center justify-between">
|
||||
@ -22,7 +25,9 @@ export default function FooterAgenda({ onSave, onCancel }: FooterAgendaProps) {
|
||||
<Label className="text-sm text-foreground">Bloqueio de Agenda</Label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" onClick={onCancel}>Cancelar</Button>
|
||||
<Button variant="ghost" onClick={onCancel}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={onSave}>Salvar</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -15,7 +15,9 @@ export default function HeaderAgenda() {
|
||||
return (
|
||||
<header className="border-b bg-background border-border">
|
||||
<div className="mx-auto w-full max-w-7xl px-8 py-3 flex items-center justify-between">
|
||||
<h1 className="text-[18px] font-semibold text-foreground">Novo Agendamento</h1>
|
||||
<h1 className="text-[18px] font-semibold text-foreground">
|
||||
Novo Agendamento
|
||||
</h1>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<nav
|
||||
@ -27,8 +29,8 @@ export default function HeaderAgenda() {
|
||||
href="/agenda"
|
||||
role="tab"
|
||||
className={`px-4 py-1.5 text-[13px] font-medium border rounded-md ${
|
||||
isAg
|
||||
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
|
||||
isAg
|
||||
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
|
||||
: "text-foreground hover:bg-muted border-input"
|
||||
}`}
|
||||
>
|
||||
@ -38,8 +40,8 @@ export default function HeaderAgenda() {
|
||||
href="/procedimento"
|
||||
role="tab"
|
||||
className={`px-4 py-1.5 text-[13px] font-medium border rounded-md ${
|
||||
isPr
|
||||
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
|
||||
isPr
|
||||
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
|
||||
: "text-foreground hover:bg-muted border-input"
|
||||
}`}
|
||||
>
|
||||
@ -49,8 +51,8 @@ export default function HeaderAgenda() {
|
||||
href="/financeiro"
|
||||
role="tab"
|
||||
className={`px-4 py-1.5 text-[13px] font-medium border rounded-md ${
|
||||
isFi
|
||||
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
|
||||
isFi
|
||||
? "bg-primary text-white border-primary dark:bg-primary dark:text-white"
|
||||
: "text-foreground hover:bg-muted border-input"
|
||||
}`}
|
||||
>
|
||||
|
||||
@ -1,16 +1,22 @@
|
||||
"use client";
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Plus, Clock, User, Calendar as CalendarIcon } from 'lucide-react';
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Plus,
|
||||
Clock,
|
||||
User,
|
||||
Calendar as CalendarIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
interface Appointment {
|
||||
id: string;
|
||||
patient: string;
|
||||
time: string;
|
||||
duration: number;
|
||||
type: 'consulta' | 'exame' | 'retorno';
|
||||
status: 'confirmed' | 'pending' | 'absent';
|
||||
type: "consulta" | "exame" | "retorno";
|
||||
status: "confirmed" | "pending" | "absent";
|
||||
professional: string;
|
||||
notes: string;
|
||||
}
|
||||
@ -21,63 +27,74 @@ interface Professional {
|
||||
specialty: string;
|
||||
}
|
||||
|
||||
interface AgendaCalendarProps {
|
||||
interface AgendaCalendarProperties {
|
||||
professionals: Professional[];
|
||||
appointments: Appointment[];
|
||||
onAddAppointment: () => void;
|
||||
onEditAppointment: (appointment: Appointment) => void;
|
||||
}
|
||||
|
||||
export default function AgendaCalendar({
|
||||
professionals,
|
||||
appointments,
|
||||
onAddAppointment,
|
||||
onEditAppointment
|
||||
}: AgendaCalendarProps) {
|
||||
const [view, setView] = useState<'day' | 'week' | 'month'>('week');
|
||||
const [selectedProfessional, setSelectedProfessional] = useState('all');
|
||||
export default function AgendaCalendar({
|
||||
professionals,
|
||||
appointments,
|
||||
onAddAppointment,
|
||||
onEditAppointment,
|
||||
}: AgendaCalendarProperties) {
|
||||
const [view, setView] = useState<"day" | "week" | "month">("week");
|
||||
const [selectedProfessional, setSelectedProfessional] = useState("all");
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
|
||||
const timeSlots = Array.from({ length: 11 }, (_, i) => {
|
||||
const hour = i + 8; // Das 8h às 18h
|
||||
return [`${hour.toString().padStart(2, '0')}:00`, `${hour.toString().padStart(2, '0')}:30`];
|
||||
const timeSlots = Array.from({ length: 11 }, (_, index) => {
|
||||
const hour = index + 8; // Das 8h às 18h
|
||||
return [
|
||||
`${hour.toString().padStart(2, "0")}:00`,
|
||||
`${hour.toString().padStart(2, "0")}:30`,
|
||||
];
|
||||
}).flat();
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'confirmed': return 'bg-green-100 border-green-500 text-green-800';
|
||||
case 'pending': return 'bg-yellow-100 border-yellow-500 text-yellow-800';
|
||||
case 'absent': return 'bg-red-100 border-red-500 text-red-800';
|
||||
default: return 'bg-gray-100 border-gray-500 text-gray-800';
|
||||
case "confirmed":
|
||||
return "bg-green-100 border-green-500 text-green-800";
|
||||
case "pending":
|
||||
return "bg-yellow-100 border-yellow-500 text-yellow-800";
|
||||
case "absent":
|
||||
return "bg-red-100 border-red-500 text-red-800";
|
||||
default:
|
||||
return "bg-gray-100 border-gray-500 text-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'consulta': return '🩺';
|
||||
case 'exame': return '📋';
|
||||
case 'retorno': return '↩️';
|
||||
default: return '📅';
|
||||
case "consulta":
|
||||
return "🩺";
|
||||
case "exame":
|
||||
return "📋";
|
||||
case "retorno":
|
||||
return "↩️";
|
||||
default:
|
||||
return "📅";
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('pt-BR', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
return date.toLocaleDateString("pt-BR", {
|
||||
weekday: "long",
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const navigateDate = (direction: 'prev' | 'next') => {
|
||||
const navigateDate = (direction: "prev" | "next") => {
|
||||
const newDate = new Date(currentDate);
|
||||
if (view === 'day') {
|
||||
newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1));
|
||||
} else if (view === 'week') {
|
||||
newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7));
|
||||
if (view === "day") {
|
||||
newDate.setDate(newDate.getDate() + (direction === "next" ? 1 : -1));
|
||||
} else if (view === "week") {
|
||||
newDate.setDate(newDate.getDate() + (direction === "next" ? 7 : -7));
|
||||
} else {
|
||||
newDate.setMonth(newDate.getMonth() + (direction === 'next' ? 1 : -1));
|
||||
newDate.setMonth(newDate.getMonth() + (direction === "next" ? 1 : -1));
|
||||
}
|
||||
setCurrentDate(newDate);
|
||||
};
|
||||
@ -86,66 +103,70 @@ export default function AgendaCalendar({
|
||||
setCurrentDate(new Date());
|
||||
};
|
||||
|
||||
|
||||
const filteredAppointments = selectedProfessional === 'all'
|
||||
? appointments
|
||||
: appointments.filter(app => app.professional === selectedProfessional);
|
||||
const filteredAppointments =
|
||||
selectedProfessional === "all"
|
||||
? appointments
|
||||
: appointments.filter((app) => app.professional === selectedProfessional);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4 sm:mb-0">Agenda</h2>
|
||||
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4 sm:mb-0">
|
||||
Agenda
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<select
|
||||
<select
|
||||
value={selectedProfessional}
|
||||
onChange={(e) => setSelectedProfessional(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="all">Todos os profissionais</option>
|
||||
{professionals.map(prof => (
|
||||
<option key={prof.id} value={prof.id}>{prof.name}</option>
|
||||
{professionals.map((prof) => (
|
||||
<option key={prof.id} value={prof.id}>
|
||||
{prof.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
|
||||
<div className="inline-flex rounded-md shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView('day')}
|
||||
onClick={() => setView("day")}
|
||||
className={`px-3 py-2 text-sm font-medium rounded-l-md ${
|
||||
view === 'day'
|
||||
? 'bg-blue-100 text-blue-700 border border-blue-300'
|
||||
: 'bg-white text-gray-700 border border-gray-300'
|
||||
view === "day"
|
||||
? "bg-blue-100 text-blue-700 border border-blue-300"
|
||||
: "bg-white text-gray-700 border border-gray-300"
|
||||
}`}
|
||||
>
|
||||
Dia
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView('week')}
|
||||
onClick={() => setView("week")}
|
||||
className={`px-3 py-2 text-sm font-medium -ml-px ${
|
||||
view === 'week'
|
||||
? 'bg-blue-100 text-blue-700 border border-blue-300'
|
||||
: 'bg-white text-gray-700 border border-gray-300'
|
||||
view === "week"
|
||||
? "bg-blue-100 text-blue-700 border border-blue-300"
|
||||
: "bg-white text-gray-700 border border-gray-300"
|
||||
}`}
|
||||
>
|
||||
Semana
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView('month')}
|
||||
onClick={() => setView("month")}
|
||||
className={`px-3 py-2 text-sm font-medium -ml-px rounded-r-md ${
|
||||
view === 'month'
|
||||
? 'bg-blue-100 text-blue-700 border border-blue-300'
|
||||
: 'bg-white text-gray-700 border border-gray-300'
|
||||
view === "month"
|
||||
? "bg-blue-100 text-blue-700 border border-blue-300"
|
||||
: "bg-white text-gray-700 border border-gray-300"
|
||||
}`}
|
||||
>
|
||||
Mês
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
||||
<button
|
||||
onClick={onAddAppointment}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
@ -159,8 +180,8 @@ export default function AgendaCalendar({
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={() => navigateDate('prev')}
|
||||
<button
|
||||
onClick={() => navigateDate("prev")}
|
||||
className="p-1 rounded-md hover:bg-gray-100"
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5 text-gray-600" />
|
||||
@ -168,13 +189,13 @@ export default function AgendaCalendar({
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
{formatDate(currentDate)}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => navigateDate('next')}
|
||||
<button
|
||||
onClick={() => navigateDate("next")}
|
||||
className="p-1 rounded-md hover:bg-gray-100"
|
||||
>
|
||||
<ChevronRight className="h-5 w-5 text-gray-600" />
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
onClick={goToToday}
|
||||
className="ml-4 px-3 py-1 text-sm border border-gray-300 rounded-md hover:bg-gray-100"
|
||||
>
|
||||
@ -188,7 +209,7 @@ export default function AgendaCalendar({
|
||||
</div>
|
||||
|
||||
{}
|
||||
{view !== 'month' && (
|
||||
{view !== "month" && (
|
||||
<div className="overflow-auto">
|
||||
<div className="min-w-full">
|
||||
<div className="flex">
|
||||
@ -196,34 +217,40 @@ export default function AgendaCalendar({
|
||||
<div className="h-12 border-b border-gray-200 flex items-center justify-center text-sm font-medium text-gray-500">
|
||||
Hora
|
||||
</div>
|
||||
{timeSlots.map(time => (
|
||||
<div key={time} className="h-16 border-b border-gray-200 flex items-center justify-center text-sm text-gray-500">
|
||||
{timeSlots.map((time) => (
|
||||
<div
|
||||
key={time}
|
||||
className="h-16 border-b border-gray-200 flex items-center justify-center text-sm text-gray-500"
|
||||
>
|
||||
{time}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="h-12 border-b border-gray-200 flex items-center justify-center text-sm font-medium text-gray-500">
|
||||
{currentDate.toLocaleDateString('pt-BR', { weekday: 'long' })}
|
||||
{currentDate.toLocaleDateString("pt-BR", { weekday: "long" })}
|
||||
</div>
|
||||
<div className="relative">
|
||||
{timeSlots.map(time => (
|
||||
<div key={time} className="h-16 border-b border-gray-200"></div>
|
||||
{timeSlots.map((time) => (
|
||||
<div
|
||||
key={time}
|
||||
className="h-16 border-b border-gray-200"
|
||||
></div>
|
||||
))}
|
||||
|
||||
{filteredAppointments.map(app => {
|
||||
const [date, timeStr] = app.time.split('T');
|
||||
const [hours, minutes] = timeStr.split(':');
|
||||
|
||||
{filteredAppointments.map((app) => {
|
||||
const [date, timeString] = app.time.split("T");
|
||||
const [hours, minutes] = timeString.split(":");
|
||||
const hour = parseInt(hours);
|
||||
const minute = parseInt(minutes);
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
key={app.id}
|
||||
className={`absolute left-1 right-1 border-l-4 rounded p-2 shadow-sm cursor-pointer ${getStatusColor(app.status)}`}
|
||||
style={{
|
||||
top: `${((hour - 8) * 64 + (minute / 60) * 64) + 48}px`,
|
||||
top: `${(hour - 8) * 64 + (minute / 60) * 64 + 48}px`,
|
||||
height: `${(app.duration / 60) * 64}px`,
|
||||
}}
|
||||
onClick={() => onEditAppointment(app)}
|
||||
@ -236,14 +263,23 @@ export default function AgendaCalendar({
|
||||
</div>
|
||||
<div className="text-xs flex items-center mt-1">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
{hours}:{minutes} - {app.type} {getTypeIcon(app.type)}
|
||||
{hours}:{minutes} - {app.type}{" "}
|
||||
{getTypeIcon(app.type)}
|
||||
</div>
|
||||
<div className="text-xs mt-1">
|
||||
{professionals.find(p => p.id === app.professional)?.name}
|
||||
{
|
||||
professionals.find(
|
||||
(p) => p.id === app.professional,
|
||||
)?.name
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs capitalize">
|
||||
{app.status === 'confirmed' ? 'confirmado' : app.status === 'pending' ? 'pendente' : 'ausente'}
|
||||
{app.status === "confirmed"
|
||||
? "confirmado"
|
||||
: app.status === "pending"
|
||||
? "pendente"
|
||||
: "ausente"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -257,15 +293,18 @@ export default function AgendaCalendar({
|
||||
)}
|
||||
|
||||
{}
|
||||
{view === 'month' && (
|
||||
{view === "month" && (
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
{filteredAppointments.map(app => {
|
||||
const [date, timeStr] = app.time.split('T');
|
||||
const [hours, minutes] = timeStr.split(':');
|
||||
|
||||
{filteredAppointments.map((app) => {
|
||||
const [date, timeString] = app.time.split("T");
|
||||
const [hours, minutes] = timeString.split(":");
|
||||
|
||||
return (
|
||||
<div key={app.id} className={`border-l-4 p-4 rounded-lg shadow-sm ${getStatusColor(app.status)}`}>
|
||||
<div
|
||||
key={app.id}
|
||||
className={`border-l-4 p-4 rounded-lg shadow-sm ${getStatusColor(app.status)}`}
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
|
||||
<div className="flex items-center">
|
||||
<User className="h-4 w-4 mr-2" />
|
||||
@ -273,10 +312,17 @@ export default function AgendaCalendar({
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-4 w-4 mr-2" />
|
||||
<span>{hours}:{minutes} - {app.type} {getTypeIcon(app.type)}</span>
|
||||
<span>
|
||||
{hours}:{minutes} - {app.type} {getTypeIcon(app.type)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm">{professionals.find(p => p.id === app.professional)?.name}</span>
|
||||
<span className="text-sm">
|
||||
{
|
||||
professionals.find((p) => p.id === app.professional)
|
||||
?.name
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{app.notes && (
|
||||
@ -285,7 +331,7 @@ export default function AgendaCalendar({
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2 flex justify-end">
|
||||
<button
|
||||
<button
|
||||
onClick={() => onEditAppointment(app)}
|
||||
className="text-blue-600 hover:text-blue-800 text-sm"
|
||||
>
|
||||
@ -300,4 +346,4 @@ export default function AgendaCalendar({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { useState, useEffect } from "react";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
interface Appointment {
|
||||
id?: string;
|
||||
patient: string;
|
||||
time: string;
|
||||
duration: number;
|
||||
type: 'consulta' | 'exame' | 'retorno';
|
||||
status: 'confirmed' | 'pending' | 'absent';
|
||||
type: "consulta" | "exame" | "retorno";
|
||||
status: "confirmed" | "pending" | "absent";
|
||||
professional: string;
|
||||
notes?: string;
|
||||
}
|
||||
@ -20,7 +20,7 @@ interface Professional {
|
||||
specialty: string;
|
||||
}
|
||||
|
||||
interface AppointmentModalProps {
|
||||
interface AppointmentModalProperties {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (appointment: Appointment) => void;
|
||||
@ -28,21 +28,21 @@ interface AppointmentModalProps {
|
||||
appointment?: Appointment | null;
|
||||
}
|
||||
|
||||
export default function AppointmentModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
professionals,
|
||||
appointment
|
||||
}: AppointmentModalProps) {
|
||||
export default function AppointmentModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
professionals,
|
||||
appointment,
|
||||
}: AppointmentModalProperties) {
|
||||
const [formData, setFormData] = useState<Appointment>({
|
||||
patient: '',
|
||||
time: '',
|
||||
patient: "",
|
||||
time: "",
|
||||
duration: 30,
|
||||
type: 'consulta',
|
||||
status: 'pending',
|
||||
professional: '',
|
||||
notes: ''
|
||||
type: "consulta",
|
||||
status: "pending",
|
||||
professional: "",
|
||||
notes: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@ -50,13 +50,13 @@ export default function AppointmentModal({
|
||||
setFormData(appointment);
|
||||
} else {
|
||||
setFormData({
|
||||
patient: '',
|
||||
time: '',
|
||||
patient: "",
|
||||
time: "",
|
||||
duration: 30,
|
||||
type: 'consulta',
|
||||
status: 'pending',
|
||||
professional: professionals[0]?.id || '',
|
||||
notes: ''
|
||||
type: "consulta",
|
||||
status: "pending",
|
||||
professional: professionals[0]?.id || "",
|
||||
notes: "",
|
||||
});
|
||||
}
|
||||
}, [appointment, professionals]);
|
||||
@ -67,11 +67,15 @@ export default function AppointmentModal({
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<
|
||||
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
||||
>,
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
setFormData((previous) => ({
|
||||
...previous,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
@ -82,13 +86,16 @@ export default function AppointmentModal({
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{appointment ? 'Editar Agendamento' : 'Novo Agendamento'}
|
||||
{appointment ? "Editar Agendamento" : "Novo Agendamento"}
|
||||
</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
@ -104,7 +111,7 @@ export default function AppointmentModal({
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Profissional
|
||||
@ -117,14 +124,14 @@ export default function AppointmentModal({
|
||||
required
|
||||
>
|
||||
<option value="">Selecione um profissional</option>
|
||||
{professionals.map(prof => (
|
||||
{professionals.map((prof) => (
|
||||
<option key={prof.id} value={prof.id}>
|
||||
{prof.name} - {prof.specialty}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
@ -139,7 +146,7 @@ export default function AppointmentModal({
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Duração (min)
|
||||
@ -156,7 +163,7 @@ export default function AppointmentModal({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
@ -173,7 +180,7 @@ export default function AppointmentModal({
|
||||
<option value="retorno">Retorno</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Status
|
||||
@ -190,21 +197,21 @@ export default function AppointmentModal({
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Observações
|
||||
</label>
|
||||
<textarea
|
||||
name="notes"
|
||||
value={formData.notes || ''}
|
||||
value={formData.notes || ""}
|
||||
onChange={handleChange}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex justify-end space-x-3 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
@ -224,4 +231,4 @@ export default function AppointmentModal({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,47 +1,59 @@
|
||||
"use client";
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Bell, Plus } from 'lucide-react';
|
||||
import { useState } from "react";
|
||||
import { Bell, Plus } from "lucide-react";
|
||||
|
||||
interface WaitingPatient {
|
||||
id: string;
|
||||
name: string;
|
||||
specialty: string;
|
||||
preferredDate: string;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
priority: "high" | "medium" | "low";
|
||||
contact: string;
|
||||
}
|
||||
|
||||
interface ListaEsperaProps {
|
||||
interface ListaEsperaProperties {
|
||||
patients: WaitingPatient[];
|
||||
onNotify: (patientId: string) => void;
|
||||
onAddToWaitlist: () => void;
|
||||
}
|
||||
|
||||
export default function ListaEspera({ patients, onNotify, onAddToWaitlist }: ListaEsperaProps) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
export default function ListaEspera({
|
||||
patients,
|
||||
onNotify,
|
||||
onAddToWaitlist,
|
||||
}: ListaEsperaProperties) {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
const filteredPatients = patients.filter(patient =>
|
||||
patient.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
patient.specialty.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const filteredPatients = patients.filter(
|
||||
(patient) =>
|
||||
patient.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
patient.specialty.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
const getPriorityLabel = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high': return 'Alta';
|
||||
case 'medium': return 'Média';
|
||||
case 'low': return 'Baixa';
|
||||
default: return priority;
|
||||
case "high":
|
||||
return "Alta";
|
||||
case "medium":
|
||||
return "Média";
|
||||
case "low":
|
||||
return "Baixa";
|
||||
default:
|
||||
return priority;
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high': return 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300';
|
||||
case 'medium': return 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300';
|
||||
case 'low': return 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300';
|
||||
default: return 'bg-muted text-muted-foreground';
|
||||
case "high":
|
||||
return "bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300";
|
||||
case "medium":
|
||||
return "bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300";
|
||||
case "low":
|
||||
return "bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300";
|
||||
default:
|
||||
return "bg-muted text-muted-foreground";
|
||||
}
|
||||
};
|
||||
|
||||
@ -49,7 +61,9 @@ export default function ListaEspera({ patients, onNotify, onAddToWaitlist }: Lis
|
||||
<div className="bg-card border border-border rounded-lg shadow">
|
||||
<div className="p-4 border-b border-border">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-xl font-semibold text-foreground mb-4 sm:mb-0">Lista de Espera Inteligente</h2>
|
||||
<h2 className="text-xl font-semibold text-foreground mb-4 sm:mb-0">
|
||||
Lista de Espera Inteligente
|
||||
</h2>
|
||||
<button
|
||||
onClick={onAddToWaitlist}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-primary-foreground bg-primary hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
|
||||
@ -79,22 +93,40 @@ export default function ListaEspera({ patients, onNotify, onAddToWaitlist }: Lis
|
||||
<table className="min-w-full divide-y divide-border">
|
||||
<thead className="bg-muted/50">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Paciente
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Especialidade
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Data Preferencial
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Prioridade
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Contato
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Ações
|
||||
</th>
|
||||
</tr>
|
||||
@ -109,10 +141,12 @@ export default function ListaEspera({ patients, onNotify, onAddToWaitlist }: Lis
|
||||
{patient.specialty}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
|
||||
{new Date(patient.preferredDate).toLocaleDateString('pt-BR')}
|
||||
{new Date(patient.preferredDate).toLocaleDateString("pt-BR")}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getPriorityColor(patient.priority)}`}>
|
||||
<span
|
||||
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getPriorityColor(patient.priority)}`}
|
||||
>
|
||||
{getPriorityLabel(patient.priority)}
|
||||
</span>
|
||||
</td>
|
||||
@ -141,4 +175,4 @@ export default function ListaEspera({ patients, onNotify, onAddToWaitlist }: Lis
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// components/agendamento/index.ts
|
||||
export { default as AgendaCalendar } from './AgendaCalendar';
|
||||
export { default as AppointmentModal } from './AppointmentModal';
|
||||
export { default as ListaEspera } from './ListaEspera';
|
||||
export { default as AgendaCalendar } from "./AgendaCalendar";
|
||||
export { default as AppointmentModal } from "./AppointmentModal";
|
||||
export { default as ListaEspera } from "./ListaEspera";
|
||||
|
||||
@ -1,14 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { CheckCircle2, Copy, Eye, EyeOff } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
|
||||
export interface CredentialsDialogProps {
|
||||
export interface CredentialsDialogProperties {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
email: string;
|
||||
@ -24,7 +31,7 @@ export function CredentialsDialog({
|
||||
password,
|
||||
userName,
|
||||
userType,
|
||||
}: CredentialsDialogProps) {
|
||||
}: CredentialsDialogProperties) {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [copiedEmail, setCopiedEmail] = useState(false);
|
||||
const [copiedPassword, setCopiedPassword] = useState(false);
|
||||
@ -52,23 +59,27 @@ export function CredentialsDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
{userType === "médico" ? "Médico" : "Paciente"} Cadastrado com Sucesso!
|
||||
{userType === "médico" ? "Médico" : "Paciente"} Cadastrado com
|
||||
Sucesso!
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
O {userType} <strong>{userName}</strong> foi cadastrado e pode fazer login com as credenciais abaixo.
|
||||
O {userType} <strong>{userName}</strong> foi cadastrado e pode fazer
|
||||
login com as credenciais abaixo.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Alert className="bg-amber-50 border-amber-200">
|
||||
<AlertDescription className="text-amber-900">
|
||||
<strong>Importante:</strong> Anote ou copie estas credenciais agora. Por segurança, essa senha não será exibida novamente.
|
||||
<strong>Importante:</strong> Anote ou copie estas credenciais agora.
|
||||
Por segurança, essa senha não será exibida novamente.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Alert className="bg-blue-50 border-blue-200">
|
||||
<AlertDescription className="text-blue-900">
|
||||
<strong>📧 Confirme o email:</strong> Um email de confirmação foi enviado para <strong>{email}</strong>.
|
||||
O {userType} deve clicar no link de confirmação antes de fazer o primeiro login.
|
||||
<strong>📧 Confirme o email:</strong> Um email de confirmação foi
|
||||
enviado para <strong>{email}</strong>. O {userType} deve clicar no
|
||||
link de confirmação antes de fazer o primeiro login.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@ -76,12 +87,7 @@ export function CredentialsDialog({
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email de Acesso</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="email"
|
||||
value={email}
|
||||
readOnly
|
||||
className="bg-muted"
|
||||
/>
|
||||
<Input id="email" value={email} readOnly className="bg-muted" />
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@ -89,7 +95,11 @@ export function CredentialsDialog({
|
||||
onClick={handleCopyEmail}
|
||||
title="Copiar email"
|
||||
>
|
||||
{copiedEmail ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||
{copiedEmail ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -113,7 +123,11 @@ export function CredentialsDialog({
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
title={showPassword ? "Ocultar senha" : "Mostrar senha"}
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
@ -123,7 +137,11 @@ export function CredentialsDialog({
|
||||
onClick={handleCopyPassword}
|
||||
title="Copiar senha"
|
||||
>
|
||||
{copiedPassword ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||
{copiedPassword ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -134,8 +152,11 @@ export function CredentialsDialog({
|
||||
<ol className="list-decimal list-inside mt-2 space-y-1">
|
||||
<li>Compartilhe estas credenciais com o {userType}</li>
|
||||
<li>
|
||||
<strong className="text-blue-700">O {userType} deve confirmar o email</strong> clicando no link enviado para{" "}
|
||||
<strong>{email}</strong> (verifique também a pasta de spam)
|
||||
<strong className="text-blue-700">
|
||||
O {userType} deve confirmar o email
|
||||
</strong>{" "}
|
||||
clicando no link enviado para <strong>{email}</strong> (verifique
|
||||
também a pasta de spam)
|
||||
</li>
|
||||
<li>
|
||||
Após confirmar o email, o {userType} deve acessar:{" "}
|
||||
|
||||
@ -1,31 +1,40 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { Bell, ChevronDown } from "lucide-react"
|
||||
import { useAuth } from "@/hooks/useAuth"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { useState, useEffect, useRef } from "react"
|
||||
import { SidebarTrigger } from "../ui/sidebar"
|
||||
import { Bell, ChevronDown } from "lucide-react";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { SidebarTrigger } from "../ui/sidebar";
|
||||
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
|
||||
|
||||
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
|
||||
export function PagesHeader({
|
||||
title = "",
|
||||
subtitle = "",
|
||||
}: {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
}) {
|
||||
const { logout, user } = useAuth();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const dropdownReference = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Fechar dropdown quando clicar fora
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
if (
|
||||
dropdownReference.current &&
|
||||
!dropdownReference.current.contains(event.target as Node)
|
||||
) {
|
||||
setDropdownOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (dropdownOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}
|
||||
}, [dropdownOpen]);
|
||||
@ -46,21 +55,23 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
|
||||
</Button>
|
||||
|
||||
<SimpleThemeToggle />
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-primary border-primary bg-transparent shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground"
|
||||
asChild
|
||||
></Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-primary border-primary bg-transparent shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground"
|
||||
asChild
|
||||
></Button>
|
||||
{/* Avatar Dropdown Simples */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
<div className="relative" ref={dropdownReference}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="relative h-8 w-8 rounded-full border-2 border-border hover:border-primary"
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src="/avatars/01.png" alt="@usuario" />
|
||||
<AvatarFallback className="bg-primary text-primary-foreground font-semibold">RA</AvatarFallback>
|
||||
<AvatarFallback className="bg-primary text-primary-foreground font-semibold">
|
||||
RA
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
|
||||
@ -70,19 +81,28 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
|
||||
<div className="p-4 border-b border-border">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-semibold leading-none">
|
||||
{user?.userType === 'administrador' ? 'Administrador da Clínica' : 'Usuário do Sistema'}
|
||||
{user?.userType === "administrador"
|
||||
? "Administrador da Clínica"
|
||||
: "Usuário do Sistema"}
|
||||
</p>
|
||||
{user?.email ? (
|
||||
<p className="text-xs leading-none text-muted-foreground">{user.email}</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">
|
||||
{user.email}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs leading-none text-muted-foreground">Email não disponível</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">
|
||||
Email não disponível
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs leading-none text-primary font-medium">
|
||||
Tipo: {user?.userType === 'administrador' ? 'Administrador' : user?.userType || 'Não definido'}
|
||||
Tipo:{" "}
|
||||
{user?.userType === "administrador"
|
||||
? "Administrador"
|
||||
: user?.userType || "Não definido"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="py-1">
|
||||
<button className="w-full text-left px-4 py-2 text-sm hover:bg-accent cursor-pointer">
|
||||
👤 Perfil
|
||||
@ -91,11 +111,11 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
|
||||
⚙️ Configurações
|
||||
</button>
|
||||
<div className="border-t border-border my-1"></div>
|
||||
<button
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setDropdownOpen(false);
|
||||
|
||||
|
||||
// Usar sempre o logout do hook useAuth (ele já redireciona corretamente)
|
||||
logout();
|
||||
}}
|
||||
@ -109,5 +129,5 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { cn } from "@/lib/utils"
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Sidebar as ShadSidebar,
|
||||
SidebarHeader,
|
||||
@ -13,9 +13,9 @@ import {
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuButton,
|
||||
SidebarRail,
|
||||
} from "@/components/ui/sidebar"
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
import {
|
||||
Home,
|
||||
@ -26,7 +26,7 @@ import {
|
||||
BarChart3,
|
||||
Stethoscope,
|
||||
User,
|
||||
} from "lucide-react"
|
||||
} from "lucide-react";
|
||||
|
||||
const navigation = [
|
||||
{ name: "Dashboard", href: "/dashboard", icon: Home },
|
||||
@ -35,10 +35,10 @@ const navigation = [
|
||||
{ name: "Médicos", href: "/doutores", icon: User },
|
||||
{ name: "Consultas", href: "/consultas", icon: UserCheck },
|
||||
{ name: "Relatórios", href: "/dashboard/relatorios", icon: BarChart3 },
|
||||
]
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname()
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<ShadSidebar
|
||||
@ -72,32 +72,35 @@ export function Sidebar() {
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{navigation.map((item) => {
|
||||
const isActive = pathname === item.href ||
|
||||
(pathname.startsWith(item.href + "/") && item.href !== "/dashboard")
|
||||
|
||||
return (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild isActive={isActive}>
|
||||
<Link href={item.href} className="flex items-center">
|
||||
<item.icon className="mr-3 h-4 w-4 shrink-0" />
|
||||
<span className="truncate group-data-[collapsible=icon]:hidden">
|
||||
{item.name}
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
})}
|
||||
const isActive =
|
||||
pathname === item.href ||
|
||||
(pathname.startsWith(item.href + "/") &&
|
||||
item.href !== "/dashboard");
|
||||
|
||||
return (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild isActive={isActive}>
|
||||
<Link href={item.href} className="flex items-center">
|
||||
<item.icon className="mr-3 h-4 w-4 shrink-0" />
|
||||
<span className="truncate group-data-[collapsible=icon]:hidden">
|
||||
{item.name}
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>{/* espaço para perfil/logout, se quiser */}</SidebarFooter>
|
||||
<SidebarFooter>
|
||||
{/* espaço para perfil/logout, se quiser */}
|
||||
</SidebarFooter>
|
||||
|
||||
{/* rail clicável/hover que ajuda a reabrir/fechar */}
|
||||
<SidebarRail />
|
||||
</ShadSidebar>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
198
susconecta/components/dialogs/update-authorizations-dialog.tsx
Normal file
198
susconecta/components/dialogs/update-authorizations-dialog.tsx
Normal file
@ -0,0 +1,198 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Loader2, ShieldCheck } from "lucide-react";
|
||||
import type { AuthorizationRole } from "@/lib/api";
|
||||
|
||||
export type AuthorizationState = {
|
||||
paciente: boolean;
|
||||
medico: boolean;
|
||||
};
|
||||
|
||||
export interface UpdateAuthorizationsDialogProperties {
|
||||
open: boolean;
|
||||
entityType: "paciente" | "medico";
|
||||
entityName?: string | null;
|
||||
initialRoles?: Partial<AuthorizationState> | null;
|
||||
loading?: boolean;
|
||||
error?: string | null;
|
||||
disableSubmit?: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: (roles: AuthorizationState) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_ROLES: AuthorizationState = { paciente: false, medico: false };
|
||||
|
||||
export function UpdateAuthorizationsDialog({
|
||||
open,
|
||||
entityType,
|
||||
entityName,
|
||||
initialRoles,
|
||||
loading = false,
|
||||
error,
|
||||
disableSubmit = false,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
}: UpdateAuthorizationsDialogProperties) {
|
||||
const [roles, setRoles] = useState<AuthorizationState>(DEFAULT_ROLES);
|
||||
console.log("[Dialog] render", {
|
||||
open,
|
||||
initialRoles,
|
||||
loading,
|
||||
disableSubmit,
|
||||
roles,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log("[Dialog] useEffect open change", open, initialRoles);
|
||||
if (open) {
|
||||
setRoles({
|
||||
paciente: initialRoles?.paciente ?? entityType === "paciente",
|
||||
medico: initialRoles?.medico ?? entityType === "medico",
|
||||
});
|
||||
}
|
||||
}, [open, initialRoles, entityType]);
|
||||
|
||||
// Debug: log when roles state changes
|
||||
useEffect(() => {
|
||||
console.log("[Dialog] roles updated", roles);
|
||||
}, [roles]);
|
||||
|
||||
const title = useMemo(
|
||||
() =>
|
||||
entityType === "paciente"
|
||||
? "Atualizar autorizações do paciente"
|
||||
: "Atualizar autorizações do médico",
|
||||
[entityType],
|
||||
);
|
||||
|
||||
const description = useMemo(
|
||||
() =>
|
||||
entityName
|
||||
? `Defina quais tipos de acesso ${entityName} poderá utilizar no sistema.`
|
||||
: "Defina quem pode acessar este perfil.",
|
||||
[entityName],
|
||||
);
|
||||
|
||||
function handleToggle(
|
||||
role: AuthorizationRole,
|
||||
value: boolean | "indeterminate",
|
||||
) {
|
||||
console.log("[Dialog] toggle", role, value);
|
||||
setRoles((previous) => ({ ...previous, [role]: value === true }));
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
console.log("[Dialog] handleSave called with roles:", roles);
|
||||
console.log(
|
||||
"[Dialog] About to call onConfirm. Typeof onConfirm is:",
|
||||
typeof onConfirm,
|
||||
);
|
||||
if (typeof onConfirm === "function") {
|
||||
onConfirm(roles);
|
||||
// Não fecha aqui - deixa o componente pai fechar após processar
|
||||
} else {
|
||||
console.error("[Dialog] onConfirm is NOT a function! It is:", onConfirm);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<ShieldCheck className="h-5 w-5" />
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3 rounded-lg border p-4">
|
||||
<Checkbox
|
||||
id="auth-paciente"
|
||||
checked={roles.paciente}
|
||||
disabled={loading}
|
||||
onCheckedChange={(value) => handleToggle("paciente", value)}
|
||||
/>
|
||||
<div>
|
||||
<Label htmlFor="auth-paciente">Acesso como paciente</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Permite que este usuário acesse o portal do paciente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 rounded-lg border p-4">
|
||||
<Checkbox
|
||||
id="auth-medico"
|
||||
checked={roles.medico}
|
||||
disabled={loading}
|
||||
onCheckedChange={(value) => handleToggle("medico", value)}
|
||||
/>
|
||||
<div>
|
||||
<Label htmlFor="auth-medico">Acesso como médico</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Permite que este usuário acesse o portal profissional.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
Você pode habilitar as duas opções para permitir login como
|
||||
paciente e médico usando as mesmas credenciais.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
{/* Debug: botão nativo para garantir onClick */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
console.log("[Debug Save] native button clicked", {
|
||||
loading,
|
||||
disableSubmit,
|
||||
roles,
|
||||
});
|
||||
handleSave();
|
||||
}}
|
||||
disabled={loading}
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground px-4 py-2 disabled:opacity-50"
|
||||
>
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}Salvar
|
||||
alterações
|
||||
</button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -1,29 +1,40 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { ChevronUp } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ChevronUp } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function Footer() {
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||
}
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="bg-background border-t border-border">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between space-y-4 md:space-y-0">
|
||||
{}
|
||||
<div className="text-muted-foreground text-sm">© 2025 MEDI Connect</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
© 2025 MEDI Connect
|
||||
</div>
|
||||
|
||||
{}
|
||||
<nav className="flex items-center space-x-8">
|
||||
<a href="#" className="text-muted-foreground hover:text-primary transition-colors text-sm">
|
||||
<a
|
||||
href="#"
|
||||
className="text-muted-foreground hover:text-primary transition-colors text-sm"
|
||||
>
|
||||
Termos
|
||||
</a>
|
||||
<a href="#" className="text-muted-foreground hover:text-primary transition-colors text-sm">
|
||||
<a
|
||||
href="#"
|
||||
className="text-muted-foreground hover:text-primary transition-colors text-sm"
|
||||
>
|
||||
Privacidade (LGPD)
|
||||
</a>
|
||||
<a href="#" className="text-muted-foreground hover:text-primary transition-colors text-sm">
|
||||
<a
|
||||
href="#"
|
||||
className="text-muted-foreground hover:text-primary transition-colors text-sm"
|
||||
>
|
||||
Ajuda
|
||||
</a>
|
||||
</nav>
|
||||
@ -41,5 +52,5 @@ export function Footer() {
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
@ -45,17 +44,24 @@ const formatValidityDate = (value: string) => {
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
export function CalendarRegistrationForm({ formData, onFormChange }: CalendarRegistrationFormProperties) {
|
||||
export function CalendarRegistrationForm({
|
||||
formData,
|
||||
onFormChange,
|
||||
}: CalendarRegistrationFormProperties) {
|
||||
const [isAdditionalInfoOpen, setIsAdditionalInfoOpen] = useState(false);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const handleChange = (
|
||||
event: React.ChangeEvent<
|
||||
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||
>,
|
||||
) => {
|
||||
const { name, value } = event.target;
|
||||
|
||||
if (name === 'validade') {
|
||||
const formattedValue = formatValidityDate(value);
|
||||
onFormChange({ ...formData, [name]: formattedValue });
|
||||
if (name === "validade") {
|
||||
const formattedValue = formatValidityDate(value);
|
||||
onFormChange({ ...formData, [name]: formattedValue });
|
||||
} else {
|
||||
onFormChange({ ...formData, [name]: value });
|
||||
onFormChange({ ...formData, [name]: value });
|
||||
}
|
||||
};
|
||||
|
||||
@ -64,184 +70,303 @@ export function CalendarRegistrationForm({ formData, onFormChange }: CalendarReg
|
||||
<div className="border border-border rounded-md p-6 space-y-4 bg-card">
|
||||
<h2 className="font-medium text-foreground">Informações do paciente</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-4">
|
||||
<div className="md:col-span-6 space-y-2">
|
||||
<Label className="text-[13px]">Nome *</Label>
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<div className="md:col-span-6 space-y-2">
|
||||
<Label className="text-[13px]">Nome *</Label>
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
name="patientName"
|
||||
placeholder="Digite o nome do paciente"
|
||||
className="h-11 pl-8 rounded-md transition-colors hover:bg-muted/30"
|
||||
value={formData.patientName || ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-3 space-y-2">
|
||||
<Label className="text-[13px]">CPF do paciente</Label>
|
||||
<Input
|
||||
name="cpf"
|
||||
placeholder="Número do CPF"
|
||||
className="h-11 rounded-md transition-colors hover:bg-muted/30"
|
||||
value={formData.cpf || ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-3 space-y-2">
|
||||
<Label className="text-[13px]">RG</Label>
|
||||
<Input
|
||||
name="rg"
|
||||
placeholder="Número do RG"
|
||||
className="h-11 rounded-md transition-colors hover:bg-muted/30"
|
||||
value={formData.rg || ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-3 space-y-2">
|
||||
<Label className="text-[13px]">Data de nascimento *</Label>
|
||||
<Input
|
||||
name="birthDate"
|
||||
type="date"
|
||||
className="h-11 rounded-md transition-colors hover:bg-muted/30"
|
||||
value={formData.birthDate || ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-3 space-y-2">
|
||||
<Label className="text-[13px]">Telefone</Label>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
name="phoneCode"
|
||||
className="h-11 w-20 rounded-md border border-gray-300 dark:border-input bg-background text-foreground px-2 text-[13px] transition-colors hover:bg-muted/30 hover:border-gray-400"
|
||||
value={formData.phoneCode || "+55"}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="+55">+55</option>
|
||||
<option value="+351">+351</option>
|
||||
<option value="+1">+1</option>
|
||||
</select>
|
||||
<Input
|
||||
name="phoneNumber"
|
||||
placeholder="(99) 99999-9999"
|
||||
className="h-11 flex-1 rounded-md transition-colors hover:bg-muted/30"
|
||||
value={formData.phoneNumber || ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-6 space-y-2">
|
||||
<Label className="text-[13px]">E-mail</Label>
|
||||
<Input
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="email@exemplo.com"
|
||||
className="h-11 rounded-md transition-colors hover:bg-muted/30"
|
||||
value={formData.email || ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-6 space-y-2">
|
||||
<Label className="text-[13px]">Convênio</Label>
|
||||
<div className="relative">
|
||||
<select
|
||||
name="convenio"
|
||||
className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400"
|
||||
value={formData.convenio || ""}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Selecione um convênio
|
||||
</option>
|
||||
<option value="sulamerica">Sulamérica</option>
|
||||
<option value="bradesco">Bradesco Saúde</option>
|
||||
<option value="amil">Amil</option>
|
||||
<option value="unimed">Unimed</option>
|
||||
</select>
|
||||
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-6 space-y-2">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Matrícula</Label>
|
||||
<Input
|
||||
name="patientName"
|
||||
placeholder="Digite o nome do paciente"
|
||||
className="h-11 pl-8 rounded-md transition-colors hover:bg-muted/30"
|
||||
value={formData.patientName || ''}
|
||||
name="matricula"
|
||||
placeholder="000000000"
|
||||
maxLength={9}
|
||||
className="h-11 rounded-md transition-colors hover:bg-muted/30"
|
||||
value={formData.matricula || ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Validade</Label>
|
||||
<Input
|
||||
name="validade"
|
||||
placeholder="00/00/0000"
|
||||
className="h-11 rounded-md transition-colors hover:bg-muted/30"
|
||||
value={formData.validade || ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-3 space-y-2">
|
||||
<Label className="text-[13px]">CPF do paciente</Label>
|
||||
<Input name="cpf" placeholder="Número do CPF" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.cpf || ''} onChange={handleChange} />
|
||||
</div>
|
||||
<div className="md:col-span-3 space-y-2">
|
||||
<Label className="text-[13px]">RG</Label>
|
||||
<Input name="rg" placeholder="Número do RG" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.rg || ''} onChange={handleChange} />
|
||||
</div>
|
||||
<div className="md:col-span-3 space-y-2">
|
||||
<Label className="text-[13px]">Data de nascimento *</Label>
|
||||
<Input name="birthDate" type="date" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.birthDate || ''} onChange={handleChange} />
|
||||
</div>
|
||||
<div className="md:col-span-3 space-y-2">
|
||||
<Label className="text-[13px]">Telefone</Label>
|
||||
<div className="flex gap-2">
|
||||
<select name="phoneCode" className="h-11 w-20 rounded-md border border-gray-300 dark:border-input bg-background text-foreground px-2 text-[13px] transition-colors hover:bg-muted/30 hover:border-gray-400" value={formData.phoneCode || '+55'} onChange={handleChange}>
|
||||
<option value="+55">+55</option>
|
||||
<option value="+351">+351</option>
|
||||
<option value="+1">+1</option>
|
||||
</select>
|
||||
<Input name="phoneNumber" placeholder="(99) 99999-9999" className="h-11 flex-1 rounded-md transition-colors hover:bg-muted/30" value={formData.phoneNumber || ''} onChange={handleChange} />
|
||||
</div>
|
||||
<div className="md:col-span-12 space-y-2">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer"
|
||||
onClick={() => setIsAdditionalInfoOpen(!isAdditionalInfoOpen)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-sm font-medium cursor-pointer text-primary m-0">
|
||||
Informações adicionais
|
||||
</Label>
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 text-primary transition-transform duration-200 ${isAdditionalInfoOpen ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-6 space-y-2">
|
||||
<Label className="text-[13px]">E-mail</Label>
|
||||
<Input name="email" type="email" placeholder="email@exemplo.com" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.email || ''} onChange={handleChange} />
|
||||
</div>
|
||||
<div className="md:col-span-6 space-y-2">
|
||||
<Label className="text-[13px]">Convênio</Label>
|
||||
<div className="relative">
|
||||
<select name="convenio" className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400" value={formData.convenio || ''} onChange={handleChange}>
|
||||
<option value="" disabled>Selecione um convênio</option>
|
||||
<option value="sulamerica">Sulamérica</option>
|
||||
<option value="bradesco">Bradesco Saúde</option>
|
||||
<option value="amil">Amil</option>
|
||||
<option value="unimed">Unimed</option>
|
||||
</select>
|
||||
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-6 space-y-2">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Matrícula</Label>
|
||||
<Input name="matricula" placeholder="000000000" maxLength={9} className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.matricula || ''} onChange={handleChange} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Validade</Label>
|
||||
<Input name="validade" placeholder="00/00/0000" className="h-11 rounded-md transition-colors hover:bg-muted/30" value={formData.validade || ''} onChange={handleChange} />
|
||||
</div>
|
||||
{isAdditionalInfoOpen && (
|
||||
<div className="space-y-2">
|
||||
<div className="relative">
|
||||
<select
|
||||
name="documentos"
|
||||
className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400"
|
||||
value={formData.documentos || ""}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Documentos e anexos
|
||||
</option>
|
||||
<option value="identidade">Identidade / CPF</option>
|
||||
<option value="comprovante_residencia">
|
||||
Comprovante de residência
|
||||
</option>
|
||||
<option value="guias">Guias / Encaminhamentos</option>
|
||||
<option value="outros">Outros</option>
|
||||
</select>
|
||||
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-12 space-y-2">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer"
|
||||
onClick={() => setIsAdditionalInfoOpen(!isAdditionalInfoOpen)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-sm font-medium cursor-pointer text-primary m-0">Informações adicionais</Label>
|
||||
<ChevronDown className={`h-4 w-4 text-primary transition-transform duration-200 ${isAdditionalInfoOpen ? 'rotate-180' : ''}`} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isAdditionalInfoOpen && (
|
||||
<div className="space-y-2">
|
||||
<div className="relative">
|
||||
<select
|
||||
name="documentos"
|
||||
className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400"
|
||||
value={formData.documentos || ''}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Documentos e anexos
|
||||
</option>
|
||||
<option value="identidade">Identidade / CPF</option>
|
||||
<option value="comprovante_residencia">Comprovante de residência</option>
|
||||
<option value="guias">Guias / Encaminhamentos</option>
|
||||
<option value="outros">Outros</option>
|
||||
</select>
|
||||
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-border rounded-md p-6 space-y-4 bg-card">
|
||||
<h2 className="font-medium text-foreground">Informações do atendimento</h2>
|
||||
<h2 className="font-medium text-foreground">
|
||||
Informações do atendimento
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Nome do profissional *</Label>
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input name="professionalName" className="h-11 w-full rounded-md pl-8 pr-12 text-[13px] transition-colors hover:bg-muted/30" value={formData.professionalName || ''} onChange={handleChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Unidade *</Label>
|
||||
<select name="unit" className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400" value={formData.unit || 'nei'} onChange={handleChange}>
|
||||
<option value="nei">Núcleo de Especialidades Integradas</option>
|
||||
<option value="cc">Clínica Central</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Data *</Label>
|
||||
<div className="relative">
|
||||
<Calendar className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input name="appointmentDate" type="date" className="h-11 w-full rounded-md pl-8 pr-3 text-[13px] transition-colors hover:bg-muted/30" value={formData.appointmentDate || ''} onChange={handleChange} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Início *</Label>
|
||||
<Input name="startTime" type="time" className="h-11 w-full rounded-md px-3 text-[13px] transition-colors hover:bg-muted/30" value={formData.startTime || ''} onChange={handleChange} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Término *</Label>
|
||||
<Input name="endTime" type="time" className="h-11 w-full rounded-md px-3 text-[13px] transition-colors hover:bg-muted/30" value={formData.endTime || ''} onChange={handleChange} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Profissional solicitante</Label>
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<select name="requestingProfessional" className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-8 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400" value={formData.requestingProfessional || ''} onChange={handleChange}>
|
||||
<option value="" disabled>Selecione solicitante</option>
|
||||
<option value="dr-a">Dr. A</option>
|
||||
<option value="dr-b">Dr. B</option>
|
||||
</select>
|
||||
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Nome do profissional *</Label>
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
name="professionalName"
|
||||
className="h-11 w-full rounded-md pl-8 pr-12 text-[13px] transition-colors hover:bg-muted/30"
|
||||
value={formData.professionalName || ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-[13px]">Tipo de atendimento *</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input type="checkbox" id="reembolso" className="h-4 w-4" />
|
||||
<Label htmlFor="reembolso" className="text-[13px] font-medium">Pagamento via Reembolso</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-1">
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input name="appointmentType" placeholder="Pesquisar" className="h-11 w-full rounded-md pl-8 pr-8 text-[13px] transition-colors hover:bg-muted/30" value={formData.appointmentType || ''} onChange={handleChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-[13px]">Observações</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input type="checkbox" id="imprimir" className="h-4 w-4" />
|
||||
<Label htmlFor="imprimir" className="text-[13px] font-medium">Imprimir na Etiqueta / Pulseira</Label>
|
||||
</div>
|
||||
</div>
|
||||
<Textarea name="notes" rows={6} className="text-[13px] min-h-[120px] resize-none rounded-md transition-colors hover:bg-muted/30" value={formData.notes || ''} onChange={handleChange} />
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Unidade *</Label>
|
||||
<select
|
||||
name="unit"
|
||||
className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-3 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400"
|
||||
value={formData.unit || "nei"}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="nei">
|
||||
Núcleo de Especialidades Integradas
|
||||
</option>
|
||||
<option value="cc">Clínica Central</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Data *</Label>
|
||||
<div className="relative">
|
||||
<Calendar className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
name="appointmentDate"
|
||||
type="date"
|
||||
className="h-11 w-full rounded-md pl-8 pr-3 text-[13px] transition-colors hover:bg-muted/30"
|
||||
value={formData.appointmentDate || ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Início *</Label>
|
||||
<Input
|
||||
name="startTime"
|
||||
type="time"
|
||||
className="h-11 w-full rounded-md px-3 text-[13px] transition-colors hover:bg-muted/30"
|
||||
value={formData.startTime || ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Término *</Label>
|
||||
<Input
|
||||
name="endTime"
|
||||
type="time"
|
||||
className="h-11 w-full rounded-md px-3 text-[13px] transition-colors hover:bg-muted/30"
|
||||
value={formData.endTime || ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[13px]">Profissional solicitante</Label>
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<select
|
||||
name="requestingProfessional"
|
||||
className="h-11 w-full rounded-md border border-gray-300 dark:border-input bg-background text-foreground pr-8 pl-8 text-[13px] appearance-none transition-colors hover:bg-muted/30 hover:border-gray-400"
|
||||
value={formData.requestingProfessional || ""}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Selecione solicitante
|
||||
</option>
|
||||
<option value="dr-a">Dr. A</option>
|
||||
<option value="dr-b">Dr. B</option>
|
||||
</select>
|
||||
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-[13px]">Tipo de atendimento *</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input type="checkbox" id="reembolso" className="h-4 w-4" />
|
||||
<Label
|
||||
htmlFor="reembolso"
|
||||
className="text-[13px] font-medium"
|
||||
>
|
||||
Pagamento via Reembolso
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-1">
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
name="appointmentType"
|
||||
placeholder="Pesquisar"
|
||||
className="h-11 w-full rounded-md pl-8 pr-8 text-[13px] transition-colors hover:bg-muted/30"
|
||||
value={formData.appointmentType || ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-[13px]">Observações</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input type="checkbox" id="imprimir" className="h-4 w-4" />
|
||||
<Label htmlFor="imprimir" className="text-[13px] font-medium">
|
||||
Imprimir na Etiqueta / Pulseira
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<Textarea
|
||||
name="notes"
|
||||
rows={6}
|
||||
className="text-[13px] min-h-[120px] resize-none rounded-md transition-colors hover:bg-muted/30"
|
||||
value={formData.notes || ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,3 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
@ -6,12 +5,39 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { AlertCircle, ChevronDown, ChevronUp, FileImage, Loader2, Save, Upload, User, X, XCircle, Trash2 } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
AlertCircle,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
FileImage,
|
||||
Loader2,
|
||||
Save,
|
||||
Upload,
|
||||
User,
|
||||
X,
|
||||
XCircle,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
Paciente,
|
||||
@ -31,13 +57,11 @@ import {
|
||||
|
||||
import { validarCPFLocal } from "@/lib/utils";
|
||||
import { verificarCpfDuplicado } from "@/lib/api";
|
||||
import { CredentialsDialog } from "@/components/credentials-dialog";
|
||||
|
||||
|
||||
import { CredentialsDialog } from "@/components/credentials-dialog";
|
||||
|
||||
type Mode = "create" | "edit";
|
||||
|
||||
export interface PatientRegistrationFormProps {
|
||||
export interface PatientRegistrationFormProperties {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
patientId?: string | number | null;
|
||||
@ -54,7 +78,7 @@ type FormData = {
|
||||
cpf: string;
|
||||
rg: string;
|
||||
sexo: string;
|
||||
birth_date: string; // 👈 corrigido
|
||||
birth_date: string; // 👈 corrigido
|
||||
email: string;
|
||||
telefone: string;
|
||||
cep: string;
|
||||
@ -75,7 +99,7 @@ const initial: FormData = {
|
||||
cpf: "",
|
||||
rg: "",
|
||||
sexo: "",
|
||||
birth_date: "", // 👈 corrigido
|
||||
birth_date: "", // 👈 corrigido
|
||||
email: "",
|
||||
telefone: "",
|
||||
cep: "",
|
||||
@ -89,8 +113,6 @@ const initial: FormData = {
|
||||
anexos: [],
|
||||
};
|
||||
|
||||
|
||||
|
||||
export function PatientRegistrationForm({
|
||||
open = true,
|
||||
onOpenChange,
|
||||
@ -99,54 +121,62 @@ export function PatientRegistrationForm({
|
||||
mode = "create",
|
||||
onSaved,
|
||||
onClose,
|
||||
}: PatientRegistrationFormProps) {
|
||||
}: PatientRegistrationFormProperties) {
|
||||
const [form, setForm] = useState<FormData>(initial);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false });
|
||||
const [expanded, setExpanded] = useState({
|
||||
dados: true,
|
||||
contato: false,
|
||||
endereco: false,
|
||||
obs: false,
|
||||
});
|
||||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
const [isSearchingCEP, setSearchingCEP] = useState(false);
|
||||
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
|
||||
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
|
||||
|
||||
|
||||
// Estados para o dialog de credenciais
|
||||
const [showCredentials, setShowCredentials] = useState(false);
|
||||
const [credentials, setCredentials] = useState<CreateUserWithPasswordResponse | null>(null);
|
||||
const [credentials, setCredentials] =
|
||||
useState<CreateUserWithPasswordResponse | null>(null);
|
||||
const [savedPatient, setSavedPatient] = useState<Paciente | null>(null);
|
||||
|
||||
const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]);
|
||||
const title = useMemo(
|
||||
() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"),
|
||||
[mode],
|
||||
);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
if (mode !== "edit" || patientId == null) return;
|
||||
if (mode !== "edit" || patientId == undefined) return;
|
||||
try {
|
||||
console.log("[PatientForm] Carregando paciente ID:", patientId);
|
||||
const p = await buscarPacientePorId(String(patientId));
|
||||
console.log("[PatientForm] Dados recebidos:", p);
|
||||
setForm((s) => ({
|
||||
...s,
|
||||
nome: p.full_name || "", // 👈 trocar nome → full_name
|
||||
nome_social: p.social_name || "",
|
||||
cpf: p.cpf || "",
|
||||
rg: p.rg || "",
|
||||
sexo: p.sex || "",
|
||||
birth_date: p.birth_date || "", // 👈 trocar data_nascimento → birth_date
|
||||
telefone: p.phone_mobile || "",
|
||||
email: p.email || "",
|
||||
cep: p.cep || "",
|
||||
logradouro: p.street || "",
|
||||
numero: p.number || "",
|
||||
complemento: p.complement || "",
|
||||
bairro: p.neighborhood || "",
|
||||
cidade: p.city || "",
|
||||
estado: p.state || "",
|
||||
observacoes: p.notes || "",
|
||||
}));
|
||||
...s,
|
||||
nome: p.full_name || "", // 👈 trocar nome → full_name
|
||||
nome_social: p.social_name || "",
|
||||
cpf: p.cpf || "",
|
||||
rg: p.rg || "",
|
||||
sexo: p.sex || "",
|
||||
birth_date: p.birth_date || "", // 👈 trocar data_nascimento → birth_date
|
||||
telefone: p.phone_mobile || "",
|
||||
email: p.email || "",
|
||||
cep: p.cep || "",
|
||||
logradouro: p.street || "",
|
||||
numero: p.number || "",
|
||||
complemento: p.complement || "",
|
||||
bairro: p.neighborhood || "",
|
||||
cidade: p.city || "",
|
||||
estado: p.state || "",
|
||||
observacoes: p.notes || "",
|
||||
}));
|
||||
|
||||
const ax = await listarAnexos(String(patientId)).catch(() => []);
|
||||
setServerAnexos(Array.isArray(ax) ? ax : []);
|
||||
} catch (err) {
|
||||
console.error("[PatientForm] Erro ao carregar paciente:", err);
|
||||
} catch (error) {
|
||||
console.error("[PatientForm] Erro ao carregar paciente:", error);
|
||||
}
|
||||
}
|
||||
load();
|
||||
@ -159,7 +189,10 @@ export function PatientRegistrationForm({
|
||||
|
||||
function formatCPF(v: string) {
|
||||
const n = v.replace(/\D/g, "").slice(0, 11);
|
||||
return n.replace(/(\d{3})(\d{3})(\d{3})(\d{0,2})/, (_, a, b, c, d) => `${a}.${b}.${c}${d ? "-" + d : ""}`);
|
||||
return n.replace(
|
||||
/(\d{3})(\d{3})(\d{3})(\d{0,2})/,
|
||||
(_, a, b, c, d) => `${a}.${b}.${c}${d ? "-" + d : ""}`,
|
||||
);
|
||||
}
|
||||
function handleCPFChange(v: string) {
|
||||
setField("cpf", formatCPF(v));
|
||||
@ -167,7 +200,10 @@ export function PatientRegistrationForm({
|
||||
|
||||
function formatCEP(v: string) {
|
||||
const n = v.replace(/\D/g, "").slice(0, 8);
|
||||
return n.replace(/(\d{5})(\d{0,3})/, (_, a, b) => `${a}${b ? "-" + b : ""}`);
|
||||
return n.replace(
|
||||
/(\d{5})(\d{0,3})/,
|
||||
(_, a, b) => `${a}${b ? "-" + b : ""}`,
|
||||
);
|
||||
}
|
||||
async function fillFromCEP(cep: string) {
|
||||
const clean = cep.replace(/\D/g, "");
|
||||
@ -199,52 +235,48 @@ export function PatientRegistrationForm({
|
||||
}
|
||||
|
||||
function toPayload(): PacienteInput {
|
||||
return {
|
||||
full_name: form.nome, // 👈 troca 'nome' por 'full_name'
|
||||
social_name: form.nome_social || null,
|
||||
cpf: form.cpf,
|
||||
rg: form.rg || null,
|
||||
sex: form.sexo || null,
|
||||
birth_date: form.birth_date || null, // 👈 troca data_nascimento → birth_date
|
||||
phone_mobile: form.telefone || null,
|
||||
email: form.email || null,
|
||||
cep: form.cep || null,
|
||||
street: form.logradouro || null,
|
||||
number: form.numero || null,
|
||||
complement: form.complemento || null,
|
||||
neighborhood: form.bairro || null,
|
||||
city: form.cidade || null,
|
||||
state: form.estado || null,
|
||||
notes: form.observacoes || null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
full_name: form.nome, // 👈 troca 'nome' por 'full_name'
|
||||
social_name: form.nome_social || null,
|
||||
cpf: form.cpf,
|
||||
rg: form.rg || null,
|
||||
sex: form.sexo || null,
|
||||
birth_date: form.birth_date || null, // 👈 troca data_nascimento → birth_date
|
||||
phone_mobile: form.telefone || null,
|
||||
email: form.email || null,
|
||||
cep: form.cep || null,
|
||||
street: form.logradouro || null,
|
||||
number: form.numero || null,
|
||||
complement: form.complemento || null,
|
||||
neighborhood: form.bairro || null,
|
||||
city: form.cidade || null,
|
||||
state: form.estado || null,
|
||||
notes: form.observacoes || null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function handleSubmit(ev: React.FormEvent) {
|
||||
ev.preventDefault();
|
||||
async function handleSubmit(event_: React.FormEvent) {
|
||||
event_.preventDefault();
|
||||
if (!validateLocal()) return;
|
||||
|
||||
|
||||
try {
|
||||
// 1) validação local
|
||||
if (!validarCPFLocal(form.cpf)) {
|
||||
setErrors((e) => ({ ...e, cpf: "CPF inválido" }));
|
||||
return;
|
||||
}
|
||||
// 1) validação local
|
||||
if (!validarCPFLocal(form.cpf)) {
|
||||
setErrors((e) => ({ ...e, cpf: "CPF inválido" }));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) checar duplicidade no banco (apenas se criando novo paciente)
|
||||
if (mode === "create") {
|
||||
const existe = await verificarCpfDuplicado(form.cpf);
|
||||
if (existe) {
|
||||
setErrors((e) => ({ ...e, cpf: "CPF já cadastrado no sistema" }));
|
||||
return;
|
||||
// 2) checar duplicidade no banco (apenas se criando novo paciente)
|
||||
if (mode === "create") {
|
||||
const existe = await verificarCpfDuplicado(form.cpf);
|
||||
if (existe) {
|
||||
setErrors((e) => ({ ...e, cpf: "CPF já cadastrado no sistema" }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erro ao validar CPF", error);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Erro ao validar CPF", err);
|
||||
}
|
||||
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
@ -254,7 +286,8 @@ export function PatientRegistrationForm({
|
||||
if (mode === "create") {
|
||||
saved = await criarPaciente(payload);
|
||||
} else {
|
||||
if (patientId == null) throw new Error("Paciente inexistente para edição");
|
||||
if (patientId == undefined)
|
||||
throw new Error("Paciente inexistente para edição");
|
||||
saved = await atualizarPaciente(String(patientId), payload);
|
||||
}
|
||||
|
||||
@ -273,66 +306,78 @@ export function PatientRegistrationForm({
|
||||
}
|
||||
|
||||
// Se for criação de novo paciente e tiver email válido, cria usuário
|
||||
if (mode === "create" && form.email && form.email.includes('@')) {
|
||||
if (mode === "create" && form.email && form.email.includes("@")) {
|
||||
console.log("🔐 Iniciando criação de usuário para o paciente...");
|
||||
console.log("📧 Email:", form.email);
|
||||
console.log("👤 Nome:", form.nome);
|
||||
console.log("📱 Telefone:", form.telefone);
|
||||
|
||||
|
||||
try {
|
||||
const userCredentials = await criarUsuarioPaciente({
|
||||
email: form.email,
|
||||
full_name: form.nome,
|
||||
phone_mobile: form.telefone,
|
||||
});
|
||||
|
||||
|
||||
console.log("✅ Usuário criado com sucesso!", userCredentials);
|
||||
console.log("🔑 Senha gerada:", userCredentials.password);
|
||||
|
||||
|
||||
// Armazena as credenciais e mostra o dialog
|
||||
console.log("📋 Antes de setCredentials - credentials atual:", credentials);
|
||||
console.log("📋 Antes de setShowCredentials - showCredentials atual:", showCredentials);
|
||||
|
||||
console.log(
|
||||
"📋 Antes de setCredentials - credentials atual:",
|
||||
credentials,
|
||||
);
|
||||
console.log(
|
||||
"📋 Antes de setShowCredentials - showCredentials atual:",
|
||||
showCredentials,
|
||||
);
|
||||
|
||||
setCredentials(userCredentials);
|
||||
setShowCredentials(true);
|
||||
|
||||
|
||||
console.log("📋 Depois de set - credentials:", userCredentials);
|
||||
console.log("📋 Depois de set - showCredentials: true");
|
||||
console.log("📋 Modo inline?", inline);
|
||||
console.log("📋 userCredentials completo:", JSON.stringify(userCredentials));
|
||||
|
||||
console.log(
|
||||
"📋 userCredentials completo:",
|
||||
JSON.stringify(userCredentials),
|
||||
);
|
||||
|
||||
// Força re-render
|
||||
setTimeout(() => {
|
||||
console.log("⏰ Timeout - credentials:", credentials);
|
||||
console.log("⏰ Timeout - showCredentials:", showCredentials);
|
||||
}, 100);
|
||||
|
||||
|
||||
console.log("📋 Credenciais definidas, dialog deve aparecer!");
|
||||
|
||||
|
||||
// Salva o paciente para chamar onSaved depois
|
||||
setSavedPatient(saved);
|
||||
|
||||
|
||||
// ⚠️ NÃO chama onSaved aqui! O dialog vai chamar quando fechar.
|
||||
// Se chamar agora, o formulário fecha e o dialog desaparece.
|
||||
console.log("⚠️ NÃO chamando onSaved ainda - aguardando dialog fechar");
|
||||
|
||||
console.log(
|
||||
"⚠️ NÃO chamando onSaved ainda - aguardando dialog fechar",
|
||||
);
|
||||
|
||||
// RETORNA AQUI para não executar o código abaixo
|
||||
return;
|
||||
|
||||
} catch (userError: any) {
|
||||
console.error("❌ ERRO ao criar usuário:", userError);
|
||||
console.error("📋 Stack trace:", userError?.stack);
|
||||
const errorMessage = userError?.message || "Erro desconhecido";
|
||||
console.error("<22> Mensagem:", errorMessage);
|
||||
|
||||
|
||||
// Mostra erro mas fecha o formulário normalmente
|
||||
alert(`Paciente cadastrado com sucesso!\n\n⚠️ Porém, houve erro ao criar usuário de acesso:\n${errorMessage}\n\nVerifique os logs do console (F12) para mais detalhes.`);
|
||||
|
||||
alert(
|
||||
`Paciente cadastrado com sucesso!\n\n⚠️ Porém, houve erro ao criar usuário de acesso:\n${errorMessage}\n\nVerifique os logs do console (F12) para mais detalhes.`,
|
||||
);
|
||||
|
||||
// Fecha o formulário mesmo com erro na criação de usuário
|
||||
setForm(initial);
|
||||
setPhotoPreview(null);
|
||||
setServerAnexos([]);
|
||||
|
||||
|
||||
if (inline) onClose?.();
|
||||
else onOpenChange?.(false);
|
||||
}
|
||||
@ -340,22 +385,24 @@ export function PatientRegistrationForm({
|
||||
console.log("⚠️ Não criará usuário. Motivo:");
|
||||
console.log(" - Mode:", mode);
|
||||
console.log(" - Email:", form.email);
|
||||
console.log(" - Tem @:", form.email?.includes('@'));
|
||||
|
||||
console.log(" - Tem @:", form.email?.includes("@"));
|
||||
|
||||
// Se não for criar usuário, fecha normalmente
|
||||
setForm(initial);
|
||||
setPhotoPreview(null);
|
||||
setServerAnexos([]);
|
||||
|
||||
|
||||
if (inline) onClose?.();
|
||||
else onOpenChange?.(false);
|
||||
|
||||
alert(mode === "create" ? "Paciente cadastrado!" : "Paciente atualizado!");
|
||||
alert(
|
||||
mode === "create" ? "Paciente cadastrado!" : "Paciente atualizado!",
|
||||
);
|
||||
}
|
||||
|
||||
onSaved?.(saved);
|
||||
} catch (err: any) {
|
||||
setErrors({ submit: err?.message || "Erro ao salvar paciente." });
|
||||
} catch (error: any) {
|
||||
setErrors({ submit: error?.message || "Erro ao salvar paciente." });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@ -370,7 +417,8 @@ export function PatientRegistrationForm({
|
||||
}
|
||||
setField("photo", f);
|
||||
const fr = new FileReader();
|
||||
fr.onload = (ev) => setPhotoPreview(String(ev.target?.result || ""));
|
||||
fr.onload = (event_) =>
|
||||
setPhotoPreview(String(event_.target?.result || ""));
|
||||
fr.readAsDataURL(f);
|
||||
}
|
||||
|
||||
@ -378,9 +426,9 @@ export function PatientRegistrationForm({
|
||||
const fs = Array.from(e.target.files || []);
|
||||
setField("anexos", [...form.anexos, ...fs]);
|
||||
}
|
||||
function removeLocalAnexo(idx: number) {
|
||||
function removeLocalAnexo(index: number) {
|
||||
const clone = [...form.anexos];
|
||||
clone.splice(idx, 1);
|
||||
clone.splice(index, 1);
|
||||
setField("anexos", clone);
|
||||
}
|
||||
|
||||
@ -398,7 +446,9 @@ export function PatientRegistrationForm({
|
||||
if (mode !== "edit" || !patientId) return;
|
||||
try {
|
||||
await removerAnexo(String(patientId), anexoId);
|
||||
setServerAnexos((prev) => prev.filter((a) => String(a.id ?? a.anexo_id) !== String(anexoId)));
|
||||
setServerAnexos((previous) =>
|
||||
previous.filter((a) => String(a.id ?? a.anexo_id) !== String(anexoId)),
|
||||
);
|
||||
} catch (e: any) {
|
||||
alert(e?.message || "Não foi possível remover o anexo.");
|
||||
}
|
||||
@ -415,7 +465,10 @@ export function PatientRegistrationForm({
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{}
|
||||
<Collapsible open={expanded.dados} onOpenChange={() => setExpanded((s) => ({ ...s, dados: !s.dados }))}>
|
||||
<Collapsible
|
||||
open={expanded.dados}
|
||||
onOpenChange={() => setExpanded((s) => ({ ...s, dados: !s.dados }))}
|
||||
>
|
||||
<Card>
|
||||
<CollapsibleTrigger asChild>
|
||||
<CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
|
||||
@ -424,7 +477,11 @@ export function PatientRegistrationForm({
|
||||
<User className="h-4 w-4" />
|
||||
Dados Pessoais
|
||||
</span>
|
||||
{expanded.dados ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
{expanded.dados ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</CollapsibleTrigger>
|
||||
@ -433,27 +490,51 @@ export function PatientRegistrationForm({
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-24 h-24 border-2 border-dashed border-muted-foreground rounded-lg flex items-center justify-center overflow-hidden">
|
||||
{photoPreview ? (
|
||||
|
||||
<img src={photoPreview} alt="Preview" className="w-full h-full object-cover" />
|
||||
<img
|
||||
src={photoPreview}
|
||||
alt="Preview"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<FileImage className="h-8 w-8 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="photo" className="cursor-pointer rounded-md transition-colors">
|
||||
<Button type="button" variant="ghost" asChild className="bg-primary text-primary-foreground border-transparent hover:bg-primary">
|
||||
<Label
|
||||
htmlFor="photo"
|
||||
className="cursor-pointer rounded-md transition-colors"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
asChild
|
||||
className="bg-primary text-primary-foreground border-transparent hover:bg-primary"
|
||||
>
|
||||
<span>
|
||||
<Upload className="mr-2 h-4 w-4 text-primary-foreground" /> Carregar Foto
|
||||
<Upload className="mr-2 h-4 w-4 text-primary-foreground" />{" "}
|
||||
Carregar Foto
|
||||
</span>
|
||||
</Button>
|
||||
</Label>
|
||||
<Input id="photo" type="file" accept="image/*" className="hidden" onChange={handlePhoto} />
|
||||
<Input
|
||||
id="photo"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handlePhoto}
|
||||
/>
|
||||
{mode === "edit" && (
|
||||
<Button type="button" variant="ghost" onClick={handleRemoverFotoServidor}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={handleRemoverFotoServidor}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" /> Remover foto
|
||||
</Button>
|
||||
)}
|
||||
{errors.photo && <p className="text-sm text-destructive">{errors.photo}</p>}
|
||||
{errors.photo && (
|
||||
<p className="text-sm text-destructive">{errors.photo}</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">Máximo 5MB</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -461,12 +542,21 @@ export function PatientRegistrationForm({
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Nome *</Label>
|
||||
<Input value={form.nome} onChange={(e) => setField("nome", e.target.value)} className={errors.nome ? "border-destructive" : ""} />
|
||||
{errors.nome && <p className="text-sm text-destructive">{errors.nome}</p>}
|
||||
<Input
|
||||
value={form.nome}
|
||||
onChange={(e) => setField("nome", e.target.value)}
|
||||
className={errors.nome ? "border-destructive" : ""}
|
||||
/>
|
||||
{errors.nome && (
|
||||
<p className="text-sm text-destructive">{errors.nome}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Nome Social</Label>
|
||||
<Input value={form.nome_social} onChange={(e) => setField("nome_social", e.target.value)} />
|
||||
<Input
|
||||
value={form.nome_social}
|
||||
onChange={(e) => setField("nome_social", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -480,18 +570,26 @@ export function PatientRegistrationForm({
|
||||
maxLength={14}
|
||||
className={errors.cpf ? "border-destructive" : ""}
|
||||
/>
|
||||
{errors.cpf && <p className="text-sm text-destructive">{errors.cpf}</p>}
|
||||
{errors.cpf && (
|
||||
<p className="text-sm text-destructive">{errors.cpf}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>RG</Label>
|
||||
<Input value={form.rg} onChange={(e) => setField("rg", e.target.value)} />
|
||||
<Input
|
||||
value={form.rg}
|
||||
onChange={(e) => setField("rg", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Sexo</Label>
|
||||
<Select value={form.sexo} onValueChange={(v) => setField("sexo", v)}>
|
||||
<Select
|
||||
value={form.sexo}
|
||||
onValueChange={(v) => setField("sexo", v)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione o sexo" />
|
||||
</SelectTrigger>
|
||||
@ -504,8 +602,11 @@ export function PatientRegistrationForm({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Data de Nascimento</Label>
|
||||
<Input type="date" value={form.birth_date} onChange={(e) => setField("birth_date", e.target.value)} />
|
||||
|
||||
<Input
|
||||
type="date"
|
||||
value={form.birth_date}
|
||||
onChange={(e) => setField("birth_date", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -514,13 +615,22 @@ export function PatientRegistrationForm({
|
||||
</Collapsible>
|
||||
|
||||
{}
|
||||
<Collapsible open={expanded.contato} onOpenChange={() => setExpanded((s) => ({ ...s, contato: !s.contato }))}>
|
||||
<Collapsible
|
||||
open={expanded.contato}
|
||||
onOpenChange={() =>
|
||||
setExpanded((s) => ({ ...s, contato: !s.contato }))
|
||||
}
|
||||
>
|
||||
<Card>
|
||||
<CollapsibleTrigger asChild>
|
||||
<CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Contato</span>
|
||||
{expanded.contato ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
{expanded.contato ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</CollapsibleTrigger>
|
||||
@ -529,11 +639,17 @@ export function PatientRegistrationForm({
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>E-mail</Label>
|
||||
<Input value={form.email} onChange={(e) => setField("email", e.target.value)} />
|
||||
<Input
|
||||
value={form.email}
|
||||
onChange={(e) => setField("email", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Telefone</Label>
|
||||
<Input value={form.telefone} onChange={(e) => setField("telefone", e.target.value)} />
|
||||
<Input
|
||||
value={form.telefone}
|
||||
onChange={(e) => setField("telefone", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -542,13 +658,22 @@ export function PatientRegistrationForm({
|
||||
</Collapsible>
|
||||
|
||||
{}
|
||||
<Collapsible open={expanded.endereco} onOpenChange={() => setExpanded((s) => ({ ...s, endereco: !s.endereco }))}>
|
||||
<Collapsible
|
||||
open={expanded.endereco}
|
||||
onOpenChange={() =>
|
||||
setExpanded((s) => ({ ...s, endereco: !s.endereco }))
|
||||
}
|
||||
>
|
||||
<Card>
|
||||
<CollapsibleTrigger asChild>
|
||||
<CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Endereço</span>
|
||||
{expanded.endereco ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
{expanded.endereco ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</CollapsibleTrigger>
|
||||
@ -570,39 +695,62 @@ export function PatientRegistrationForm({
|
||||
disabled={isSearchingCEP}
|
||||
className={errors.cep ? "border-destructive" : ""}
|
||||
/>
|
||||
{isSearchingCEP && <Loader2 className="absolute right-3 top-3 h-4 w-4 animate-spin" />}
|
||||
{isSearchingCEP && (
|
||||
<Loader2 className="absolute right-3 top-3 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
{errors.cep && <p className="text-sm text-destructive">{errors.cep}</p>}
|
||||
{errors.cep && (
|
||||
<p className="text-sm text-destructive">{errors.cep}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Logradouro</Label>
|
||||
<Input value={form.logradouro} onChange={(e) => setField("logradouro", e.target.value)} />
|
||||
<Input
|
||||
value={form.logradouro}
|
||||
onChange={(e) => setField("logradouro", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Número</Label>
|
||||
<Input value={form.numero} onChange={(e) => setField("numero", e.target.value)} />
|
||||
<Input
|
||||
value={form.numero}
|
||||
onChange={(e) => setField("numero", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Complemento</Label>
|
||||
<Input value={form.complemento} onChange={(e) => setField("complemento", e.target.value)} />
|
||||
<Input
|
||||
value={form.complemento}
|
||||
onChange={(e) => setField("complemento", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Bairro</Label>
|
||||
<Input value={form.bairro} onChange={(e) => setField("bairro", e.target.value)} />
|
||||
<Input
|
||||
value={form.bairro}
|
||||
onChange={(e) => setField("bairro", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Cidade</Label>
|
||||
<Input value={form.cidade} onChange={(e) => setField("cidade", e.target.value)} />
|
||||
<Input
|
||||
value={form.cidade}
|
||||
onChange={(e) => setField("cidade", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Estado</Label>
|
||||
<Input value={form.estado} onChange={(e) => setField("estado", e.target.value)} placeholder="UF" />
|
||||
<Input
|
||||
value={form.estado}
|
||||
onChange={(e) => setField("estado", e.target.value)}
|
||||
placeholder="UF"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -611,13 +759,20 @@ export function PatientRegistrationForm({
|
||||
</Collapsible>
|
||||
|
||||
{}
|
||||
<Collapsible open={expanded.obs} onOpenChange={() => setExpanded((s) => ({ ...s, obs: !s.obs }))}>
|
||||
<Collapsible
|
||||
open={expanded.obs}
|
||||
onOpenChange={() => setExpanded((s) => ({ ...s, obs: !s.obs }))}
|
||||
>
|
||||
<Card>
|
||||
<CollapsibleTrigger asChild>
|
||||
<CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Observações e Anexos</span>
|
||||
{expanded.obs ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
{expanded.obs ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</CollapsibleTrigger>
|
||||
@ -625,27 +780,50 @@ export function PatientRegistrationForm({
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Observações</Label>
|
||||
<Textarea rows={4} value={form.observacoes} onChange={(e) => setField("observacoes", e.target.value)} />
|
||||
<Textarea
|
||||
rows={4}
|
||||
value={form.observacoes}
|
||||
onChange={(e) => setField("observacoes", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Adicionar anexos</Label>
|
||||
<div className="border-2 border-dashed rounded-lg p-4">
|
||||
<Label htmlFor="anexos" className="cursor-pointer block w-full rounded-md p-4 bg-primary text-primary-foreground">
|
||||
<Label
|
||||
htmlFor="anexos"
|
||||
className="cursor-pointer block w-full rounded-md p-4 bg-primary text-primary-foreground"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center text-center">
|
||||
<Upload className="h-7 w-7 mb-2 text-primary-foreground" />
|
||||
<p className="text-sm text-primary-foreground">Clique para adicionar documentos (PDF, imagens, etc.)</p>
|
||||
<p className="text-sm text-primary-foreground">
|
||||
Clique para adicionar documentos (PDF, imagens, etc.)
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
<Input id="anexos" type="file" multiple className="hidden" onChange={addLocalAnexos} />
|
||||
<Input
|
||||
id="anexos"
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={addLocalAnexos}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{form.anexos.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{form.anexos.map((f, i) => (
|
||||
<div key={`${f.name}-${i}`} className="flex items-center justify-between p-2 border rounded">
|
||||
{form.anexos.map((f, index) => (
|
||||
<div
|
||||
key={`${f.name}-${index}`}
|
||||
className="flex items-center justify-between p-2 border rounded"
|
||||
>
|
||||
<span className="text-sm">{f.name}</span>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={() => removeLocalAnexo(i)}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeLocalAnexo(index)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@ -661,9 +839,21 @@ export function PatientRegistrationForm({
|
||||
{serverAnexos.map((ax) => {
|
||||
const id = ax.id ?? ax.anexo_id ?? ax.uuid ?? "";
|
||||
return (
|
||||
<div key={String(id)} className="flex items-center justify-between p-2 border rounded">
|
||||
<span className="text-sm">{ax.nome || ax.filename || `Anexo ${id}`}</span>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={() => handleRemoverAnexoServidor(String(id))}>
|
||||
<div
|
||||
key={String(id)}
|
||||
className="flex items-center justify-between p-2 border rounded"
|
||||
>
|
||||
<span className="text-sm">
|
||||
{ax.nome || ax.filename || `Anexo ${id}`}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleRemoverAnexoServidor(String(id))
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@ -679,13 +869,26 @@ export function PatientRegistrationForm({
|
||||
|
||||
{}
|
||||
<div className="flex justify-end gap-4 pt-6 border-t">
|
||||
<Button type="button" variant="outline" onClick={() => (inline ? onClose?.() : onOpenChange?.(false))} disabled={isSubmitting}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => (inline ? onClose?.() : onOpenChange?.(false))}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<XCircle className="mr-2 h-4 w-4" />
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}
|
||||
{isSubmitting ? "Salvando..." : mode === "create" ? "Salvar Paciente" : "Atualizar Paciente"}
|
||||
{isSubmitting ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{isSubmitting
|
||||
? "Salvando..."
|
||||
: mode === "create"
|
||||
? "Salvar Paciente"
|
||||
: "Atualizar Paciente"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@ -696,10 +899,15 @@ export function PatientRegistrationForm({
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">{content}</div>
|
||||
|
||||
|
||||
{/* Debug */}
|
||||
{console.log("🎨 RENDER inline - credentials:", credentials, "showCredentials:", showCredentials)}
|
||||
|
||||
{console.log(
|
||||
"🎨 RENDER inline - credentials:",
|
||||
credentials,
|
||||
"showCredentials:",
|
||||
showCredentials,
|
||||
)}
|
||||
|
||||
{/* Dialog de credenciais */}
|
||||
{credentials && (
|
||||
<CredentialsDialog
|
||||
@ -708,14 +916,19 @@ export function PatientRegistrationForm({
|
||||
console.log("🔄 CredentialsDialog onOpenChange:", open);
|
||||
setShowCredentials(open);
|
||||
if (!open) {
|
||||
console.log("🔄 Dialog fechando - chamando onSaved e limpando formulário");
|
||||
|
||||
console.log(
|
||||
"🔄 Dialog fechando - chamando onSaved e limpando formulário",
|
||||
);
|
||||
|
||||
// Chama onSaved com o paciente salvo
|
||||
if (savedPatient) {
|
||||
console.log("✅ Chamando onSaved com paciente:", savedPatient.id);
|
||||
console.log(
|
||||
"✅ Chamando onSaved com paciente:",
|
||||
savedPatient.id,
|
||||
);
|
||||
onSaved?.(savedPatient);
|
||||
}
|
||||
|
||||
|
||||
// Limpa o formulário e fecha
|
||||
setForm(initial);
|
||||
setPhotoPreview(null);
|
||||
@ -737,8 +950,13 @@ export function PatientRegistrationForm({
|
||||
|
||||
return (
|
||||
<>
|
||||
{console.log("🎨 RENDER dialog - credentials:", credentials, "showCredentials:", showCredentials)}
|
||||
|
||||
{console.log(
|
||||
"🎨 RENDER dialog - credentials:",
|
||||
credentials,
|
||||
"showCredentials:",
|
||||
showCredentials,
|
||||
)}
|
||||
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
@ -749,7 +967,7 @@ export function PatientRegistrationForm({
|
||||
{content}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
||||
{/* Dialog de credenciais */}
|
||||
{credentials && (
|
||||
<CredentialsDialog
|
||||
|
||||
@ -50,14 +50,13 @@ export function Header() {
|
||||
className="text-primary border-primary bg-transparent shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground"
|
||||
asChild
|
||||
>
|
||||
|
||||
<Link href="/login-paciente">Sou Paciente</Link>
|
||||
</Button>
|
||||
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm shadow-blue-500/10 border border-blue-200 dark:shadow-none dark:border-transparent">
|
||||
<Link href="/login">Sou Profissional de Saúde</Link>
|
||||
</Button>
|
||||
<Link href="/login-admin">
|
||||
<Button
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-primary border-primary bg-transparent shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground cursor-pointer"
|
||||
>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Shield, Clock, Users } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Shield, Clock, Users } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export function HeroSection() {
|
||||
return (
|
||||
@ -14,19 +14,21 @@ export function HeroSection() {
|
||||
APROXIMANDO MÉDICOS E PACIENTES
|
||||
</div>
|
||||
<h1 className="text-3xl lg:text-4xl font-bold text-foreground leading-tight text-balance">
|
||||
Segurança, <span className="text-primary">Confiabilidade</span> e{" "}
|
||||
<span className="text-primary">Rapidez</span>
|
||||
Segurança, <span className="text-primary">Confiabilidade</span>{" "}
|
||||
e <span className="text-primary">Rapidez</span>
|
||||
</h1>
|
||||
<div className="space-y-1 text-base text-muted-foreground">
|
||||
<p>Experimente o futuro dos agendamentos.</p>
|
||||
<p>Encontre profissionais capacitados e marque já sua consulta.</p>
|
||||
<p>
|
||||
Encontre profissionais capacitados e marque já sua consulta.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Button
|
||||
size="lg"
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-primary hover:bg-primary/90 text-primary-foreground cursor-pointer shadow-sm shadow-blue-500/10 border border-blue-200 dark:shadow-none dark:border-transparent"
|
||||
asChild
|
||||
>
|
||||
@ -62,7 +64,9 @@ export function HeroSection() {
|
||||
<Shield className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground">Laudos digitais e padronizados</h3>
|
||||
<h3 className="font-semibold text-foreground">
|
||||
Laudos digitais e padronizados
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -71,7 +75,9 @@ export function HeroSection() {
|
||||
<Clock className="w-4 h-4 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground">Notificações automáticas ao paciente</h3>
|
||||
<h3 className="font-semibold text-foreground">
|
||||
Notificações automáticas ao paciente
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -80,11 +86,13 @@ export function HeroSection() {
|
||||
<Users className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground">LGPD: controle de acesso e consentimento</h3>
|
||||
<h3 className="font-semibold text-foreground">
|
||||
LGPD: controle de acesso e consentimento
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import * as React from "react";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function SimpleThemeToggle() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme === "dark" ? "light" : "dark")
|
||||
}
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleTheme}
|
||||
className="hover:text-muted-foreground cursor-pointer !shadow-sm !shadow-black/10 !border-2 !border-black dark:!shadow-none dark:!border-border"
|
||||
>
|
||||
@ -23,5 +23,5 @@ export function SimpleThemeToggle() {
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Alternar tema</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import * as React from "react";
|
||||
import {
|
||||
ThemeProvider as NextThemesProvider,
|
||||
type ThemeProviderProps,
|
||||
} from 'next-themes'
|
||||
} from "next-themes";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
export function ThemeProvider({ children, ...properties }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...properties}>{children}</NextThemesProvider>;
|
||||
}
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
import * as React from "react";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme } = useTheme()
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
@ -33,5 +33,5 @@ export function ThemeToggle() {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,34 +1,34 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...properties} />;
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
@ -36,31 +36,31 @@ function AccordionTrigger({
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
|
||||
@ -1,52 +1,58 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
import * as React from "react";
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...properties} />;
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
)
|
||||
<AlertDialogPrimitive.Trigger
|
||||
data-slot="alert-dialog-trigger"
|
||||
{...properties}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
)
|
||||
<AlertDialogPrimitive.Portal
|
||||
data-slot="alert-dialog-portal"
|
||||
{...properties}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
@ -55,91 +61,91 @@ function AlertDialogContent({
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Action
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -154,4 +160,4 @@ export {
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
@ -16,51 +16,51 @@ const alertVariants = cva(
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function AlertTitle({ className, ...properties }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
|
||||
|
||||
function AspectRatio({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
||||
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
|
||||
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...properties} />;
|
||||
}
|
||||
|
||||
export { AspectRatio }
|
||||
export { AspectRatio };
|
||||
|
||||
@ -1,53 +1,53 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
import * as React from "react";
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
@ -22,25 +22,25 @@ const badgeVariants = cva(
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
const Comp = asChild ? Slot : "span";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge, badgeVariants };
|
||||
|
||||
@ -1,55 +1,64 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||
function Breadcrumb({ ...properties }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...properties} />;
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
function BreadcrumbList({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"ol">) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
function BreadcrumbItem({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
function BreadcrumbPage({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
@ -57,15 +66,15 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
@ -73,16 +82,16 @@ function BreadcrumbSeparator({
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
@ -90,12 +99,12 @@ function BreadcrumbEllipsis({
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -106,4 +115,4 @@ export {
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
@ -32,28 +32,28 @@ const buttonVariants = cva(
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export { Button, buttonVariants };
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react"
|
||||
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
||||
} from "lucide-react";
|
||||
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
@ -19,11 +19,11 @@ function Calendar({
|
||||
buttonVariant = "ghost",
|
||||
formatters,
|
||||
components,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
const defaultClassNames = getDefaultClassNames();
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
@ -32,7 +32,7 @@ function Calendar({
|
||||
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||
className
|
||||
className,
|
||||
)}
|
||||
captionLayout={captionLayout}
|
||||
formatters={{
|
||||
@ -44,150 +44,156 @@ function Calendar({
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn(
|
||||
"flex gap-4 flex-col md:flex-row relative",
|
||||
defaultClassNames.months
|
||||
defaultClassNames.months,
|
||||
),
|
||||
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||
nav: cn(
|
||||
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||
defaultClassNames.nav
|
||||
defaultClassNames.nav,
|
||||
),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_previous
|
||||
defaultClassNames.button_previous,
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_next
|
||||
defaultClassNames.button_next,
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||
defaultClassNames.month_caption
|
||||
defaultClassNames.month_caption,
|
||||
),
|
||||
dropdowns: cn(
|
||||
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||
defaultClassNames.dropdowns
|
||||
defaultClassNames.dropdowns,
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||
defaultClassNames.dropdown_root
|
||||
defaultClassNames.dropdown_root,
|
||||
),
|
||||
dropdown: cn(
|
||||
"absolute bg-popover inset-0 opacity-0",
|
||||
defaultClassNames.dropdown
|
||||
defaultClassNames.dropdown,
|
||||
),
|
||||
caption_label: cn(
|
||||
"select-none font-medium",
|
||||
captionLayout === "label"
|
||||
? "text-sm"
|
||||
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
||||
defaultClassNames.caption_label
|
||||
defaultClassNames.caption_label,
|
||||
),
|
||||
table: "w-full border-collapse",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
||||
defaultClassNames.weekday
|
||||
defaultClassNames.weekday,
|
||||
),
|
||||
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||
week_number_header: cn(
|
||||
"select-none w-(--cell-size)",
|
||||
defaultClassNames.week_number_header
|
||||
defaultClassNames.week_number_header,
|
||||
),
|
||||
week_number: cn(
|
||||
"text-[0.8rem] select-none text-muted-foreground",
|
||||
defaultClassNames.week_number
|
||||
defaultClassNames.week_number,
|
||||
),
|
||||
day: cn(
|
||||
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||
defaultClassNames.day
|
||||
defaultClassNames.day,
|
||||
),
|
||||
range_start: cn(
|
||||
"rounded-l-md bg-accent",
|
||||
defaultClassNames.range_start
|
||||
defaultClassNames.range_start,
|
||||
),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||
today: cn(
|
||||
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today
|
||||
defaultClassNames.today,
|
||||
),
|
||||
outside: cn(
|
||||
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||
defaultClassNames.outside
|
||||
defaultClassNames.outside,
|
||||
),
|
||||
disabled: cn(
|
||||
"text-muted-foreground opacity-50",
|
||||
defaultClassNames.disabled
|
||||
defaultClassNames.disabled,
|
||||
),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
Root: ({ className, rootRef, ...properties_ }) => {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
{...properties_}
|
||||
/>
|
||||
)
|
||||
);
|
||||
},
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
Chevron: ({ className, orientation, ...properties_ }) => {
|
||||
if (orientation === "left") {
|
||||
return (
|
||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
<ChevronLeftIcon
|
||||
className={cn("size-4", className)}
|
||||
{...properties_}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (orientation === "right") {
|
||||
return (
|
||||
<ChevronRightIcon
|
||||
className={cn("size-4", className)}
|
||||
{...props}
|
||||
{...properties_}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
<ChevronDownIcon
|
||||
className={cn("size-4", className)}
|
||||
{...properties_}
|
||||
/>
|
||||
);
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: ({ children, ...props }) => {
|
||||
WeekNumber: ({ children, ...properties_ }) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
<td {...properties_}>
|
||||
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
);
|
||||
},
|
||||
...components,
|
||||
}}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CalendarDayButton({
|
||||
className,
|
||||
day,
|
||||
modifiers,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DayButton>) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
const defaultClassNames = getDefaultClassNames();
|
||||
|
||||
const ref = React.useRef<HTMLButtonElement>(null)
|
||||
const reference = React.useRef<HTMLButtonElement>(null);
|
||||
React.useEffect(() => {
|
||||
if (modifiers.focused) ref.current?.focus()
|
||||
}, [modifiers.focused])
|
||||
if (modifiers.focused) reference.current?.focus();
|
||||
}, [modifiers.focused]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
ref={reference}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
@ -203,11 +209,11 @@ function CalendarDayButton({
|
||||
className={cn(
|
||||
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
||||
defaultClassNames.day,
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Calendar, CalendarDayButton }
|
||||
export { Calendar, CalendarDayButton };
|
||||
|
||||
@ -1,84 +1,90 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function Card({ className, ...properties }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardHeader({ className, ...properties }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardTitle({ className, ...properties }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardDescription({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardAction({ className, ...properties }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardContent({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardFooter({ className, ...properties }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -89,4 +95,4 @@ export {
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,45 +1,47 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react"
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||
} from "embla-carousel-react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||
type CarouselOptions = UseCarouselParameters[0]
|
||||
type CarouselPlugin = UseCarouselParameters[1]
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions
|
||||
plugins?: CarouselPlugin
|
||||
orientation?: "horizontal" | "vertical"
|
||||
setApi?: (api: CarouselApi) => void
|
||||
}
|
||||
type CarouselProperties = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
type CarouselContextProperties = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProperties;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||
const CarouselContext = React.createContext<CarouselContextProperties | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />")
|
||||
throw new Error("useCarousel must be used within a <Carousel />");
|
||||
}
|
||||
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
||||
function Carousel({
|
||||
@ -49,72 +51,72 @@ function Carousel({
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
...properties
|
||||
}: React.ComponentProps<"div"> & CarouselProperties) {
|
||||
const [carouselReference, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
plugins,
|
||||
);
|
||||
const [canScrollPrevious, setCanScrollPrevious] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) return
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
if (!api) return;
|
||||
setCanScrollPrevious(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
const scrollPrevious = React.useCallback(() => {
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
event.preventDefault();
|
||||
scrollPrevious();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext]
|
||||
)
|
||||
[scrollPrevious, scrollNext],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) return
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
if (!api || !setApi) return;
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) return
|
||||
onSelect(api)
|
||||
api.on("reInit", onSelect)
|
||||
api.on("select", onSelect)
|
||||
if (!api) return;
|
||||
onSelect(api);
|
||||
api.on("reInit", onSelect);
|
||||
api.on("select", onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect)
|
||||
}
|
||||
}, [api, onSelect])
|
||||
api?.off("select", onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
carouselRef: carouselReference,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollPrev: scrollPrevious,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollPrev: canScrollPrevious,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
@ -124,16 +126,19 @@ function Carousel({
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
data-slot="carousel"
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
function CarouselContent({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -145,16 +150,19 @@ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { orientation } = useCarousel()
|
||||
function CarouselItem({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -164,20 +172,20 @@ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselPrevious({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
@ -189,25 +197,25 @@ function CarouselPrevious({
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -left-12 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<ArrowLeft />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselNext({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
@ -219,16 +227,16 @@ function CarouselNext({
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -right-12 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<ArrowRight />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -238,4 +246,4 @@ export {
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,37 +1,37 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const
|
||||
const THEMES = { light: "", dark: ".dark" } as const;
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode
|
||||
icon?: React.ComponentType
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig
|
||||
}
|
||||
type ChartContextProperties = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
const ChartContext = React.createContext<ChartContextProperties | null>(null);
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext)
|
||||
const context = React.useContext(ChartContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
throw new Error("useChart must be used within a <ChartContainer />");
|
||||
}
|
||||
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
||||
function ChartContainer({
|
||||
@ -39,15 +39,15 @@ function ChartContainer({
|
||||
className,
|
||||
children,
|
||||
config,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"div"> & {
|
||||
config: ChartConfig
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"]
|
||||
>["children"];
|
||||
}) {
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
@ -56,9 +56,9 @@ function ChartContainer({
|
||||
data-chart={chartId}
|
||||
className={cn(
|
||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
@ -66,16 +66,16 @@ function ChartContainer({
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme || config.color
|
||||
)
|
||||
([, config]) => config.theme || config.color,
|
||||
);
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -89,20 +89,20 @@ ${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`
|
||||
`,
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
@ -120,40 +120,40 @@ function ChartTooltipContent({
|
||||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
}) {
|
||||
const { config } = useChart()
|
||||
const { config } = useChart();
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label
|
||||
: itemConfig?.label;
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
@ -162,34 +162,34 @@ function ChartTooltipContent({
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
])
|
||||
]);
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||
indicator === "dot" && "items-center"
|
||||
indicator === "dot" && "items-center",
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
@ -209,7 +209,7 @@ function ChartTooltipContent({
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
}
|
||||
},
|
||||
)}
|
||||
style={
|
||||
{
|
||||
@ -223,7 +223,7 @@ function ChartTooltipContent({
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
nestLabel ? "items-end" : "items-center",
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
@ -241,14 +241,14 @@ function ChartTooltipContent({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
@ -258,13 +258,13 @@ function ChartLegendContent({
|
||||
nameKey,
|
||||
}: React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}) {
|
||||
const { config } = useChart()
|
||||
const { config } = useChart();
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -272,18 +272,18 @@ function ChartLegendContent({
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const key = `${nameKey || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
@ -298,20 +298,20 @@ function ChartLegendContent({
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string
|
||||
key: string,
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
@ -319,15 +319,15 @@ function getPayloadConfigFromPayload(
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
@ -335,12 +335,12 @@ function getPayloadConfigFromPayload(
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string
|
||||
] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config]
|
||||
: config[key as keyof typeof config];
|
||||
}
|
||||
|
||||
export {
|
||||
@ -350,4 +350,4 @@ export {
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
@ -26,7 +26,7 @@ function Checkbox({
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
export { Checkbox };
|
||||
|
||||
@ -1,33 +1,33 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...properties} />;
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
|
||||
@ -1,32 +1,32 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
@ -35,15 +35,15 @@ function CommandDialog({
|
||||
children,
|
||||
className,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string
|
||||
description?: string
|
||||
className?: string
|
||||
showCloseButton?: boolean
|
||||
title?: string;
|
||||
description?: string;
|
||||
className?: string;
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<Dialog {...properties}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
@ -57,12 +57,12 @@ function CommandDialog({
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
@ -74,101 +74,101 @@ function CommandInput({
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -181,4 +181,4 @@ export {
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,65 +1,76 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ContextMenu({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...properties} />;
|
||||
}
|
||||
|
||||
function ContextMenuTrigger({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||
)
|
||||
<ContextMenuPrimitive.Trigger
|
||||
data-slot="context-menu-trigger"
|
||||
{...properties}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuGroup({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||
)
|
||||
<ContextMenuPrimitive.Group
|
||||
data-slot="context-menu-group"
|
||||
{...properties}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuPortal({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||
)
|
||||
<ContextMenuPrimitive.Portal
|
||||
data-slot="context-menu-portal"
|
||||
{...properties}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSub({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
|
||||
return (
|
||||
<ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...properties} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuRadioGroup({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioGroup
|
||||
data-slot="context-menu-radio-group"
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
@ -67,35 +78,35 @@ function ContextMenuSubTrigger({
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
data-slot="context-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuContent({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
@ -103,22 +114,22 @@ function ContextMenuContent({
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Item
|
||||
@ -127,28 +138,28 @@ function ContextMenuItem({
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
data-slot="context-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
@ -157,22 +168,22 @@ function ContextMenuCheckboxItem({
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
data-slot="context-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
@ -181,15 +192,15 @@ function ContextMenuRadioItem({
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Label
|
||||
@ -197,40 +208,40 @@ function ContextMenuLabel({
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
data-slot="context-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="context-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -249,4 +260,4 @@ export {
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,58 +1,58 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...properties} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...properties} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...properties} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...properties} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
@ -61,9 +61,9 @@ function DialogContent({
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
@ -77,56 +77,62 @@ function DialogContent({
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DialogHeader({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DialogFooter({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -140,4 +146,4 @@ export {
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,54 +1,54 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
import * as React from "react";
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...properties} />;
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...properties} />;
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...properties} />;
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...properties} />;
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
@ -61,64 +61,70 @@ function DrawerContent({
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DrawerHeader({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn(
|
||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DrawerFooter({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -132,4 +138,4 @@ export {
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,40 +1,45 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
return (
|
||||
<DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...properties} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
<DropdownMenuPrimitive.Portal
|
||||
data-slot="dropdown-menu-portal"
|
||||
{...properties}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
@ -43,30 +48,33 @@ function DropdownMenuContent({
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
<DropdownMenuPrimitive.Group
|
||||
data-slot="dropdown-menu-group"
|
||||
{...properties}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
@ -75,28 +83,28 @@ function DropdownMenuItem({
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
@ -105,33 +113,33 @@ function DropdownMenuCheckboxItem({
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
@ -140,15 +148,15 @@ function DropdownMenuRadioItem({
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
@ -156,55 +164,57 @@ function DropdownMenuLabel({
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
return (
|
||||
<DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...properties} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
@ -212,30 +222,30 @@ function DropdownMenuSubTrigger({
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -254,4 +264,4 @@ export {
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
@ -11,49 +11,49 @@ import {
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
} from "react-hook-form"
|
||||
} from "react-hook-form";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
const Form = FormProvider
|
||||
const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
{} as FormFieldContextValue,
|
||||
);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
...properties
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
<FormFieldContext.Provider value={{ name: properties.name }}>
|
||||
<Controller {...properties} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState } = useFormContext()
|
||||
const formState = useFormState({ name: fieldContext.name })
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState } = useFormContext();
|
||||
const formState = useFormState({ name: fieldContext.name });
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
throw new Error("useFormField should be used within <FormField>");
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
id,
|
||||
@ -62,36 +62,36 @@ const useFormField = () => {
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
id: string;
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
{} as FormItemContextValue,
|
||||
);
|
||||
|
||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const id = React.useId()
|
||||
function FormItem({ className, ...properties }: React.ComponentProps<"div">) {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
data-slot="form-item"
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function FormLabel({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
const { error, formItemId } = useFormField()
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
@ -99,13 +99,14 @@ function FormLabel({
|
||||
data-error={!!error}
|
||||
className={cn("data-[error=true]:text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
function FormControl({ ...properties }: React.ComponentProps<typeof Slot>) {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
@ -117,30 +118,33 @@ function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { formDescriptionId } = useFormField()
|
||||
function FormDescription({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"p">) {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-description"
|
||||
id={formDescriptionId}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message ?? "") : props.children
|
||||
function FormMessage({ className, ...properties }: React.ComponentProps<"p">) {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message ?? "") : properties.children;
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -148,11 +152,11 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
data-slot="form-message"
|
||||
id={formMessageId}
|
||||
className={cn("text-destructive text-sm", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -164,4 +168,4 @@ export {
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,29 +1,32 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||
import * as React from "react";
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function HoverCard({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...properties} />;
|
||||
}
|
||||
|
||||
function HoverCardTrigger({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
||||
return (
|
||||
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
||||
)
|
||||
<HoverCardPrimitive.Trigger
|
||||
data-slot="hover-card-trigger"
|
||||
{...properties}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function HoverCardContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
||||
return (
|
||||
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
||||
@ -33,12 +36,12 @@ function HoverCardContent({
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
</HoverCardPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||
|
||||
@ -1,50 +1,53 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { OTPInput, OTPInputContext } from "input-otp"
|
||||
import { MinusIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import { OTPInput, OTPInputContext } from "input-otp";
|
||||
import { MinusIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function InputOTP({
|
||||
className,
|
||||
containerClassName,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof OTPInput> & {
|
||||
containerClassName?: string
|
||||
containerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<OTPInput
|
||||
data-slot="input-otp"
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-disabled:opacity-50",
|
||||
containerClassName
|
||||
containerClassName,
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function InputOTPGroup({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-group"
|
||||
className={cn("flex items-center", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPSlot({
|
||||
index,
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"div"> & {
|
||||
index: number
|
||||
index: number;
|
||||
}) {
|
||||
const inputOTPContext = React.useContext(OTPInputContext)
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -52,9 +55,9 @@ function InputOTPSlot({
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
@ -63,15 +66,15 @@ function InputOTPSlot({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||
function InputOTPSeparator({ ...properties }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||
<div data-slot="input-otp-separator" role="separator" {...properties}>
|
||||
<MinusIcon />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
function Input({
|
||||
className,
|
||||
type,
|
||||
...properties
|
||||
}: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
@ -13,11 +17,11 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
"focus-visible:border-primary focus-visible:ring-primary/20 focus-visible:ring-2",
|
||||
"hover:border-gray-400 dark:hover:border-gray-500",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Input }
|
||||
export { Input };
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Label }
|
||||
export { Label };
|
||||
|
||||
@ -1,67 +1,70 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Menubar({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
|
||||
return (
|
||||
<MenubarPrimitive.Root
|
||||
data-slot="menubar"
|
||||
className={cn(
|
||||
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarMenu({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
||||
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
|
||||
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...properties} />;
|
||||
}
|
||||
|
||||
function MenubarGroup({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
||||
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
|
||||
return <MenubarPrimitive.Group data-slot="menubar-group" {...properties} />;
|
||||
}
|
||||
|
||||
function MenubarPortal({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
||||
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
|
||||
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...properties} />;
|
||||
}
|
||||
|
||||
function MenubarRadioGroup({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
|
||||
)
|
||||
<MenubarPrimitive.RadioGroup
|
||||
data-slot="menubar-radio-group"
|
||||
{...properties}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarTrigger({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
|
||||
return (
|
||||
<MenubarPrimitive.Trigger
|
||||
data-slot="menubar-trigger"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarContent({
|
||||
@ -69,7 +72,7 @@ function MenubarContent({
|
||||
align = "start",
|
||||
alignOffset = -4,
|
||||
sideOffset = 8,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
|
||||
return (
|
||||
<MenubarPortal>
|
||||
@ -80,22 +83,22 @@ function MenubarContent({
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
</MenubarPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<MenubarPrimitive.Item
|
||||
@ -104,28 +107,28 @@ function MenubarItem({
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<MenubarPrimitive.CheckboxItem
|
||||
data-slot="menubar-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
@ -134,22 +137,22 @@ function MenubarCheckboxItem({
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
|
||||
return (
|
||||
<MenubarPrimitive.RadioItem
|
||||
data-slot="menubar-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
@ -158,15 +161,15 @@ function MenubarRadioItem({
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MenubarPrimitive.Label
|
||||
@ -174,55 +177,55 @@ function MenubarLabel({
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarSeparator({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
|
||||
return (
|
||||
<MenubarPrimitive.Separator
|
||||
data-slot="menubar-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarShortcut({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="menubar-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarSub({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
||||
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
|
||||
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...properties} />;
|
||||
}
|
||||
|
||||
function MenubarSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MenubarPrimitive.SubTrigger
|
||||
@ -230,30 +233,30 @@ function MenubarSubTrigger({
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarSubContent({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
|
||||
return (
|
||||
<MenubarPrimitive.SubContent
|
||||
data-slot="menubar-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -273,4 +276,4 @@ export {
|
||||
MenubarSub,
|
||||
MenubarSubTrigger,
|
||||
MenubarSubContent,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import * as React from "react"
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function NavigationMenu({
|
||||
className,
|
||||
children,
|
||||
viewport = true,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||
viewport?: boolean
|
||||
viewport?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Root
|
||||
@ -19,59 +19,59 @@ function NavigationMenu({
|
||||
data-viewport={viewport}
|
||||
className={cn(
|
||||
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{children}
|
||||
{viewport && <NavigationMenuViewport />}
|
||||
</NavigationMenuPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuList({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.List
|
||||
data-slot="navigation-menu-list"
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuItem({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Item
|
||||
data-slot="navigation-menu-item"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
)
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
|
||||
);
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
data-slot="navigation-menu-trigger"
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDownIcon
|
||||
@ -79,12 +79,12 @@ function NavigationMenuTrigger({
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuContent({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Content
|
||||
@ -92,67 +92,67 @@ function NavigationMenuContent({
|
||||
className={cn(
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuViewport({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full left-0 isolate z-50 flex justify-center"
|
||||
"absolute top-full left-0 isolate z-50 flex justify-center",
|
||||
)}
|
||||
>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
className={cn(
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuLink({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Link
|
||||
data-slot="navigation-menu-link"
|
||||
className={cn(
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuIndicator({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
data-slot="navigation-menu-indicator"
|
||||
className={cn(
|
||||
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -165,4 +165,4 @@ export {
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
navigationMenuTriggerStyle,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,53 +1,53 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react"
|
||||
} from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
function Pagination({ className, ...properties }: React.ComponentProps<"nav">) {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationContent({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="pagination-content"
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||
return <li data-slot="pagination-item" {...props} />
|
||||
function PaginationItem({ ...properties }: React.ComponentProps<"li">) {
|
||||
return <li data-slot="pagination-item" {...properties} />;
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
type PaginationLinkProperties = {
|
||||
isActive?: boolean;
|
||||
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||
React.ComponentProps<"a">
|
||||
React.ComponentProps<"a">;
|
||||
|
||||
function PaginationLink({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) {
|
||||
...properties
|
||||
}: PaginationLinkProperties) {
|
||||
return (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
@ -58,62 +58,62 @@ function PaginationLink({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationPrevious({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="hidden sm:block">Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationNext({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationEllipsis({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
data-slot="pagination-ellipsis"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -124,4 +124,4 @@ export {
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,27 +1,29 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...properties} />;
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
return (
|
||||
<PopoverPrimitive.Trigger data-slot="popover-trigger" {...properties} />
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
@ -31,18 +33,18 @@ function PopoverContent({
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-white text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[9999] w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...properties} />;
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
@ -25,7 +25,7 @@ function Progress({
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Progress }
|
||||
export { Progress };
|
||||
|
||||
@ -1,36 +1,36 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { CircleIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function RadioGroup({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
data-slot="radio-group"
|
||||
className={cn("grid gap-3", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function RadioGroupItem({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator
|
||||
data-slot="radio-group-indicator"
|
||||
@ -39,7 +39,7 @@ function RadioGroupItem({
|
||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
export { RadioGroup, RadioGroupItem };
|
||||
|
||||
@ -1,48 +1,50 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { GripVerticalIcon } from "lucide-react"
|
||||
import * as ResizablePrimitive from "react-resizable-panels"
|
||||
import * as React from "react";
|
||||
import { GripVerticalIcon } from "lucide-react";
|
||||
import * as ResizablePrimitive from "react-resizable-panels";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ResizablePanelGroup({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
|
||||
return (
|
||||
<ResizablePrimitive.PanelGroup
|
||||
data-slot="resizable-panel-group"
|
||||
className={cn(
|
||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ResizablePanel({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
|
||||
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
|
||||
return (
|
||||
<ResizablePrimitive.Panel data-slot="resizable-panel" {...properties} />
|
||||
);
|
||||
}
|
||||
|
||||
function ResizableHandle({
|
||||
withHandle,
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||
withHandle?: boolean
|
||||
withHandle?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
data-slot="resizable-handle"
|
||||
className={cn(
|
||||
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
||||
@ -50,7 +52,7 @@ function ResizableHandle({
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.PanelResizeHandle>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
@ -25,13 +25,13 @@ function ScrollArea({
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
@ -43,16 +43,16 @@ function ScrollBar({
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
export { ScrollArea, ScrollBar };
|
||||
|
||||
@ -1,36 +1,36 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Select({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
return <SelectPrimitive.Root data-slot="select" {...properties} />;
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...properties} />;
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...properties} />;
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
size?: "sm" | "default";
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
@ -42,23 +42,23 @@ function SelectTrigger({
|
||||
"focus-visible:border-primary focus-visible:ring-primary/20 focus-visible:ring-2",
|
||||
"hover:border-gray-400 dark:hover:border-gray-500",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive aria-invalid:border-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
@ -68,17 +68,17 @@ function SelectContent({
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@ -86,35 +86,35 @@ function SelectContent({
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
@ -123,56 +123,56 @@ function SelectItem({
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -186,4 +186,4 @@ export {
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
@ -18,11 +18,11 @@ function Separator({
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
export { Separator };
|
||||
|
||||
@ -1,56 +1,58 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
function Sheet({
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...properties} />;
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...properties} />;
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...properties} />;
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...properties} />;
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
@ -67,9 +69,9 @@ function SheetContent({
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
side === "bottom" &&
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
@ -78,53 +80,59 @@ function SheetContent({
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function SheetHeader({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function SheetFooter({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -136,4 +144,4 @@ export {
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,97 +1,99 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, VariantProps } from "class-variance-authority"
|
||||
import { PanelLeftIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import { PanelLeftIcon } from "lucide-react";
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
} from "@/components/ui/sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
const SIDEBAR_WIDTH = "16rem"
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "16rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed"
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
openMobile: boolean
|
||||
setOpenMobile: (open: boolean) => void
|
||||
isMobile: boolean
|
||||
toggleSidebar: () => void
|
||||
}
|
||||
type SidebarContextProperties = {
|
||||
state: "expanded" | "collapsed";
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openMobile: boolean;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
||||
const SidebarContext = React.createContext<SidebarContextProperties | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext)
|
||||
const context = React.useContext(SidebarContext);
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.");
|
||||
}
|
||||
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
||||
function SidebarProvider({
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
open: openProperty,
|
||||
onOpenChange: setOpenProperty,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}) {
|
||||
const isMobile = useIsMobile()
|
||||
const [openMobile, setOpenMobile] = React.useState(false)
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
||||
const open = openProp ?? _open
|
||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||
const open = openProperty ?? _open;
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState)
|
||||
const openState = typeof value === "function" ? value(open) : value;
|
||||
if (setOpenProperty) {
|
||||
setOpenProperty(openState);
|
||||
} else {
|
||||
_setOpen(openState)
|
||||
_setOpen(openState);
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
[setOpenProp, open]
|
||||
)
|
||||
[setOpenProperty, open],
|
||||
);
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
||||
}, [isMobile, setOpen, setOpenMobile])
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
|
||||
}, [isMobile, setOpen, setOpenMobile]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
@ -100,20 +102,20 @@ function SidebarProvider({
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault()
|
||||
toggleSidebar()
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [toggleSidebar])
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed"
|
||||
const state = open ? "expanded" : "collapsed";
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
const contextValue = React.useMemo<SidebarContextProperties>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
@ -123,8 +125,8 @@ function SidebarProvider({
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||
)
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
@ -140,15 +142,15 @@ function SidebarProvider({
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function Sidebar({
|
||||
@ -157,13 +159,13 @@ function Sidebar({
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right"
|
||||
variant?: "sidebar" | "floating" | "inset"
|
||||
collapsible?: "offcanvas" | "icon" | "none"
|
||||
side?: "left" | "right";
|
||||
variant?: "sidebar" | "floating" | "inset";
|
||||
collapsible?: "offcanvas" | "icon" | "none";
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
@ -171,18 +173,18 @@ function Sidebar({
|
||||
data-slot="sidebar"
|
||||
className={cn(
|
||||
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...properties}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
@ -202,7 +204,7 @@ function Sidebar({
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -223,7 +225,7 @@ function Sidebar({
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
@ -237,9 +239,9 @@ function Sidebar({
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
@ -250,15 +252,15 @@ function Sidebar({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarTrigger({
|
||||
className,
|
||||
onClick,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<Button
|
||||
@ -268,19 +270,22 @@ function SidebarTrigger({
|
||||
size="icon"
|
||||
className={cn("size-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
toggleSidebar()
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<PanelLeftIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
function SidebarRail({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"button">) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
@ -297,108 +302,123 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
function SidebarInset({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"main">) {
|
||||
return (
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
className={cn(
|
||||
"bg-background relative flex w-full flex-1 flex-col",
|
||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarInput({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof Input>) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="sidebar-input"
|
||||
data-sidebar="input"
|
||||
className={cn("bg-background h-8 w-full shadow-none", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function SidebarHeader({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-header"
|
||||
data-sidebar="header"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function SidebarFooter({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-footer"
|
||||
data-sidebar="footer"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarSeparator({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="sidebar-separator"
|
||||
data-sidebar="separator"
|
||||
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function SidebarContent({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function SidebarGroup({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group"
|
||||
data-sidebar="group"
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupLabel({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@ -407,19 +427,19 @@ function SidebarGroupLabel({
|
||||
className={cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupAction({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@ -430,47 +450,50 @@ function SidebarGroupAction({
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupContent({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group-content"
|
||||
data-sidebar="group-content"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
function SidebarMenu({ className, ...properties }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu"
|
||||
data-sidebar="menu"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
function SidebarMenuItem({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-item"
|
||||
data-sidebar="menu-item"
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
@ -492,8 +515,8 @@ const sidebarMenuButtonVariants = cva(
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function SidebarMenuButton({
|
||||
asChild = false,
|
||||
@ -502,14 +525,14 @@ function SidebarMenuButton({
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
isActive?: boolean
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const { isMobile, state } = useSidebar()
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
@ -518,18 +541,18 @@ function SidebarMenuButton({
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
@ -542,19 +565,19 @@ function SidebarMenuButton({
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuAction({
|
||||
className,
|
||||
asChild = false,
|
||||
showOnHover = false,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
showOnHover?: boolean
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@ -570,16 +593,16 @@ function SidebarMenuAction({
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuBadge({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
@ -592,31 +615,31 @@ function SidebarMenuBadge({
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSkeleton({
|
||||
className,
|
||||
showIcon = false,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean
|
||||
showIcon?: boolean;
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||
}, [])
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-skeleton"
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
@ -634,10 +657,13 @@ function SidebarMenuSkeleton({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
function SidebarMenuSub({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu-sub"
|
||||
@ -645,25 +671,25 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
className={cn(
|
||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSubItem({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-sub-item"
|
||||
data-sidebar="menu-sub-item"
|
||||
className={cn("group/menu-sub-item relative", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSubButton({
|
||||
@ -671,13 +697,13 @@ function SidebarMenuSubButton({
|
||||
size = "md",
|
||||
isActive = false,
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
size?: "sm" | "md"
|
||||
isActive?: boolean
|
||||
asChild?: boolean;
|
||||
size?: "sm" | "md";
|
||||
isActive?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@ -691,14 +717,13 @@ function SidebarMenuSubButton({
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@ -724,4 +749,4 @@ export {
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function Skeleton({ className, ...properties }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
export { Skeleton };
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
import * as React from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Slider({
|
||||
className,
|
||||
@ -11,7 +11,7 @@ function Slider({
|
||||
value,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||
const _values = React.useMemo(
|
||||
() =>
|
||||
@ -20,8 +20,8 @@ function Slider({
|
||||
: Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [min, max],
|
||||
[value, defaultValue, min, max]
|
||||
)
|
||||
[value, defaultValue, min, max],
|
||||
);
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
@ -32,20 +32,20 @@ function Slider({
|
||||
max={max}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
data-slot="slider-track"
|
||||
className={cn(
|
||||
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
||||
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
data-slot="slider-range"
|
||||
className={cn(
|
||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
||||
)}
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
@ -57,7 +57,7 @@ function Slider({
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Slider }
|
||||
export { Slider };
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner";
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
const Toaster = ({ ...properties }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
@ -17,9 +17,9 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster }
|
||||
export { Toaster };
|
||||
|
||||
@ -1,31 +1,31 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
export { Switch };
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
function Table({ className, ...properties }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
@ -13,95 +13,104 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
function TableHeader({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
function TableBody({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
function TableFooter({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
function TableRow({ className, ...properties }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
function TableHead({ className, ...properties }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
function TableCell({ className, ...properties }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -113,4 +122,4 @@ export {
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,66 +1,66 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
function Textarea({
|
||||
className,
|
||||
...properties
|
||||
}: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
@ -12,11 +15,11 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
"focus-visible:border-primary focus-visible:ring-primary/20 focus-visible:ring-2",
|
||||
"hover:border-gray-400 dark:hover:border-gray-500",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
export { Textarea };
|
||||
|
||||
@ -1,28 +1,28 @@
|
||||
"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) => (
|
||||
>(({ className, ...properties }, reference) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
ref={reference}
|
||||
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
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
));
|
||||
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",
|
||||
@ -37,87 +37,87 @@ const toastVariants = cva(
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
>(({ className, variant, ...properties }, reference) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
ref={reference}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
);
|
||||
});
|
||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
>(({ className, ...properties }, reference) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
ref={reference}
|
||||
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
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
));
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
>(({ className, ...properties }, reference) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
ref={reference}
|
||||
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
|
||||
className,
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<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) => (
|
||||
>(({ className, ...properties }, reference) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
ref={reference}
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
));
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
>(({ className, ...properties }, reference) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
ref={reference}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
));
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
type ToastProperties = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastProperties as ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
@ -126,4 +126,4 @@ export {
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
@ -8,16 +8,16 @@ import {
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast"
|
||||
} from "@/components/ui/toast";
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
const { toasts } = useToast();
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
{toasts.map(function ({ id, title, description, action, ...properties }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<Toast key={id} {...properties}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
@ -27,9 +27,9 @@ export function Toaster() {
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
);
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
||||
import { type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||
import { type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { toggleVariants } from "@/components/ui/toggle"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toggleVariants } from "@/components/ui/toggle";
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
})
|
||||
});
|
||||
|
||||
function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
@ -29,15 +29,15 @@ function ToggleGroup({
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleGroupItem({
|
||||
@ -45,10 +45,10 @@ function ToggleGroupItem({
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
const context = React.useContext(ToggleGroupContext)
|
||||
const context = React.useContext(ToggleGroupContext);
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
@ -61,13 +61,13 @@ function ToggleGroupItem({
|
||||
size: context.size || size,
|
||||
}),
|
||||
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem }
|
||||
export { ToggleGroup, ToggleGroupItem };
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
@ -25,23 +25,23 @@ const toggleVariants = cva(
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function Toggle({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
export { Toggle, toggleVariants };
|
||||
|
||||
@ -1,44 +1,46 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
{...properties}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...properties} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
return (
|
||||
<TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...properties} />
|
||||
);
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
...properties
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
@ -47,15 +49,15 @@ function TooltipContent({
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...properties}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
@ -1,19 +1,21 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile
|
||||
return !!isMobile;
|
||||
}
|
||||
|
||||
@ -1,78 +1,75 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
const TOAST_LIMIT = 1;
|
||||
const TOAST_REMOVE_DELAY = 1000000;
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
};
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
let count = 0
|
||||
let count = 0;
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
type ActionType = typeof actionTypes;
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
type: ActionType["ADD_TOAST"];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
type: ActionType["UPDATE_TOAST"];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
type: ActionType["DISMISS_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
type: ActionType["REMOVE_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
};
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
@ -80,27 +77,27 @@ export const reducer = (state: State, action: Action): State => {
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
};
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
const { toastId } = action;
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@ -111,84 +108,84 @@ export const reducer = (state: State, action: Action): State => {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
: t,
|
||||
),
|
||||
}
|
||||
};
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
const listeners: Array<(state: State) => void> = [];
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
let memoryState: State = { toasts: [] };
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
listener(memoryState);
|
||||
});
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
type Toast = Omit<ToasterToast, "id">;
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
function toast({ ...properties }: Toast) {
|
||||
const id = genId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
const update = (properties_: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
toast: { ...properties_, id },
|
||||
});
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
...properties,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
if (!open) dismiss();
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
export { useToast, toast };
|
||||
|
||||
@ -12,74 +12,80 @@ import { dirname } from "path";
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
{
|
||||
ignores: ["node_modules/**", ".next/**", "out/**", "build/**", "next-env.d.ts"],
|
||||
ignores: [
|
||||
"node_modules/**",
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
],
|
||||
},
|
||||
// Base JS/TS config
|
||||
{
|
||||
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"@next/next": nextPlugin,
|
||||
"@next/next": nextPlugin,
|
||||
},
|
||||
rules: {
|
||||
...nextPlugin.configs.recommended.rules,
|
||||
...nextPlugin.configs["core-web-vitals"].rules,
|
||||
}
|
||||
...nextPlugin.configs.recommended.rules,
|
||||
...nextPlugin.configs["core-web-vitals"].rules,
|
||||
},
|
||||
},
|
||||
// TypeScript specific config
|
||||
{
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
plugins: {
|
||||
"unicorn": unicornPlugin,
|
||||
unicorn: unicornPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...tseslint.configs.recommended.rules,
|
||||
...unicornPlugin.configs.recommended.rules,
|
||||
// Disable noisy unicorn rules
|
||||
"unicorn/prevent-abbreviations": "warn",
|
||||
"unicorn/filename-case": "off",
|
||||
"unicorn/no-null": "warn",
|
||||
"unicorn/consistent-function-scoping": "off",
|
||||
"unicorn/no-array-for-each": "off",
|
||||
"unicorn/catch-error-name": "off",
|
||||
"unicorn/explicit-length-check": "off",
|
||||
"unicorn/no-array-reduce": "off",
|
||||
"unicorn/prefer-spread": "off",
|
||||
"unicorn/no-document-cookie": "off",
|
||||
"unicorn/prefer-query-selector": "off",
|
||||
"unicorn/prefer-add-event-listener": "off",
|
||||
"unicorn/prefer-string-slice": "off",
|
||||
"unicorn/prefer-string-replace-all": "off",
|
||||
"unicorn/prefer-number-properties": "off",
|
||||
"unicorn/consistent-existence-index-check": "off",
|
||||
"unicorn/no-negated-condition": "off",
|
||||
"unicorn/switch-case-braces": "off",
|
||||
"unicorn/prefer-global-this": "off",
|
||||
"unicorn/no-useless-undefined": "off",
|
||||
"unicorn/no-array-callback-reference": "off",
|
||||
"unicorn/no-array-sort": "off",
|
||||
"unicorn/numeric-separators-style": "off",
|
||||
"unicorn/prefer-optional-catch-binding": "off",
|
||||
"unicorn/prefer-ternary": "off",
|
||||
"unicorn/prefer-code-point": "off",
|
||||
"unicorn/prefer-single-call": "off",
|
||||
}
|
||||
...tseslint.configs.recommended.rules,
|
||||
...unicornPlugin.configs.recommended.rules,
|
||||
// Disable noisy unicorn rules
|
||||
"unicorn/prevent-abbreviations": "warn",
|
||||
"unicorn/filename-case": "off",
|
||||
"unicorn/no-null": "warn",
|
||||
"unicorn/consistent-function-scoping": "off",
|
||||
"unicorn/no-array-for-each": "off",
|
||||
"unicorn/catch-error-name": "off",
|
||||
"unicorn/explicit-length-check": "off",
|
||||
"unicorn/no-array-reduce": "off",
|
||||
"unicorn/prefer-spread": "off",
|
||||
"unicorn/no-document-cookie": "off",
|
||||
"unicorn/prefer-query-selector": "off",
|
||||
"unicorn/prefer-add-event-listener": "off",
|
||||
"unicorn/prefer-string-slice": "off",
|
||||
"unicorn/prefer-string-replace-all": "off",
|
||||
"unicorn/prefer-number-properties": "off",
|
||||
"unicorn/consistent-existence-index-check": "off",
|
||||
"unicorn/no-negated-condition": "off",
|
||||
"unicorn/switch-case-braces": "off",
|
||||
"unicorn/prefer-global-this": "off",
|
||||
"unicorn/no-useless-undefined": "off",
|
||||
"unicorn/no-array-callback-reference": "off",
|
||||
"unicorn/no-array-sort": "off",
|
||||
"unicorn/numeric-separators-style": "off",
|
||||
"unicorn/prefer-optional-catch-binding": "off",
|
||||
"unicorn/prefer-ternary": "off",
|
||||
"unicorn/prefer-code-point": "off",
|
||||
"unicorn/prefer-single-call": "off",
|
||||
},
|
||||
},
|
||||
prettierRecommended,
|
||||
];
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState } from "react";
|
||||
|
||||
export interface Appointment {
|
||||
id: string;
|
||||
patient: string;
|
||||
time: string;
|
||||
duration: number;
|
||||
type: 'consulta' | 'exame' | 'retorno';
|
||||
status: 'confirmed' | 'pending' | 'absent';
|
||||
type: "consulta" | "exame" | "retorno";
|
||||
status: "confirmed" | "pending" | "absent";
|
||||
professional: string;
|
||||
notes?: string;
|
||||
}
|
||||
@ -23,7 +22,7 @@ export interface WaitingPatient {
|
||||
name: string;
|
||||
specialty: string;
|
||||
preferredDate: string;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
priority: "high" | "medium" | "low";
|
||||
contact: string;
|
||||
}
|
||||
|
||||
@ -31,26 +30,27 @@ export const useAgenda = () => {
|
||||
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
||||
const [waitingList, setWaitingList] = useState<WaitingPatient[]>([]);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedAppointment, setSelectedAppointment] = useState<Appointment | null>(null);
|
||||
const [selectedAppointment, setSelectedAppointment] =
|
||||
useState<Appointment | null>(null);
|
||||
const [isWaitlistModalOpen, setIsWaitlistModalOpen] = useState(false);
|
||||
|
||||
const professionals: Professional[] = [
|
||||
{ id: '1', name: 'Dr. Carlos Silva', specialty: 'Cardiologia' },
|
||||
{ id: '2', name: 'Dra. Maria Santos', specialty: 'Dermatologia' },
|
||||
{ id: '3', name: 'Dr. João Oliveira', specialty: 'Ortopedia' },
|
||||
{ id: "1", name: "Dr. Carlos Silva", specialty: "Cardiologia" },
|
||||
{ id: "2", name: "Dra. Maria Santos", specialty: "Dermatologia" },
|
||||
{ id: "3", name: "Dr. João Oliveira", specialty: "Ortopedia" },
|
||||
];
|
||||
|
||||
const handleSaveAppointment = (appointment: Appointment) => {
|
||||
if (appointment.id) {
|
||||
|
||||
setAppointments(prev => prev.map(a => a.id === appointment.id ? appointment : a));
|
||||
setAppointments((previous) =>
|
||||
previous.map((a) => (a.id === appointment.id ? appointment : a)),
|
||||
);
|
||||
} else {
|
||||
|
||||
const newAppointment = {
|
||||
...appointment,
|
||||
id: Date.now().toString(),
|
||||
};
|
||||
setAppointments(prev => [...prev, newAppointment]);
|
||||
setAppointments((previous) => [...previous, newAppointment]);
|
||||
}
|
||||
};
|
||||
|
||||
@ -70,7 +70,6 @@ export const useAgenda = () => {
|
||||
};
|
||||
|
||||
const handleNotifyPatient = (patientId: string) => {
|
||||
|
||||
console.log(`Notificando paciente ${patientId}`);
|
||||
};
|
||||
|
||||
@ -92,4 +91,4 @@ export const useAgenda = () => {
|
||||
handleNotifyPatient,
|
||||
handleAddToWaitlist,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
export function useForceDefaultTheme() {
|
||||
const { setTheme } = useTheme()
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
// Força tema claro sempre que o componente montar
|
||||
document.documentElement.classList.remove('dark')
|
||||
localStorage.setItem('theme', 'light')
|
||||
setTheme('light')
|
||||
}, [setTheme])
|
||||
}
|
||||
document.documentElement.classList.remove("dark");
|
||||
localStorage.setItem("theme", "light");
|
||||
setTheme("light");
|
||||
}, [setTheme]);
|
||||
}
|
||||
|
||||
@ -1,19 +1,21 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile
|
||||
return !!isMobile;
|
||||
}
|
||||
|
||||
@ -1,78 +1,74 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import * as React from "react"
|
||||
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
const TOAST_LIMIT = 1;
|
||||
const TOAST_REMOVE_DELAY = 1000000;
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
};
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
let count = 0
|
||||
let count = 0;
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
type ActionType = typeof actionTypes;
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
type: ActionType["ADD_TOAST"];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
type: ActionType["UPDATE_TOAST"];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
type: ActionType["DISMISS_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
type: ActionType["REMOVE_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
};
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
@ -80,26 +76,25 @@ export const reducer = (state: State, action: Action): State => {
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
};
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
const { toastId } = action;
|
||||
|
||||
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@ -110,84 +105,84 @@ export const reducer = (state: State, action: Action): State => {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
: t,
|
||||
),
|
||||
}
|
||||
};
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
const listeners: Array<(state: State) => void> = [];
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
let memoryState: State = { toasts: [] };
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
listener(memoryState);
|
||||
});
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
type Toast = Omit<ToasterToast, "id">;
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
function toast({ ...properties }: Toast) {
|
||||
const id = genId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
const update = (properties_: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
toast: { ...properties_, id },
|
||||
});
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
...properties,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
if (!open) dismiss();
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
export { useToast, toast };
|
||||
|
||||
@ -1,252 +1,270 @@
|
||||
'use client'
|
||||
import { createContext, useContext, useEffect, useState, ReactNode, useCallback, useMemo, useRef } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { loginUser, logoutUser, AuthenticationError } from '@/lib/auth'
|
||||
import { isExpired, parseJwt } from '@/lib/jwt'
|
||||
import { httpClient } from '@/lib/http'
|
||||
import type {
|
||||
AuthContextType,
|
||||
UserData,
|
||||
"use client";
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { loginUser, logoutUser, AuthenticationError } from "@/lib/auth";
|
||||
import { isExpired, parseJwt } from "@/lib/jwt";
|
||||
import { httpClient } from "@/lib/http";
|
||||
import type {
|
||||
AuthContextType,
|
||||
UserData,
|
||||
AuthStatus,
|
||||
UserType
|
||||
} from '@/types/auth'
|
||||
import { AUTH_STORAGE_KEYS, LOGIN_ROUTES } from '@/types/auth'
|
||||
UserType,
|
||||
} from "@/types/auth";
|
||||
import { AUTH_STORAGE_KEYS, LOGIN_ROUTES } from "@/types/auth";
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [authStatus, setAuthStatus] = useState<AuthStatus>('loading')
|
||||
const [user, setUser] = useState<UserData | null>(null)
|
||||
const [token, setToken] = useState<string | null>(null)
|
||||
const router = useRouter()
|
||||
const hasInitialized = useRef(false)
|
||||
const [authStatus, setAuthStatus] = useState<AuthStatus>("loading");
|
||||
const [user, setUser] = useState<UserData | null>(null);
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
// Utilitários de armazenamento memorizados
|
||||
const clearAuthData = useCallback(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem(AUTH_STORAGE_KEYS.TOKEN)
|
||||
localStorage.removeItem(AUTH_STORAGE_KEYS.USER)
|
||||
localStorage.removeItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN)
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem(AUTH_STORAGE_KEYS.TOKEN);
|
||||
localStorage.removeItem(AUTH_STORAGE_KEYS.USER);
|
||||
localStorage.removeItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN);
|
||||
// Manter USER_TYPE para redirecionamento correto
|
||||
}
|
||||
setUser(null)
|
||||
setToken(null)
|
||||
setAuthStatus('unauthenticated')
|
||||
console.log('[AUTH] Dados de autenticação limpos - logout realizado')
|
||||
}, [])
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
setAuthStatus("unauthenticated");
|
||||
console.log("[AUTH] Dados de autenticação limpos - logout realizado");
|
||||
}, []);
|
||||
|
||||
const saveAuthData = useCallback((
|
||||
accessToken: string,
|
||||
userData: UserData,
|
||||
refreshToken?: string
|
||||
) => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Persistir dados de forma atômica
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, accessToken)
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(userData))
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, userData.userType)
|
||||
|
||||
if (refreshToken) {
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, refreshToken)
|
||||
const saveAuthData = useCallback(
|
||||
(accessToken: string, userData: UserData, refreshToken?: string) => {
|
||||
try {
|
||||
if (typeof window !== "undefined") {
|
||||
// Persistir dados de forma atômica
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, accessToken);
|
||||
localStorage.setItem(
|
||||
AUTH_STORAGE_KEYS.USER,
|
||||
JSON.stringify(userData),
|
||||
);
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, userData.userType);
|
||||
|
||||
if (refreshToken) {
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
setToken(accessToken);
|
||||
setUser(userData);
|
||||
setAuthStatus("authenticated");
|
||||
|
||||
console.log("[AUTH] LOGIN realizado - Dados salvos!", {
|
||||
userType: userData.userType,
|
||||
email: userData.email,
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[AUTH] Erro ao salvar dados:", error);
|
||||
clearAuthData();
|
||||
}
|
||||
|
||||
setToken(accessToken)
|
||||
setUser(userData)
|
||||
setAuthStatus('authenticated')
|
||||
|
||||
console.log('[AUTH] LOGIN realizado - Dados salvos!', {
|
||||
userType: userData.userType,
|
||||
email: userData.email,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro ao salvar dados:', error)
|
||||
clearAuthData()
|
||||
}
|
||||
}, [clearAuthData])
|
||||
},
|
||||
[clearAuthData],
|
||||
);
|
||||
|
||||
// Verificação inicial de autenticação
|
||||
const checkAuth = useCallback(async (): Promise<void> => {
|
||||
if (typeof window === 'undefined') {
|
||||
setAuthStatus('unauthenticated')
|
||||
return
|
||||
if (typeof window === "undefined") {
|
||||
setAuthStatus("unauthenticated");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const storedToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN)
|
||||
const storedUser = localStorage.getItem(AUTH_STORAGE_KEYS.USER)
|
||||
const storedToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN);
|
||||
const storedUser = localStorage.getItem(AUTH_STORAGE_KEYS.USER);
|
||||
|
||||
console.log('[AUTH] Verificando sessão...', {
|
||||
console.log("[AUTH] Verificando sessão...", {
|
||||
hasToken: !!storedToken,
|
||||
hasUser: !!storedUser,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
})
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
});
|
||||
|
||||
// Pequeno delay para visualizar logs
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
|
||||
if (!storedToken || !storedUser) {
|
||||
console.log('[AUTH] Dados ausentes - sessão inválida')
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
clearAuthData()
|
||||
return
|
||||
console.log("[AUTH] Dados ausentes - sessão inválida");
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
clearAuthData();
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar se token está expirado
|
||||
if (isExpired(storedToken)) {
|
||||
console.log('[AUTH] Token expirado - tentando renovar...')
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
const refreshToken = localStorage.getItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN)
|
||||
console.log("[AUTH] Token expirado - tentando renovar...");
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
const refreshToken = localStorage.getItem(
|
||||
AUTH_STORAGE_KEYS.REFRESH_TOKEN,
|
||||
);
|
||||
if (refreshToken && !isExpired(refreshToken)) {
|
||||
// Tentar renovar via HTTP client (que já tem a lógica)
|
||||
try {
|
||||
await httpClient.get('/auth/v1/me') // Trigger refresh se necessário
|
||||
|
||||
await httpClient.get("/auth/v1/me"); // Trigger refresh se necessário
|
||||
|
||||
// Se chegou aqui, refresh foi bem-sucedido
|
||||
const newToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN)
|
||||
const userData = JSON.parse(storedUser) as UserData
|
||||
|
||||
const newToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN);
|
||||
const userData = JSON.parse(storedUser) as UserData;
|
||||
|
||||
if (newToken && newToken !== storedToken) {
|
||||
setToken(newToken)
|
||||
setUser(userData)
|
||||
setAuthStatus('authenticated')
|
||||
console.log('[AUTH] Token RENOVADO automaticamente!')
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
return
|
||||
setToken(newToken);
|
||||
setUser(userData);
|
||||
setAuthStatus("authenticated");
|
||||
console.log("[AUTH] Token RENOVADO automaticamente!");
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
return;
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.log('❌ [AUTH] Falha no refresh automático')
|
||||
await new Promise(resolve => setTimeout(resolve, 400))
|
||||
console.log("❌ [AUTH] Falha no refresh automático");
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
}
|
||||
}
|
||||
|
||||
clearAuthData()
|
||||
return
|
||||
|
||||
clearAuthData();
|
||||
return;
|
||||
}
|
||||
|
||||
// Restaurar sessão válida
|
||||
const userData = JSON.parse(storedUser) as UserData
|
||||
setToken(storedToken)
|
||||
setUser(userData)
|
||||
setAuthStatus('authenticated')
|
||||
const userData = JSON.parse(storedUser) as UserData;
|
||||
setToken(storedToken);
|
||||
setUser(userData);
|
||||
setAuthStatus("authenticated");
|
||||
|
||||
console.log('[AUTH] Sessão RESTAURADA com sucesso!', {
|
||||
console.log("[AUTH] Sessão RESTAURADA com sucesso!", {
|
||||
userId: userData.id,
|
||||
userType: userData.userType,
|
||||
email: userData.email,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
})
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro na verificação:', error)
|
||||
clearAuthData()
|
||||
console.error("[AUTH] Erro na verificação:", error);
|
||||
clearAuthData();
|
||||
}
|
||||
}, [clearAuthData])
|
||||
}, [clearAuthData]);
|
||||
|
||||
// Login memoizado
|
||||
const login = useCallback(async (
|
||||
email: string,
|
||||
password: string,
|
||||
userType: UserType
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
console.log('[AUTH] Iniciando login:', { email, userType })
|
||||
|
||||
const response = await loginUser(email, password, userType)
|
||||
|
||||
saveAuthData(
|
||||
response.access_token,
|
||||
response.user,
|
||||
response.refresh_token
|
||||
)
|
||||
const login = useCallback(
|
||||
async (
|
||||
email: string,
|
||||
password: string,
|
||||
userType: UserType,
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
console.log("[AUTH] Iniciando login:", { email, userType });
|
||||
|
||||
console.log('[AUTH] Login realizado com sucesso')
|
||||
return true
|
||||
const response = await loginUser(email, password, userType);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro no login:', error)
|
||||
|
||||
if (error instanceof AuthenticationError) {
|
||||
throw error
|
||||
saveAuthData(
|
||||
response.access_token,
|
||||
response.user,
|
||||
response.refresh_token,
|
||||
);
|
||||
|
||||
console.log("[AUTH] Login realizado com sucesso");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[AUTH] Erro no login:", error);
|
||||
|
||||
if (error instanceof AuthenticationError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AuthenticationError(
|
||||
"Erro inesperado durante o login",
|
||||
"UNKNOWN_ERROR",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
throw new AuthenticationError(
|
||||
'Erro inesperado durante o login',
|
||||
'UNKNOWN_ERROR',
|
||||
error
|
||||
)
|
||||
}
|
||||
}, [saveAuthData])
|
||||
},
|
||||
[saveAuthData],
|
||||
);
|
||||
|
||||
// Logout memoizado
|
||||
const logout = useCallback(async (): Promise<void> => {
|
||||
console.log('[AUTH] Iniciando logout')
|
||||
|
||||
const currentUserType = user?.userType ||
|
||||
(typeof window !== 'undefined' ? localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE) : null) ||
|
||||
'profissional'
|
||||
|
||||
console.log("[AUTH] Iniciando logout");
|
||||
|
||||
const currentUserType =
|
||||
user?.userType ||
|
||||
(typeof window !== "undefined"
|
||||
? localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE)
|
||||
: null) ||
|
||||
"profissional";
|
||||
|
||||
try {
|
||||
if (token) {
|
||||
await logoutUser(token)
|
||||
console.log('[AUTH] Logout realizado na API')
|
||||
await logoutUser(token);
|
||||
console.log("[AUTH] Logout realizado na API");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro no logout da API:', error)
|
||||
console.error("[AUTH] Erro no logout da API:", error);
|
||||
}
|
||||
|
||||
clearAuthData()
|
||||
|
||||
clearAuthData();
|
||||
|
||||
// Redirecionamento baseado no tipo de usuário
|
||||
const loginRoute = LOGIN_ROUTES[currentUserType as UserType] || '/login'
|
||||
|
||||
console.log('[AUTH] Redirecionando para:', loginRoute)
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = loginRoute
|
||||
const loginRoute = LOGIN_ROUTES[currentUserType as UserType] || "/login";
|
||||
|
||||
console.log("[AUTH] Redirecionando para:", loginRoute);
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.href = loginRoute;
|
||||
}
|
||||
}, [user?.userType, token, clearAuthData])
|
||||
}, [user?.userType, token, clearAuthData]);
|
||||
|
||||
// Refresh token memoizado (usado pelo HTTP client)
|
||||
const refreshToken = useCallback(async (): Promise<boolean> => {
|
||||
// Esta função é principalmente para compatibilidade
|
||||
// O refresh real é feito pelo HTTP client
|
||||
return false
|
||||
}, [])
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
// Getters memorizados
|
||||
const contextValue = useMemo(() => ({
|
||||
authStatus,
|
||||
user,
|
||||
token,
|
||||
login,
|
||||
logout,
|
||||
refreshToken
|
||||
}), [authStatus, user, token, login, logout, refreshToken])
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
authStatus,
|
||||
user,
|
||||
token,
|
||||
login,
|
||||
logout,
|
||||
refreshToken,
|
||||
}),
|
||||
[authStatus, user, token, login, logout, refreshToken],
|
||||
);
|
||||
|
||||
// Inicialização única
|
||||
useEffect(() => {
|
||||
if (!hasInitialized.current && typeof window !== 'undefined') {
|
||||
hasInitialized.current = true
|
||||
checkAuth()
|
||||
if (!hasInitialized.current && typeof window !== "undefined") {
|
||||
hasInitialized.current = true;
|
||||
checkAuth();
|
||||
}
|
||||
}, [checkAuth])
|
||||
}, [checkAuth]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
<AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext)
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth deve ser usado dentro de AuthProvider')
|
||||
throw new Error("useAuth deve ser usado dentro de AuthProvider");
|
||||
}
|
||||
return context
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,2 +1,117 @@
|
||||
import { ENV_CONFIG } from '@/lib/env-config';
|
||||
import { API_KEY } from '@/lib/config';
|
||||
import { API_KEY } from "@/lib/config";
|
||||
|
||||
export type AssignmentRole = "medico" | "enfermeiro";
|
||||
|
||||
export type PatientAssignment = {
|
||||
id: string;
|
||||
patient_id: string;
|
||||
user_id: string;
|
||||
role: AssignmentRole;
|
||||
created_at: string;
|
||||
created_by?: string | null;
|
||||
};
|
||||
|
||||
export type CreatePatientAssignmentInput = {
|
||||
patient_id: string;
|
||||
user_id: string;
|
||||
role: AssignmentRole;
|
||||
created_by?: string | null;
|
||||
};
|
||||
|
||||
const API_BASE =
|
||||
process.env.NEXT_PUBLIC_ASSIGNMENTS_API_BASE ??
|
||||
"https://mock.apidog.com/m1/1053378-0-default";
|
||||
const REST = `${API_BASE}/rest/v1`;
|
||||
|
||||
function getAuthToken(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return (
|
||||
localStorage.getItem("auth_token") ||
|
||||
localStorage.getItem("token") ||
|
||||
sessionStorage.getItem("auth_token") ||
|
||||
sessionStorage.getItem("token")
|
||||
);
|
||||
}
|
||||
|
||||
function baseHeaders(): HeadersInit {
|
||||
const headers: Record<string, string> = {
|
||||
apikey: API_KEY,
|
||||
Accept: "application/json",
|
||||
};
|
||||
|
||||
const token = getAuthToken();
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function parseResponse<T>(response: Response): Promise<T> {
|
||||
let body: any = null;
|
||||
|
||||
try {
|
||||
body = await response.json();
|
||||
} catch (error) {
|
||||
// Resposta sem corpo (204, etc.)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage =
|
||||
body?.message ||
|
||||
body?.error ||
|
||||
response.statusText ||
|
||||
"Erro ao comunicar com o serviço de atribuições";
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return (body?.data ?? body) as T;
|
||||
}
|
||||
|
||||
export async function listarAtribuicoesPacientes(options?: {
|
||||
patientId?: string;
|
||||
userId?: string;
|
||||
}): Promise<PatientAssignment[]> {
|
||||
const query = new URLSearchParams();
|
||||
|
||||
if (options?.patientId) {
|
||||
query.set("patient_id", `eq.${options.patientId}`);
|
||||
}
|
||||
|
||||
if (options?.userId) {
|
||||
query.set("user_id", `eq.${options.userId}`);
|
||||
}
|
||||
|
||||
const queryString = query.toString();
|
||||
const url = `${REST}/patient_assignments${queryString ? `?${queryString}` : ""}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: baseHeaders(),
|
||||
});
|
||||
|
||||
return parseResponse<PatientAssignment[]>(response);
|
||||
}
|
||||
|
||||
export async function criarAtribuicaoPaciente(
|
||||
input: CreatePatientAssignmentInput,
|
||||
): Promise<PatientAssignment> {
|
||||
const url = `${REST}/patient_assignments`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...baseHeaders(),
|
||||
"Content-Type": "application/json",
|
||||
Prefer: "return=representation",
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
const data = await parseResponse<PatientAssignment[] | PatientAssignment>(
|
||||
response,
|
||||
);
|
||||
|
||||
return Array.isArray(data) ? data[0] : data;
|
||||
}
|
||||
|
||||
@ -1,14 +1,20 @@
|
||||
import type {
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
RefreshTokenResponse,
|
||||
import type {
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
RefreshTokenResponse,
|
||||
AuthError,
|
||||
UserData
|
||||
} from '@/types/auth';
|
||||
UserData,
|
||||
} from "@/types/auth";
|
||||
|
||||
import { API_CONFIG, AUTH_ENDPOINTS, DEFAULT_HEADERS, API_KEY, buildApiUrl } from '@/lib/config';
|
||||
import { debugRequest } from '@/lib/debug-utils';
|
||||
import { ENV_CONFIG } from '@/lib/env-config';
|
||||
import {
|
||||
API_CONFIG,
|
||||
AUTH_ENDPOINTS,
|
||||
DEFAULT_HEADERS,
|
||||
API_KEY,
|
||||
buildApiUrl,
|
||||
} from "@/lib/config";
|
||||
import { debugRequest } from "@/lib/debug-utils";
|
||||
import { ENV_CONFIG } from "@/lib/env-config";
|
||||
|
||||
/**
|
||||
* Classe de erro customizada para autenticação
|
||||
@ -17,10 +23,10 @@ export class AuthenticationError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public code: string,
|
||||
public details?: any
|
||||
public details?: any,
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'AuthenticationError';
|
||||
this.name = "AuthenticationError";
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,9 +36,9 @@ export class AuthenticationError extends Error {
|
||||
function getAuthHeaders(token: string): Record<string, string> {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"apikey": API_KEY,
|
||||
"Authorization": `Bearer ${token}`,
|
||||
Accept: "application/json",
|
||||
apikey: API_KEY,
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
}
|
||||
|
||||
@ -42,8 +48,8 @@ function getAuthHeaders(token: string): Record<string, string> {
|
||||
function getLoginHeaders(): Record<string, string> {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"apikey": API_KEY,
|
||||
Accept: "application/json",
|
||||
apikey: API_KEY,
|
||||
};
|
||||
}
|
||||
|
||||
@ -51,24 +57,32 @@ function getLoginHeaders(): Record<string, string> {
|
||||
* Utilitário para processar resposta da API
|
||||
*/
|
||||
async function processResponse<T>(response: Response): Promise<T> {
|
||||
console.log(`[AUTH] Response status: ${response.status} ${response.statusText}`);
|
||||
|
||||
console.log(
|
||||
`[AUTH] Response status: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
|
||||
let data: any = null;
|
||||
|
||||
|
||||
try {
|
||||
const text = await response.text();
|
||||
if (text) {
|
||||
data = JSON.parse(text);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[AUTH] Response sem JSON ou vazia (normal para alguns endpoints)');
|
||||
console.log(
|
||||
"[AUTH] Response sem JSON ou vazia (normal para alguns endpoints)",
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = data?.message || data?.error || response.statusText || 'Erro na autenticação';
|
||||
const errorMessage =
|
||||
data?.message ||
|
||||
data?.error ||
|
||||
response.statusText ||
|
||||
"Erro na autenticação";
|
||||
const errorCode = data?.code || String(response.status);
|
||||
|
||||
console.error('[AUTH ERROR]', {
|
||||
|
||||
console.error("[AUTH ERROR]", {
|
||||
url: response.url,
|
||||
status: response.status,
|
||||
data,
|
||||
@ -77,7 +91,7 @@ async function processResponse<T>(response: Response): Promise<T> {
|
||||
throw new AuthenticationError(errorMessage, errorCode, data);
|
||||
}
|
||||
|
||||
console.log('[AUTH] Response data:', data);
|
||||
console.log("[AUTH] Response data:", data);
|
||||
return data as T;
|
||||
}
|
||||
|
||||
@ -85,83 +99,86 @@ async function processResponse<T>(response: Response): Promise<T> {
|
||||
* Serviço para fazer login e obter token JWT
|
||||
*/
|
||||
export async function loginUser(
|
||||
email: string,
|
||||
password: string,
|
||||
userType: 'profissional' | 'paciente' | 'administrador'
|
||||
email: string,
|
||||
password: string,
|
||||
userType: "profissional" | "paciente" | "administrador",
|
||||
): Promise<LoginResponse> {
|
||||
let url = AUTH_ENDPOINTS.LOGIN;
|
||||
|
||||
|
||||
const payload = {
|
||||
email,
|
||||
password,
|
||||
};
|
||||
|
||||
console.log('[AUTH-API] Iniciando login...', {
|
||||
email,
|
||||
userType,
|
||||
console.log("[AUTH-API] Iniciando login...", {
|
||||
email,
|
||||
userType,
|
||||
url,
|
||||
payload,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
});
|
||||
|
||||
console.log('🔑 [AUTH-API] Credenciais sendo usadas no login:');
|
||||
console.log('📧 Email:', email);
|
||||
console.log('🔐 Senha:', password);
|
||||
console.log('👤 UserType:', userType);
|
||||
|
||||
console.log("🔑 [AUTH-API] Credenciais sendo usadas no login:");
|
||||
console.log("📧 Email:", email);
|
||||
console.log("🔐 Senha:", password);
|
||||
console.log("👤 UserType:", userType);
|
||||
|
||||
// Delay para visualizar na aba Network
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
try {
|
||||
console.log('[AUTH-API] Enviando requisição de login...');
|
||||
|
||||
console.log("[AUTH-API] Enviando requisição de login...");
|
||||
|
||||
// Debug: Log request sem credenciais sensíveis
|
||||
debugRequest('POST', url, getLoginHeaders(), payload);
|
||||
|
||||
debugRequest("POST", url, getLoginHeaders(), payload);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: getLoginHeaders(),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
console.log(`[AUTH-API] Login response: ${response.status} ${response.statusText}`, {
|
||||
url: response.url,
|
||||
status: response.status,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
});
|
||||
console.log(
|
||||
`[AUTH-API] Login response: ${response.status} ${response.statusText}`,
|
||||
{
|
||||
url: response.url,
|
||||
status: response.status,
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
},
|
||||
);
|
||||
|
||||
// Se falhar, mostrar detalhes do erro
|
||||
if (!response.ok) {
|
||||
try {
|
||||
const errorText = await response.text();
|
||||
console.error('[AUTH-API] Erro detalhado:', {
|
||||
console.error("[AUTH-API] Erro detalhado:", {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: errorText,
|
||||
headers: Object.fromEntries(response.headers.entries())
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[AUTH-API] Não foi possível ler erro da resposta');
|
||||
console.error("[AUTH-API] Não foi possível ler erro da resposta");
|
||||
}
|
||||
}
|
||||
|
||||
// Delay adicional para ver status code
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const data = await processResponse<any>(response);
|
||||
|
||||
console.log('[AUTH] Dados recebidos da API:', data);
|
||||
|
||||
|
||||
console.log("[AUTH] Dados recebidos da API:", data);
|
||||
|
||||
// Verificar se recebemos os dados necessários
|
||||
if (!data || (!data.access_token && !data.token)) {
|
||||
console.error('[AUTH] API não retornou token válido:', data);
|
||||
console.error("[AUTH] API não retornou token válido:", data);
|
||||
throw new AuthenticationError(
|
||||
'API não retornou token de acesso',
|
||||
'NO_TOKEN_RECEIVED',
|
||||
data
|
||||
"API não retornou token de acesso",
|
||||
"NO_TOKEN_RECEIVED",
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Adaptar resposta da sua API para o formato esperado
|
||||
const adaptedResponse: LoginResponse = {
|
||||
access_token: data.access_token || data.token,
|
||||
@ -170,36 +187,36 @@ export async function loginUser(
|
||||
user: {
|
||||
id: data.user?.id || data.id || "1",
|
||||
email: email,
|
||||
name: data.user?.name || data.name || email.split('@')[0],
|
||||
name: data.user?.name || data.name || email.split("@")[0],
|
||||
userType: userType,
|
||||
profile: data.user?.profile || data.profile || {}
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[AUTH-API] LOGIN REALIZADO COM SUCESSO!', {
|
||||
token: adaptedResponse.access_token?.substring(0, 20) + '...',
|
||||
user: {
|
||||
email: adaptedResponse.user.email,
|
||||
userType: adaptedResponse.user.userType
|
||||
profile: data.user?.profile || data.profile || {},
|
||||
},
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
};
|
||||
|
||||
console.log("[AUTH-API] LOGIN REALIZADO COM SUCESSO!", {
|
||||
token: adaptedResponse.access_token?.substring(0, 20) + "...",
|
||||
user: {
|
||||
email: adaptedResponse.user.email,
|
||||
userType: adaptedResponse.user.userType,
|
||||
},
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
});
|
||||
|
||||
// Delay final para visualizar sucesso
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
return adaptedResponse;
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro no login:', error);
|
||||
|
||||
console.error("[AUTH] Erro no login:", error);
|
||||
|
||||
if (error instanceof AuthenticationError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
throw new AuthenticationError(
|
||||
'Email ou senha incorretos',
|
||||
'INVALID_CREDENTIALS',
|
||||
error
|
||||
"Email ou senha incorretos",
|
||||
"INVALID_CREDENTIALS",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -210,83 +227,87 @@ export async function loginUser(
|
||||
export async function logoutUser(token: string): Promise<void> {
|
||||
const url = AUTH_ENDPOINTS.LOGOUT;
|
||||
|
||||
console.log('[AUTH-API] Fazendo logout na API...', {
|
||||
console.log("[AUTH-API] Fazendo logout na API...", {
|
||||
url,
|
||||
hasToken: !!token,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
});
|
||||
|
||||
// Delay para visualizar na aba Network
|
||||
await new Promise(resolve => setTimeout(resolve, 400));
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
|
||||
try {
|
||||
console.log('[AUTH-API] Enviando requisição de logout...');
|
||||
|
||||
console.log("[AUTH-API] Enviando requisição de logout...");
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: getAuthHeaders(token),
|
||||
});
|
||||
|
||||
console.log(`[AUTH-API] Logout response: ${response.status} ${response.statusText}`, {
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[AUTH-API] Logout response: ${response.status} ${response.statusText}`,
|
||||
{
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
},
|
||||
);
|
||||
|
||||
// Delay para ver status code
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||
|
||||
// Logout pode retornar 200, 204 ou até 401 (se token já expirou)
|
||||
// Todos são considerados "sucesso" para logout
|
||||
if (response.ok || response.status === 401) {
|
||||
console.log('[AUTH] Logout realizado com sucesso na API');
|
||||
console.log("[AUTH] Logout realizado com sucesso na API");
|
||||
return;
|
||||
}
|
||||
|
||||
// Se chegou aqui, algo deu errado mas não é crítico para logout
|
||||
console.warn('[AUTH] API retornou status inesperado:', response.status);
|
||||
|
||||
console.warn("[AUTH] API retornou status inesperado:", response.status);
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro ao chamar API de logout:', error);
|
||||
console.error("[AUTH] Erro ao chamar API de logout:", error);
|
||||
}
|
||||
|
||||
|
||||
// Para logout, sempre continuamos mesmo com erro na API
|
||||
// Isso evita que o usuário fique "preso" se a API estiver indisponível
|
||||
console.log('[AUTH] Logout concluído (local sempre executado)');
|
||||
console.log("[AUTH] Logout concluído (local sempre executado)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Serviço para renovar token JWT
|
||||
*/
|
||||
export async function refreshAuthToken(refreshToken: string): Promise<RefreshTokenResponse> {
|
||||
export async function refreshAuthToken(
|
||||
refreshToken: string,
|
||||
): Promise<RefreshTokenResponse> {
|
||||
const url = AUTH_ENDPOINTS.REFRESH;
|
||||
|
||||
console.log('[AUTH] Renovando token');
|
||||
console.log("[AUTH] Renovando token");
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"apikey": API_KEY,
|
||||
Accept: "application/json",
|
||||
apikey: API_KEY,
|
||||
},
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
});
|
||||
|
||||
const data = await processResponse<RefreshTokenResponse>(response);
|
||||
|
||||
console.log('[AUTH] Token renovado com sucesso');
|
||||
|
||||
console.log("[AUTH] Token renovado com sucesso");
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro ao renovar token:', error);
|
||||
|
||||
console.error("[AUTH] Erro ao renovar token:", error);
|
||||
|
||||
if (error instanceof AuthenticationError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
throw new AuthenticationError(
|
||||
'Não foi possível renovar a sessão',
|
||||
'REFRESH_ERROR',
|
||||
error
|
||||
"Não foi possível renovar a sessão",
|
||||
"REFRESH_ERROR",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -297,29 +318,32 @@ export async function refreshAuthToken(refreshToken: string): Promise<RefreshTok
|
||||
export async function getCurrentUser(token: string): Promise<UserData> {
|
||||
const url = AUTH_ENDPOINTS.USER;
|
||||
|
||||
console.log('[AUTH] Obtendo dados do usuário atual');
|
||||
console.log("[AUTH] Obtendo dados do usuário atual");
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: getAuthHeaders(token),
|
||||
});
|
||||
|
||||
const data = await processResponse<UserData>(response);
|
||||
|
||||
console.log('[AUTH] Dados do usuário obtidos:', { id: data.id, email: data.email });
|
||||
|
||||
console.log("[AUTH] Dados do usuário obtidos:", {
|
||||
id: data.id,
|
||||
email: data.email,
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro ao obter usuário atual:', error);
|
||||
|
||||
console.error("[AUTH] Erro ao obter usuário atual:", error);
|
||||
|
||||
if (error instanceof AuthenticationError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
throw new AuthenticationError(
|
||||
'Não foi possível obter dados do usuário',
|
||||
'USER_DATA_ERROR',
|
||||
error
|
||||
"Não foi possível obter dados do usuário",
|
||||
"USER_DATA_ERROR",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -331,8 +355,8 @@ export function isTokenExpired(expiryTimestamp: number): boolean {
|
||||
const now = Date.now();
|
||||
const expiry = expiryTimestamp * 1000; // Converter para milliseconds
|
||||
const buffer = 5 * 60 * 1000; // Buffer de 5 minutos
|
||||
|
||||
return now >= (expiry - buffer);
|
||||
|
||||
return now >= expiry - buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -341,13 +365,13 @@ export function isTokenExpired(expiryTimestamp: number): boolean {
|
||||
export function createAuthenticatedFetch(getToken: () => string | null) {
|
||||
return async (url: string, options: RequestInit = {}): Promise<Response> => {
|
||||
const token = getToken();
|
||||
|
||||
|
||||
if (token) {
|
||||
const headers = {
|
||||
...options.headers,
|
||||
...getAuthHeaders(token),
|
||||
};
|
||||
|
||||
|
||||
options = {
|
||||
...options,
|
||||
headers,
|
||||
@ -356,4 +380,4 @@ export function createAuthenticatedFetch(getToken: () => string | null) {
|
||||
|
||||
return fetch(url, options);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ENV_CONFIG } from './env-config';
|
||||
import { ENV_CONFIG } from "./env-config";
|
||||
|
||||
export const API_CONFIG = {
|
||||
BASE_URL: ENV_CONFIG.SUPABASE_URL + "/rest/v1",
|
||||
@ -12,11 +12,11 @@ export const API_KEY = ENV_CONFIG.SUPABASE_ANON_KEY;
|
||||
|
||||
export const DEFAULT_HEADERS = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
Accept: "application/json",
|
||||
} as const;
|
||||
|
||||
export function buildApiUrl(endpoint: string): string {
|
||||
const baseUrl = API_CONFIG.BASE_URL.replace(/\/$/, '');
|
||||
const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||
const baseUrl = API_CONFIG.BASE_URL.replace(/\/$/, "");
|
||||
const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
||||
return `${baseUrl}${cleanEndpoint}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,23 +6,31 @@ export function debugRequest(
|
||||
method: string,
|
||||
url: string,
|
||||
headers: Record<string, string>,
|
||||
body?: any
|
||||
body?: any,
|
||||
) {
|
||||
if (process.env.NODE_ENV !== 'development') return;
|
||||
if (process.env.NODE_ENV !== "development") return;
|
||||
|
||||
const headersWithoutSensitive = Object.keys(headers).reduce((acc, key) => {
|
||||
// Não logar valores sensíveis, apenas nomes
|
||||
if (key.toLowerCase().includes('apikey') || key.toLowerCase().includes('authorization')) {
|
||||
acc[key] = '[REDACTED]';
|
||||
} else {
|
||||
acc[key] = headers[key];
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
const headersWithoutSensitive = Object.keys(headers).reduce(
|
||||
(accumulator, key) => {
|
||||
// Não logar valores sensíveis, apenas nomes
|
||||
if (
|
||||
key.toLowerCase().includes("apikey") ||
|
||||
key.toLowerCase().includes("authorization")
|
||||
) {
|
||||
accumulator[key] = "[REDACTED]";
|
||||
} else {
|
||||
accumulator[key] = headers[key];
|
||||
}
|
||||
return accumulator;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
const bodyShape = body ? Object.keys(typeof body === 'string' ? JSON.parse(body) : body) : [];
|
||||
const bodyShape = body
|
||||
? Object.keys(typeof body === "string" ? JSON.parse(body) : body)
|
||||
: [];
|
||||
|
||||
console.log('[DEBUG] Request Preview:', {
|
||||
console.log("[DEBUG] Request Preview:", {
|
||||
method,
|
||||
path: new URL(url).pathname,
|
||||
query: new URL(url).search,
|
||||
@ -31,4 +39,4 @@ export function debugRequest(
|
||||
bodyShape,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user