Projektstart
This commit is contained in:
187
frontend/src/admin.tsx
Normal file
187
frontend/src/admin.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiFetch } from "./api";
|
||||
|
||||
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;
|
||||
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;
|
||||
};
|
||||
|
||||
export default function AdminPanel({ token }: Props) {
|
||||
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [accounts, setAccounts] = useState<Account[]>([]);
|
||||
const [jobs, setJobs] = useState<Job[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<"tenants" | "users" | "accounts" | "jobs">("tenants");
|
||||
|
||||
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 ?? []);
|
||||
};
|
||||
|
||||
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 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)));
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="admin-panel">
|
||||
<div className="admin-tabs">
|
||||
{(["tenants", "users", "accounts", "jobs"] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
className={activeTab === tab ? "active" : ""}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeTab === "tenants" && (
|
||||
<div className="card">
|
||||
<h3>Tenants</h3>
|
||||
{tenants.map((tenant) => (
|
||||
<div key={tenant.id} className="list-item">
|
||||
<div>
|
||||
<strong>{tenant.name}</strong>
|
||||
<p>{tenant._count?.users ?? 0} users · {tenant._count?.mailboxAccounts ?? 0} accounts · {tenant._count?.jobs ?? 0} jobs</p>
|
||||
</div>
|
||||
<button className="ghost" onClick={() => toggleTenant(tenant)}>
|
||||
{tenant.isActive ? "Disable" : "Enable"}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "users" && (
|
||||
<div className="card">
|
||||
<h3>Users</h3>
|
||||
{users.map((user) => (
|
||||
<div key={user.id} className="list-item">
|
||||
<div>
|
||||
<strong>{user.email}</strong>
|
||||
<p>{user.role} · {user.tenant?.name ?? "-"}</p>
|
||||
</div>
|
||||
<div className="inline-actions">
|
||||
<button className="ghost" onClick={() => setRole(user, user.role === "ADMIN" ? "USER" : "ADMIN")}
|
||||
>
|
||||
{user.role === "ADMIN" ? "Make USER" : "Make ADMIN"}
|
||||
</button>
|
||||
<button className="ghost" onClick={() => toggleUser(user)}>
|
||||
{user.isActive ? "Disable" : "Enable"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "accounts" && (
|
||||
<div className="card">
|
||||
<h3>Accounts</h3>
|
||||
{accounts.map((account) => (
|
||||
<div key={account.id} className="list-item">
|
||||
<div>
|
||||
<strong>{account.email}</strong>
|
||||
<p>{account.provider} · {account.tenant?.name ?? "-"}</p>
|
||||
</div>
|
||||
<button className="ghost" onClick={() => toggleAccount(account)}>
|
||||
{account.isActive ? "Disable" : "Enable"}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "jobs" && (
|
||||
<div className="card">
|
||||
<h3>Jobs</h3>
|
||||
{jobs.map((job) => (
|
||||
<div key={job.id} className="list-item">
|
||||
<div>
|
||||
<strong>{job.status}</strong>
|
||||
<p>{job.tenant?.name ?? "-"} · {job.mailboxAccount?.email ?? "-"}</p>
|
||||
</div>
|
||||
<span>{new Date(job.createdAt).toLocaleString()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user