Projektstart

This commit is contained in:
2026-01-22 15:49:12 +01:00
parent 7212eb6f7a
commit 57e5f652f8
10637 changed files with 2598792 additions and 64 deletions

187
frontend/src/admin.tsx Normal file
View 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>
);
}