Dental Care

This commit is contained in:
Iliyan Angelov
2025-11-16 14:29:51 +02:00
commit 39077550ef
194 changed files with 43197 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
"use server";
import { getServerSession } from "@/lib/auth-session/get-session";
import { forbidden, unauthorized } from "next/navigation";
import { setTimeout } from "node:timers/promises";
export async function deleteApplication() {
const session = await getServerSession();
const user = session?.user;
if (!user) unauthorized();
if (user.role !== "admin") forbidden();
// Delete app...
await setTimeout(800);
}

View File

@@ -0,0 +1,49 @@
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import { AdminAppointmentsTable } from "@/components/admin/appointments-table";
import { requireAdmin } from "@/lib/auth-session/auth-server";
import { safeFindManyAppointments } from "@/lib/utils/appointment-helpers";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Appointment Management",
};
// Force dynamic rendering since this page uses authentication (headers)
export const dynamic = "force-dynamic";
export default async function AppointmentManagementPage() {
const { user } = await requireAdmin();
// Add pagination limit to prevent loading too much data at once
// Use safe find to filter out orphaned appointments
const appointments = await safeFindManyAppointments({
take: 100, // Limit to 100 most recent appointments
include: {
patient: true,
dentist: true,
service: true,
payment: true,
},
orderBy: {
date: "desc",
},
});
return (
<DashboardLayout
user={{ ...user, role: user.role || "admin" }}
role="admin"
>
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6 px-4 lg:px-6">
<div>
<h1 className="text-3xl font-bold">Appointment Management</h1>
<p className="text-muted-foreground">
Manage all appointments in the system
</p>
</div>
<AdminAppointmentsTable appointments={appointments} />
</div>
</DashboardLayout>
);
}

View File

@@ -0,0 +1,62 @@
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import { AdminDentistsTable } from "@/components/admin/dentists-table";
import { requireAdmin } from "@/lib/auth-session/auth-server";
import { prisma } from "@/lib/types/prisma";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Dentist Management",
};
// Force dynamic rendering since this page uses authentication (headers)
export const dynamic = "force-dynamic";
export default async function DentistManagementPage() {
const { user } = await requireAdmin();
const dentistsData = await prisma.user.findMany({
take: 50, // Limit to 50 dentists to prevent excessive data loading
where: {
role: "dentist",
},
include: {
appointmentsAsDentist: {
take: 10, // Limit appointments per dentist to avoid N+1 issue
include: {
service: true,
patient: true,
},
orderBy: {
date: "desc",
},
},
},
orderBy: {
createdAt: "desc",
},
});
// Transform the data to match the expected Dentist type
const dentists = dentistsData.map((dentist) => ({
...dentist,
experience: dentist.experience !== null ? String(dentist.experience) : null,
}));
return (
<DashboardLayout
user={{ ...user, role: user.role || "admin" }}
role="admin"
>
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6 px-4 lg:px-6">
<div>
<h1 className="text-3xl font-bold">Dentist Management</h1>
<p className="text-muted-foreground">
Manage all dentists in the system
</p>
</div>
<AdminDentistsTable dentists={dentists} />
</div>
</DashboardLayout>
);
}

197
app/(main)/admin/page.tsx Normal file
View File

@@ -0,0 +1,197 @@
import { ChartAreaInteractive } from "@/components/chart/chart-area-interactive";
import { AdminAppointmentsTable } from "@/components/admin/appointments-table";
import { SectionCards } from "@/components/layout/section-cards";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import type { Metadata } from "next";
import { requireAdmin } from "@/lib/auth-session/auth-server";
import { prisma } from "@/lib/types/prisma";
import { safeFindManyAppointments } from "@/lib/utils/appointment-helpers";
export const metadata: Metadata = {
title: "Dashboard",
};
// Force dynamic rendering since this page uses authentication (headers)
export const dynamic = "force-dynamic";
export default async function Page() {
// Require admin role - will redirect to home page (/) if not admin
const { user } = await requireAdmin();
// Calculate date ranges once for reuse
const now = new Date();
const DAY_IN_MS = 24 * 60 * 60 * 1000;
const thirtyDaysAgo = new Date(now.getTime() - 30 * DAY_IN_MS);
const sixtyDaysAgo = new Date(now.getTime() - 60 * DAY_IN_MS);
const ninetyDaysAgo = new Date(now.getTime() - 90 * DAY_IN_MS);
// Run all count queries in parallel for better performance
const [
totalAppointments,
previousAppointments,
newPatients,
previousPatients,
payments,
previousPayments,
completedAppointments,
previousCompleted,
appointmentsForChart,
appointments,
] = await Promise.all([
// Total appointments in last 30 days
prisma.appointment.count({
where: {
createdAt: { gte: thirtyDaysAgo },
},
}),
// Previous period appointments for comparison
prisma.appointment.count({
where: {
createdAt: { gte: sixtyDaysAgo, lt: thirtyDaysAgo },
},
}),
// New patients in last 30 days
prisma.user.count({
where: {
role: "patient",
createdAt: { gte: thirtyDaysAgo },
},
}),
// Previous period patients
prisma.user.count({
where: {
role: "patient",
createdAt: { gte: sixtyDaysAgo, lt: thirtyDaysAgo },
},
}),
// Revenue this month
prisma.payment.aggregate({
where: {
status: "paid",
paidAt: { gte: thirtyDaysAgo },
},
_sum: {
amount: true,
},
}),
// Previous period revenue
prisma.payment.aggregate({
where: {
status: "paid",
paidAt: { gte: sixtyDaysAgo, lt: thirtyDaysAgo },
},
_sum: {
amount: true,
},
}),
// Calculate completed appointments for satisfaction (mock calculation)
prisma.appointment.count({
where: {
status: "completed",
updatedAt: { gte: thirtyDaysAgo },
},
}),
// Previous completed
prisma.appointment.count({
where: {
status: "completed",
updatedAt: { gte: sixtyDaysAgo, lt: thirtyDaysAgo },
},
}),
// Fetch appointments for chart (last 90 days)
prisma.appointment.findMany({
where: {
createdAt: { gte: ninetyDaysAgo },
},
select: {
createdAt: true,
},
orderBy: {
createdAt: "asc",
},
}),
// Fetch recent appointments for table
// Use safe find to filter out orphaned appointments
safeFindManyAppointments({
take: 20,
orderBy: {
createdAt: "desc",
},
include: {
patient: true,
dentist: true,
service: true,
payment: true,
},
}),
]);
const revenue = payments._sum.amount || 0;
const previousRevenue = previousPayments._sum.amount || 0;
// Calculate percentage changes
const appointmentChange =
previousAppointments > 0
? ((totalAppointments - previousAppointments) / previousAppointments) *
100
: 0;
const patientChange =
previousPatients > 0
? ((newPatients - previousPatients) / previousPatients) * 100
: 0;
const revenueChange =
previousRevenue > 0
? ((revenue - previousRevenue) / previousRevenue) * 100
: 0;
const satisfactionChange =
previousCompleted > 0
? ((completedAppointments - previousCompleted) / previousCompleted) * 100
: 0;
// Mock satisfaction rate (in a real app, this would come from reviews/ratings)
const satisfactionRate = 98.5;
// Group appointments by date for chart
const chartData = appointmentsForChart.reduce(
(acc: Record<string, number>, appointment) => {
const date = appointment.createdAt.toISOString().split("T")[0];
acc[date] = (acc[date] || 0) + 1;
return acc;
},
{}
);
// Convert to array format for chart
const chartDataArray = Object.entries(chartData).map(([date, count]) => ({
date,
appointments: count,
}));
const dashboardStats = {
totalAppointments,
appointmentChange,
newPatients,
patientChange,
revenue,
revenueChange,
satisfactionRate,
satisfactionChange,
};
return (
<DashboardLayout
user={{ ...user, role: user.role || "admin" }}
role="admin"
>
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
<SectionCards stats={dashboardStats} />
<div className="px-4 lg:px-6">
<ChartAreaInteractive data={chartDataArray} />
</div>
<div className="px-4 lg:px-6">
<AdminAppointmentsTable appointments={appointments} />
</div>
</div>
</DashboardLayout>
);
}

View File

@@ -0,0 +1,62 @@
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import { AdminPatientsTable } from "@/components/admin/patients-table";
import { requireAdmin } from "@/lib/auth-session/auth-server";
import { prisma } from "@/lib/types/prisma";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Patient Management",
};
// Force dynamic rendering since this page uses authentication (headers)
export const dynamic = "force-dynamic";
export default async function PatientManagementPage() {
const { user } = await requireAdmin();
const patients = await prisma.user.findMany({
take: 50, // Limit to 50 patients to prevent excessive data loading
where: {
role: "patient",
},
include: {
appointmentsAsPatient: {
take: 10, // Limit appointments per patient to avoid N+1 issue
include: {
service: true,
dentist: true,
},
orderBy: {
date: "desc",
},
},
payments: {
take: 10, // Limit payments per patient
orderBy: {
createdAt: "desc",
},
},
},
orderBy: {
createdAt: "desc",
},
});
return (
<DashboardLayout
user={{ ...user, role: user.role || "admin" }}
role="admin"
>
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6 px-4 lg:px-6">
<div>
<h1 className="text-3xl font-bold">Patient Management</h1>
<p className="text-muted-foreground">
Manage all patients in the system
</p>
</div>
<AdminPatientsTable patients={patients} />
</div>
</DashboardLayout>
);
}

View File

@@ -0,0 +1,53 @@
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import { AdminServicesTable } from "@/components/admin/services-table";
import { requireAdmin } from "@/lib/auth-session/auth-server";
import { prisma } from "@/lib/types/prisma";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Service Management",
};
// Force dynamic rendering since this page uses authentication (headers)
export const dynamic = "force-dynamic";
export default async function ServiceManagementPage() {
const { user } = await requireAdmin();
const servicesData = await prisma.service.findMany({
take: 100, // Limit to 100 services
include: {
appointments: {
take: 5, // Limit appointments per service to avoid N+1 issue
orderBy: {
date: "desc",
},
},
},
orderBy: {
name: "asc",
},
});
// Transform the data to match the expected Service type
const services = servicesData.map((service) => ({
...service,
description: service.description ?? "",
}));
return (
<DashboardLayout
user={{ ...user, role: user.role || "admin" }}
role="admin"
>
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6 px-4 lg:px-6">
<div>
<h1 className="text-3xl font-bold">Service Management</h1>
<p className="text-muted-foreground">Manage all dental services</p>
</div>
<AdminServicesTable services={services} />
</div>
</DashboardLayout>
);
}

View File

@@ -0,0 +1,19 @@
import { requireAdmin } from "@/lib/auth-session/auth-server";
import { AdminSettingsContent } from "@/components/admin/settings-content";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
// Force dynamic rendering since this page uses authentication (headers)
export const dynamic = "force-dynamic";
export default async function AdminSettingsPage() {
const { user } = await requireAdmin();
return (
<DashboardLayout
user={{ ...user, role: user.role || "admin" }}
role="admin"
>
<AdminSettingsContent user={{ ...user, role: user.role || "admin" }} />
</DashboardLayout>
);
}

View File

@@ -0,0 +1,42 @@
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import { AdminUsersTable } from "@/components/admin/users-table";
import { requireAdmin } from "@/lib/auth-session/auth-server";
import { prisma } from "@/lib/types/prisma";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "User Management",
};
// Force dynamic rendering since this page uses authentication (headers)
export const dynamic = "force-dynamic";
export default async function UserManagementPage() {
const { user } = await requireAdmin();
const usersRaw = await prisma.user.findMany({
take: 100, // Limit to 100 most recent users
orderBy: {
createdAt: "desc",
},
});
const users = usersRaw.map((u) => ({ ...u, role: u.role ?? undefined }));
return (
<DashboardLayout
user={{ ...user, role: user.role || "admin" }}
role="admin"
>
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6 px-4 lg:px-6">
<div>
<h1 className="text-3xl font-bold">User Management</h1>
<p className="text-muted-foreground">
Manage all users in the system
</p>
</div>
<AdminUsersTable users={users} />
</div>
</DashboardLayout>
);
}