All the initial admin requests approval flow

This commit is contained in:
2026-04-11 11:54:21 +02:00
parent 94bfd25553
commit 9a96d74f5c
7 changed files with 604 additions and 88 deletions

View File

@@ -2,7 +2,7 @@ import { getSessionUser } from "@/lib/session";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { listTenants } from "@/lib/k8s"; import { listTenants } from "@/lib/k8s";
import { StatusBadge } from "@/components/ui/status-badge"; import { AdminPanel } from "@/components/admin/admin-panel";
export default async function AdminPage() { export default async function AdminPage() {
const user = await getSessionUser(); const user = await getSessionUser();
@@ -30,76 +30,7 @@ export default async function AdminPage() {
</div> </div>
<div className="animate-in animate-in-delay-1"> <div className="animate-in animate-in-delay-1">
<div className="flex items-baseline gap-3 mb-4"> <AdminPanel initialTenants={tenants} />
<h2 className="text-xs font-semibold uppercase tracking-wider text-text-muted">
{t("allTenants")}
</h2>
<span className="font-mono text-xs text-text-muted tabular-nums">
{tenants.length}
</span>
</div>
{tenants.length === 0 ? (
<div className="bg-surface-1 border border-border rounded-xl p-12 text-center">
<p className="text-text-secondary text-sm">{t("noTenants")}</p>
</div>
) : (
<div className="bg-surface-1 border border-border rounded-xl overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border text-left">
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
{t("name")}
</th>
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
{t("displayName")}
</th>
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
{t("phase")}
</th>
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
{t("packages")}
</th>
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
{t("created")}
</th>
</tr>
</thead>
<tbody>
{tenants.map((tenant) => (
<tr
key={tenant.metadata.name}
className="border-b border-border last:border-0 hover:bg-surface-2/50 transition-colors"
>
<td className="px-5 py-3 font-mono text-xs text-accent">
{tenant.metadata.name}
</td>
<td className="px-5 py-3 text-text-primary">
{tenant.spec.displayName}
</td>
<td className="px-5 py-3">
<StatusBadge
phase={tenant.status?.phase ?? "Pending"}
/>
</td>
<td className="px-5 py-3 text-xs text-text-secondary font-mono">
{tenant.spec.packages?.join(", ") || "—"}
</td>
<td className="px-5 py-3 text-xs text-text-muted tabular-nums">
{tenant.metadata.creationTimestamp
? new Date(
tenant.metadata.creationTimestamp
).toLocaleDateString()
: "—"}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div> </div>
</div> </div>
); );

View File

@@ -6,6 +6,7 @@ import { createTenant } from "@/lib/k8s";
/** /**
* POST /api/admin/requests/[id]/approve * POST /api/admin/requests/[id]/approve
* Approve a tenant request: create the PiecedTenant CR and update status. * Approve a tenant request: create the PiecedTenant CR and update status.
* Also supports re-approving a previously rejected request.
*/ */
export async function POST( export async function POST(
request: Request, request: Request,
@@ -29,7 +30,7 @@ export async function POST(
); );
} }
if (tenantRequest.status !== "pending") { if (tenantRequest.status !== "pending" && tenantRequest.status !== "rejected") {
return NextResponse.json( return NextResponse.json(
{ error: `Request is already ${tenantRequest.status}` }, { error: `Request is already ${tenantRequest.status}` },
{ status: 400 } { status: 400 }

View File

@@ -0,0 +1,508 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { useTranslations } from "next-intl";
import type { PiecedTenant, TenantRequest } from "@/types";
import { StatusBadge } from "@/components/ui/status-badge";
import Link from "next/link";
type Tab = "requests" | "tenants";
type RequestFilter = "all" | "pending" | "provisioning" | "approved" | "rejected";
interface AdminPanelProps {
initialTenants: PiecedTenant[];
}
export function AdminPanel({ initialTenants }: AdminPanelProps) {
const t = useTranslations("admin");
const [tab, setTab] = useState<Tab>("requests");
const [requests, setRequests] = useState<TenantRequest[]>([]);
const [filter, setFilter] = useState<RequestFilter>("all");
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState<string | null>(null);
const [rejectModal, setRejectModal] = useState<string | null>(null);
const [rejectNotes, setRejectNotes] = useState("");
const [error, setError] = useState("");
const fetchRequests = useCallback(async () => {
try {
const url =
filter === "all"
? "/api/admin/requests"
: `/api/admin/requests?status=${filter}`;
const res = await fetch(url);
if (!res.ok) throw new Error("Failed to fetch");
const data = await res.json();
setRequests(data);
} catch (e: any) {
setError(e.message);
} finally {
setLoading(false);
}
}, [filter]);
useEffect(() => {
if (tab === "requests") {
setLoading(true);
fetchRequests();
}
}, [tab, filter, fetchRequests]);
const handleApprove = async (id: string) => {
setActionLoading(id);
setError("");
try {
const res = await fetch(`/api/admin/requests/${id}/approve`, {
method: "POST",
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Approve failed");
}
await fetchRequests();
} catch (e: any) {
setError(e.message);
} finally {
setActionLoading(null);
}
};
const handleReject = async (id: string) => {
setActionLoading(id);
setError("");
try {
const res = await fetch(`/api/admin/requests/${id}/reject`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ adminNotes: rejectNotes || undefined }),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Reject failed");
}
setRejectModal(null);
setRejectNotes("");
await fetchRequests();
} catch (e: any) {
setError(e.message);
} finally {
setActionLoading(null);
}
};
const FILTERS: RequestFilter[] = [
"all",
"pending",
"provisioning",
"approved",
"rejected",
];
const pendingCount = requests.filter((r) => r.status === "pending").length;
return (
<>
{/* Tab bar */}
<div className="flex gap-1 border-b border-border mb-6">
<button
onClick={() => setTab("requests")}
className={`px-4 py-2.5 text-sm font-medium transition-colors relative ${
tab === "requests"
? "text-accent"
: "text-text-muted hover:text-text-secondary"
}`}
>
{t("requests")}
{pendingCount > 0 && tab !== "requests" && (
<span className="ml-1.5 inline-flex items-center justify-center h-4 min-w-[16px] px-1 text-[10px] font-bold bg-accent text-white rounded-full">
{pendingCount}
</span>
)}
{tab === "requests" && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent" />
)}
</button>
<button
onClick={() => setTab("tenants")}
className={`px-4 py-2.5 text-sm font-medium transition-colors relative ${
tab === "tenants"
? "text-accent"
: "text-text-muted hover:text-text-secondary"
}`}
>
{t("allTenants")}
<span className="ml-1.5 text-xs text-text-muted tabular-nums">
{initialTenants.length}
</span>
{tab === "tenants" && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent" />
)}
</button>
</div>
{/* Error banner */}
{error && (
<div className="mb-4 text-xs text-red-400 bg-red-400/10 border border-red-400/20 rounded-lg px-3 py-2">
{error}
<button
onClick={() => setError("")}
className="ml-2 text-red-300 hover:text-red-200"
>
</button>
</div>
)}
{/* ───── REQUESTS TAB ───── */}
{tab === "requests" && (
<>
<div className="flex gap-1.5 mb-4 flex-wrap">
{FILTERS.map((f) => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-3 py-1 text-xs rounded-full transition-colors ${
filter === f
? "bg-accent text-white"
: "bg-surface-2 text-text-muted hover:text-text-secondary border border-border"
}`}
>
{t(`filter_${f}`)}
</button>
))}
</div>
{loading ? (
<div className="bg-surface-1 border border-border rounded-xl p-12 text-center">
<div className="h-5 w-5 border-2 border-accent border-t-transparent rounded-full animate-spin mx-auto mb-2" />
<p className="text-text-muted text-xs">{t("loadingRequests")}</p>
</div>
) : requests.length === 0 ? (
<div className="bg-surface-1 border border-border rounded-xl p-12 text-center">
<p className="text-text-secondary text-sm">{t("noRequests")}</p>
</div>
) : (
<div className="bg-surface-1 border border-border rounded-xl overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border text-left">
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
{t("company")}
</th>
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
{t("contact")}
</th>
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted hidden md:table-cell">
{t("agentName")}
</th>
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted hidden lg:table-cell">
{t("packages")}
</th>
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
{t("status")}
</th>
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted hidden md:table-cell">
{t("submitted")}
</th>
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
{t("actions")}
</th>
</tr>
</thead>
<tbody>
{requests.map((req) => (
<tr
key={req.id}
className="border-b border-border last:border-0 hover:bg-surface-2/50 transition-colors"
>
<td className="px-4 py-3">
<div className="font-medium text-text-primary text-sm">
{req.companyName}
</div>
</td>
<td className="px-4 py-3">
<div className="text-text-primary text-sm">
{req.contactName}
</div>
<div className="text-text-muted text-xs">
{req.contactEmail}
</div>
</td>
<td className="px-4 py-3 font-mono text-xs text-text-secondary hidden md:table-cell">
{req.agentName}
</td>
<td className="px-4 py-3 text-xs text-text-secondary font-mono hidden lg:table-cell">
{req.packages?.length
? req.packages.join(", ")
: "—"}
</td>
<td className="px-4 py-3">
<RequestStatusBadge status={req.status} />
</td>
<td className="px-4 py-3 text-xs text-text-muted tabular-nums hidden md:table-cell">
{new Date(req.createdAt).toLocaleDateString()}
</td>
<td className="px-4 py-3">
<div className="flex gap-1.5">
{req.status === "pending" && (
<>
<button
onClick={() => handleApprove(req.id)}
disabled={actionLoading === req.id}
className="px-2.5 py-1 text-xs font-medium bg-emerald-500/15 text-emerald-400 rounded-md hover:bg-emerald-500/25 transition-colors disabled:opacity-50"
>
{actionLoading === req.id
? "…"
: t("approve")}
</button>
<button
onClick={() => setRejectModal(req.id)}
disabled={actionLoading === req.id}
className="px-2.5 py-1 text-xs font-medium bg-red-500/15 text-red-400 rounded-md hover:bg-red-500/25 transition-colors disabled:opacity-50"
>
{t("reject")}
</button>
</>
)}
{(req.status === "provisioning" ||
req.status === "approved") &&
req.tenantName && (
<Link
href={`/tenants/${req.tenantName}`}
className="px-2.5 py-1 text-xs font-medium bg-accent/15 text-accent rounded-md hover:bg-accent/25 transition-colors"
>
{t("viewTenant")}
</Link>
)}
{req.status === "rejected" && (
<button
onClick={() => handleApprove(req.id)}
disabled={actionLoading === req.id}
className="px-2.5 py-1 text-xs font-medium bg-amber-500/15 text-amber-400 rounded-md hover:bg-amber-500/25 transition-colors disabled:opacity-50"
>
{t("reApprove")}
</button>
)}
</div>
{req.adminNotes && (
<p className="text-[11px] text-text-muted mt-1 max-w-[200px] truncate">
{req.adminNotes}
</p>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</>
)}
{/* ───── TENANTS TAB ───── */}
{tab === "tenants" && (
<>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
<SummaryCard
label={t("totalTenants")}
value={initialTenants.length}
/>
<SummaryCard
label={t("running")}
value={
initialTenants.filter((t) => t.status?.phase === "Running")
.length
}
color="text-emerald-400"
/>
<SummaryCard
label={t("provisioning")}
value={
initialTenants.filter(
(t) =>
t.status?.phase === "Provisioning" ||
t.status?.phase === "Pending"
).length
}
color="text-amber-400"
/>
<SummaryCard
label={t("errors")}
value={
initialTenants.filter((t) => t.status?.phase === "Error").length
}
color="text-red-400"
/>
</div>
{initialTenants.length === 0 ? (
<div className="bg-surface-1 border border-border rounded-xl p-12 text-center">
<p className="text-text-secondary text-sm">{t("noTenants")}</p>
</div>
) : (
<div className="bg-surface-1 border border-border rounded-xl overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border text-left">
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
{t("name")}
</th>
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
{t("displayName")}
</th>
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
{t("phase")}
</th>
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted hidden md:table-cell">
{t("packages")}
</th>
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted hidden md:table-cell">
{t("created")}
</th>
<th className="px-5 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
{t("actions")}
</th>
</tr>
</thead>
<tbody>
{initialTenants.map((tenant) => (
<tr
key={tenant.metadata.name}
className="border-b border-border last:border-0 hover:bg-surface-2/50 transition-colors"
>
<td className="px-5 py-3 font-mono text-xs text-accent">
{tenant.metadata.name}
</td>
<td className="px-5 py-3 text-text-primary">
{tenant.spec.displayName}
</td>
<td className="px-5 py-3">
<StatusBadge
phase={tenant.status?.phase ?? "Pending"}
/>
</td>
<td className="px-5 py-3 text-xs text-text-secondary font-mono hidden md:table-cell">
{tenant.spec.packages?.join(", ") || "—"}
</td>
<td className="px-5 py-3 text-xs text-text-muted tabular-nums hidden md:table-cell">
{tenant.metadata.creationTimestamp
? new Date(
tenant.metadata.creationTimestamp
).toLocaleDateString()
: "—"}
</td>
<td className="px-5 py-3">
<Link
href={`/tenants/${tenant.metadata.name}`}
className="px-2.5 py-1 text-xs font-medium bg-accent/15 text-accent rounded-md hover:bg-accent/25 transition-colors"
>
{t("manage")}
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</>
)}
{/* ───── REJECT MODAL ───── */}
{rejectModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="bg-surface-1 border border-border rounded-xl p-6 max-w-md w-full mx-4 shadow-2xl">
<h3 className="font-display text-lg font-semibold text-text-primary mb-4">
{t("rejectTitle")}
</h3>
<label className="block text-xs font-semibold uppercase tracking-wider text-text-muted mb-1.5">
{t("adminNotesLabel")}
</label>
<textarea
value={rejectNotes}
onChange={(e) => setRejectNotes(e.target.value)}
placeholder={t("adminNotesPlaceholder")}
rows={3}
className="w-full px-3 py-2 bg-surface-2 border border-border rounded-lg text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors resize-none mb-4"
/>
<div className="flex gap-2 justify-end">
<button
onClick={() => {
setRejectModal(null);
setRejectNotes("");
}}
className="px-4 py-2 text-sm text-text-secondary hover:text-text-primary transition-colors"
>
{t("cancelAction")}
</button>
<button
onClick={() => handleReject(rejectModal)}
disabled={actionLoading === rejectModal}
className="px-4 py-2 text-sm font-medium bg-red-500/15 text-red-400 rounded-lg hover:bg-red-500/25 transition-colors disabled:opacity-50"
>
{actionLoading === rejectModal ? "…" : t("confirmReject")}
</button>
</div>
</div>
</div>
)}
</>
);
}
function RequestStatusBadge({ status }: { status: string }) {
const colors: Record<string, string> = {
pending: "bg-blue-400/15 text-blue-400",
approved: "bg-emerald-400/15 text-emerald-400",
provisioning: "bg-amber-400/15 text-amber-400",
active: "bg-emerald-400/15 text-emerald-400",
rejected: "bg-red-400/15 text-red-400",
};
return (
<span
className={`inline-flex items-center gap-1.5 px-2 py-0.5 text-xs font-medium rounded-full ${colors[status] || "bg-surface-2 text-text-muted"}`}
>
<span
className={`h-1.5 w-1.5 rounded-full ${
status === "pending"
? "bg-blue-400"
: status === "approved" || status === "active"
? "bg-emerald-400"
: status === "provisioning"
? "bg-amber-400 animate-pulse"
: status === "rejected"
? "bg-red-400"
: "bg-text-muted"
}`}
/>
{status}
</span>
);
}
function SummaryCard({
label,
value,
color,
}: {
label: string;
value: number;
color?: string;
}) {
return (
<div className="bg-surface-1 border border-border rounded-xl p-4">
<p className="text-xs text-text-muted uppercase tracking-wider mb-1">
{label}
</p>
<p
className={`font-mono text-2xl font-semibold tabular-nums ${color || "text-text-primary"}`}
>
{value}
</p>
</div>
);
}

View File

@@ -90,7 +90,6 @@
}, },
"admin": { "admin": {
"title": "Plattform-Admin", "title": "Plattform-Admin",
"subtitle": "Alle Tenants der Plattform",
"allTenants": "Tenants", "allTenants": "Tenants",
"noTenants": "Noch keine Tenants bereitgestellt.", "noTenants": "Noch keine Tenants bereitgestellt.",
"noAccess": "Unzureichende Berechtigungen für diese Ansicht.", "noAccess": "Unzureichende Berechtigungen für diese Ansicht.",
@@ -100,7 +99,6 @@
"packages": "Pakete", "packages": "Pakete",
"created": "Erstellt", "created": "Erstellt",
"manage": "Verwalten", "manage": "Verwalten",
"requests": "Onboarding-Anträge",
"pendingRequests": "Offene Anträge", "pendingRequests": "Offene Anträge",
"approve": "Genehmigen", "approve": "Genehmigen",
"reject": "Ablehnen", "reject": "Ablehnen",
@@ -108,9 +106,30 @@
"contact": "Kontakt", "contact": "Kontakt",
"status": "Status", "status": "Status",
"submitted": "Eingereicht", "submitted": "Eingereicht",
"noRequests": "Keine offenen Anträge.",
"approveConfirm": "Diesen Antrag genehmigen und Bereitstellung starten?", "approveConfirm": "Diesen Antrag genehmigen und Bereitstellung starten?",
"rejectConfirm": "Diesen Antrag ablehnen?" "rejectConfirm": "Diesen Antrag ablehnen?",
"subtitle": "Onboarding-Anfragen und Mandanten-Lebenszyklus verwalten",
"requests": "Anfragen",
"reApprove": "Erneut genehmigen",
"agentName": "Agent",
"actions": "Aktionen",
"noRequests": "Keine Anfragen gefunden.",
"loadingRequests": "Anfragen werden geladen…",
"rejectTitle": "Anfrage ablehnen",
"adminNotesLabel": "Notizen (optional)",
"adminNotesPlaceholder": "Grund der Ablehnung…",
"cancelAction": "Abbrechen",
"confirmReject": "Ablehnen",
"viewTenant": "Anzeigen",
"filter_all": "Alle",
"filter_pending": "Ausstehend",
"filter_provisioning": "Bereitstellung",
"filter_approved": "Genehmigt",
"filter_rejected": "Abgelehnt",
"totalTenants": "Gesamt",
"running": "Aktiv",
"provisioning": "Bereitstellung",
"errors": "Fehler"
}, },
"tenantDetail": { "tenantDetail": {
"agent": "Agent", "agent": "Agent",

View File

@@ -90,7 +90,7 @@
}, },
"admin": { "admin": {
"title": "Platform Admin", "title": "Platform Admin",
"subtitle": "All tenants across the platform", "subtitle": "Manage onboarding requests and tenant lifecycle",
"allTenants": "Tenants", "allTenants": "Tenants",
"noTenants": "No tenants provisioned yet.", "noTenants": "No tenants provisioned yet.",
"noAccess": "Insufficient permissions for this view.", "noAccess": "Insufficient permissions for this view.",
@@ -100,17 +100,36 @@
"packages": "Packages", "packages": "Packages",
"created": "Created", "created": "Created",
"manage": "Manage", "manage": "Manage",
"requests": "Onboarding Requests", "requests": "Requests",
"pendingRequests": "Pending Requests", "pendingRequests": "Pending Requests",
"approve": "Approve", "approve": "Approve",
"reject": "Reject", "reject": "Reject",
"reApprove": "Re-approve",
"company": "Company", "company": "Company",
"contact": "Contact", "contact": "Contact",
"agentName": "Agent",
"status": "Status", "status": "Status",
"submitted": "Submitted", "submitted": "Submitted",
"noRequests": "No pending requests.", "actions": "Actions",
"noRequests": "No requests found.",
"loadingRequests": "Loading requests…",
"approveConfirm": "Approve this request and start provisioning?", "approveConfirm": "Approve this request and start provisioning?",
"rejectConfirm": "Reject this request?" "rejectConfirm": "Reject this request?",
"rejectTitle": "Reject request",
"adminNotesLabel": "Notes (optional)",
"adminNotesPlaceholder": "Reason for rejection…",
"cancelAction": "Cancel",
"confirmReject": "Reject",
"viewTenant": "View",
"filter_all": "All",
"filter_pending": "Pending",
"filter_provisioning": "Provisioning",
"filter_approved": "Approved",
"filter_rejected": "Rejected",
"totalTenants": "Total",
"running": "Running",
"provisioning": "Provisioning",
"errors": "Errors"
}, },
"tenantDetail": { "tenantDetail": {
"agent": "Agent", "agent": "Agent",

View File

@@ -90,7 +90,6 @@
}, },
"admin": { "admin": {
"title": "Admin plateforme", "title": "Admin plateforme",
"subtitle": "Tous les tenants de la plateforme",
"allTenants": "Tenants", "allTenants": "Tenants",
"noTenants": "Aucun tenant provisionné.", "noTenants": "Aucun tenant provisionné.",
"noAccess": "Permissions insuffisantes pour cette vue.", "noAccess": "Permissions insuffisantes pour cette vue.",
@@ -100,7 +99,6 @@
"packages": "Paquets", "packages": "Paquets",
"created": "Créé", "created": "Créé",
"manage": "Gérer", "manage": "Gérer",
"requests": "Demandes d'intégration",
"pendingRequests": "Demandes en attente", "pendingRequests": "Demandes en attente",
"approve": "Approuver", "approve": "Approuver",
"reject": "Refuser", "reject": "Refuser",
@@ -108,9 +106,30 @@
"contact": "Contact", "contact": "Contact",
"status": "Statut", "status": "Statut",
"submitted": "Envoyé", "submitted": "Envoyé",
"noRequests": "Aucune demande en attente.",
"approveConfirm": "Approuver cette demande et lancer la mise en service ?", "approveConfirm": "Approuver cette demande et lancer la mise en service ?",
"rejectConfirm": "Refuser cette demande ?" "rejectConfirm": "Refuser cette demande ?",
"subtitle": "Gérer les demandes d'intégration et le cycle de vie des locataires",
"requests": "Demandes",
"reApprove": "Ré-approuver",
"agentName": "Agent",
"actions": "Actions",
"noRequests": "Aucune demande trouvée.",
"loadingRequests": "Chargement des demandes…",
"rejectTitle": "Rejeter la demande",
"adminNotesLabel": "Notes (optionnel)",
"adminNotesPlaceholder": "Raison du rejet…",
"cancelAction": "Annuler",
"confirmReject": "Rejeter",
"viewTenant": "Voir",
"filter_all": "Tous",
"filter_pending": "En attente",
"filter_provisioning": "Provisionnement",
"filter_approved": "Approuvé",
"filter_rejected": "Rejeté",
"totalTenants": "Total",
"running": "Actif",
"provisioning": "Provisionnement",
"errors": "Erreurs"
}, },
"tenantDetail": { "tenantDetail": {
"agent": "Agent", "agent": "Agent",

View File

@@ -90,7 +90,6 @@
}, },
"admin": { "admin": {
"title": "Admin piattaforma", "title": "Admin piattaforma",
"subtitle": "Tutti i tenant della piattaforma",
"allTenants": "Tenant", "allTenants": "Tenant",
"noTenants": "Nessun tenant ancora attivato.", "noTenants": "Nessun tenant ancora attivato.",
"noAccess": "Permessi insufficienti per questa vista.", "noAccess": "Permessi insufficienti per questa vista.",
@@ -100,7 +99,6 @@
"packages": "Pacchetti", "packages": "Pacchetti",
"created": "Creato", "created": "Creato",
"manage": "Gestisci", "manage": "Gestisci",
"requests": "Richieste di attivazione",
"pendingRequests": "Richieste in sospeso", "pendingRequests": "Richieste in sospeso",
"approve": "Approva", "approve": "Approva",
"reject": "Rifiuta", "reject": "Rifiuta",
@@ -108,9 +106,30 @@
"contact": "Contatto", "contact": "Contatto",
"status": "Stato", "status": "Stato",
"submitted": "Inviato", "submitted": "Inviato",
"noRequests": "Nessuna richiesta in sospeso.",
"approveConfirm": "Approvare questa richiesta e avviare l'attivazione?", "approveConfirm": "Approvare questa richiesta e avviare l'attivazione?",
"rejectConfirm": "Rifiutare questa richiesta?" "rejectConfirm": "Rifiutare questa richiesta?",
"subtitle": "Gestire le richieste di onboarding e il ciclo di vita dei tenant",
"requests": "Richieste",
"reApprove": "Ri-approva",
"agentName": "Agente",
"actions": "Azioni",
"noRequests": "Nessuna richiesta trovata.",
"loadingRequests": "Caricamento richieste…",
"rejectTitle": "Rifiuta richiesta",
"adminNotesLabel": "Note (opzionale)",
"adminNotesPlaceholder": "Motivo del rifiuto…",
"cancelAction": "Annulla",
"confirmReject": "Rifiuta",
"viewTenant": "Vedi",
"filter_all": "Tutti",
"filter_pending": "In attesa",
"filter_provisioning": "Provisioning",
"filter_approved": "Approvato",
"filter_rejected": "Rifiutato",
"totalTenants": "Totale",
"running": "Attivo",
"provisioning": "Provisioning",
"errors": "Errori"
}, },
"tenantDetail": { "tenantDetail": {
"agent": "Agente", "agent": "Agente",