"use client"; import { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useTranslations, useFormatter } from "next-intl"; import { Card } from "@/components/ui/card"; import { Modal } from "@/components/ui/modal"; import { StatusBadge } from "@/components/ui/status-badge"; import { formatDateTime, formatRelative } from "@/lib/format"; interface RequestSummary { id: string; instanceName?: string | null; agentName: string; packages: string[]; status: string; adminNotes?: string; tenantName?: string; dismissedAt?: string | null; createdAt?: string; updatedAt?: string; } interface TenantSummary { name: string; displayName: string; phase: string; conditions: Array<{ type: string; status: string; reason?: string; message?: string; lastTransitionTime?: string; }>; } interface SingleRequestState { request: RequestSummary; tenant: TenantSummary | null; } interface Props { requestId: string; /** * Whether the viewer can act on this request — cancel a pending one, * dismiss a rejected one, etc. True for owner + platform; false for * `user`-role customers (who shouldn't see in-flight requests at all, * but defence in depth — `canSeeInflightRequests` already gates the * dashboard side). */ canAct: boolean; } /** * ProvisioningStatus * * Polls /api/onboarding?id= every 5s until the request reaches * a terminal state. Slice 3: takes a `requestId` prop so multiple of * these can render on the same dashboard for different in-flight * requests. * * Slice 7 / Bug 6 + 13: * - pending → cancel + edit buttons * - rejected → admin notes block + dismiss button * - cancelled → small acknowledgement card + dismiss button * - terminal Ready/Active states unchanged */ export function ProvisioningStatus({ requestId, canAct }: Props) { const t = useTranslations("onboarding"); const tCommon = useTranslations("common"); const f = useFormatter(); const router = useRouter(); const [data, setData] = useState(null); const [error, setError] = useState(""); const [actionPending, setActionPending] = useState(false); const [confirmCancel, setConfirmCancel] = useState(false); const poll = useCallback(async () => { try { const res = await fetch( `/api/onboarding?id=${encodeURIComponent(requestId)}` ); if (!res.ok) throw new Error("Failed to fetch status"); const json = await res.json(); setData(json); } catch (err: any) { setError(err.message); } }, [requestId]); useEffect(() => { poll(); const status = data?.request?.status; const phase = data?.tenant?.phase; const terminal = status === "rejected" || status === "cancelled" || status === "active" || phase === "Ready" || phase === "Running"; if (terminal) return; const interval = setInterval(poll, 5000); return () => clearInterval(interval); }, [poll, data?.request?.status, data?.tenant?.phase]); const handleCancel = async () => { setActionPending(true); setError(""); try { const res = await fetch( `/api/onboarding/${encodeURIComponent(requestId)}`, { method: "DELETE" } ); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.error || t("cancelFailed")); } setConfirmCancel(false); // Re-poll so the card transitions to "cancelled" state without a // full route refresh — the dashboard's surrounding tenant cards // are unaffected. await poll(); router.refresh(); } catch (err: any) { setError(err.message); } finally { setActionPending(false); } }; const handleDismiss = async () => { setActionPending(true); setError(""); try { const res = await fetch( `/api/onboarding/${encodeURIComponent(requestId)}/dismiss`, { method: "POST" } ); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.error || t("dismissFailed")); } // Server-rendered list query (`listActiveTenantRequestsByOrgId`) // filters out dismissed rows — refresh to drop this card. router.refresh(); } catch (err: any) { setError(err.message); } finally { setActionPending(false); } }; if (error && !data) { return (
{error}
); } if (!data) { return (
{t("loading")}
); } const status = data.request.status; const label = data.request.instanceName || data.request.tenantName || data.request.agentName; // ─── Pending: awaiting admin approval ─────────────────────────────── if (status === "pending") { return (

{t("pendingTitle")}

{label && (

{label}

)}

{t("pendingDescription")}

{data.request.createdAt && (

{t("submittedAt")}{" "} {formatRelative(data.request.createdAt, f)} {" "} ({formatDateTime(data.request.createdAt, f)})

)} {/* Bug 6 — owner-only edit + cancel actions while still pending. Once admin acts, both buttons disappear (the status branch changes). */} {canAct && (
{t("editRequest")}
)} {error && (

{error}

)}
{confirmCancel && ( setConfirmCancel(false)} ariaLabel={t("cancelConfirmRequestTitle")} >

{t("cancelConfirmRequestTitle")}

{t("cancelConfirmRequestDescription")}

)}
); } // ─── Rejected: admin declined ─────────────────────────────────────── if (status === "rejected") { return (

{t("rejectedTitle")}

{label && (

{label}

)}

{t("rejectedDescription")}

{data.request.adminNotes && (
{t("rejectionReason")}
{data.request.adminNotes}
)} {/* Bug 13: dismiss removes this card from the dashboard but keeps the row in the DB for audit. The customer can also just resubmit via the wizard — both paths are valid. */} {canAct && (
)} {error &&

{error}

}
); } // ─── Cancelled: customer cancelled before admin acted (Bug 6) ────── if (status === "cancelled") { return (

{t("cancelledTitle")}

{label && (

{label}

)}

{t("cancelledDescription")}

{canAct && (
)} {error &&

{error}

}
); } // ─── Provisioning: approved, operator working ────────────────────── if ( status === "approved" || status === "provisioning" || (status === "active" && data.tenant && data.tenant.phase !== "Ready") ) { const phase = data.tenant?.phase ?? "Pending"; const conditions = data.tenant?.conditions ?? []; return (

{t("provisioningTitle")}

{label && (

{label}

)}

{t("provisioningDescription")}

{t("phase")}
{conditions.map((c, i) => (
{c.type} {c.reason || c.status}
))}
); } // ─── Active / Ready ───────────────────────────────────────────────── if (status === "active") { return (

{t("readyTitle")}

{label && (

{label}

)}

{t("readyDescription")}

); } return null; }