Session 6.3
This commit is contained in:
240
src/components/onboarding/provisioning-status.tsx
Normal file
240
src/components/onboarding/provisioning-status.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { StatusBadge } from "@/components/ui/status-badge";
|
||||
|
||||
interface OnboardingState {
|
||||
state: string;
|
||||
request?: {
|
||||
id: string;
|
||||
status: string;
|
||||
companyName: string;
|
||||
agentName: string;
|
||||
adminNotes?: string;
|
||||
};
|
||||
tenant?: {
|
||||
name: string;
|
||||
phase: string;
|
||||
message?: string;
|
||||
conditions?: Array<{
|
||||
type: string;
|
||||
status: string;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
lastTransitionTime?: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export function ProvisioningStatus() {
|
||||
const t = useTranslations("onboarding");
|
||||
const [data, setData] = useState<OnboardingState | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const poll = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/onboarding");
|
||||
if (!res.ok) throw new Error("Failed to fetch status");
|
||||
const json = await res.json();
|
||||
setData(json);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
poll();
|
||||
|
||||
// Poll every 5 seconds while not in a terminal state
|
||||
const interval = setInterval(() => {
|
||||
if (
|
||||
data?.state === "provisioned" ||
|
||||
data?.state === "rejected" ||
|
||||
data?.state === "active"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
poll();
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [poll, data?.state]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<div className="text-xs text-red-400">{error}</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center gap-2 text-sm text-text-muted">
|
||||
<div className="h-4 w-4 border-2 border-accent border-t-transparent rounded-full animate-spin" />
|
||||
{t("loading")}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Pending admin approval
|
||||
if (data.state === "pending") {
|
||||
return (
|
||||
<Card className="animate-in">
|
||||
<div className="text-center py-6">
|
||||
<div className="h-14 w-14 rounded-xl bg-amber-500/15 flex items-center justify-center mx-auto mb-4">
|
||||
<svg
|
||||
className="h-7 w-7 text-amber-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="font-display text-lg font-semibold text-text-primary mb-2">
|
||||
{t("pendingTitle")}
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary max-w-sm mx-auto">
|
||||
{t("pendingDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Rejected
|
||||
if (data.state === "rejected") {
|
||||
return (
|
||||
<Card className="animate-in">
|
||||
<div className="text-center py-6">
|
||||
<div className="h-14 w-14 rounded-xl bg-red-500/15 flex items-center justify-center mx-auto mb-4">
|
||||
<svg
|
||||
className="h-7 w-7 text-red-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="font-display text-lg font-semibold text-text-primary mb-2">
|
||||
{t("rejectedTitle")}
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary max-w-sm mx-auto">
|
||||
{t("rejectedDescription")}
|
||||
</p>
|
||||
{data.request?.adminNotes && (
|
||||
<p className="text-xs text-text-muted mt-3 bg-surface-2 border border-border rounded-lg p-3 max-w-sm mx-auto">
|
||||
{data.request.adminNotes}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Provisioning in progress
|
||||
if (
|
||||
data.state === "approved" ||
|
||||
data.state === "provisioning"
|
||||
) {
|
||||
const phase = data.tenant?.phase ?? "Pending";
|
||||
const conditions = data.tenant?.conditions ?? [];
|
||||
|
||||
return (
|
||||
<Card className="animate-in">
|
||||
<div className="text-center py-4 mb-4">
|
||||
<div className="h-14 w-14 rounded-xl bg-accent/15 flex items-center justify-center mx-auto mb-4">
|
||||
<div className="h-6 w-6 border-2 border-accent border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
<h2 className="font-display text-lg font-semibold text-text-primary mb-2">
|
||||
{t("provisioningTitle")}
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary">
|
||||
{t("provisioningDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between bg-surface-2 border border-border rounded-lg px-4 py-2">
|
||||
<span className="text-xs text-text-muted">{t("phase")}</span>
|
||||
<StatusBadge phase={phase} />
|
||||
</div>
|
||||
{conditions.map((c, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between bg-surface-2 border border-border rounded-lg px-4 py-2"
|
||||
>
|
||||
<span className="text-xs text-text-muted">{c.type}</span>
|
||||
<span
|
||||
className={`text-xs font-mono ${
|
||||
c.status === "True"
|
||||
? "text-emerald-400"
|
||||
: c.status === "False"
|
||||
? "text-red-400"
|
||||
: "text-text-muted"
|
||||
}`}
|
||||
>
|
||||
{c.reason || c.status}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Provisioned / Running
|
||||
if (data.state === "provisioned") {
|
||||
return (
|
||||
<Card className="animate-in">
|
||||
<div className="text-center py-6">
|
||||
<div className="h-14 w-14 rounded-xl bg-emerald-500/15 flex items-center justify-center mx-auto mb-4">
|
||||
<svg
|
||||
className="h-7 w-7 text-emerald-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="font-display text-lg font-semibold text-text-primary mb-2">
|
||||
{t("readyTitle")}
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary max-w-sm mx-auto mb-4">
|
||||
{t("readyDescription")}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="py-2 px-6 bg-accent text-white text-sm font-medium rounded-lg hover:bg-accent-dim transition-colors"
|
||||
>
|
||||
{t("goToDashboard")}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user