refactor(db): migrate app pages and APIs from Prisma facade to Drizzle (3)
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
2cd0863cb8
commit
580972c0a0
40 files changed
+1089
-808
No files matched your search
@@ -1,11 +1,12 @@
|
||||
import { asc, count, eq, inArray } from "drizzle-orm";
|
||||
import Link from "next/link";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { disbandGuild } from "@/actions/admin-guilds";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, Guilds, GuildsForumsThreads, GuildsMembers, User } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export default async function AdminGuildDetailPage({
|
||||
params,
|
||||
@@ -23,49 +24,65 @@ export default async function AdminGuildDetailPage({
|
||||
const id = Number(rawId);
|
||||
if (!Number.isFinite(id) || id <= 0) notFound();
|
||||
|
||||
const guild = await prisma.guilds.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
userId: true,
|
||||
roomId: true,
|
||||
badge: true,
|
||||
dateCreated: true,
|
||||
state: true,
|
||||
forum: true,
|
||||
},
|
||||
});
|
||||
const [guild] = await db
|
||||
.select({
|
||||
id: Guilds.id,
|
||||
name: Guilds.name,
|
||||
description: Guilds.description,
|
||||
userId: Guilds.userId,
|
||||
roomId: Guilds.roomId,
|
||||
badge: Guilds.badge,
|
||||
dateCreated: Guilds.dateCreated,
|
||||
state: Guilds.state,
|
||||
forum: Guilds.forum,
|
||||
})
|
||||
.from(Guilds)
|
||||
.where(eq(Guilds.id, id))
|
||||
.limit(1);
|
||||
if (!guild) notFound();
|
||||
|
||||
const [owner, members, memberCount, threadCount] = await Promise.all([
|
||||
prisma.user
|
||||
.findUnique({
|
||||
where: { id: guild.userId },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
prisma.guildsMembers
|
||||
.findMany({
|
||||
where: { guildId: id },
|
||||
orderBy: [{ levelId: "asc" }, { memberSince: "asc" }],
|
||||
take: 100,
|
||||
select: { userId: true, levelId: true, memberSince: true },
|
||||
})
|
||||
.catch(() => []),
|
||||
prisma.guildsMembers.count({ where: { guildId: id } }).catch(() => 0),
|
||||
prisma.guildsForumsThreads.count({ where: { guildId: id } }).catch(() => 0),
|
||||
]);
|
||||
const [ownerRows, members, memberCountRows, threadCountRows] =
|
||||
await Promise.all([
|
||||
db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(eq(User.id, guild.userId))
|
||||
.limit(1)
|
||||
.catch(() => []),
|
||||
db
|
||||
.select({
|
||||
userId: GuildsMembers.userId,
|
||||
levelId: GuildsMembers.levelId,
|
||||
memberSince: GuildsMembers.memberSince,
|
||||
})
|
||||
.from(GuildsMembers)
|
||||
.where(eq(GuildsMembers.guildId, id))
|
||||
.orderBy(asc(GuildsMembers.levelId), asc(GuildsMembers.memberSince))
|
||||
.limit(100)
|
||||
.catch(() => []),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(GuildsMembers)
|
||||
.where(eq(GuildsMembers.guildId, id))
|
||||
.catch(() => [{ total: 0 }]),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(GuildsForumsThreads)
|
||||
.where(eq(GuildsForumsThreads.guildId, id))
|
||||
.catch(() => [{ total: 0 }]),
|
||||
]);
|
||||
|
||||
const owner = ownerRows[0] ?? null;
|
||||
const memberCount = Number(memberCountRows[0]?.total ?? 0);
|
||||
const threadCount = Number(threadCountRows[0]?.total ?? 0);
|
||||
|
||||
const memberUserIds = members.map((m) => m.userId).filter((uid) => uid > 0);
|
||||
const memberUsers =
|
||||
memberUserIds.length > 0
|
||||
? await prisma.user
|
||||
.findMany({
|
||||
where: { id: { in: memberUserIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
? await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, memberUserIds))
|
||||
.catch(() => [])
|
||||
: [];
|
||||
const usernameById = new Map(memberUsers.map((u) => [u.id, u.username]));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { count, desc, eq, inArray, like, or } from "drizzle-orm";
|
||||
import { Shield } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -7,9 +8,9 @@ import { AdminSimplePager } from "@/components/admin/admin-simple-pager";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db, Guilds, GuildsMembers, User } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export default async function AdminGuildsPage({
|
||||
searchParams,
|
||||
@@ -40,34 +41,36 @@ export default async function AdminGuildsPage({
|
||||
|
||||
try {
|
||||
const where = q
|
||||
? {
|
||||
OR: [
|
||||
{ name: { contains: q } },
|
||||
{ description: { contains: q } },
|
||||
...(Number.isFinite(Number(q)) && Number(q) > 0
|
||||
? [{ id: Number(q) }]
|
||||
: []),
|
||||
],
|
||||
}
|
||||
: {};
|
||||
? or(
|
||||
like(Guilds.name, `%${q}%`),
|
||||
like(Guilds.description, `%${q}%`),
|
||||
...(Number.isFinite(Number(q)) && Number(q) > 0
|
||||
? [eq(Guilds.id, Number(q))]
|
||||
: []),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
total = await prisma.guilds.count({ where });
|
||||
const [totalRow] = await db
|
||||
.select({ total: count() })
|
||||
.from(Guilds)
|
||||
.where(where);
|
||||
total = Number(totalRow?.total ?? 0);
|
||||
const pagination = calcPagination(total, parsed.page, parsed.perPage);
|
||||
rows = await prisma.guilds.findMany({
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
userId: true,
|
||||
badge: true,
|
||||
dateCreated: true,
|
||||
roomId: true,
|
||||
},
|
||||
orderBy: { id: "desc" },
|
||||
skip: pagination.offset,
|
||||
take: pagination.perPage,
|
||||
});
|
||||
rows = await db
|
||||
.select({
|
||||
id: Guilds.id,
|
||||
name: Guilds.name,
|
||||
description: Guilds.description,
|
||||
userId: Guilds.userId,
|
||||
badge: Guilds.badge,
|
||||
dateCreated: Guilds.dateCreated,
|
||||
roomId: Guilds.roomId,
|
||||
})
|
||||
.from(Guilds)
|
||||
.where(where)
|
||||
.orderBy(desc(Guilds.id))
|
||||
.limit(pagination.perPage)
|
||||
.offset(pagination.offset);
|
||||
} catch {
|
||||
loadError = true;
|
||||
}
|
||||
@@ -80,27 +83,28 @@ export default async function AdminGuildsPage({
|
||||
|
||||
const [owners, memberGroups] = await Promise.all([
|
||||
ownerIds.length
|
||||
? prisma.user
|
||||
.findMany({
|
||||
where: { id: { in: ownerIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
? db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, ownerIds))
|
||||
.catch(() => [])
|
||||
: Promise.resolve([]),
|
||||
guildIds.length
|
||||
? prisma.guildsMembers
|
||||
.groupBy({
|
||||
by: ["guildId"],
|
||||
where: { guildId: { in: guildIds } },
|
||||
_count: { _all: true },
|
||||
? db
|
||||
.select({
|
||||
guildId: GuildsMembers.guildId,
|
||||
total: count(),
|
||||
})
|
||||
.from(GuildsMembers)
|
||||
.where(inArray(GuildsMembers.guildId, guildIds))
|
||||
.groupBy(GuildsMembers.guildId)
|
||||
.catch(() => [])
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const ownerById = new Map(owners.map((u) => [u.id, u.username]));
|
||||
const membersByGuild = new Map(
|
||||
memberGroups.map((g) => [g.guildId, g._count._all]),
|
||||
memberGroups.map((g) => [g.guildId, Number(g.total)]),
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { deleteHelpQuestion, updateHelpQuestion } from "@/actions/admin-help";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, WebsiteHelpCenterCategories } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export default async function EditHelpQuestion({
|
||||
params,
|
||||
@@ -20,13 +21,14 @@ export default async function EditHelpQuestion({
|
||||
|
||||
const { id } = await params;
|
||||
const { error } = await searchParams;
|
||||
let entry: Awaited<
|
||||
ReturnType<typeof prisma.websiteHelpCenterCategories.findUnique>
|
||||
> = null;
|
||||
let entry: typeof WebsiteHelpCenterCategories.$inferSelect | null = null;
|
||||
try {
|
||||
entry = await prisma.websiteHelpCenterCategories.findUnique({
|
||||
where: { id: BigInt(id) },
|
||||
});
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(WebsiteHelpCenterCategories)
|
||||
.where(eq(WebsiteHelpCenterCategories.id, BigInt(id)))
|
||||
.limit(1);
|
||||
entry = row ?? null;
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, WebsiteHelpCenterCategories } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export default async function AdminHelpQuestions() {
|
||||
const { session, permissions } = await getAdminContext();
|
||||
@@ -13,14 +14,16 @@ export default async function AdminHelpQuestions() {
|
||||
}
|
||||
|
||||
const t = await getTranslations("pages.admin.helpCenter");
|
||||
let entries: Awaited<
|
||||
ReturnType<typeof prisma.websiteHelpCenterCategories.findMany>
|
||||
> = [];
|
||||
let entries: (typeof WebsiteHelpCenterCategories.$inferSelect)[] = [];
|
||||
try {
|
||||
entries = await prisma.websiteHelpCenterCategories.findMany({
|
||||
orderBy: [{ position: "asc" }, { id: "asc" }],
|
||||
take: 500,
|
||||
});
|
||||
entries = await db
|
||||
.select()
|
||||
.from(WebsiteHelpCenterCategories)
|
||||
.orderBy(
|
||||
asc(WebsiteHelpCenterCategories.position),
|
||||
asc(WebsiteHelpCenterCategories.id),
|
||||
)
|
||||
.limit(500);
|
||||
} catch {
|
||||
entries = [];
|
||||
}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { and, asc, eq, gt, inArray, or } from "drizzle-orm";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
||||
import { positiveBigInt } from "@/lib/api";
|
||||
import { activeBanWhere } from "@/lib/bans";
|
||||
import { unixNow } from "@/lib/bans";
|
||||
import {
|
||||
Ban,
|
||||
db,
|
||||
User,
|
||||
WebsiteHelpCenterCategories,
|
||||
WebsiteHelpCenterTicketReplies,
|
||||
WebsiteHelpCenterTickets,
|
||||
} from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { AdminHelpTicketDetail } from "./admin-help-ticket-detail";
|
||||
|
||||
export default async function AdminHelpTicketDetailPage({
|
||||
@@ -23,32 +31,33 @@ export default async function AdminHelpTicketDetailPage({
|
||||
const ticketId = positiveBigInt(id);
|
||||
if (!ticketId) notFound();
|
||||
|
||||
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
|
||||
where: { id: ticketId },
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
categoryId: true,
|
||||
title: true,
|
||||
content: true,
|
||||
open: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
const [ticket] = await db
|
||||
.select({
|
||||
id: WebsiteHelpCenterTickets.id,
|
||||
userId: WebsiteHelpCenterTickets.userId,
|
||||
categoryId: WebsiteHelpCenterTickets.categoryId,
|
||||
title: WebsiteHelpCenterTickets.title,
|
||||
content: WebsiteHelpCenterTickets.content,
|
||||
open: WebsiteHelpCenterTickets.open,
|
||||
createdAt: WebsiteHelpCenterTickets.createdAt,
|
||||
updatedAt: WebsiteHelpCenterTickets.updatedAt,
|
||||
})
|
||||
.from(WebsiteHelpCenterTickets)
|
||||
.where(eq(WebsiteHelpCenterTickets.id, ticketId))
|
||||
.limit(1);
|
||||
|
||||
if (!ticket) notFound();
|
||||
|
||||
const replies = await prisma.websiteHelpCenterTicketReplies.findMany({
|
||||
where: { ticketId: ticket.id },
|
||||
orderBy: { id: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
content: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
const replies = await db
|
||||
.select({
|
||||
id: WebsiteHelpCenterTicketReplies.id,
|
||||
userId: WebsiteHelpCenterTicketReplies.userId,
|
||||
content: WebsiteHelpCenterTicketReplies.content,
|
||||
createdAt: WebsiteHelpCenterTicketReplies.createdAt,
|
||||
})
|
||||
.from(WebsiteHelpCenterTicketReplies)
|
||||
.where(eq(WebsiteHelpCenterTicketReplies.ticketId, ticket.id))
|
||||
.orderBy(asc(WebsiteHelpCenterTicketReplies.id));
|
||||
|
||||
const authorIds = [
|
||||
...new Set([
|
||||
@@ -57,32 +66,43 @@ export default async function AdminHelpTicketDetailPage({
|
||||
]),
|
||||
];
|
||||
|
||||
const [users, category, activeBan] = await Promise.all([
|
||||
const now = unixNow();
|
||||
const [users, categoryRows, activeBanRows] = await Promise.all([
|
||||
authorIds.length > 0
|
||||
? prisma.user.findMany({
|
||||
where: { id: { in: authorIds } },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
rank: true,
|
||||
mail: true,
|
||||
},
|
||||
})
|
||||
? db
|
||||
.select({
|
||||
id: User.id,
|
||||
username: User.username,
|
||||
rank: User.rank,
|
||||
mail: User.mail,
|
||||
})
|
||||
.from(User)
|
||||
.where(inArray(User.id, authorIds))
|
||||
: Promise.resolve([]),
|
||||
ticket.categoryId != null
|
||||
? prisma.websiteHelpCenterCategories.findUnique({
|
||||
where: { id: ticket.categoryId },
|
||||
select: { name: true },
|
||||
})
|
||||
: Promise.resolve(null),
|
||||
? db
|
||||
.select({ name: WebsiteHelpCenterCategories.name })
|
||||
.from(WebsiteHelpCenterCategories)
|
||||
.where(eq(WebsiteHelpCenterCategories.id, ticket.categoryId))
|
||||
.limit(1)
|
||||
: Promise.resolve([]),
|
||||
ticket.userId != null
|
||||
? prisma.ban.findFirst({
|
||||
where: { userId: ticket.userId, ...activeBanWhere() },
|
||||
select: { id: true },
|
||||
})
|
||||
: Promise.resolve(null),
|
||||
? db
|
||||
.select({ id: Ban.id })
|
||||
.from(Ban)
|
||||
.where(
|
||||
and(
|
||||
eq(Ban.userId, ticket.userId),
|
||||
or(eq(Ban.banExpire, 0), gt(Ban.banExpire, now)),
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const category = categoryRows[0] ?? null;
|
||||
const activeBan = activeBanRows[0] ?? null;
|
||||
|
||||
const userById = new Map(users.map((u) => [u.id, u]));
|
||||
const openerId = ticket.userId;
|
||||
const minStaffRank = await getMinStaffRank();
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import {
|
||||
and,
|
||||
asc,
|
||||
count,
|
||||
desc,
|
||||
eq,
|
||||
inArray,
|
||||
like,
|
||||
or,
|
||||
type SQL,
|
||||
} from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import {
|
||||
db,
|
||||
User,
|
||||
WebsiteHelpCenterTicketReplies,
|
||||
WebsiteHelpCenterTickets,
|
||||
} from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { type HelpTicketRow, HelpTicketsTable } from "./help-tickets-table";
|
||||
|
||||
export default async function AdminHelpTicketsPage({
|
||||
@@ -24,87 +40,95 @@ export default async function AdminHelpTicketsPage({
|
||||
const parsed = parseListParams(new URLSearchParams(raw));
|
||||
const statusFilter = raw.filter_status || "open";
|
||||
|
||||
const conditions: any[] = [];
|
||||
const conditions: SQL[] = [];
|
||||
|
||||
if (statusFilter === "open") {
|
||||
conditions.push({ open: true });
|
||||
conditions.push(eq(WebsiteHelpCenterTickets.open, true));
|
||||
} else if (statusFilter === "closed") {
|
||||
conditions.push({ open: false });
|
||||
conditions.push(eq(WebsiteHelpCenterTickets.open, false));
|
||||
}
|
||||
|
||||
if (parsed.search.trim()) {
|
||||
const q = parsed.search.trim();
|
||||
const asId = /^\d+$/.test(q) ? BigInt(q) : null;
|
||||
const matchingUsers = await prisma.user
|
||||
.findMany({
|
||||
where: { username: { contains: q } },
|
||||
select: { id: true },
|
||||
take: 50,
|
||||
})
|
||||
const matchingUsers = await db
|
||||
.select({ id: User.id })
|
||||
.from(User)
|
||||
.where(like(User.username, `%${q}%`))
|
||||
.limit(50)
|
||||
.catch(() => []);
|
||||
|
||||
conditions.push({
|
||||
OR: [
|
||||
{ title: { contains: q } },
|
||||
{ content: { contains: q } },
|
||||
...(matchingUsers.length > 0
|
||||
? [{ userId: { in: matchingUsers.map((u) => u.id) } }]
|
||||
: []),
|
||||
...(asId !== null ? [{ id: asId }] : []),
|
||||
],
|
||||
});
|
||||
const searchOr = [
|
||||
like(WebsiteHelpCenterTickets.title, `%${q}%`),
|
||||
like(WebsiteHelpCenterTickets.content, `%${q}%`),
|
||||
...(matchingUsers.length > 0
|
||||
? [
|
||||
inArray(
|
||||
WebsiteHelpCenterTickets.userId,
|
||||
matchingUsers.map((u) => u.id),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...(asId !== null ? [eq(WebsiteHelpCenterTickets.id, asId)] : []),
|
||||
];
|
||||
conditions.push(or(...searchOr)!);
|
||||
}
|
||||
|
||||
const where: any =
|
||||
conditions.length === 0
|
||||
? {}
|
||||
: conditions.length === 1
|
||||
? conditions[0]
|
||||
: { AND: conditions };
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
const SORT_MAP: Record<string, any> = {
|
||||
title: { title: "asc" },
|
||||
open: { open: "desc" },
|
||||
user: { userId: "asc" },
|
||||
date: { createdAt: "desc" },
|
||||
updated: { updatedAt: "desc" },
|
||||
id: { id: "desc" },
|
||||
};
|
||||
const orderCol =
|
||||
parsed.sort === "title"
|
||||
? WebsiteHelpCenterTickets.title
|
||||
: parsed.sort === "open"
|
||||
? WebsiteHelpCenterTickets.open
|
||||
: parsed.sort === "user"
|
||||
? WebsiteHelpCenterTickets.userId
|
||||
: parsed.sort === "date"
|
||||
? WebsiteHelpCenterTickets.createdAt
|
||||
: parsed.sort === "id"
|
||||
? WebsiteHelpCenterTickets.id
|
||||
: WebsiteHelpCenterTickets.updatedAt;
|
||||
const orderBy = parsed.order === "asc" ? asc(orderCol) : desc(orderCol);
|
||||
|
||||
const baseOrder = SORT_MAP[parsed.sort ?? "updated"] ?? { updatedAt: "desc" };
|
||||
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder;
|
||||
const orderBy = {
|
||||
[orderField]: parsed.order,
|
||||
} as any;
|
||||
|
||||
const [total, openCount, closedCount, queues] = await Promise.all([
|
||||
prisma.websiteHelpCenterTickets.count({ where }).catch(() => 0),
|
||||
prisma.websiteHelpCenterTickets
|
||||
.count({ where: { open: true } })
|
||||
.catch(() => 0),
|
||||
prisma.websiteHelpCenterTickets
|
||||
.count({ where: { open: false } })
|
||||
.catch(() => 0),
|
||||
const [totalRows, openRows, closedRows, queues] = await Promise.all([
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(WebsiteHelpCenterTickets)
|
||||
.where(where)
|
||||
.catch(() => [{ total: 0 }]),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(WebsiteHelpCenterTickets)
|
||||
.where(eq(WebsiteHelpCenterTickets.open, true))
|
||||
.catch(() => [{ total: 0 }]),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(WebsiteHelpCenterTickets)
|
||||
.where(eq(WebsiteHelpCenterTickets.open, false))
|
||||
.catch(() => [{ total: 0 }]),
|
||||
fetchTicketQueueOpenCounts(),
|
||||
]);
|
||||
|
||||
const total = Number(totalRows[0]?.total ?? 0);
|
||||
const openCount = Number(openRows[0]?.total ?? 0);
|
||||
const closedCount = Number(closedRows[0]?.total ?? 0);
|
||||
|
||||
const pagination = calcPagination(total, parsed.page, parsed.perPage);
|
||||
|
||||
const tickets = await prisma.websiteHelpCenterTickets
|
||||
.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
skip: pagination.offset,
|
||||
take: pagination.perPage,
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
title: true,
|
||||
open: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
const tickets = await db
|
||||
.select({
|
||||
id: WebsiteHelpCenterTickets.id,
|
||||
userId: WebsiteHelpCenterTickets.userId,
|
||||
title: WebsiteHelpCenterTickets.title,
|
||||
open: WebsiteHelpCenterTickets.open,
|
||||
createdAt: WebsiteHelpCenterTickets.createdAt,
|
||||
updatedAt: WebsiteHelpCenterTickets.updatedAt,
|
||||
})
|
||||
.from(WebsiteHelpCenterTickets)
|
||||
.where(where)
|
||||
.orderBy(orderBy)
|
||||
.limit(pagination.perPage)
|
||||
.offset(pagination.offset)
|
||||
.catch(() => []);
|
||||
|
||||
const ticketIds = tickets.map((ticket) => ticket.id);
|
||||
@@ -118,26 +142,27 @@ export default async function AdminHelpTicketsPage({
|
||||
|
||||
const [replyGroups, users] = await Promise.all([
|
||||
ticketIds.length > 0
|
||||
? prisma.websiteHelpCenterTicketReplies
|
||||
.groupBy({
|
||||
by: ["ticketId"],
|
||||
where: { ticketId: { in: ticketIds } },
|
||||
_count: true,
|
||||
? db
|
||||
.select({
|
||||
ticketId: WebsiteHelpCenterTicketReplies.ticketId,
|
||||
total: count(),
|
||||
})
|
||||
.from(WebsiteHelpCenterTicketReplies)
|
||||
.where(inArray(WebsiteHelpCenterTicketReplies.ticketId, ticketIds))
|
||||
.groupBy(WebsiteHelpCenterTicketReplies.ticketId)
|
||||
.catch(() => [])
|
||||
: Promise.resolve([]),
|
||||
userIds.length > 0
|
||||
? prisma.user
|
||||
.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
? db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, userIds))
|
||||
.catch(() => [])
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const replyCountByTicket = new Map(
|
||||
replyGroups.map((g) => [String(g.ticketId), g._count]),
|
||||
replyGroups.map((g) => [String(g.ticketId), Number(g.total)]),
|
||||
);
|
||||
const usernameById = new Map(users.map((u) => [u.id, u.username]));
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
AclModelPermission,
|
||||
AclPermission,
|
||||
AclRole,
|
||||
db,
|
||||
WebsiteHousekeepingPermissions,
|
||||
} from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { getAuditLogs } from "@/lib/services/audit";
|
||||
import { AuditSection, ExportSection } from "./client";
|
||||
|
||||
@@ -31,12 +38,14 @@ export default async function AdminHousekeeping({
|
||||
const sp = await searchParams;
|
||||
const tab = sp.tab ?? "overview";
|
||||
|
||||
type HKPerm = Awaited<
|
||||
ReturnType<typeof prisma.websiteHousekeepingPermissions.findMany>
|
||||
>[0];
|
||||
const allPermissions: HKPerm[] = await prisma.websiteHousekeepingPermissions
|
||||
.findMany({ orderBy: [{ groupName: "asc" }, { permission: "asc" }] })
|
||||
.catch(() => []);
|
||||
const allPermissions = await db
|
||||
.select()
|
||||
.from(WebsiteHousekeepingPermissions)
|
||||
.orderBy(
|
||||
asc(WebsiteHousekeepingPermissions.groupName),
|
||||
asc(WebsiteHousekeepingPermissions.permission),
|
||||
)
|
||||
.catch((): (typeof WebsiteHousekeepingPermissions.$inferSelect)[] => []);
|
||||
|
||||
const ranks = allPermissions.map((p) => p.minRank);
|
||||
const maxRank = ranks.length ? Math.max(...ranks) : 0;
|
||||
@@ -48,34 +57,43 @@ export default async function AdminHousekeeping({
|
||||
groupCounts[g] = (groupCounts[g] || 0) + 1;
|
||||
}
|
||||
|
||||
const rankGroups: Record<string, typeof allPermissions> = {};
|
||||
const rankGroups: Record<
|
||||
string,
|
||||
(typeof WebsiteHousekeepingPermissions.$inferSelect)[]
|
||||
> = {};
|
||||
for (const p of allPermissions) {
|
||||
if (!rankGroups[p.minRank]) rankGroups[p.minRank] = [];
|
||||
rankGroups[p.minRank].push(p);
|
||||
const rk = String(p.minRank);
|
||||
if (!rankGroups[rk]) rankGroups[rk] = [];
|
||||
rankGroups[rk].push(p);
|
||||
}
|
||||
|
||||
const aclSlugs = await prisma.aclPermission
|
||||
.findMany({ select: { slug: true } })
|
||||
const aclSlugs = await db
|
||||
.select({ slug: AclPermission.slug })
|
||||
.from(AclPermission)
|
||||
.catch(() => [] as { slug: string }[]);
|
||||
const aclSlugSet = new Set(aclSlugs.map((p) => p.slug));
|
||||
|
||||
const aclRolePerms = await prisma.aclModelPermission
|
||||
.findMany({
|
||||
where: { modelType: "Role" },
|
||||
include: {
|
||||
permission: { select: { slug: true } },
|
||||
role: { select: { slug: true } },
|
||||
},
|
||||
const aclRolePerms = await db
|
||||
.select({
|
||||
roleSlug: AclRole.slug,
|
||||
permissionSlug: AclPermission.slug,
|
||||
})
|
||||
.catch(() => []);
|
||||
.from(AclModelPermission)
|
||||
.innerJoin(
|
||||
AclPermission,
|
||||
eq(AclModelPermission.permissionId, AclPermission.id),
|
||||
)
|
||||
.innerJoin(AclRole, eq(AclModelPermission.modelId, AclRole.id))
|
||||
.where(eq(AclModelPermission.modelType, "Role"))
|
||||
.catch((): { roleSlug: string; permissionSlug: string }[] => []);
|
||||
|
||||
const aclMinRankBySlug = new Map<string, number>();
|
||||
for (const row of aclRolePerms) {
|
||||
const roleSlug = row.role.slug;
|
||||
const roleSlug = row.roleSlug;
|
||||
if (!roleSlug.startsWith("rank_")) continue;
|
||||
const rank = Number(roleSlug.slice(5));
|
||||
if (!Number.isFinite(rank)) continue;
|
||||
const slug = row.permission.slug;
|
||||
const slug = row.permissionSlug;
|
||||
const prev = aclMinRankBySlug.get(slug);
|
||||
if (prev === undefined || rank < prev) aclMinRankBySlug.set(slug, rank);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { desc } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
@@ -8,9 +9,9 @@ import {
|
||||
} from "@/actions/admin-ip";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, WebsiteIpBlacklist, WebsiteIpWhitelist } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type IpRow = {
|
||||
id: bigint;
|
||||
@@ -30,19 +31,21 @@ export default async function AdminIp() {
|
||||
let blacklist: IpRow[] = [];
|
||||
|
||||
try {
|
||||
whitelist = await prisma.websiteIpWhitelist.findMany({
|
||||
orderBy: { id: "desc" },
|
||||
take: 200,
|
||||
});
|
||||
whitelist = await db
|
||||
.select()
|
||||
.from(WebsiteIpWhitelist)
|
||||
.orderBy(desc(WebsiteIpWhitelist.id))
|
||||
.limit(200);
|
||||
} catch {
|
||||
whitelist = [];
|
||||
}
|
||||
|
||||
try {
|
||||
blacklist = await prisma.websiteIpBlacklist.findMany({
|
||||
orderBy: { id: "desc" },
|
||||
take: 200,
|
||||
});
|
||||
blacklist = await db
|
||||
.select()
|
||||
.from(WebsiteIpBlacklist)
|
||||
.orderBy(desc(WebsiteIpBlacklist.id))
|
||||
.limit(200);
|
||||
} catch {
|
||||
blacklist = [];
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Package } from "lucide-react";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { db, ItemsBase } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { ItemsBaseEditForm } from "./items-base-edit-form";
|
||||
|
||||
export default async function ItemsBaseDetailPage({
|
||||
@@ -21,7 +22,11 @@ export default async function ItemsBaseDetailPage({
|
||||
const id = Number(rawId);
|
||||
if (!Number.isFinite(id) || id <= 0) notFound();
|
||||
|
||||
const item = await prisma.itemsBase.findUnique({ where: { id } });
|
||||
const [item] = await db
|
||||
.select()
|
||||
.from(ItemsBase)
|
||||
.where(eq(ItemsBase.id, id))
|
||||
.limit(1);
|
||||
if (!item) notFound();
|
||||
|
||||
const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { LogOut } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -15,9 +16,9 @@ import {
|
||||
ADMIN_NAV_CONFIG_KEY,
|
||||
parseAdminNavConfig,
|
||||
} from "@/lib/admin-nav-config";
|
||||
import { db, User } from "@/lib/db";
|
||||
import { setCsrfCookie } from "@/lib/foundation/security";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
// Request-time auth: requireStaff, CSRF cookie, and optional 2FA gate cannot be
|
||||
@@ -37,11 +38,12 @@ export default async function AdminLayout({
|
||||
csrfToken = "";
|
||||
}
|
||||
if (await siteSettings.getBool("force_staff_2fa", false)) {
|
||||
const u = await prisma.user
|
||||
.findUnique({
|
||||
where: { id: staff.id },
|
||||
select: { twoFactorConfirmedAt: true },
|
||||
})
|
||||
const u = await db
|
||||
.select({ twoFactorConfirmedAt: User.twoFactorConfirmedAt })
|
||||
.from(User)
|
||||
.where(eq(User.id, staff.id))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0] ?? null)
|
||||
.catch(() => null);
|
||||
if (!u?.twoFactorConfirmedAt) redirect("/settings/2fa?error=staffrequired");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type ListParamsInput = {
|
||||
searchParams: Promise<Record<string, string>>;
|
||||
@@ -25,6 +26,11 @@ function parsePageParams(
|
||||
return { search: parsed.search.trim(), page: parsed.page, perPage };
|
||||
}
|
||||
|
||||
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
|
||||
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
|
||||
return rows ?? [];
|
||||
}
|
||||
|
||||
export type ChatLogListRow = {
|
||||
id: number;
|
||||
username: string;
|
||||
@@ -51,35 +57,35 @@ export async function loadChatLogList({
|
||||
username: string | null;
|
||||
};
|
||||
|
||||
const [rawRows, countRows] = search
|
||||
const [rawRowsResult, countRows] = search
|
||||
? await Promise.all([
|
||||
prisma.$queryRaw<RawRow[]>`
|
||||
rawRows<RawRow>(sql`
|
||||
SELECT c.room_id AS roomId, c.message, c.timestamp, u.username AS username
|
||||
FROM chatlogs_room c
|
||||
LEFT JOIN users u ON u.id = c.user_from_id
|
||||
WHERE c.message LIKE ${like} OR u.username LIKE ${like}
|
||||
ORDER BY c.timestamp DESC
|
||||
LIMIT ${perPage} OFFSET ${offset}`,
|
||||
prisma.$queryRaw<{ count: bigint }[]>`
|
||||
LIMIT ${perPage} OFFSET ${offset}`),
|
||||
rawRows<{ count: bigint }>(sql`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM chatlogs_room c
|
||||
LEFT JOIN users u ON u.id = c.user_from_id
|
||||
WHERE c.message LIKE ${like} OR u.username LIKE ${like}`,
|
||||
WHERE c.message LIKE ${like} OR u.username LIKE ${like}`),
|
||||
])
|
||||
: await Promise.all([
|
||||
prisma.$queryRaw<RawRow[]>`
|
||||
rawRows<RawRow>(sql`
|
||||
SELECT c.room_id AS roomId, c.message, c.timestamp, u.username AS username
|
||||
FROM chatlogs_room c
|
||||
LEFT JOIN users u ON u.id = c.user_from_id
|
||||
ORDER BY c.timestamp DESC
|
||||
LIMIT ${perPage} OFFSET ${offset}`,
|
||||
prisma.$queryRaw<{ count: bigint }[]>`
|
||||
SELECT COUNT(*) AS count FROM chatlogs_room`,
|
||||
LIMIT ${perPage} OFFSET ${offset}`),
|
||||
rawRows<{ count: bigint }>(sql`
|
||||
SELECT COUNT(*) AS count FROM chatlogs_room`),
|
||||
]);
|
||||
|
||||
const total = Number(countRows[0]?.count ?? 0);
|
||||
const pagination = calcPagination(total, page, perPage);
|
||||
const rows: ChatLogListRow[] = rawRows.map((row, index) => ({
|
||||
const rows: ChatLogListRow[] = rawRowsResult.map((row, index) => ({
|
||||
id: pagination.offset + index + 1,
|
||||
username: row.username ?? "-",
|
||||
message: row.message,
|
||||
@@ -116,9 +122,9 @@ export async function loadCommandLogList({
|
||||
username: string | null;
|
||||
};
|
||||
|
||||
const [rawRows, countRows] = search
|
||||
const [rawRowsResult, countRows] = search
|
||||
? await Promise.all([
|
||||
prisma.$queryRaw<RawRow[]>`
|
||||
rawRows<RawRow>(sql`
|
||||
SELECT c.command, c.params, c.timestamp, u.username AS username
|
||||
FROM commandlogs c
|
||||
LEFT JOIN users u ON u.id = c.user_id
|
||||
@@ -126,29 +132,29 @@ export async function loadCommandLogList({
|
||||
OR c.params LIKE ${like}
|
||||
OR u.username LIKE ${like}
|
||||
ORDER BY c.timestamp DESC
|
||||
LIMIT ${perPage} OFFSET ${offset}`,
|
||||
prisma.$queryRaw<{ count: bigint }[]>`
|
||||
LIMIT ${perPage} OFFSET ${offset}`),
|
||||
rawRows<{ count: bigint }>(sql`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM commandlogs c
|
||||
LEFT JOIN users u ON u.id = c.user_id
|
||||
WHERE c.command LIKE ${like}
|
||||
OR c.params LIKE ${like}
|
||||
OR u.username LIKE ${like}`,
|
||||
OR u.username LIKE ${like}`),
|
||||
])
|
||||
: await Promise.all([
|
||||
prisma.$queryRaw<RawRow[]>`
|
||||
rawRows<RawRow>(sql`
|
||||
SELECT c.command, c.params, c.timestamp, u.username AS username
|
||||
FROM commandlogs c
|
||||
LEFT JOIN users u ON u.id = c.user_id
|
||||
ORDER BY c.timestamp DESC
|
||||
LIMIT ${perPage} OFFSET ${offset}`,
|
||||
prisma.$queryRaw<{ count: bigint }[]>`
|
||||
SELECT COUNT(*) AS count FROM commandlogs`,
|
||||
LIMIT ${perPage} OFFSET ${offset}`),
|
||||
rawRows<{ count: bigint }>(sql`
|
||||
SELECT COUNT(*) AS count FROM commandlogs`),
|
||||
]);
|
||||
|
||||
const total = Number(countRows[0]?.count ?? 0);
|
||||
const pagination = calcPagination(total, page, perPage);
|
||||
const rows: CommandLogListRow[] = rawRows.map((row, index) => ({
|
||||
const rows: CommandLogListRow[] = rawRowsResult.map((row, index) => ({
|
||||
id: pagination.offset + index + 1,
|
||||
username: row.username ?? "-",
|
||||
command: row.command,
|
||||
@@ -185,9 +191,9 @@ export async function loadTradeLogList({
|
||||
userTwoName: string | null;
|
||||
};
|
||||
|
||||
const [rawRows, countRows] = search
|
||||
const [rawRowsResult, countRows] = search
|
||||
? await Promise.all([
|
||||
prisma.$queryRaw<RawRow[]>`
|
||||
rawRows<RawRow>(sql`
|
||||
SELECT t.id, t.timestamp,
|
||||
u1.username AS userOneName, u2.username AS userTwoName
|
||||
FROM room_trade_log t
|
||||
@@ -195,30 +201,30 @@ export async function loadTradeLogList({
|
||||
LEFT JOIN users u2 ON u2.id = t.user_two_id
|
||||
WHERE u1.username LIKE ${like} OR u2.username LIKE ${like}
|
||||
ORDER BY t.timestamp DESC
|
||||
LIMIT ${perPage} OFFSET ${offset}`,
|
||||
prisma.$queryRaw<{ count: bigint }[]>`
|
||||
LIMIT ${perPage} OFFSET ${offset}`),
|
||||
rawRows<{ count: bigint }>(sql`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM room_trade_log t
|
||||
LEFT JOIN users u1 ON u1.id = t.user_one_id
|
||||
LEFT JOIN users u2 ON u2.id = t.user_two_id
|
||||
WHERE u1.username LIKE ${like} OR u2.username LIKE ${like}`,
|
||||
WHERE u1.username LIKE ${like} OR u2.username LIKE ${like}`),
|
||||
])
|
||||
: await Promise.all([
|
||||
prisma.$queryRaw<RawRow[]>`
|
||||
rawRows<RawRow>(sql`
|
||||
SELECT t.id, t.timestamp,
|
||||
u1.username AS userOneName, u2.username AS userTwoName
|
||||
FROM room_trade_log t
|
||||
LEFT JOIN users u1 ON u1.id = t.user_one_id
|
||||
LEFT JOIN users u2 ON u2.id = t.user_two_id
|
||||
ORDER BY t.timestamp DESC
|
||||
LIMIT ${perPage} OFFSET ${offset}`,
|
||||
prisma.$queryRaw<{ count: bigint }[]>`
|
||||
SELECT COUNT(*) AS count FROM room_trade_log`,
|
||||
LIMIT ${perPage} OFFSET ${offset}`),
|
||||
rawRows<{ count: bigint }>(sql`
|
||||
SELECT COUNT(*) AS count FROM room_trade_log`),
|
||||
]);
|
||||
|
||||
const total = Number(countRows[0]?.count ?? 0);
|
||||
const pagination = calcPagination(total, page, perPage);
|
||||
const rows: TradeLogListRow[] = rawRows.map((row) => ({
|
||||
const rows: TradeLogListRow[] = rawRowsResult.map((row) => ({
|
||||
id: row.id,
|
||||
userOneName: row.userOneName ?? "-",
|
||||
userTwoName: row.userTwoName ?? "-",
|
||||
|
||||
+57
-36
@@ -1,20 +1,29 @@
|
||||
import {
|
||||
and,
|
||||
asc,
|
||||
count,
|
||||
desc,
|
||||
inArray,
|
||||
like,
|
||||
type SQL,
|
||||
sql,
|
||||
} from "drizzle-orm";
|
||||
import { ClipboardList } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { buildStaffActivityWhere } from "@/lib/admin/log-filters";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db, StaffActivities, User } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { StaffActivitiesTable } from "./staff-activities-table";
|
||||
|
||||
const SORT_MAP: Record<string, "createdAt" | "userId" | "action"> = {
|
||||
when: "createdAt",
|
||||
staff: "userId",
|
||||
action: "action",
|
||||
};
|
||||
const SORT_MAP = {
|
||||
when: StaffActivities.createdAt,
|
||||
staff: StaffActivities.userId,
|
||||
action: StaffActivities.action,
|
||||
} as const;
|
||||
|
||||
export default async function AdminStaffActivities({
|
||||
searchParams,
|
||||
@@ -30,36 +39,49 @@ export default async function AdminStaffActivities({
|
||||
const raw = await searchParams;
|
||||
const parsed = parseListParams(new URLSearchParams(raw));
|
||||
const authorizationOnly = raw.filter_authorization === "1";
|
||||
const whereClause = buildStaffActivityWhere({
|
||||
q: parsed.search,
|
||||
authorizationOnly,
|
||||
});
|
||||
|
||||
const sortField = SORT_MAP[parsed.sort ?? ""] ?? "createdAt";
|
||||
const conditions: SQL[] = [];
|
||||
if (parsed.search.trim()) {
|
||||
const q = parsed.search.trim();
|
||||
conditions.push(
|
||||
sql`(${like(StaffActivities.action, `%${q}%`)} OR ${like(StaffActivities.description, `%${q}%`)} OR ${like(StaffActivities.ipAddress, `%${q}%`)})`,
|
||||
);
|
||||
}
|
||||
if (authorizationOnly) {
|
||||
conditions.push(like(StaffActivities.action, "permission.%"));
|
||||
}
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
const total = await prisma.staffActivities
|
||||
.count({ where: whereClause })
|
||||
.catch(() => 0);
|
||||
const sortCol =
|
||||
SORT_MAP[(parsed.sort ?? "") as keyof typeof SORT_MAP] ??
|
||||
StaffActivities.createdAt;
|
||||
const orderBy = parsed.order === "asc" ? asc(sortCol) : desc(sortCol);
|
||||
|
||||
const [totalRows] = await db
|
||||
.select({ total: count() })
|
||||
.from(StaffActivities)
|
||||
.where(where)
|
||||
.catch(() => [{ total: 0 }]);
|
||||
const total = Number(totalRows?.total ?? 0);
|
||||
|
||||
const pagination = calcPagination(total, parsed.page, parsed.perPage);
|
||||
|
||||
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: { [sortField]: parsed.order },
|
||||
skip: pagination.offset,
|
||||
take: pagination.perPage,
|
||||
const activities = await db
|
||||
.select({
|
||||
id: StaffActivities.id,
|
||||
userId: StaffActivities.userId,
|
||||
action: StaffActivities.action,
|
||||
description: StaffActivities.description,
|
||||
targetType: StaffActivities.targetType,
|
||||
targetId: StaffActivities.targetId,
|
||||
ipAddress: StaffActivities.ipAddress,
|
||||
createdAt: StaffActivities.createdAt,
|
||||
})
|
||||
.from(StaffActivities)
|
||||
.where(where)
|
||||
.orderBy(orderBy)
|
||||
.limit(pagination.perPage)
|
||||
.offset(pagination.offset)
|
||||
.catch(() => []);
|
||||
|
||||
const staffIds = Array.from(
|
||||
@@ -67,11 +89,10 @@ export default async function AdminStaffActivities({
|
||||
).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 },
|
||||
})
|
||||
const users = await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, staffIds))
|
||||
.catch(() => []);
|
||||
nameById = new Map(users.map((x) => [x.id, x.username]));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { inArray } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { saveMaintenance } from "@/actions/admin-maintenance";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, WebsiteSetting } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const KEYS = [
|
||||
"maintenance_enabled",
|
||||
@@ -22,10 +23,10 @@ export default async function AdminMaintenance() {
|
||||
|
||||
let rows: { key: string; value: string }[] = [];
|
||||
try {
|
||||
rows = await prisma.websiteSetting.findMany({
|
||||
where: { key: { in: [...KEYS] } },
|
||||
select: { key: true, value: true },
|
||||
});
|
||||
rows = await db
|
||||
.select({ key: WebsiteSetting.key, value: WebsiteSetting.value })
|
||||
.from(WebsiteSetting)
|
||||
.where(inArray(WebsiteSetting.key, [...KEYS]));
|
||||
} catch {
|
||||
rows = [];
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { count, desc, eq, inArray } from "drizzle-orm";
|
||||
import { Store } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -8,9 +9,9 @@ import { AdminSimplePager } from "@/components/admin/admin-simple-pager";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db, MarketplaceItems, User } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export default async function AdminMarketplacePage({
|
||||
searchParams,
|
||||
@@ -28,27 +29,37 @@ export default async function AdminMarketplacePage({
|
||||
const parsed = parseListParams(new URLSearchParams(raw));
|
||||
const filter = raw.filter === "sold" ? "sold" : "active";
|
||||
|
||||
const where = filter === "sold" ? { state: 2 } : { state: 1 };
|
||||
const where = eq(MarketplaceItems.state, filter === "sold" ? 2 : 1);
|
||||
|
||||
let total = 0;
|
||||
let rows: Awaited<ReturnType<typeof prisma.marketplaceItems.findMany>> = [];
|
||||
let rows: (typeof MarketplaceItems.$inferSelect)[] = [];
|
||||
let activeCount = 0;
|
||||
let soldCount = 0;
|
||||
let loadError = false;
|
||||
|
||||
try {
|
||||
[total, activeCount, soldCount] = await Promise.all([
|
||||
prisma.marketplaceItems.count({ where }),
|
||||
prisma.marketplaceItems.count({ where: { state: 1 } }),
|
||||
prisma.marketplaceItems.count({ where: { state: 2 } }),
|
||||
const [totalRows, activeRows, soldRows] = await Promise.all([
|
||||
db.select({ total: count() }).from(MarketplaceItems).where(where),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(MarketplaceItems)
|
||||
.where(eq(MarketplaceItems.state, 1)),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(MarketplaceItems)
|
||||
.where(eq(MarketplaceItems.state, 2)),
|
||||
]);
|
||||
total = Number(totalRows[0]?.total ?? 0);
|
||||
activeCount = Number(activeRows[0]?.total ?? 0);
|
||||
soldCount = Number(soldRows[0]?.total ?? 0);
|
||||
const pagination = calcPagination(total, parsed.page, parsed.perPage);
|
||||
rows = await prisma.marketplaceItems.findMany({
|
||||
where,
|
||||
orderBy: { timestamp: "desc" },
|
||||
skip: pagination.offset,
|
||||
take: pagination.perPage,
|
||||
});
|
||||
rows = await db
|
||||
.select()
|
||||
.from(MarketplaceItems)
|
||||
.where(where)
|
||||
.orderBy(desc(MarketplaceItems.timestamp))
|
||||
.limit(pagination.perPage)
|
||||
.offset(pagination.offset);
|
||||
} catch {
|
||||
loadError = true;
|
||||
}
|
||||
@@ -59,11 +70,10 @@ export default async function AdminMarketplacePage({
|
||||
];
|
||||
const users =
|
||||
userIds.length > 0
|
||||
? await prisma.user
|
||||
.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
? await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, userIds))
|
||||
.catch(() => [])
|
||||
: [];
|
||||
const usernameById = new Map(users.map((u) => [u.id, u.username]));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { count, desc, eq, inArray } from "drizzle-orm";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { Ban, db, SupportTickets, User } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { CfhDetail } from "./cfh-detail";
|
||||
|
||||
export default async function CfhDetailPage({
|
||||
@@ -16,9 +17,11 @@ export default async function CfhDetailPage({
|
||||
const ticketId = Number(id);
|
||||
if (Number.isNaN(ticketId)) notFound();
|
||||
|
||||
const ticket = await prisma.supportTickets.findUnique({
|
||||
where: { id: ticketId },
|
||||
});
|
||||
const [ticket] = await db
|
||||
.select()
|
||||
.from(SupportTickets)
|
||||
.where(eq(SupportTickets.id, ticketId))
|
||||
.limit(1);
|
||||
if (!ticket) notFound();
|
||||
|
||||
const canEdit = canAccess(
|
||||
@@ -30,36 +33,42 @@ export default async function CfhDetailPage({
|
||||
// Fetch sender, reported, mod info
|
||||
const userIds = [ticket.senderId, ticket.reportedId, ticket.modId].filter(
|
||||
Boolean,
|
||||
);
|
||||
) as number[];
|
||||
const users =
|
||||
userIds.length > 0
|
||||
? await prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
look: true,
|
||||
rank: true,
|
||||
online: true,
|
||||
ipRegister: true,
|
||||
mail: true,
|
||||
},
|
||||
})
|
||||
? await db
|
||||
.select({
|
||||
id: User.id,
|
||||
username: User.username,
|
||||
look: User.look,
|
||||
rank: User.rank,
|
||||
online: User.online,
|
||||
ipRegister: User.ipRegister,
|
||||
mail: User.mail,
|
||||
})
|
||||
.from(User)
|
||||
.where(inArray(User.id, userIds))
|
||||
: [];
|
||||
const userMap = Object.fromEntries(users.map((u) => [u.id, u]));
|
||||
|
||||
// Get ban/mute history for reported user
|
||||
const reportedId = ticket.reportedId;
|
||||
const [banCount, recentBans] = reportedId
|
||||
const [banCountRows, recentBans] = reportedId
|
||||
? await Promise.all([
|
||||
prisma.ban.count({ where: { userId: reportedId } }),
|
||||
prisma.ban.findMany({
|
||||
where: { userId: reportedId },
|
||||
orderBy: { id: "desc" },
|
||||
take: 5,
|
||||
}),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(Ban)
|
||||
.where(eq(Ban.userId, reportedId)),
|
||||
db
|
||||
.select()
|
||||
.from(Ban)
|
||||
.where(eq(Ban.userId, reportedId))
|
||||
.orderBy(desc(Ban.id))
|
||||
.limit(5),
|
||||
])
|
||||
: [0, []];
|
||||
: [[{ total: 0 }], []];
|
||||
|
||||
const banCount = Number(banCountRows[0]?.total ?? 0);
|
||||
|
||||
return (
|
||||
<CfhDetail
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import {
|
||||
and,
|
||||
asc,
|
||||
count,
|
||||
desc,
|
||||
eq,
|
||||
inArray,
|
||||
like,
|
||||
or,
|
||||
type SQL,
|
||||
} from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db, SupportTickets, User } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { type CfhRow, CfhTable } from "./cfh-table";
|
||||
|
||||
export default async function CfhListPage({
|
||||
@@ -23,9 +34,9 @@ export default async function CfhListPage({
|
||||
const stateFilter =
|
||||
stateRaw !== undefined && stateRaw !== "" ? Number(stateRaw) : -1;
|
||||
|
||||
const conditions: any[] = [];
|
||||
const conditions: SQL[] = [];
|
||||
if (stateFilter >= 0) {
|
||||
conditions.push({ state: stateFilter });
|
||||
conditions.push(eq(SupportTickets.state, stateFilter));
|
||||
}
|
||||
|
||||
if (parsed.search.trim()) {
|
||||
@@ -33,63 +44,63 @@ export default async function CfhListPage({
|
||||
const asId = Number(q);
|
||||
let userIds: number[] = [];
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
where: { username: { contains: q } },
|
||||
select: { id: true },
|
||||
take: 50,
|
||||
});
|
||||
const users = await db
|
||||
.select({ id: User.id })
|
||||
.from(User)
|
||||
.where(like(User.username, `%${q}%`))
|
||||
.limit(50);
|
||||
userIds = users.map((u) => u.id);
|
||||
} catch {
|
||||
userIds = [];
|
||||
}
|
||||
|
||||
conditions.push({
|
||||
OR: [
|
||||
{ issue: { contains: q } },
|
||||
...(Number.isFinite(asId) && asId > 0 ? [{ id: asId }] : []),
|
||||
...(userIds.length
|
||||
? [
|
||||
{ senderId: { in: userIds } },
|
||||
{ reportedId: { in: userIds } },
|
||||
{ modId: { in: userIds } },
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
const searchOr = [
|
||||
like(SupportTickets.issue, `%${q}%`),
|
||||
...(Number.isFinite(asId) && asId > 0
|
||||
? [eq(SupportTickets.id, asId)]
|
||||
: []),
|
||||
...(userIds.length
|
||||
? [
|
||||
inArray(SupportTickets.senderId, userIds),
|
||||
inArray(SupportTickets.reportedId, userIds),
|
||||
inArray(SupportTickets.modId, userIds),
|
||||
]
|
||||
: []),
|
||||
];
|
||||
conditions.push(or(...searchOr)!);
|
||||
}
|
||||
|
||||
const where: any =
|
||||
conditions.length === 0
|
||||
? {}
|
||||
: conditions.length === 1
|
||||
? conditions[0]
|
||||
: { AND: conditions };
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
const SORT_MAP: Record<string, any> = {
|
||||
id: { id: "desc" },
|
||||
issue: { issue: "asc" },
|
||||
sender: { senderId: "asc" },
|
||||
reported: { reportedId: "asc" },
|
||||
mod: { modId: "asc" },
|
||||
state: { state: "asc" },
|
||||
};
|
||||
const orderCol =
|
||||
parsed.sort === "issue"
|
||||
? SupportTickets.issue
|
||||
: parsed.sort === "sender"
|
||||
? SupportTickets.senderId
|
||||
: parsed.sort === "reported"
|
||||
? SupportTickets.reportedId
|
||||
: parsed.sort === "mod"
|
||||
? SupportTickets.modId
|
||||
: parsed.sort === "state"
|
||||
? SupportTickets.state
|
||||
: SupportTickets.id;
|
||||
const orderBy = parsed.order === "asc" ? asc(orderCol) : desc(orderCol);
|
||||
|
||||
const baseOrder = SORT_MAP[parsed.sort ?? "id"] ?? { id: "desc" };
|
||||
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder;
|
||||
const orderBy = {
|
||||
[orderField]: parsed.order,
|
||||
} as any;
|
||||
|
||||
const total = await prisma.supportTickets.count({ where }).catch(() => 0);
|
||||
const [totalRows] = await db
|
||||
.select({ total: count() })
|
||||
.from(SupportTickets)
|
||||
.where(where)
|
||||
.catch(() => [{ total: 0 }]);
|
||||
const total = Number(totalRows?.total ?? 0);
|
||||
const pagination = calcPagination(total, parsed.page, parsed.perPage);
|
||||
|
||||
const tickets = await prisma.supportTickets
|
||||
.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
skip: pagination.offset,
|
||||
take: pagination.perPage,
|
||||
})
|
||||
const tickets = await db
|
||||
.select()
|
||||
.from(SupportTickets)
|
||||
.where(where)
|
||||
.orderBy(orderBy)
|
||||
.limit(pagination.perPage)
|
||||
.offset(pagination.offset)
|
||||
.catch(() => []);
|
||||
|
||||
const userIds = [
|
||||
@@ -98,15 +109,15 @@ export default async function CfhListPage({
|
||||
...tickets.map((x) => x.reportedId),
|
||||
...tickets.map((x) => x.modId),
|
||||
]),
|
||||
].filter(Boolean);
|
||||
].filter(Boolean) as number[];
|
||||
|
||||
let userMap = new Map<number, string>();
|
||||
if (userIds.length > 0) {
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true },
|
||||
});
|
||||
const users = await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, userIds));
|
||||
userMap = new Map(users.map((u) => [u.id, u.username]));
|
||||
} catch {
|
||||
userMap = new Map();
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { and, count, desc, eq, gte, inArray, like } from "drizzle-orm";
|
||||
import {
|
||||
AlertTriangle,
|
||||
Ban,
|
||||
Ban as BanIcon,
|
||||
Clock,
|
||||
Gavel,
|
||||
Radio,
|
||||
Shield,
|
||||
User,
|
||||
User as UserIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -13,8 +14,8 @@ import { StatsCard } from "@/components/admin/stats-card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
||||
import { AdminAuditLog, Ban, db, SupportTickets, User } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const CFH_STATES: Record<number, { label: string; class: string }> = {
|
||||
0: {
|
||||
@@ -38,33 +39,53 @@ export default async function ModerationDashboard() {
|
||||
const minStaffRank = await getMinStaffRank();
|
||||
|
||||
const [
|
||||
openCfh,
|
||||
pickedCfh,
|
||||
todayBans,
|
||||
modsOnline,
|
||||
openCfhRows,
|
||||
pickedCfhRows,
|
||||
todayBansRows,
|
||||
modsOnlineRows,
|
||||
recentCfh,
|
||||
recentModActions,
|
||||
] = await Promise.all([
|
||||
prisma.supportTickets.count({ where: { state: 0 } }),
|
||||
prisma.supportTickets.count({ where: { state: 1 } }),
|
||||
prisma.ban.count({ where: { timestamp: { gte: todayStart } } }),
|
||||
prisma.user.count({
|
||||
where: { online: "1", rank: { gte: minStaffRank } },
|
||||
}),
|
||||
prisma.supportTickets.findMany({
|
||||
orderBy: { id: "desc" },
|
||||
take: 10,
|
||||
}),
|
||||
prisma.adminAuditLog.findMany({
|
||||
where: {
|
||||
action: { startsWith: "mod_" },
|
||||
},
|
||||
orderBy: { id: "desc" },
|
||||
take: 10,
|
||||
include: { user: { select: { username: true } } },
|
||||
}),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(SupportTickets)
|
||||
.where(eq(SupportTickets.state, 0)),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(SupportTickets)
|
||||
.where(eq(SupportTickets.state, 1)),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(Ban)
|
||||
.where(gte(Ban.timestamp, todayStart)),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(User)
|
||||
.where(and(eq(User.online, "1"), gte(User.rank, minStaffRank))),
|
||||
db.select().from(SupportTickets).orderBy(desc(SupportTickets.id)).limit(10),
|
||||
db
|
||||
.select({
|
||||
id: AdminAuditLog.id,
|
||||
action: AdminAuditLog.action,
|
||||
details: AdminAuditLog.details,
|
||||
createdAt: AdminAuditLog.createdAt,
|
||||
userId: AdminAuditLog.userId,
|
||||
target: AdminAuditLog.target,
|
||||
targetId: AdminAuditLog.targetId,
|
||||
username: User.username,
|
||||
})
|
||||
.from(AdminAuditLog)
|
||||
.leftJoin(User, eq(AdminAuditLog.userId, User.id))
|
||||
.where(like(AdminAuditLog.action, "mod_%"))
|
||||
.orderBy(desc(AdminAuditLog.id))
|
||||
.limit(10),
|
||||
]);
|
||||
|
||||
const openCfh = Number(openCfhRows[0]?.total ?? 0);
|
||||
const pickedCfh = Number(pickedCfhRows[0]?.total ?? 0);
|
||||
const todayBans = Number(todayBansRows[0]?.total ?? 0);
|
||||
const modsOnline = Number(modsOnlineRows[0]?.total ?? 0);
|
||||
|
||||
// Fetch usernames for CFH tickets
|
||||
const userIds = [
|
||||
...new Set([
|
||||
@@ -72,14 +93,14 @@ export default async function ModerationDashboard() {
|
||||
...recentCfh.map((t) => t.reportedId),
|
||||
...recentCfh.map((t) => t.modId),
|
||||
]),
|
||||
].filter(Boolean);
|
||||
].filter(Boolean) as number[];
|
||||
|
||||
const users =
|
||||
userIds.length > 0
|
||||
? await prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
? await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, userIds))
|
||||
: [];
|
||||
const userMap = new Map(users.map((u) => [u.id, u.username]));
|
||||
|
||||
@@ -97,7 +118,7 @@ export default async function ModerationDashboard() {
|
||||
<StatsCard
|
||||
title="Bans Today"
|
||||
value={todayBans}
|
||||
icon={<Ban className="h-4 w-4" />}
|
||||
icon={<BanIcon className="h-4 w-4" />}
|
||||
href="/admin/bans"
|
||||
/>
|
||||
<StatsCard
|
||||
@@ -190,13 +211,13 @@ export default async function ModerationDashboard() {
|
||||
key={log.id}
|
||||
className="flex items-center gap-3 py-2.5 first:pt-0 last:pb-0"
|
||||
>
|
||||
<User className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
<UserIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{log.action.replace("mod_", "").replace(/_/g, " ")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
by {log.user?.username ?? "?"} · {log.target}{" "}
|
||||
by {log.username ?? "?"} · {log.target}{" "}
|
||||
{log.targetId ? `#${log.targetId}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { and, count, desc, gte, inArray, like } from "drizzle-orm";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { AvatarImage } from "@/components/shared/avatar-image";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
||||
import {
|
||||
AdminAuditLog,
|
||||
db,
|
||||
SupportTickets,
|
||||
User,
|
||||
WebsiteTicket,
|
||||
} from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export default async function ModTeamPage() {
|
||||
const { session, permissions } = await getAdminContext();
|
||||
@@ -13,48 +20,67 @@ export default async function ModTeamPage() {
|
||||
redirect("/admin");
|
||||
|
||||
const minStaffRank = await getMinStaffRank();
|
||||
const staff = await prisma.user.findMany({
|
||||
where: { rank: { gte: minStaffRank } },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
look: true,
|
||||
rank: true,
|
||||
online: true,
|
||||
lastLogin: true,
|
||||
lastOnline: true,
|
||||
},
|
||||
orderBy: [{ online: "desc" }, { rank: "desc" }],
|
||||
});
|
||||
const staff = await db
|
||||
.select({
|
||||
id: User.id,
|
||||
username: User.username,
|
||||
look: User.look,
|
||||
rank: User.rank,
|
||||
online: User.online,
|
||||
lastLogin: User.lastLogin,
|
||||
lastOnline: User.lastOnline,
|
||||
})
|
||||
.from(User)
|
||||
.where(gte(User.rank, minStaffRank))
|
||||
.orderBy(desc(User.online), desc(User.rank));
|
||||
|
||||
const staffIds = staff.map((s) => s.id);
|
||||
|
||||
// Get CFH ticket counts per mod and web ticket counts
|
||||
const [cfhCounts, webTicketCounts, modActionCounts] = await Promise.all([
|
||||
prisma.supportTickets.groupBy({
|
||||
by: ["modId"],
|
||||
where: { modId: { in: staff.map((s) => s.id) } },
|
||||
_count: { _all: true },
|
||||
}),
|
||||
prisma.websiteTicket.groupBy({
|
||||
by: ["assigneeId"],
|
||||
where: { assigneeId: { in: staff.map((s) => s.id) } },
|
||||
_count: { _all: true },
|
||||
}),
|
||||
prisma.adminAuditLog.groupBy({
|
||||
by: ["userId"],
|
||||
where: {
|
||||
userId: { in: staff.map((s) => s.id) },
|
||||
action: { startsWith: "mod_" },
|
||||
},
|
||||
_count: { _all: true },
|
||||
}),
|
||||
staffIds.length
|
||||
? db
|
||||
.select({
|
||||
modId: SupportTickets.modId,
|
||||
total: count(),
|
||||
})
|
||||
.from(SupportTickets)
|
||||
.where(inArray(SupportTickets.modId, staffIds))
|
||||
.groupBy(SupportTickets.modId)
|
||||
: Promise.resolve([]),
|
||||
staffIds.length
|
||||
? db
|
||||
.select({
|
||||
assigneeId: WebsiteTicket.assigneeId,
|
||||
total: count(),
|
||||
})
|
||||
.from(WebsiteTicket)
|
||||
.where(inArray(WebsiteTicket.assigneeId, staffIds))
|
||||
.groupBy(WebsiteTicket.assigneeId)
|
||||
: Promise.resolve([]),
|
||||
staffIds.length
|
||||
? db
|
||||
.select({
|
||||
userId: AdminAuditLog.userId,
|
||||
total: count(),
|
||||
})
|
||||
.from(AdminAuditLog)
|
||||
.where(
|
||||
and(
|
||||
inArray(AdminAuditLog.userId, staffIds),
|
||||
like(AdminAuditLog.action, "mod_%"),
|
||||
),
|
||||
)
|
||||
.groupBy(AdminAuditLog.userId)
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const cfhMap = new Map(cfhCounts.map((c) => [c.modId, c._count._all]));
|
||||
const cfhMap = new Map(cfhCounts.map((c) => [c.modId, Number(c.total)]));
|
||||
const webMap = new Map(
|
||||
webTicketCounts.map((c) => [c.assigneeId, c._count._all]),
|
||||
webTicketCounts.map((c) => [c.assigneeId, Number(c.total)]),
|
||||
);
|
||||
const actionMap = new Map(
|
||||
modActionCounts.map((c) => [c.userId, c._count._all]),
|
||||
modActionCounts.map((c) => [c.userId, Number(c.total)]),
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { asc, inArray, sql } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { db, NavigatorFlatcats, NavigatorPubliccats, Rooms } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
// navigator_flatcats — emulator-owned flat (private-room) categories shown in the
|
||||
// navigator. The Prisma delegate exists because the table has a primary key.
|
||||
// navigator.
|
||||
type Flatcat = {
|
||||
id: number;
|
||||
caption: string;
|
||||
@@ -18,16 +19,13 @@ type Flatcat = {
|
||||
orderNum: number;
|
||||
};
|
||||
|
||||
// navigator_publics — emulator-owned public-room assignments. This table has no
|
||||
// primary/unique key, so Prisma marks the model `@@ignore` and generates NO
|
||||
// delegate (prisma.navigatorPublics does not exist). It must be read via raw SQL.
|
||||
// navigator_publics — keyless table, read via raw SQL.
|
||||
type PublicRow = {
|
||||
public_cat_id: number;
|
||||
room_id: number;
|
||||
visible: string;
|
||||
};
|
||||
|
||||
// Reusable yes/no pill so emulator boolean-ish "1"/"0" columns read consistently.
|
||||
function YesNo({ on }: { on: boolean }) {
|
||||
return (
|
||||
<span
|
||||
@@ -47,48 +45,46 @@ export default async function AdminNavigation() {
|
||||
const t = await getTranslations("pages.admin.navigation");
|
||||
let flatcats: Flatcat[] = [];
|
||||
try {
|
||||
flatcats = await prisma.navigatorFlatcats.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
caption: true,
|
||||
captionSave: true,
|
||||
minRank: true,
|
||||
maxUserCount: true,
|
||||
canTrade: true,
|
||||
public: true,
|
||||
listType: true,
|
||||
orderNum: true,
|
||||
},
|
||||
orderBy: [{ orderNum: "asc" }, { id: "asc" }],
|
||||
});
|
||||
flatcats = await db
|
||||
.select({
|
||||
id: NavigatorFlatcats.id,
|
||||
caption: NavigatorFlatcats.caption,
|
||||
captionSave: NavigatorFlatcats.captionSave,
|
||||
minRank: NavigatorFlatcats.minRank,
|
||||
maxUserCount: NavigatorFlatcats.maxUserCount,
|
||||
canTrade: NavigatorFlatcats.canTrade,
|
||||
public: NavigatorFlatcats.public,
|
||||
listType: NavigatorFlatcats.listType,
|
||||
orderNum: NavigatorFlatcats.orderNum,
|
||||
})
|
||||
.from(NavigatorFlatcats)
|
||||
.orderBy(asc(NavigatorFlatcats.orderNum), asc(NavigatorFlatcats.id));
|
||||
} catch {
|
||||
flatcats = [];
|
||||
}
|
||||
|
||||
// Raw read because navigator_publics is an `@@ignore`d (keyless) model.
|
||||
let publics: PublicRow[] = [];
|
||||
try {
|
||||
publics = await prisma.$queryRaw<PublicRow[]>`
|
||||
const [rows] = (await db.execute(sql`
|
||||
SELECT public_cat_id, room_id, visible
|
||||
FROM navigator_publics
|
||||
ORDER BY public_cat_id ASC, room_id ASC
|
||||
`;
|
||||
`)) as unknown as [PublicRow[], unknown];
|
||||
publics = rows ?? [];
|
||||
} catch {
|
||||
publics = [];
|
||||
}
|
||||
|
||||
// Resolve the public-category names and room names referenced by the rows above
|
||||
// so the read-only view is legible. Each lookup degrades to an empty map on error.
|
||||
const catIds = Array.from(new Set(publics.map((p) => p.public_cat_id)));
|
||||
const roomIds = Array.from(new Set(publics.map((p) => p.room_id)));
|
||||
|
||||
let catNameById = new Map<number, string>();
|
||||
if (catIds.length) {
|
||||
try {
|
||||
const cats = await prisma.navigatorPubliccats.findMany({
|
||||
where: { id: { in: catIds } },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
const cats = await db
|
||||
.select({ id: NavigatorPubliccats.id, name: NavigatorPubliccats.name })
|
||||
.from(NavigatorPubliccats)
|
||||
.where(inArray(NavigatorPubliccats.id, catIds));
|
||||
catNameById = new Map(cats.map((c) => [c.id, c.name]));
|
||||
} catch {
|
||||
catNameById = new Map();
|
||||
@@ -98,10 +94,10 @@ export default async function AdminNavigation() {
|
||||
let roomNameById = new Map<number, string>();
|
||||
if (roomIds.length) {
|
||||
try {
|
||||
const rooms = await prisma.rooms.findMany({
|
||||
where: { id: { in: roomIds } },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
const rooms = await db
|
||||
.select({ id: Rooms.id, name: Rooms.name })
|
||||
.from(Rooms)
|
||||
.where(inArray(Rooms.id, roomIds));
|
||||
roomNameById = new Map(rooms.map((r) => [r.id, r.name]));
|
||||
} catch {
|
||||
roomNameById = new Map();
|
||||
@@ -134,7 +130,6 @@ export default async function AdminNavigation() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── navigator_flatcats ───────────────────────────────── */}
|
||||
<section className="mt-6">
|
||||
<h2 className="admin-section-title">{t("flatCategories")}</h2>
|
||||
<p className="text-xs theme-text-muted dark:theme-text-muted">
|
||||
@@ -186,7 +181,6 @@ export default async function AdminNavigation() {
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ── navigator_publics ────────────────────────────────── */}
|
||||
<section className="mt-6">
|
||||
<h2 className="admin-section-title">{t("publicRooms")}</h2>
|
||||
<p className="text-xs theme-text-muted dark:theme-text-muted">
|
||||
|
||||
+39
-27
@@ -1,3 +1,4 @@
|
||||
import { count, desc, eq, gt, inArray, or } from "drizzle-orm";
|
||||
import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
@@ -9,57 +10,68 @@ import {
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { activeBanWhere } from "@/lib/bans";
|
||||
import { unixNow } from "@/lib/bans";
|
||||
import {
|
||||
Ban as BanTable,
|
||||
db,
|
||||
StaffActivities,
|
||||
User,
|
||||
WebsiteArticles,
|
||||
} from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type RankCount = { rank: number; count: number };
|
||||
|
||||
export default async function AdminDashboard() {
|
||||
const t = await getTranslations("pages.admin.dashboard");
|
||||
const now = unixNow();
|
||||
let users = -1;
|
||||
let online = 0;
|
||||
let articles = 0;
|
||||
let bans = 0;
|
||||
try {
|
||||
[users, online, articles, bans] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
prisma.user.count({ where: { online: "1" } }),
|
||||
prisma.websiteArticles.count(),
|
||||
prisma.ban.count({
|
||||
where: activeBanWhere(),
|
||||
}),
|
||||
const [usersRow, onlineRow, articlesRow, bansRow] = await Promise.all([
|
||||
db.select({ total: count() }).from(User),
|
||||
db.select({ total: count() }).from(User).where(eq(User.online, "1")),
|
||||
db.select({ total: count() }).from(WebsiteArticles),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(BanTable)
|
||||
.where(or(eq(BanTable.banExpire, 0), gt(BanTable.banExpire, now))),
|
||||
]);
|
||||
users = usersRow[0]?.total ?? 0;
|
||||
online = onlineRow[0]?.total ?? 0;
|
||||
articles = articlesRow[0]?.total ?? 0;
|
||||
bans = bansRow[0]?.total ?? 0;
|
||||
} catch {}
|
||||
const dbOk = users >= 0;
|
||||
|
||||
const rankGroups = await prisma.user
|
||||
.groupBy({
|
||||
by: ["rank"],
|
||||
_count: { _all: true },
|
||||
orderBy: { rank: "desc" },
|
||||
})
|
||||
.catch(() => [] as Array<{ rank: number; _count: { _all: number } }>);
|
||||
const rankGroups = await db
|
||||
.select({ rank: User.rank, total: count() })
|
||||
.from(User)
|
||||
.groupBy(User.rank)
|
||||
.orderBy(desc(User.rank))
|
||||
.catch(() => [] as Array<{ rank: number; total: number }>);
|
||||
const rankCounts: RankCount[] = rankGroups
|
||||
.map((g) => ({ rank: g.rank, count: g._count._all }))
|
||||
.map((g) => ({ rank: g.rank, count: Number(g.total) }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
const maxRankCount = rankCounts.reduce((m, r) => Math.max(m, r.count), 0);
|
||||
|
||||
const activity = await prisma.staffActivities
|
||||
.findMany({ orderBy: { createdAt: "desc" }, take: 8 })
|
||||
.catch(
|
||||
() => [] as Awaited<ReturnType<typeof prisma.staffActivities.findMany>>,
|
||||
);
|
||||
const activity = await db
|
||||
.select()
|
||||
.from(StaffActivities)
|
||||
.orderBy(desc(StaffActivities.createdAt))
|
||||
.limit(8)
|
||||
.catch(() => [] as (typeof StaffActivities.$inferSelect)[]);
|
||||
const staffIds = Array.from(
|
||||
new Set(activity.map((a) => Number(a.userId))),
|
||||
).filter((n) => n > 0);
|
||||
let nameById = new Map<number, string>();
|
||||
if (staffIds.length) {
|
||||
const u = await prisma.user
|
||||
.findMany({
|
||||
where: { id: { in: staffIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
const u = await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, staffIds))
|
||||
.catch(() => []);
|
||||
nameById = new Map(u.map((x) => [x.id, x.username]));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { count, desc, inArray } from "drizzle-orm";
|
||||
import { Camera } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -7,8 +8,8 @@ import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { CameraWeb, db, User } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export default async function AdminPhotos({
|
||||
searchParams,
|
||||
@@ -23,16 +24,21 @@ export default async function AdminPhotos({
|
||||
const t = await getTranslations("pages.admin.photos");
|
||||
const parsed = parseListParams(new URLSearchParams(await searchParams));
|
||||
|
||||
const total = await prisma.cameraWeb.count().catch(() => 0);
|
||||
const [totalRow] = await db
|
||||
.select({ total: count() })
|
||||
.from(CameraWeb)
|
||||
.catch(() => [{ total: 0 }]);
|
||||
const total = Number(totalRow?.total ?? 0);
|
||||
const pagination = calcPagination(total, parsed.page, parsed.perPage || 24);
|
||||
|
||||
let photos: Awaited<ReturnType<typeof prisma.cameraWeb.findMany>> = [];
|
||||
let photos: (typeof CameraWeb.$inferSelect)[] = [];
|
||||
try {
|
||||
photos = await prisma.cameraWeb.findMany({
|
||||
orderBy: { timestamp: "desc" },
|
||||
skip: pagination.offset,
|
||||
take: pagination.perPage,
|
||||
});
|
||||
photos = await db
|
||||
.select()
|
||||
.from(CameraWeb)
|
||||
.orderBy(desc(CameraWeb.timestamp))
|
||||
.limit(pagination.perPage)
|
||||
.offset(pagination.offset);
|
||||
} catch {
|
||||
photos = [];
|
||||
}
|
||||
@@ -40,11 +46,10 @@ export default async function AdminPhotos({
|
||||
const userIds = [...new Set(photos.map((p) => p.userId))];
|
||||
const users =
|
||||
userIds.length > 0
|
||||
? await prisma.user
|
||||
.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
? await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, userIds))
|
||||
.catch(() => [])
|
||||
: [];
|
||||
const usernameById = new Map(users.map((u) => [u.id, u.username]));
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { db, WebsitePoll } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { PollForm } from "../poll-form";
|
||||
import { PollQuestionsManager } from "./poll-questions";
|
||||
import { PollResultsView } from "./poll-results";
|
||||
@@ -26,12 +27,12 @@ export default async function PollDetailPage({
|
||||
const t = await getTranslations("pages.admin.polls");
|
||||
const tHubs = await getTranslations("pages.admin.hubs");
|
||||
|
||||
const poll = await prisma.websitePoll.findUnique({
|
||||
where: { id: pollId },
|
||||
include: {
|
||||
const poll = await db.query.WebsitePoll.findFirst({
|
||||
where: eq(WebsitePoll.id, pollId),
|
||||
with: {
|
||||
questions: {
|
||||
orderBy: { sortOrder: "asc" },
|
||||
include: {
|
||||
orderBy: (q, { asc }) => [asc(q.sortOrder)],
|
||||
with: {
|
||||
votes: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { desc } from "drizzle-orm";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
createApiKey,
|
||||
@@ -6,8 +7,8 @@ import {
|
||||
} from "@/actions/admin-radio-api-keys";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, RadioApiKeys } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const metadata = { title: "Radio API Keys" };
|
||||
|
||||
@@ -34,22 +35,21 @@ export default async function AdminRadioApiKeysPage() {
|
||||
let dbError = false;
|
||||
|
||||
try {
|
||||
keys = await prisma.radioApiKeys
|
||||
.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
key: true,
|
||||
allowedIps: true,
|
||||
rateLimit: true,
|
||||
lastUsedAt: true,
|
||||
expiresAt: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: [{ isActive: "desc" }, { id: "desc" }],
|
||||
take: 500,
|
||||
keys = await db
|
||||
.select({
|
||||
id: RadioApiKeys.id,
|
||||
name: RadioApiKeys.name,
|
||||
key: RadioApiKeys.key,
|
||||
allowedIps: RadioApiKeys.allowedIps,
|
||||
rateLimit: RadioApiKeys.rateLimit,
|
||||
lastUsedAt: RadioApiKeys.lastUsedAt,
|
||||
expiresAt: RadioApiKeys.expiresAt,
|
||||
isActive: RadioApiKeys.isActive,
|
||||
createdAt: RadioApiKeys.createdAt,
|
||||
})
|
||||
.from(RadioApiKeys)
|
||||
.orderBy(desc(RadioApiKeys.isActive), desc(RadioApiKeys.id))
|
||||
.limit(500)
|
||||
.catch(() => {
|
||||
dbError = true;
|
||||
return [];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import Image from "next/image";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
@@ -7,8 +8,8 @@ import {
|
||||
} from "@/actions/admin-radio-autodj";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, RadioAutoDjPlaylist } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const metadata = { title: "Radio AutoDJ" };
|
||||
|
||||
@@ -39,22 +40,24 @@ export default async function AdminRadioAutoDjPage() {
|
||||
let dbError = false;
|
||||
|
||||
try {
|
||||
tracks = await prisma.radioAutoDjPlaylist
|
||||
.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
artist: true,
|
||||
album: true,
|
||||
artworkUrl: true,
|
||||
duration: true,
|
||||
playCount: true,
|
||||
lastPlayedAt: true,
|
||||
isActive: true,
|
||||
sortOrder: true,
|
||||
},
|
||||
orderBy: [{ sortOrder: "asc" }, { title: "asc" }],
|
||||
tracks = await db
|
||||
.select({
|
||||
id: RadioAutoDjPlaylist.id,
|
||||
title: RadioAutoDjPlaylist.title,
|
||||
artist: RadioAutoDjPlaylist.artist,
|
||||
album: RadioAutoDjPlaylist.album,
|
||||
artworkUrl: RadioAutoDjPlaylist.artworkUrl,
|
||||
duration: RadioAutoDjPlaylist.duration,
|
||||
playCount: RadioAutoDjPlaylist.playCount,
|
||||
lastPlayedAt: RadioAutoDjPlaylist.lastPlayedAt,
|
||||
isActive: RadioAutoDjPlaylist.isActive,
|
||||
sortOrder: RadioAutoDjPlaylist.sortOrder,
|
||||
})
|
||||
.from(RadioAutoDjPlaylist)
|
||||
.orderBy(
|
||||
asc(RadioAutoDjPlaylist.sortOrder),
|
||||
asc(RadioAutoDjPlaylist.title),
|
||||
)
|
||||
.catch(() => {
|
||||
dbError = true;
|
||||
return [];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import NextImage from "next/image";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
@@ -7,8 +8,8 @@ import {
|
||||
} from "@/actions/admin-radio-extra";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, RadioBanners } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type Banner = {
|
||||
id: bigint;
|
||||
@@ -28,19 +29,19 @@ export default async function AdminRadioBannersPage() {
|
||||
let dbError = false;
|
||||
|
||||
try {
|
||||
banners = await prisma.radioBanners.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
imagePath: true,
|
||||
title: true,
|
||||
description: true,
|
||||
sortOrder: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: [{ sortOrder: "asc" }, { id: "asc" }],
|
||||
});
|
||||
banners = await db
|
||||
.select({
|
||||
id: RadioBanners.id,
|
||||
userId: RadioBanners.userId,
|
||||
imagePath: RadioBanners.imagePath,
|
||||
title: RadioBanners.title,
|
||||
description: RadioBanners.description,
|
||||
sortOrder: RadioBanners.sortOrder,
|
||||
isActive: RadioBanners.isActive,
|
||||
createdAt: RadioBanners.createdAt,
|
||||
})
|
||||
.from(RadioBanners)
|
||||
.orderBy(asc(RadioBanners.sortOrder), asc(RadioBanners.id));
|
||||
} catch {
|
||||
dbError = true;
|
||||
banners = [];
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { desc, inArray } from "drizzle-orm";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { db, RadioHistory, User } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type History = {
|
||||
id: bigint;
|
||||
@@ -31,28 +32,28 @@ export default async function AdminRadioHistoryPage() {
|
||||
let dbError = false;
|
||||
|
||||
try {
|
||||
history = await prisma.radioHistory.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
showName: true,
|
||||
startedAt: true,
|
||||
endedAt: true,
|
||||
listenersCount: true,
|
||||
notes: true,
|
||||
},
|
||||
orderBy: { startedAt: "desc" },
|
||||
take: 100,
|
||||
});
|
||||
history = await db
|
||||
.select({
|
||||
id: RadioHistory.id,
|
||||
userId: RadioHistory.userId,
|
||||
showName: RadioHistory.showName,
|
||||
startedAt: RadioHistory.startedAt,
|
||||
endedAt: RadioHistory.endedAt,
|
||||
listenersCount: RadioHistory.listenersCount,
|
||||
notes: RadioHistory.notes,
|
||||
})
|
||||
.from(RadioHistory)
|
||||
.orderBy(desc(RadioHistory.startedAt))
|
||||
.limit(100);
|
||||
|
||||
const ids = Array.from(
|
||||
new Set(history.map((h) => Number(h.userId))),
|
||||
).filter((n) => Number.isFinite(n) && n > 0);
|
||||
if (ids.length > 0) {
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: ids } },
|
||||
select: { id: true, username: true },
|
||||
});
|
||||
const users = await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, ids));
|
||||
names = new Map(users.map((u) => [String(u.id), u.username]));
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { desc } from "drizzle-orm";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { deleteShout } from "@/actions/admin-radio-moderation";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, RadioShouts } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type Shout = {
|
||||
id: bigint;
|
||||
@@ -18,17 +19,17 @@ export default async function AdminRadioModerationPage() {
|
||||
|
||||
let shouts: Shout[] = [];
|
||||
try {
|
||||
shouts = await prisma.radioShouts.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
message: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
orderBy: { id: "desc" },
|
||||
take: 50,
|
||||
});
|
||||
shouts = await db
|
||||
.select({
|
||||
id: RadioShouts.id,
|
||||
userId: RadioShouts.userId,
|
||||
message: RadioShouts.message,
|
||||
createdAt: RadioShouts.createdAt,
|
||||
updatedAt: RadioShouts.updatedAt,
|
||||
})
|
||||
.from(RadioShouts)
|
||||
.orderBy(desc(RadioShouts.id))
|
||||
.limit(50);
|
||||
} catch {
|
||||
shouts = [];
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { desc } from "drizzle-orm";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { deleteShout } from "@/actions/admin-radio-moderation";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, RadioApplications, RadioSchedules, RadioShouts } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type Shout = {
|
||||
id: bigint;
|
||||
@@ -52,48 +53,53 @@ export default async function AdminRadioPage() {
|
||||
let schedules: Schedule[] = [];
|
||||
|
||||
try {
|
||||
shouts = await prisma.radioShouts.findMany({
|
||||
select: { id: true, userId: true, message: true, createdAt: true },
|
||||
orderBy: { id: "desc" },
|
||||
take: 25,
|
||||
});
|
||||
shouts = await db
|
||||
.select({
|
||||
id: RadioShouts.id,
|
||||
userId: RadioShouts.userId,
|
||||
message: RadioShouts.message,
|
||||
createdAt: RadioShouts.createdAt,
|
||||
})
|
||||
.from(RadioShouts)
|
||||
.orderBy(desc(RadioShouts.id))
|
||||
.limit(25);
|
||||
} catch {
|
||||
shouts = [];
|
||||
}
|
||||
|
||||
try {
|
||||
applications = await prisma.radioApplications.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
realName: true,
|
||||
age: true,
|
||||
status: true,
|
||||
musicStyle: true,
|
||||
discordUsername: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: { id: "desc" },
|
||||
take: 25,
|
||||
});
|
||||
applications = await db
|
||||
.select({
|
||||
id: RadioApplications.id,
|
||||
userId: RadioApplications.userId,
|
||||
realName: RadioApplications.realName,
|
||||
age: RadioApplications.age,
|
||||
status: RadioApplications.status,
|
||||
musicStyle: RadioApplications.musicStyle,
|
||||
discordUsername: RadioApplications.discordUsername,
|
||||
createdAt: RadioApplications.createdAt,
|
||||
})
|
||||
.from(RadioApplications)
|
||||
.orderBy(desc(RadioApplications.id))
|
||||
.limit(25);
|
||||
} catch {
|
||||
applications = [];
|
||||
}
|
||||
|
||||
try {
|
||||
schedules = await prisma.radioSchedules.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
day: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
showName: true,
|
||||
isActive: true,
|
||||
},
|
||||
orderBy: { id: "desc" },
|
||||
take: 25,
|
||||
});
|
||||
schedules = await db
|
||||
.select({
|
||||
id: RadioSchedules.id,
|
||||
userId: RadioSchedules.userId,
|
||||
day: RadioSchedules.day,
|
||||
startTime: RadioSchedules.startTime,
|
||||
endTime: RadioSchedules.endTime,
|
||||
showName: RadioSchedules.showName,
|
||||
isActive: RadioSchedules.isActive,
|
||||
})
|
||||
.from(RadioSchedules)
|
||||
.orderBy(desc(RadioSchedules.id))
|
||||
.limit(25);
|
||||
} catch {
|
||||
schedules = [];
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
createRadioRank,
|
||||
@@ -6,7 +7,7 @@ import {
|
||||
} from "@/actions/admin-radio-extra";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { db, RadioRanks } from "@/lib/db";
|
||||
|
||||
type Rank = {
|
||||
id: bigint;
|
||||
@@ -23,16 +24,16 @@ export default async function AdminRadioRanksPage() {
|
||||
let dbError = false;
|
||||
|
||||
try {
|
||||
ranks = await prisma.radioRanks.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
badgeCode: true,
|
||||
isActive: true,
|
||||
},
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
ranks = await db
|
||||
.select({
|
||||
id: RadioRanks.id,
|
||||
name: RadioRanks.name,
|
||||
description: RadioRanks.description,
|
||||
badgeCode: RadioRanks.badgeCode,
|
||||
isActive: RadioRanks.isActive,
|
||||
})
|
||||
.from(RadioRanks)
|
||||
.orderBy(asc(RadioRanks.id));
|
||||
} catch {
|
||||
dbError = true;
|
||||
ranks = [];
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "@/actions/admin-radio-extra";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { db, WebsiteSetting } from "@/lib/db";
|
||||
|
||||
type Field = { key: string; comment: string };
|
||||
type Group = { title: string; fields: Field[] };
|
||||
@@ -364,9 +364,13 @@ export default async function AdminRadioSettingsPage() {
|
||||
let extra: Field[] = [];
|
||||
|
||||
try {
|
||||
const rows = await prisma.websiteSetting.findMany({
|
||||
select: { key: true, value: true, comment: true },
|
||||
});
|
||||
const rows = await db
|
||||
.select({
|
||||
key: WebsiteSetting.key,
|
||||
value: WebsiteSetting.value,
|
||||
comment: WebsiteSetting.comment,
|
||||
})
|
||||
.from(WebsiteSetting);
|
||||
values = new Map(rows.map((r) => [r.key, r.value]));
|
||||
extra = rows
|
||||
.filter(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
@@ -8,8 +9,8 @@ import {
|
||||
} from "@/actions/admin-rare-values";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, WebsiteRareValueCategories, WebsiteRareValues } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const CURRENCY_TYPES = ["diamonds", "credits", "duckets", "points"];
|
||||
|
||||
@@ -21,23 +22,25 @@ export default async function AdminRareValues() {
|
||||
|
||||
const t = await getTranslations("pages.admin.rareValues");
|
||||
|
||||
let categories: Awaited<
|
||||
ReturnType<typeof prisma.websiteRareValueCategories.findMany>
|
||||
> = [];
|
||||
let categories: (typeof WebsiteRareValueCategories.$inferSelect)[] = [];
|
||||
try {
|
||||
categories = await prisma.websiteRareValueCategories.findMany({
|
||||
orderBy: [{ priority: "asc" }, { name: "asc" }],
|
||||
});
|
||||
categories = await db
|
||||
.select()
|
||||
.from(WebsiteRareValueCategories)
|
||||
.orderBy(
|
||||
asc(WebsiteRareValueCategories.priority),
|
||||
asc(WebsiteRareValueCategories.name),
|
||||
);
|
||||
} catch {
|
||||
categories = [];
|
||||
}
|
||||
|
||||
let values: Awaited<ReturnType<typeof prisma.websiteRareValues.findMany>> =
|
||||
[];
|
||||
let values: (typeof WebsiteRareValues.$inferSelect)[] = [];
|
||||
try {
|
||||
values = await prisma.websiteRareValues.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
values = await db
|
||||
.select()
|
||||
.from(WebsiteRareValues)
|
||||
.orderBy(asc(WebsiteRareValues.name));
|
||||
} catch {
|
||||
values = [];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { desc, eq, inArray } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
|
||||
import { db, Items, ItemsBase, User } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { loadRoom } from "../../_lib/load-room";
|
||||
import { FurniClient } from "./furni-client";
|
||||
|
||||
@@ -18,10 +19,11 @@ export default async function RoomFurniPage({
|
||||
const room = await loadRoom(id);
|
||||
|
||||
// Load items in this room. items.item_id → items_base.id
|
||||
const items = await prisma.items.findMany({
|
||||
where: { roomId: room.id },
|
||||
orderBy: { id: "desc" },
|
||||
});
|
||||
const items = await db
|
||||
.select()
|
||||
.from(Items)
|
||||
.where(eq(Items.roomId, room.id))
|
||||
.orderBy(desc(Items.id));
|
||||
|
||||
// Resolve item bases
|
||||
const baseIds = Array.from(
|
||||
@@ -30,10 +32,15 @@ export default async function RoomFurniPage({
|
||||
),
|
||||
);
|
||||
const bases = baseIds.length
|
||||
? await prisma.itemsBase.findMany({
|
||||
where: { id: { in: baseIds } },
|
||||
select: { id: true, itemName: true, publicName: true, type: true },
|
||||
})
|
||||
? await db
|
||||
.select({
|
||||
id: ItemsBase.id,
|
||||
itemName: ItemsBase.itemName,
|
||||
publicName: ItemsBase.publicName,
|
||||
type: ItemsBase.type,
|
||||
})
|
||||
.from(ItemsBase)
|
||||
.where(inArray(ItemsBase.id, baseIds))
|
||||
: [];
|
||||
const baseMap = new Map(bases.map((b) => [b.id, b]));
|
||||
|
||||
@@ -42,10 +49,10 @@ export default async function RoomFurniPage({
|
||||
new Set(items.map((i) => i.userId).filter((u) => u > 0)),
|
||||
);
|
||||
const users = userIds.length
|
||||
? await prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
? await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, userIds))
|
||||
: [];
|
||||
const userMap = new Map(users.map((u) => [u.id, u.username]));
|
||||
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { notFound } from "next/navigation";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { db, Rooms, User } from "@/lib/db";
|
||||
|
||||
export async function loadRoom(id: string) {
|
||||
const roomId = parseInt(id, 10);
|
||||
if (Number.isNaN(roomId)) notFound();
|
||||
|
||||
const room = await prisma.rooms.findUnique({
|
||||
where: { id: roomId },
|
||||
});
|
||||
const [room] = await db
|
||||
.select()
|
||||
.from(Rooms)
|
||||
.where(eq(Rooms.id, roomId))
|
||||
.limit(1);
|
||||
|
||||
if (!room) notFound();
|
||||
const owner =
|
||||
room.ownerId > 0
|
||||
? await prisma.user.findUnique({
|
||||
where: { id: room.ownerId },
|
||||
select: { id: true, username: true, look: true },
|
||||
})
|
||||
? ((
|
||||
await db
|
||||
.select({
|
||||
id: User.id,
|
||||
username: User.username,
|
||||
look: User.look,
|
||||
})
|
||||
.from(User)
|
||||
.where(eq(User.id, room.ownerId))
|
||||
.limit(1)
|
||||
)[0] ?? null)
|
||||
: null;
|
||||
return { ...room, owner };
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import { Settings } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { db, WebsiteSetting } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { AdvancedSettingsPanel } from "./advanced-settings-panel";
|
||||
import { FIELD_DEFAULTS, MANAGED_SETTING_KEYS } from "./cms-settings-config";
|
||||
import { CmsSettingsForm } from "./cms-settings-form";
|
||||
@@ -22,11 +23,12 @@ export default async function AdminSettings() {
|
||||
);
|
||||
const t = await getTranslations("pages.admin.settings");
|
||||
|
||||
let settings: Awaited<ReturnType<typeof prisma.websiteSetting.findMany>> = [];
|
||||
let settings: (typeof WebsiteSetting.$inferSelect)[] = [];
|
||||
try {
|
||||
settings = await prisma.websiteSetting.findMany({
|
||||
orderBy: { key: "asc" },
|
||||
});
|
||||
settings = await db
|
||||
.select()
|
||||
.from(WebsiteSetting)
|
||||
.orderBy(asc(WebsiteSetting.key));
|
||||
} catch {
|
||||
settings = [];
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Package } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { deleteShopArticle, updateShopArticle } from "@/actions/admin-shop";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, WebsiteShopArticles } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export default async function EditShopArticle({
|
||||
params,
|
||||
@@ -20,12 +21,14 @@ export default async function EditShopArticle({
|
||||
const t = await getTranslations("pages.admin.shop");
|
||||
|
||||
const { id } = await params;
|
||||
let pkg: Awaited<ReturnType<typeof prisma.websiteShopArticles.findUnique>> =
|
||||
null;
|
||||
let pkg: typeof WebsiteShopArticles.$inferSelect | null = null;
|
||||
try {
|
||||
pkg = await prisma.websiteShopArticles.findUnique({
|
||||
where: { id: BigInt(id) },
|
||||
});
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(WebsiteShopArticles)
|
||||
.where(eq(WebsiteShopArticles.id, BigInt(id)))
|
||||
.limit(1);
|
||||
pkg = row ?? null;
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
|
||||
+19
-17
@@ -1,3 +1,4 @@
|
||||
import { asc, count, desc } from "drizzle-orm";
|
||||
import { ShoppingCart } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -8,9 +9,9 @@ import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { CurrencyIcon } from "@/components/shared/currency-icon";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db, WebsitePaypalTransactions, WebsiteShopArticles } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const PAID_STATUS = new Set(["completed", "paid", "success", "approved"]);
|
||||
const RECENT_ORDERS = 25;
|
||||
@@ -65,22 +66,24 @@ export default async function AdminShop({
|
||||
const parsed = parseListParams(new URLSearchParams(raw));
|
||||
|
||||
let packageTotal = 0;
|
||||
let packages: Awaited<
|
||||
ReturnType<typeof prisma.websiteShopArticles.findMany>
|
||||
> = [];
|
||||
let packages: (typeof WebsiteShopArticles.$inferSelect)[] = [];
|
||||
let loadError = false;
|
||||
try {
|
||||
packageTotal = await prisma.websiteShopArticles.count();
|
||||
const [totalRow] = await db
|
||||
.select({ total: count() })
|
||||
.from(WebsiteShopArticles);
|
||||
packageTotal = Number(totalRow?.total ?? 0);
|
||||
const pagination = calcPagination(
|
||||
packageTotal,
|
||||
parsed.page,
|
||||
parsed.perPage,
|
||||
);
|
||||
packages = await prisma.websiteShopArticles.findMany({
|
||||
orderBy: [{ position: "asc" }, { name: "asc" }],
|
||||
skip: pagination.offset,
|
||||
take: pagination.perPage,
|
||||
});
|
||||
packages = await db
|
||||
.select()
|
||||
.from(WebsiteShopArticles)
|
||||
.orderBy(asc(WebsiteShopArticles.position), asc(WebsiteShopArticles.name))
|
||||
.limit(pagination.perPage)
|
||||
.offset(pagination.offset);
|
||||
} catch {
|
||||
packages = [];
|
||||
packageTotal = 0;
|
||||
@@ -93,14 +96,13 @@ export default async function AdminShop({
|
||||
parsed.perPage,
|
||||
);
|
||||
|
||||
let orders: Awaited<
|
||||
ReturnType<typeof prisma.websitePaypalTransactions.findMany>
|
||||
> = [];
|
||||
let orders: (typeof WebsitePaypalTransactions.$inferSelect)[] = [];
|
||||
try {
|
||||
orders = await prisma.websitePaypalTransactions.findMany({
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: RECENT_ORDERS,
|
||||
});
|
||||
orders = await db
|
||||
.select()
|
||||
.from(WebsitePaypalTransactions)
|
||||
.orderBy(desc(WebsitePaypalTransactions.createdAt))
|
||||
.limit(RECENT_ORDERS);
|
||||
} catch {
|
||||
orders = [];
|
||||
loadError = true;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { asc, count, gt } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { CatalogItems, db, Soundtracks } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import {
|
||||
DEFAULT_SOUNDTRACK_BASE_URL,
|
||||
@@ -18,28 +19,31 @@ export default async function SoundsPage() {
|
||||
const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank);
|
||||
|
||||
const [rows, usage, baseUrlSetting] = await Promise.all([
|
||||
prisma.soundtracks.findMany({
|
||||
orderBy: { id: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
author: true,
|
||||
length: true,
|
||||
track: true,
|
||||
},
|
||||
}),
|
||||
prisma.catalogItems.groupBy({
|
||||
by: ["songId"],
|
||||
where: { songId: { gt: 0 } },
|
||||
_count: { _all: true },
|
||||
}),
|
||||
db
|
||||
.select({
|
||||
id: Soundtracks.id,
|
||||
code: Soundtracks.code,
|
||||
name: Soundtracks.name,
|
||||
author: Soundtracks.author,
|
||||
length: Soundtracks.length,
|
||||
track: Soundtracks.track,
|
||||
})
|
||||
.from(Soundtracks)
|
||||
.orderBy(asc(Soundtracks.id)),
|
||||
db
|
||||
.select({
|
||||
songId: CatalogItems.songId,
|
||||
total: count(),
|
||||
})
|
||||
.from(CatalogItems)
|
||||
.where(gt(CatalogItems.songId, 0))
|
||||
.groupBy(CatalogItems.songId),
|
||||
siteSettings.get("soundtrack_base_url", DEFAULT_SOUNDTRACK_BASE_URL),
|
||||
]);
|
||||
|
||||
const usageMap: Record<number, number> = {};
|
||||
for (const row of usage) {
|
||||
if (row.songId != null) usageMap[row.songId] = row._count._all;
|
||||
if (row.songId != null) usageMap[row.songId] = Number(row.total);
|
||||
}
|
||||
|
||||
const soundtracks = rows.map((r) => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminSimplePager } from "@/components/admin/admin-simple-pager";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type SubRow = {
|
||||
id: number;
|
||||
@@ -14,6 +15,19 @@ type SubRow = {
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
type CountRow = { c: number | bigint };
|
||||
type SubSqlRow = {
|
||||
id: number;
|
||||
user_id: number | null;
|
||||
subscription_type: string | null;
|
||||
expires_at: number;
|
||||
};
|
||||
|
||||
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
|
||||
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
|
||||
return rows ?? [];
|
||||
}
|
||||
|
||||
export default async function AdminSubscriptions({
|
||||
searchParams,
|
||||
}: {
|
||||
@@ -36,39 +50,32 @@ export default async function AdminSubscriptions({
|
||||
let loadError = false;
|
||||
try {
|
||||
const [countRows, typeRows, soonRows] = await Promise.all([
|
||||
prisma.$queryRaw<{ c: bigint }[]>`
|
||||
rawRows<CountRow>(sql`
|
||||
SELECT COUNT(*) AS c
|
||||
FROM users_subscriptions
|
||||
WHERE active = 1
|
||||
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
|
||||
`,
|
||||
prisma.$queryRaw<{ c: bigint }[]>`
|
||||
`),
|
||||
rawRows<CountRow>(sql`
|
||||
SELECT COUNT(DISTINCT subscription_type) AS c
|
||||
FROM users_subscriptions
|
||||
WHERE active = 1
|
||||
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
|
||||
`,
|
||||
prisma.$queryRaw<{ c: bigint }[]>`
|
||||
`),
|
||||
rawRows<CountRow>(sql`
|
||||
SELECT COUNT(*) AS c
|
||||
FROM users_subscriptions
|
||||
WHERE active = 1
|
||||
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
|
||||
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) <= ${now + 7 * 24 * 60 * 60}
|
||||
`,
|
||||
`),
|
||||
]);
|
||||
total = Number(countRows[0]?.c ?? 0);
|
||||
types = Number(typeRows[0]?.c ?? 0);
|
||||
expiringSoon = Number(soonRows[0]?.c ?? 0);
|
||||
const pagination = calcPagination(total, parsed.page, parsed.perPage);
|
||||
|
||||
const rows = await prisma.$queryRaw<
|
||||
{
|
||||
id: number;
|
||||
user_id: number | null;
|
||||
subscription_type: string | null;
|
||||
expires_at: number;
|
||||
}[]
|
||||
>`
|
||||
const rows = await rawRows<SubSqlRow>(sql`
|
||||
SELECT id,
|
||||
user_id,
|
||||
subscription_type,
|
||||
@@ -78,7 +85,7 @@ export default async function AdminSubscriptions({
|
||||
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
|
||||
ORDER BY expires_at ASC
|
||||
LIMIT ${pagination.perPage} OFFSET ${pagination.offset}
|
||||
`;
|
||||
`);
|
||||
|
||||
active = rows.map((r) => ({
|
||||
id: Number(r.id),
|
||||
|
||||
+20
-11
@@ -1,11 +1,12 @@
|
||||
import { asc, count } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { createTag, deleteTag, updateTag } from "@/actions/admin-tags";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, Taggables, Tags } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type Tag = {
|
||||
id: bigint;
|
||||
@@ -26,18 +27,26 @@ export default async function AdminTagsPage() {
|
||||
let dbError = false;
|
||||
|
||||
try {
|
||||
tags = await prisma.tags.findMany({
|
||||
select: { id: true, name: true, backgroundColor: true, createdAt: true },
|
||||
orderBy: { name: "asc" },
|
||||
take: 500,
|
||||
});
|
||||
tags = await db
|
||||
.select({
|
||||
id: Tags.id,
|
||||
name: Tags.name,
|
||||
backgroundColor: Tags.backgroundColor,
|
||||
createdAt: Tags.createdAt,
|
||||
})
|
||||
.from(Tags)
|
||||
.orderBy(asc(Tags.name))
|
||||
.limit(500);
|
||||
|
||||
// How many things each tag is attached to (taggables join table).
|
||||
const counts = await prisma.taggables.groupBy({
|
||||
by: ["tagId"],
|
||||
_count: { tagId: true },
|
||||
});
|
||||
usage = new Map(counts.map((c) => [String(c.tagId), c._count.tagId]));
|
||||
const counts = await db
|
||||
.select({
|
||||
tagId: Taggables.tagId,
|
||||
total: count(),
|
||||
})
|
||||
.from(Taggables)
|
||||
.groupBy(Taggables.tagId);
|
||||
usage = new Map(counts.map((c) => [String(c.tagId), Number(c.total)]));
|
||||
} catch {
|
||||
dbError = true;
|
||||
tags = [];
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { createTeam, deleteTeam } from "@/actions/admin-teams";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, WebsiteTeams } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type TeamRow = {
|
||||
id: bigint;
|
||||
@@ -24,17 +25,17 @@ export default async function AdminTeams() {
|
||||
const t = await getTranslations("pages.admin.teams");
|
||||
let teams: TeamRow[] = [];
|
||||
try {
|
||||
teams = await prisma.websiteTeams.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
rankName: true,
|
||||
badge: true,
|
||||
jobDescription: true,
|
||||
staffColor: true,
|
||||
hiddenRank: true,
|
||||
},
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
teams = await db
|
||||
.select({
|
||||
id: WebsiteTeams.id,
|
||||
rankName: WebsiteTeams.rankName,
|
||||
badge: WebsiteTeams.badge,
|
||||
jobDescription: WebsiteTeams.jobDescription,
|
||||
staffColor: WebsiteTeams.staffColor,
|
||||
hiddenRank: WebsiteTeams.hiddenRank,
|
||||
})
|
||||
.from(WebsiteTeams)
|
||||
.orderBy(asc(WebsiteTeams.id));
|
||||
} catch {
|
||||
teams = [];
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user