This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
+153
-129
@@ -1,5 +1,5 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Bell } from "lucide-react";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { sendHotelAlert } from "@/actions/admin-alerts";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
@@ -10,147 +10,171 @@ const CRITICAL = new Set(["critical", "error", "danger"]);
|
||||
const WARNING = new Set(["warning", "warn"]);
|
||||
|
||||
function fromDate(d: Date | null | undefined): string {
|
||||
return d ? d.toISOString().slice(0, 16).replace("T", " ") : "—";
|
||||
return d ? d.toISOString().slice(0, 16).replace("T", " ") : "—";
|
||||
}
|
||||
|
||||
function severityColor(severity: string): string {
|
||||
switch (severity.toLowerCase()) {
|
||||
case "critical":
|
||||
case "error":
|
||||
case "danger":
|
||||
return "var(--color-danger)";
|
||||
case "warning":
|
||||
case "warn":
|
||||
return "var(--admin-accent)";
|
||||
case "success":
|
||||
return "var(--color-secondary)";
|
||||
case "info":
|
||||
case "notice":
|
||||
return "var(--color-accent)";
|
||||
default:
|
||||
return "var(--admin-text-muted)";
|
||||
}
|
||||
switch (severity.toLowerCase()) {
|
||||
case "critical":
|
||||
case "error":
|
||||
case "danger":
|
||||
return "var(--color-danger)";
|
||||
case "warning":
|
||||
case "warn":
|
||||
return "var(--admin-accent)";
|
||||
case "success":
|
||||
return "var(--color-secondary)";
|
||||
case "info":
|
||||
case "notice":
|
||||
return "var(--color-accent)";
|
||||
default:
|
||||
return "var(--admin-text-muted)";
|
||||
}
|
||||
}
|
||||
|
||||
function Badge({ label, color }: { label: string; color: string }) {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
padding: "0.1rem 0.55rem",
|
||||
borderRadius: "999px",
|
||||
background: color,
|
||||
color: "#fff",
|
||||
fontSize: "0.72rem",
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.02em",
|
||||
textTransform: "uppercase",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
padding: "0.1rem 0.55rem",
|
||||
borderRadius: "999px",
|
||||
background: color,
|
||||
color: "#fff",
|
||||
fontSize: "0.72rem",
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.02em",
|
||||
textTransform: "uppercase",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function AdminAlerts() {
|
||||
const t = await getTranslations("pages.admin.alerts");
|
||||
const t = await getTranslations("pages.admin.alerts");
|
||||
|
||||
let alerts: Awaited<ReturnType<typeof prisma.alertLogs.findMany>>;
|
||||
try {
|
||||
alerts = await prisma.alertLogs.findMany({
|
||||
orderBy: { id: "desc" },
|
||||
take: 100,
|
||||
});
|
||||
} catch {
|
||||
alerts = [];
|
||||
}
|
||||
let alerts: Awaited<ReturnType<typeof prisma.alertLogs.findMany>>;
|
||||
try {
|
||||
alerts = await prisma.alertLogs.findMany({
|
||||
orderBy: { id: "desc" },
|
||||
take: 100,
|
||||
});
|
||||
} catch {
|
||||
alerts = [];
|
||||
}
|
||||
|
||||
const critical = alerts.filter((a) => CRITICAL.has((a.severity || "").toLowerCase())).length;
|
||||
const warnings = alerts.filter((a) => WARNING.has((a.severity || "").toLowerCase())).length;
|
||||
const unread = alerts.filter((a) => !a.isRead).length;
|
||||
const critical = alerts.filter((a) =>
|
||||
CRITICAL.has((a.severity || "").toLowerCase()),
|
||||
).length;
|
||||
const warnings = alerts.filter((a) =>
|
||||
WARNING.has((a.severity || "").toLowerCase()),
|
||||
).length;
|
||||
const unread = alerts.filter((a) => !a.isRead).length;
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[var(--admin-accent)]/20 to-[var(--admin-accent)]/5 grid place-items-center">
|
||||
<Bell size={20} className="text-[var(--admin-accent)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="m-0 text-xl font-extrabold text-[var(--admin-text)]">{t("title")}</h1>
|
||||
<p className="m-0 text-xs text-[var(--admin-text-muted)] mt-0.5">{t("subtitle")}</p>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<main>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[var(--admin-accent)]/20 to-[var(--admin-accent)]/5 grid place-items-center">
|
||||
<Bell size={20} className="text-[var(--admin-accent)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="m-0 text-xl font-extrabold text-[var(--admin-text)]">
|
||||
{t("title")}
|
||||
</h1>
|
||||
<p className="m-0 text-xs text-[var(--admin-text-muted)] mt-0.5">
|
||||
{t("subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard label={t("alertsShown")} value={alerts.length} icon="🔔" />
|
||||
<StatusCard label={t("critical")} value={critical} state={critical > 0 ? "danger" : "ok"} icon="🚨" />
|
||||
<StatusCard
|
||||
label={t("warnings")}
|
||||
value={warnings}
|
||||
state={warnings > 0 ? "warn" : "neutral"}
|
||||
icon="⚠️"
|
||||
/>
|
||||
<StatusCard label={t("unread")} value={unread} state={unread > 0 ? "warn" : "ok"} icon="📨" />
|
||||
</div>
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard label={t("alertsShown")} value={alerts.length} icon="🔔" />
|
||||
<StatusCard
|
||||
label={t("critical")}
|
||||
value={critical}
|
||||
state={critical > 0 ? "danger" : "ok"}
|
||||
icon="🚨"
|
||||
/>
|
||||
<StatusCard
|
||||
label={t("warnings")}
|
||||
value={warnings}
|
||||
state={warnings > 0 ? "warn" : "neutral"}
|
||||
icon="⚠️"
|
||||
/>
|
||||
<StatusCard
|
||||
label={t("unread")}
|
||||
value={unread}
|
||||
state={unread > 0 ? "warn" : "ok"}
|
||||
icon="📨"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form action={sendHotelAlert} className="admin-card mb-6">
|
||||
<h3 className="mt-0">{t("form.send")}</h3>
|
||||
<p className="text-xs theme-text-muted dark:theme-text-muted">
|
||||
{t.rich("form.description", {
|
||||
code: (chunks) => <code>{chunks}</code>,
|
||||
})}
|
||||
</p>
|
||||
<div className="flex gap-2 flex-wrap items-start">
|
||||
<textarea
|
||||
name="message"
|
||||
required
|
||||
maxLength={1000}
|
||||
rows={2}
|
||||
placeholder={t("form.messagePlaceholder")}
|
||||
className="flex-1 min-w-[240px]"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
{t("form.send")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<form action={sendHotelAlert} className="admin-card mb-6">
|
||||
<h3 className="mt-0">{t("form.send")}</h3>
|
||||
<p className="text-xs theme-text-muted dark:theme-text-muted">
|
||||
{t.rich("form.description", {
|
||||
code: (chunks) => <code>{chunks}</code>,
|
||||
})}
|
||||
</p>
|
||||
<div className="flex gap-2 flex-wrap items-start">
|
||||
<textarea
|
||||
name="message"
|
||||
required
|
||||
maxLength={1000}
|
||||
rows={2}
|
||||
placeholder={t("form.messagePlaceholder")}
|
||||
className="flex-1 min-w-[240px]"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
{t("form.send")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section className="admin-card">
|
||||
<h3 className="mt-0">{t("recentAlerts", { count: alerts.length })}</h3>
|
||||
{alerts.length === 0 ? (
|
||||
<div className="admin-empty">{t("noAlerts")}</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("colWhen")}</th>
|
||||
<th>{t("colSeverity")}</th>
|
||||
<th>{t("colType")}</th>
|
||||
<th>{t("colMessage")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{alerts.map((a) => (
|
||||
<tr key={String(a.id)}>
|
||||
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||
{fromDate(a.createdAt)}
|
||||
</td>
|
||||
<td>
|
||||
<Badge label={a.severity || "—"} color={severityColor(a.severity)} />
|
||||
</td>
|
||||
<td>
|
||||
<Badge label={a.type || "—"} color="var(--admin-text-muted)" />
|
||||
</td>
|
||||
<td>{a.message}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
<section className="admin-card">
|
||||
<h3 className="mt-0">{t("recentAlerts", { count: alerts.length })}</h3>
|
||||
{alerts.length === 0 ? (
|
||||
<div className="admin-empty">{t("noAlerts")}</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("colWhen")}</th>
|
||||
<th>{t("colSeverity")}</th>
|
||||
<th>{t("colType")}</th>
|
||||
<th>{t("colMessage")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{alerts.map((a) => (
|
||||
<tr key={String(a.id)}>
|
||||
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||
{fromDate(a.createdAt)}
|
||||
</td>
|
||||
<td>
|
||||
<Badge
|
||||
label={a.severity || "—"}
|
||||
color={severityColor(a.severity)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Badge
|
||||
label={a.type || "—"}
|
||||
color="var(--admin-text-muted)"
|
||||
/>
|
||||
</td>
|
||||
<td>{a.message}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user