import { useEffect, useState } from "react"; import { apiFetch, createEventSourceFor } from "./api"; import { downloadFile } from "./export"; import { downloadExport } from "./exportHistory"; import { useTranslation } from "react-i18next"; type Tenant = { id: string; name: string; isActive: boolean; _count?: { users: number; mailboxAccounts: number; jobs: number }; }; type User = { id: string; email: string; role: string; isActive: boolean; tenant?: { id: string; name: string } | null; }; type Account = { id: string; email: string; provider: string; isActive: boolean; hasOauth?: boolean; oauthExpiresAt?: string | null; oauthLastCheckedAt?: string | null; oauthLastErrorCode?: string | null; tenant?: { id: string; name: string } | null; }; type Job = { id: string; status: string; createdAt: string; tenant?: { id: string; name: string } | null; mailboxAccount?: { id: string; email: string } | null; }; type Props = { token: string; onImpersonate: (token: string) => void; }; export default function AdminPanel({ token, onImpersonate }: Props) { const { t } = useTranslation(); const [tenants, setTenants] = useState([]); const [users, setUsers] = useState([]); const [accounts, setAccounts] = useState([]); const [jobs, setJobs] = useState([]); const [activeTab, setActiveTab] = useState<"tenants" | "users" | "accounts" | "jobs">("tenants"); const [resetUserId, setResetUserId] = useState(null); const [resetPassword, setResetPassword] = useState(""); const [exportTenantId, setExportTenantId] = useState(null); const [exportStatus, setExportStatus] = useState<"idle" | "loading" | "done" | "failed">("idle"); const [exportJobId, setExportJobId] = useState(null); const [exportHistory, setExportHistory] = useState<{ id: string; status: string; expiresAt?: string | null; createdAt?: string; progress?: number }[]>([]); const [exportFilter, setExportFilter] = useState<"all" | "active" | "done" | "failed" | "expired">("all"); const [exportScope, setExportScope] = useState<"all" | "users" | "accounts" | "jobs" | "rules">("all"); const [exportFormat, setExportFormat] = useState<"json" | "csv" | "zip">("json"); const [tenantSort, setTenantSort] = useState<"recent" | "oldest" | "name">("recent"); const [userSort, setUserSort] = useState<"recent" | "oldest" | "email">("recent"); const [accountSort, setAccountSort] = useState<"recent" | "oldest" | "email">("recent"); const [jobSort, setJobSort] = useState<"recent" | "oldest" | "status">("recent"); const loadAll = async () => { const tenantData = await apiFetch("/admin/tenants", {}, token); setTenants(tenantData.tenants ?? []); const usersData = await apiFetch("/admin/users", {}, token); setUsers(usersData.users ?? []); const accountsData = await apiFetch("/admin/accounts", {}, token); setAccounts(accountsData.accounts ?? []); const jobsData = await apiFetch("/admin/jobs", {}, token); setJobs(jobsData.jobs ?? []); const exportsData = await apiFetch("/admin/exports", {}, token); setExportHistory(exportsData.exports ?? []); }; useEffect(() => { loadAll().catch(() => undefined); }, []); const toggleTenant = async (tenant: Tenant) => { const result = await apiFetch( `/admin/tenants/${tenant.id}`, { method: "PUT", body: JSON.stringify({ isActive: !tenant.isActive }) }, token ); setTenants((prev) => prev.map((item) => (item.id === tenant.id ? result.tenant : item))); }; const exportTenant = async (tenant: Tenant) => { setExportTenantId(tenant.id); setExportStatus("loading"); if (exportFormat === "json") { const result = await apiFetch(`/admin/tenants/${tenant.id}/export?scope=${exportScope}`, {}, token); const blob = new Blob([JSON.stringify(result, null, 2)], { type: "application/json" }); downloadFile(blob, `tenant-${tenant.id}.json`); } else if (exportFormat === "csv") { await exportTenantCsv(tenant); return; } else { const result = await apiFetch(`/admin/tenants/${tenant.id}/export?format=zip&scope=${exportScope}`, {}, token); setExportJobId(result.jobId); setExportHistory((prev) => [{ id: result.jobId, status: "QUEUED" }, ...prev]); const source = createEventSourceFor(`exports/${result.jobId}`, token); source.onmessage = async (event) => { const data = JSON.parse(event.data); setExportHistory((prev) => prev.map((item) => (item.id === data.id ? { ...item, status: data.status, expiresAt: data.expiresAt, progress: data.progress } : item)) ); if (data.status === "DONE") { const response = await downloadExport(token, result.jobId); const blob = await response.blob(); downloadFile(blob, `tenant-${tenant.id}.zip`); setExportStatus("done"); source.close(); setTimeout(() => setExportStatus("idle"), 1500); } else if (data.status === "FAILED") { setExportStatus("failed"); source.close(); } }; return; } setExportStatus("done"); setTimeout(() => setExportStatus("idle"), 1500); }; const exportTenantCsv = async (tenant: Tenant) => { setExportTenantId(tenant.id); setExportStatus("loading"); const base = import.meta.env.VITE_API_URL ?? "http://localhost:8000"; const response = await fetch(`${base}/admin/tenants/${tenant.id}/export?format=csv&scope=${exportScope}`, { headers: { Authorization: `Bearer ${token}` } }); const text = await response.text(); const blob = new Blob([text], { type: "text/csv" }); downloadFile(blob, `tenant-${tenant.id}.csv`); setExportStatus("done"); setTimeout(() => setExportStatus("idle"), 1500); }; const deleteTenant = async (tenant: Tenant) => { if (!confirm(t("adminDeleteConfirm", { name: tenant.name }))) return; await apiFetch(`/admin/tenants/${tenant.id}`, { method: "DELETE" }, token); setTenants((prev) => prev.filter((item) => item.id !== tenant.id)); }; const toggleUser = async (user: User) => { const result = await apiFetch( `/admin/users/${user.id}`, { method: "PUT", body: JSON.stringify({ isActive: !user.isActive }) }, token ); setUsers((prev) => prev.map((item) => (item.id === user.id ? result.user : item))); }; const toggleAccount = async (account: Account) => { const result = await apiFetch( `/admin/accounts/${account.id}`, { method: "PUT", body: JSON.stringify({ isActive: !account.isActive }) }, token ); setAccounts((prev) => prev.map((item) => (item.id === account.id ? result.account : item))); }; const setRole = async (user: User, role: "USER" | "ADMIN") => { const result = await apiFetch( `/admin/users/${user.id}/role`, { method: "PUT", body: JSON.stringify({ role }) }, token ); setUsers((prev) => prev.map((item) => (item.id === user.id ? result.user : item))); }; const impersonate = async (user: User) => { const result = await apiFetch(`/admin/impersonate/${user.id}`, { method: "POST" }, token); onImpersonate(result.token); }; const resetPasswordForUser = async () => { if (!resetUserId || resetPassword.length < 10) return; await apiFetch(`/admin/users/${resetUserId}/reset`, { method: "POST", body: JSON.stringify({ password: resetPassword }) }, token); setResetUserId(null); setResetPassword(""); }; const cancelJob = async (job: Job) => { await apiFetch(`/admin/jobs/${job.id}/cancel`, { method: "POST" }, token); setJobs((prev) => prev.map((item) => (item.id === job.id ? { ...item, status: "CANCELED" } : item))); }; const retryJob = async (job: Job) => { await apiFetch(`/admin/jobs/${job.id}/retry`, { method: "POST" }, token); loadAll().catch(() => undefined); }; const sortBy = (items: T[], mode: string, getKey: (item: T) => string) => { const sorted = [...items]; if (mode === "name" || mode === "email" || mode === "status") { sorted.sort((a, b) => getKey(a).localeCompare(getKey(b))); } else if (mode === "oldest") { sorted.reverse(); } return sorted; }; const tenantsSorted = sortBy(tenants, tenantSort, (t) => t.name); const usersSorted = sortBy(users, userSort, (u) => u.email); const accountsSorted = sortBy(accounts, accountSort, (a) => a.email); const jobsSorted = sortBy(jobs, jobSort, (j) => j.status); const exportsFiltered = exportHistory.filter((item) => { const expired = item.expiresAt ? new Date(item.expiresAt) < new Date() : false; if (exportFilter === "expired") return expired; if (exportFilter === "active") return item.status === "QUEUED" || item.status === "RUNNING"; if (exportFilter === "done") return item.status === "DONE"; if (exportFilter === "failed") return item.status === "FAILED"; return true; }); const mapJobStatus = (status: string) => { switch (status) { case "RUNNING": return t("statusRunning"); case "QUEUED": return t("statusQueued"); case "SUCCEEDED": return t("statusSucceeded"); case "FAILED": return t("statusFailed"); case "CANCELED": return t("statusCanceled"); default: return status; } }; return (
{(["tenants", "users", "accounts", "jobs"] as const).map((tab) => ( ))}
{activeTab === "tenants" && (

{t("adminTenants")}

{t("adminTenantExport")}

{t("adminExportHint")}

{tenantsSorted.map((tenant) => (
{tenant.name} {tenant.isActive ? t("adminActive") : t("adminInactive")}

{t("countUsers", { count: tenant._count?.users ?? 0 })} ·{" "} {t("countAccounts", { count: tenant._count?.mailboxAccounts ?? 0 })} ·{" "} {t("countJobs", { count: tenant._count?.jobs ?? 0 })}

))} {exportTenantId && (

{exportStatus === "loading" ? t("adminExporting") : exportStatus === "done" ? t("adminExportDone") : exportStatus === "failed" ? t("adminExportFailed") : ""}

)} {exportHistory.length > 0 && (

{t("exportHistory")}

{exportsFiltered.map((item) => (
{item.id.slice(0, 6)}

{item.expiresAt && new Date(item.expiresAt) < new Date() ? t("exportStatusExpired") : item.status === "QUEUED" ? t("exportStatusQueued") : item.status === "RUNNING" ? t("exportStatusRunning") : item.status === "DONE" ? t("exportStatusDone") : t("exportStatusFailed")}

{item.progress !== undefined && (

{t("exportProgress", { progress: item.progress })}

)}
{item.createdAt ? new Date(item.createdAt).toLocaleString() : "-"} ·{" "} {t("exportExpires")}: {item.expiresAt ? new Date(item.expiresAt).toLocaleString() : "-"}
))}
)}
)} {activeTab === "users" && (

{t("adminUsers")}

{usersSorted.map((user) => (
{user.email} {user.isActive ? t("adminActive") : t("adminInactive")}

{user.role} · {user.tenant?.name ?? "-"}

))} {resetUserId && (

{t("adminResetTitle")}

setResetPassword(event.target.value)} />
)}
)} {activeTab === "accounts" && (

{t("adminAccounts")}

{accountsSorted.map((account) => (
{account.email} {account.isActive ? t("adminActive") : t("adminInactive")}

{account.provider} · {account.tenant?.name ?? "-"}

{account.hasOauth && (

{t("statusLabel")}: {account.oauthLastErrorCode ? t("badgeUnhealthy") : t("badgeHealthy")} ·{" "} {t("adminExpiresAt")}: {account.oauthExpiresAt ? new Date(account.oauthExpiresAt).toLocaleString() : t("oauthStatusUnknown")} ·{" "} {t("adminLastConnected")}: {account.oauthLastCheckedAt ? new Date(account.oauthLastCheckedAt).toLocaleString() : t("oauthStatusUnknown")}

)} {account.oauthLastErrorCode && (

{account.oauthLastErrorCode === "invalid_grant" ? t("oauthErrorInvalidGrant") : account.oauthLastErrorCode === "token_expired" ? t("oauthErrorExpired") : t("oauthErrorUnknown")}

)}
))}
)} {activeTab === "jobs" && (

{t("adminJobs")}

{jobsSorted.map((job) => (
{mapJobStatus(job.status)}

{job.tenant?.name ?? "-"} · {job.mailboxAccount?.email ?? "-"}

{new Date(job.createdAt).toLocaleString()}
))}
)}
); }