LIC-04: dev-server fuer adminapi + next.js lizenz-modul-verwaltungsoberflaeche
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { fetchOverview, toggleFlag, type Overview } from "@/lib/api";
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
ok: "In Ordnung",
|
||||
warning: "Nahe am Limit",
|
||||
exceeded: "Limit überschritten",
|
||||
};
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
ok: "#2e7d32",
|
||||
warning: "#ed6c02",
|
||||
exceeded: "#c62828",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const [tenantId, setTenantId] = useState("");
|
||||
const [overview, setOverview] = useState<Overview | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [toggleError, setToggleError] = useState<string | null>(null);
|
||||
|
||||
async function load() {
|
||||
if (!tenantId.trim()) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await fetchOverview(tenantId.trim());
|
||||
setOverview(data);
|
||||
} catch (e: any) {
|
||||
setError(e.message ?? "Unbekannter Fehler");
|
||||
setOverview(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onToggle(key: string, next: boolean) {
|
||||
setToggleError(null);
|
||||
try {
|
||||
await toggleFlag(tenantId.trim(), key, next);
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
setToggleError(e.message ?? "Umschalten fehlgeschlagen");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main style={{ maxWidth: 800, margin: "0 auto", padding: "2rem 1rem" }}>
|
||||
<h1>Lizenz- und Modulverwaltung</h1>
|
||||
|
||||
<div style={{ display: "flex", gap: "0.5rem", marginBottom: "1.5rem" }}>
|
||||
<input
|
||||
value={tenantId}
|
||||
onChange={(e) => setTenantId(e.target.value)}
|
||||
placeholder="Tenant-ID eingeben"
|
||||
style={{ flex: 1, padding: "0.5rem", fontSize: "1rem" }}
|
||||
/>
|
||||
<button onClick={load} disabled={loading} style={{ padding: "0.5rem 1rem" }}>
|
||||
{loading ? "Lädt…" : "Anzeigen"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p style={{ color: "#c62828" }} role="alert">
|
||||
Fehler: {error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{overview && (
|
||||
<>
|
||||
<section style={{ background: "white", padding: "1rem", borderRadius: 8, marginBottom: "1.5rem" }}>
|
||||
<h2>Lizenzstatus</h2>
|
||||
<p>
|
||||
<strong>Plan:</strong> {overview.plan}{" "}
|
||||
{overview.expired && (
|
||||
<span style={{ color: "#c62828", fontWeight: "bold" }}>
|
||||
(abgelaufen)
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Gültig bis:</strong>{" "}
|
||||
{new Date(overview.valid_until).toLocaleDateString("de-DE")}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Freigeschaltete Module:</strong>{" "}
|
||||
{overview.modules.length > 0 ? overview.modules.join(", ") : "keine"}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ background: "white", padding: "1rem", borderRadius: 8, marginBottom: "1.5rem" }}>
|
||||
<h2>Feature-Flags</h2>
|
||||
{toggleError && (
|
||||
<p style={{ color: "#c62828" }} role="alert">
|
||||
{toggleError}
|
||||
</p>
|
||||
)}
|
||||
{overview.flags.length === 0 && <p>Keine Feature-Flags definiert.</p>}
|
||||
<ul style={{ listStyle: "none", padding: 0 }}>
|
||||
{overview.flags.map((f) => (
|
||||
<li
|
||||
key={f.key}
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "0.5rem 0",
|
||||
borderBottom: "1px solid #eee",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div>{f.key}</div>
|
||||
{!f.licensed && f.deny_grund && (
|
||||
<div style={{ fontSize: "0.85rem", color: "#c62828" }}>
|
||||
{f.deny_grund}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={f.enabled}
|
||||
disabled={!f.licensed && !f.enabled}
|
||||
onChange={(e) => onToggle(f.key, e.target.checked)}
|
||||
/>{" "}
|
||||
aktiv
|
||||
</label>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section style={{ background: "white", padding: "1rem", borderRadius: 8 }}>
|
||||
<h2>Nutzungsstand</h2>
|
||||
{overview.usage.map((u) => {
|
||||
const pct = u.limit > 0 ? Math.min(100, (u.value / u.limit) * 100) : 0;
|
||||
return (
|
||||
<div key={u.metric} style={{ marginBottom: "1rem" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<span>{u.metric}</span>
|
||||
<span style={{ color: STATUS_COLOR[u.status] }}>
|
||||
{u.value} / {u.limit > 0 ? u.limit : "unbegrenzt"} —{" "}
|
||||
{STATUS_LABEL[u.status]}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
background: "#eee",
|
||||
borderRadius: 4,
|
||||
height: 8,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${u.limit > 0 ? pct : 0}%`,
|
||||
background: STATUS_COLOR[u.status],
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user