Files
EpicNext-Cms/src/app/admin/logs/page.tsx
T
Remco 0323c3fcaa fix: improve error handling in admin pages to show user-friendly error messages
- Add error display to help questions new/edit pages
- Improve error visibility in admin-badges, admin-applications, admin-logs, admin-users pages
- Update createHelpQuestion action to properly handle and display unique name collisions and database errors
- Add user-friendly error messages to admin error handling
- Enhances admin page error reporting for better user experience
2026-07-01 21:25:30 +02:00

188 lines
5.5 KiB
TypeScript

import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
const PER_PAGE = 25;
function fromDate(d: Date | null | undefined): string {
return d ? d.toISOString().slice(0, 16).replace("T", " ") : "—";
}
export default async function AdminStaffActivities({
searchParams,
}: {
searchParams: Promise<{ q?: string; page?: string; staffId?: string; action?: string }>;
}) {
const sp = await searchParams;
const q = (sp.q ?? "").trim();
const page = Math.max(1, Number(sp.page ?? "1") || 1);
const staffId = sp.staffId ? Number(sp.staffId) : null;
const action = sp.action ?? null;
let whereClause = {};
if (q) {
whereClause = {
OR: [
{ action: { contains: q } },
{ description: { contains: q } },
{ ipAddress: { contains: q } },
],
};
}
if (staffId) {
whereClause = {
...whereClause,
userId: BigInt(staffId),
};
}
if (action) {
whereClause = {
...whereClause,
action: { contains: action },
};
}
const activities = await prisma.staffActivities
.findMany({
where: whereClause,
select: {
id: true,
userId: true,
action: true,
description: true,
targetType: true,
targetId: true,
ipAddress: true,
createdAt: true,
},
orderBy: { createdAt: "desc" },
skip: (page - 1) * PER_PAGE,
take: PER_PAGE,
})
.catch(() => []);
const staffIds = Array.from(new Set(activities.map((a) => Number(a.userId)))).filter((n) => n > 0);
let nameById = new Map<number, string>();
if (staffIds.length) {
const users = await prisma.user
.findMany({ where: { id: { in: staffIds } }, select: { id: true, username: true } })
.catch(() => []);
nameById = new Map(users.map((x) => [x.id, x.username]));
}
const total = await prisma.staffActivities.count({ where: whereClause }).catch(() => 0);
const pages = Math.max(1, Math.ceil(total / PER_PAGE));
const onlineCount = activities.filter((a) => a.createdAt && new Date(a.createdAt).getTime() > Date.now() - 60000).length;
return (
<main>
<h1>Staff Activities</h1>
<p className="muted" style={{ marginTop: "-0.4rem" }}>
Auditor trail of all staff actions in the hotel.
</p>
<div className="admin-stats">
<StatusCard label="Total activities" value={total} icon="📋" />
<StatusCard label="On this page" value={activities.length} icon="📄" />
<StatusCard
label="Recent (≤1min)"
value={onlineCount}
state={onlineCount > 0 ? "ok" : "neutral"}
icon="🟢"
/>
</div>
<form
className="card"
style={{ display: "flex", gap: "0.5rem", marginBottom: "1.5rem", flexWrap: "wrap" }}
>
<input
name="q"
defaultValue={q}
placeholder="Search action, description, IP..."
style={{ flex: 1, minWidth: 200 }}
/>
<input
name="staffId"
defaultValue={staffId?.toString()}
placeholder="Staff ID"
style={{ width: 120 }}
/>
<input
name="action"
defaultValue={action ?? ""}
placeholder="Action type (e.g., rank_change)"
style={{ width: 160 }}
/>
<button type="submit" className="btn btn-primary">
Filter
</button>
{(q || staffId || action) && (
<a href="/admin/logs" className="btn btn-outline">
Clear filters
</a>
)}
</form>
<section className="admin-section">
<h2>Activities ({total})</h2>
{activities.length === 0 ? (
<p className="muted">No staff activities match this search.</p>
) : (
<div style={{ overflowX: "auto" }}>
<table>
<thead>
<tr>
<th>When</th>
<th>Staff</th>
<th>Action</th>
<th>Description</th>
<th>Target</th>
<th>IP Address</th>
</tr>
</thead>
<tbody>
{activities.map((a) => (
<tr key={String(a.id)}>
<td className="muted" style={{ whiteSpace: "nowrap" }}>{fromDate(a.createdAt)}</td>
<td className="muted">#{Number(a.userId)} ({nameById.get(Number(a.userId)) ?? "unknown"})</td>
<td>
<span className="admin-badge">{a.action}</span>
</td>
<td>{a.description}</td>
<td>
{a.targetType && a.targetId ? (
<span className="muted">{a.targetType === "user" ? "User " : ""}#{String(a.targetId)}</span>
) : (
<span className="muted">—</span>
)}
</td>
<td className="muted">{a.ipAddress || "—"}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
<p className="muted">
Page {page} / {pages} · {total} activities
{page < pages ? (
<>
{" · "}
<a href={`/admin/logs?q=${encodeURIComponent(q)}&staffId=${staffId || ""}&action=${action || ""}&page=${page + 1}`}>Next →</a>
</>
) : null}
</p>
</main>
);
}