الملفات
back_end_oudelaa/oudelaa_dashboard/app/(dashboard)/settings/page.tsx
boutmoun123 8863f61d00
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
Add Oudelaa dashboard API integration
2026-05-25 20:36:52 +03:00

390 أسطر
16 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { LogOut, RefreshCcw, RotateCcw, Save } from "lucide-react";
import { NoPermissionState } from "@/components/auth/no-permission-state";
import { useSuperAdminSession } from "@/components/auth/session-context";
import { PageHeader } from "@/components/dashboard/page-header";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/components/ui/toast";
import { listSuperAdminSessions } from "@/lib/api/auth";
import {
getSuperAdminSettings,
getSuperAdminSettingsHistory,
restoreSuperAdminSettingsHistory,
updateSuperAdminSettings,
} from "@/lib/api/superadmin";
import { logoutSuperAdmin, refreshSuperAdmin } from "@/lib/auth/client";
import { formatDateTime } from "@/lib/format";
import { SUPERADMIN_PERMISSIONS, hasPermission } from "@/lib/permissions";
import type {
SessionItem,
SessionsResponse,
SuperAdminSettingsHistoryEntry,
SuperAdminSettingsResponse,
} from "@/types/api";
const EMPTY_SESSIONS: SessionsResponse = { items: [] };
export default function SettingsPage() {
const { permissions } = useSuperAdminSession();
const [historyItems, setHistoryItems] = useState<SuperAdminSettingsHistoryEntry[]>([]);
const [sessions, setSessions] = useState<SessionItem[]>([]);
const [settingsResponse, setSettingsResponse] = useState<SuperAdminSettingsResponse | null>(null);
const [saving, setSaving] = useState(false);
const [loading, setLoading] = useState(false);
const { toast } = useToast();
const router = useRouter();
const canReadSettings = hasPermission(permissions, SUPERADMIN_PERMISSIONS.SETTINGS_READ);
const canWriteSettings = hasPermission(permissions, SUPERADMIN_PERMISSIONS.SETTINGS_WRITE);
const canManageSessions = hasPermission(
permissions,
SUPERADMIN_PERMISSIONS.SESSIONS_MANAGE,
);
useEffect(() => {
let active = true;
const loadData = async () => {
if (!canReadSettings) {
setLoading(false);
return;
}
try {
const [sessionsResponse, nextSettings, nextHistory] = await Promise.all([
canManageSessions ? listSuperAdminSessions() : Promise.resolve(EMPTY_SESSIONS),
getSuperAdminSettings(),
getSuperAdminSettingsHistory({ page: 1, limit: 8, sortOrder: "desc" }),
]);
if (!active) return;
setSessions(sessionsResponse.items ?? []);
setSettingsResponse(nextSettings);
setHistoryItems(nextHistory.items ?? []);
} catch (error) {
if (!active) return;
toast({ title: "Failed to load settings", description: String(error), variant: "danger" });
}
};
void loadData();
return () => {
active = false;
};
}, [canManageSessions, canReadSettings, toast]);
const settings = settingsResponse?.settings;
const runtime = settingsResponse?.runtime;
const updateField = <K extends keyof NonNullable<typeof settings>>(key: K, value: NonNullable<typeof settings>[K]) => {
setSettingsResponse((prev) =>
prev
? {
...prev,
settings: {
...prev.settings,
[key]: value,
},
}
: prev,
);
};
if (!canReadSettings) {
return (
<div className="space-y-5 pb-8">
<PageHeader
title="Settings"
subtitle="Central SuperAdmin runtime settings with change history and rollback."
/>
<NoPermissionState description="This page needs the settings.read permission." />
</div>
);
}
return (
<div className="space-y-5 pb-8">
<PageHeader
title="Settings"
subtitle="Stored SuperAdmin settings, history, and live runtime visibility."
actions={
<Button
onClick={async () => {
if (!settings || !canWriteSettings) return;
setSaving(true);
try {
const next = await updateSuperAdminSettings(settings);
setSettingsResponse(next);
const history = await getSuperAdminSettingsHistory({ page: 1, limit: 8, sortOrder: "desc" });
setHistoryItems(history.items ?? []);
toast({ title: "Settings saved", description: "Central settings updated successfully.", variant: "success" });
} catch (error) {
toast({ title: "Save failed", description: String(error), variant: "danger" });
} finally {
setSaving(false);
}
}}
disabled={!settings || !canWriteSettings || saving}
>
<Save className="h-4 w-4" />
Save settings
</Button>
}
/>
<section className="grid gap-4 xl:grid-cols-12">
<Card className="xl:col-span-4">
<CardHeader>
<CardTitle>Session controls</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Button
variant="outline"
className="w-full"
disabled={loading}
onClick={async () => {
setLoading(true);
try {
await refreshSuperAdmin();
toast({ title: "Session refreshed", description: "Cookies were refreshed successfully.", variant: "success" });
} catch (error) {
toast({ title: "Refresh failed", description: String(error), variant: "danger" });
} finally {
setLoading(false);
}
}}
>
<RefreshCcw className="h-4 w-4" />
Refresh session
</Button>
<Button
variant="danger"
className="w-full"
disabled={loading}
onClick={async () => {
setLoading(true);
try {
await logoutSuperAdmin();
router.replace("/login");
} catch (error) {
toast({ title: "Logout failed", description: String(error), variant: "danger" });
} finally {
setLoading(false);
}
}}
>
<LogOut className="h-4 w-4" />
Logout
</Button>
{canManageSessions ? (
<div className="rounded-xl border border-border/70 bg-secondary/20 p-4">
<div className="text-xs text-muted-foreground">Visible sessions</div>
<div className="mt-2 text-2xl font-bold text-foreground">{sessions.length}</div>
</div>
) : null}
<div className="rounded-xl border border-border/70 bg-secondary/20 p-4">
<div className="text-xs text-muted-foreground">Session strategy</div>
<div className="mt-2 text-sm text-foreground">{runtime?.sessionStrategy ?? "httpOnly_cookies"}</div>
</div>
</CardContent>
</Card>
<Card className="xl:col-span-8">
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Stored settings</CardTitle>
<Badge variant={canWriteSettings ? "warning" : "muted"}>
{canWriteSettings ? "Editable" : "Read only"}
</Badge>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-3 sm:grid-cols-2">
<Input
disabled={!canWriteSettings}
placeholder="Site name"
value={settings?.siteName ?? ""}
onChange={(event) => updateField("siteName", event.target.value)}
/>
<Input
disabled={!canWriteSettings}
placeholder="PUBLIC_BASE_URL"
value={settings?.publicBaseUrl ?? ""}
onChange={(event) => updateField("publicBaseUrl", event.target.value)}
/>
</div>
<Input
disabled={!canWriteSettings}
placeholder="Dashboard API base URL"
value={settings?.dashboardApiBaseUrl ?? ""}
onChange={(event) => updateField("dashboardApiBaseUrl", event.target.value)}
/>
<Input
disabled={!canWriteSettings}
placeholder="CORS origins comma separated"
value={(settings?.corsOrigins ?? []).join(",")}
onChange={(event) =>
updateField(
"corsOrigins",
event.target.value
.split(",")
.map((value) => value.trim())
.filter(Boolean),
)
}
/>
<Textarea
disabled={!canWriteSettings}
placeholder="Operational notes"
value={settings?.notes ?? ""}
onChange={(event) => updateField("notes", event.target.value)}
/>
<div className="grid gap-3 sm:grid-cols-2">
<div className="flex items-center justify-between rounded-xl border border-border/70 bg-secondary/20 p-4">
<div>
<div className="text-sm font-medium text-foreground">Maintenance mode</div>
<div className="text-xs text-muted-foreground">Stored toggle, not an immediate runtime switch.</div>
</div>
<Switch
className={!canWriteSettings ? "pointer-events-none opacity-50" : undefined}
checked={Boolean(settings?.maintenanceMode)}
onCheckedChange={(value) => {
if (!canWriteSettings) return;
updateField("maintenanceMode", value);
}}
/>
</div>
<div className="flex items-center justify-between rounded-xl border border-border/70 bg-secondary/20 p-4">
<div>
<div className="text-sm font-medium text-foreground">Email enabled</div>
<div className="text-xs text-muted-foreground">Mirrors stored email capability policy.</div>
</div>
<Switch
className={!canWriteSettings ? "pointer-events-none opacity-50" : undefined}
checked={Boolean(settings?.emailEnabled)}
onCheckedChange={(value) => {
if (!canWriteSettings) return;
updateField("emailEnabled", value);
}}
/>
</div>
<div className="flex items-center justify-between rounded-xl border border-border/70 bg-secondary/20 p-4">
<div>
<div className="text-sm font-medium text-foreground">Marketplace auto approve</div>
<div className="text-xs text-muted-foreground">Stored policy for future moderation behavior.</div>
</div>
<Switch
className={!canWriteSettings ? "pointer-events-none opacity-50" : undefined}
checked={Boolean(settings?.marketplaceAutoApprove)}
onCheckedChange={(value) => {
if (!canWriteSettings) return;
updateField("marketplaceAutoApprove", value);
}}
/>
</div>
<div className="flex items-center justify-between rounded-xl border border-border/70 bg-secondary/20 p-4">
<div>
<div className="text-sm font-medium text-foreground">Auto hide flagged content</div>
<div className="text-xs text-muted-foreground">Stored moderation policy for future use.</div>
</div>
<Switch
className={!canWriteSettings ? "pointer-events-none opacity-50" : undefined}
checked={Boolean(settings?.contentAutoHideFlagged)}
onCheckedChange={(value) => {
if (!canWriteSettings) return;
updateField("contentAutoHideFlagged", value);
}}
/>
</div>
</div>
</CardContent>
</Card>
</section>
<section className="grid gap-4 xl:grid-cols-12">
<Card className="xl:col-span-5">
<CardHeader>
<CardTitle>Live runtime snapshot</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="rounded-xl border border-border/70 bg-secondary/20 p-4">
<div className="text-xs text-muted-foreground">API base</div>
<div className="mt-2 break-all text-sm text-foreground">{runtime?.publicBaseUrl ?? "-"}</div>
</div>
<div className="rounded-xl border border-border/70 bg-secondary/20 p-4">
<div className="text-xs text-muted-foreground">Global prefix</div>
<div className="mt-2 text-sm text-foreground">{runtime?.globalPrefix ?? "-"}</div>
</div>
<div className="rounded-xl border border-border/70 bg-secondary/20 p-4">
<div className="text-xs text-muted-foreground">Storage</div>
<div className="mt-2 text-sm text-foreground">
{runtime?.storageProvider ?? "-"} / {runtime?.storageBasePath ?? "-"}
</div>
</div>
<div className="rounded-xl border border-border/70 bg-secondary/20 p-4">
<div className="text-xs text-muted-foreground">Infrastructure</div>
<div className="mt-2 text-sm text-foreground">
queue={String(runtime?.queueEnabled ?? false)} / redis={String(runtime?.redisEnabled ?? false)}
</div>
</div>
</CardContent>
</Card>
<Card className="xl:col-span-7">
<CardHeader>
<CardTitle>Settings history</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{historyItems.map((item) => (
<div key={item._id} className="rounded-xl border border-border/70 bg-secondary/20 p-4">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-sm font-medium text-foreground">{item.updatedBy}</div>
<div className="mt-1 text-xs text-muted-foreground">
{formatDateTime(item.createdAt)} - {(item.changedFields ?? []).join(", ") || "no fields"}
</div>
</div>
<Button
size="sm"
variant="outline"
disabled={!canWriteSettings}
onClick={async () => {
try {
const restored = await restoreSuperAdminSettingsHistory(item._id);
setSettingsResponse(restored);
const history = await getSuperAdminSettingsHistory({ page: 1, limit: 8, sortOrder: "desc" });
setHistoryItems(history.items ?? []);
toast({ title: "Version restored", description: item._id, variant: "warning" });
} catch (error) {
toast({ title: "Restore failed", description: String(error), variant: "danger" });
}
}}
>
<RotateCcw className="h-4 w-4" />
Restore
</Button>
</div>
<div className="mt-3 flex flex-wrap gap-2">
{(item.changedFields ?? []).map((field) => (
<Badge key={field} variant="muted">
{field}
</Badge>
))}
</div>
</div>
))}
</CardContent>
</Card>
</section>
</div>
);
}