feat(admin): ban appeals, photos polish, economy adjust, staff smoke
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
fe46bd0544
commit
3ad3f9512c
36 files changed
+1185
-293
No files matched your search
@@ -10,6 +10,7 @@ import {
|
||||
VolumeX,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { setTradeLock } from "@/actions/bulk-users";
|
||||
import {
|
||||
alertUser,
|
||||
disconnectUser,
|
||||
@@ -234,6 +235,40 @@ export function UserQuickActions({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trade lock (DB only — no RCON helper) */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Trade lock (CMS DB)
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
const until = Math.floor(Date.now() / 1000) + 7 * 24 * 3600;
|
||||
run(() => setTradeLock({ userId, untilUnix: until }), {
|
||||
successMessage: `${username} trade-locked for 7 days.`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Lock 7d
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
run(() => setTradeLock({ userId, untilUnix: 0 }), {
|
||||
successMessage: `${username} trade lock cleared.`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clear lock
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { Users } from "lucide-react";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { fetchAdminList } from "@/lib/admin-list";
|
||||
import { canAccess, PERMS } from "@/lib/permissions";
|
||||
import { CreateUserDialog } from "./_components/create-user-dialog";
|
||||
@@ -43,17 +46,22 @@ export default async function UsersPage({
|
||||
searchParams,
|
||||
);
|
||||
|
||||
const t = await getTranslations("pages.admin.users");
|
||||
const canEdit = canAccess(result.permissions, PERMS.USERS_EDIT, result.rank);
|
||||
const canBan = canAccess(result.permissions, PERMS.USERS_BAN, result.rank);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-muted-foreground">
|
||||
{result.total.toLocaleString()} users total
|
||||
</p>
|
||||
{canEdit && <CreateUserDialog sessionRank={result.rank} />}
|
||||
</div>
|
||||
<AdminPageShell
|
||||
icon={Users}
|
||||
title={t("title")}
|
||||
subtitle={t("subtitle")}
|
||||
actions={
|
||||
canEdit ? <CreateUserDialog sessionRank={result.rank} /> : undefined
|
||||
}
|
||||
>
|
||||
<p className="text-muted-foreground m-0">
|
||||
{result.total.toLocaleString()} users total
|
||||
</p>
|
||||
|
||||
<UsersTable
|
||||
data={{
|
||||
@@ -66,6 +74,6 @@ export default async function UsersPage({
|
||||
canEdit={canEdit}
|
||||
canBan={canBan}
|
||||
/>
|
||||
</div>
|
||||
</AdminPageShell>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { getWatchedUserIds } from "@/lib/services/watch";
|
||||
@@ -23,6 +24,7 @@ export default async function UserShowPage({
|
||||
redirect("/admin");
|
||||
}
|
||||
|
||||
const t = await getTranslations("pages.admin.users.detail");
|
||||
const canEdit = canAccess(permissions, PERMS.USERS_EDIT, session.user.rank);
|
||||
const data = await loadUserById(id);
|
||||
const sanctions = await loadUserSanctions(data.user.id);
|
||||
@@ -53,15 +55,17 @@ export default async function UserShowPage({
|
||||
>
|
||||
<Tabs defaultValue="info" className="space-y-4 min-w-0">
|
||||
<TabsList>
|
||||
<TabsTrigger value="info">Account Info</TabsTrigger>
|
||||
<TabsTrigger value="info">{t("tabInfo")}</TabsTrigger>
|
||||
<TabsTrigger value="badges">
|
||||
Badges ({data.user.badges.length})
|
||||
{t("tabBadges", { count: data.user.badges.length })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="rooms">
|
||||
{t("tabRooms", { count: data.roomCount })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="rooms">Rooms ({data.roomCount})</TabsTrigger>
|
||||
<TabsTrigger value="sanctions">
|
||||
Sanctions ({sanctions.events.length})
|
||||
{t("tabSanctions", { count: sanctions.events.length })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="activity">Activity</TabsTrigger>
|
||||
<TabsTrigger value="activity">{t("tabActivity")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="info">
|
||||
|
||||
@@ -5,9 +5,9 @@ import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
bulkAdjustCurrency,
|
||||
bulkBan,
|
||||
bulkGiveBadge,
|
||||
bulkGiveCurrency,
|
||||
bulkUnban,
|
||||
} from "@/actions/bulk-users";
|
||||
import { DataTable } from "@/components/admin/data-table";
|
||||
@@ -283,20 +283,20 @@ export function UsersTable({ data, canEdit, canBan }: UsersTableProps) {
|
||||
if (pending?.kind !== "give-currency") return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const result = await bulkGiveCurrency({
|
||||
const result = await bulkAdjustCurrency({
|
||||
userIds: pending.ids,
|
||||
amount: Number(currencyAmount),
|
||||
type: currencyType,
|
||||
});
|
||||
if (result.ok) {
|
||||
const data = result.data as {
|
||||
given: number;
|
||||
adjusted: number;
|
||||
total: number;
|
||||
failedIds: { userId: number; reason: string }[];
|
||||
};
|
||||
reportResult(
|
||||
`Given ${currencyAmount} ${currencyType}`,
|
||||
data.given,
|
||||
`Adjusted ${currencyAmount} ${currencyType}`,
|
||||
data.adjusted,
|
||||
data.total,
|
||||
data.failedIds.length,
|
||||
);
|
||||
@@ -344,7 +344,7 @@ export function UsersTable({ data, canEdit, canBan }: UsersTableProps) {
|
||||
bulkActions.push({ label: "Unban", value: "unban" });
|
||||
}
|
||||
if (canEdit) {
|
||||
bulkActions.push({ label: "Give credits", value: "give-currency" });
|
||||
bulkActions.push({ label: "Adjust currency", value: "give-currency" });
|
||||
bulkActions.push({ label: "Give badge", value: "give-badge" });
|
||||
}
|
||||
|
||||
@@ -424,7 +424,7 @@ export function UsersTable({ data, canEdit, canBan }: UsersTableProps) {
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Give currency to{" "}
|
||||
Adjust currency for{" "}
|
||||
{pending?.kind === "give-currency" ? pending.ids.length : 0} users
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -459,7 +459,9 @@ export function UsersTable({ data, canEdit, canBan }: UsersTableProps) {
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="give-amount">Amount</Label>
|
||||
<Label htmlFor="give-amount">
|
||||
Amount (positive = give, negative = take)
|
||||
</Label>
|
||||
<Input
|
||||
id="give-amount"
|
||||
type="number"
|
||||
@@ -478,9 +480,9 @@ export function UsersTable({ data, canEdit, canBan }: UsersTableProps) {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={confirmGiveCurrency}
|
||||
disabled={busy || !currencyAmount}
|
||||
disabled={busy || !currencyAmount || Number(currencyAmount) === 0}
|
||||
>
|
||||
Give
|
||||
Apply
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
Reference in new issue
Block a user