197 lines
7.3 KiB
TypeScript
197 lines
7.3 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useMemo, useState } from "react";
|
||
import Link from "next/link";
|
||
import { useParams } from "next/navigation";
|
||
import { AuthGuard } from "@/components/AuthGuard";
|
||
import { Button } from "@/components/ui";
|
||
import { useMe } from "@/hooks/useMe";
|
||
import { loadCommercialProducts, loadSetupChecklistFromApi } from "../adapters";
|
||
import type { AdapterResult, CommercialProduct, SetupChecklistItem } from "../types";
|
||
import { resolveProductLaunchUrl } from "../types";
|
||
import {
|
||
CommercialEmptyState,
|
||
CommercialSkeleton,
|
||
PlatformShell,
|
||
SetupChecklist,
|
||
} from "../components";
|
||
import { pushRecent } from "../launcher-prefs";
|
||
|
||
/**
|
||
* Generic first-run product setup wizard.
|
||
* Steps come from commercial checklist API filtered by product — no vertical hardcoding.
|
||
*/
|
||
function SetupWizardInner() {
|
||
const params = useParams();
|
||
const productCode = decodeURIComponent(String(params.productCode || ""));
|
||
const { me } = useMe();
|
||
const [product, setProduct] = useState<CommercialProduct | null>(null);
|
||
const [productsAvail, setProductsAvail] = useState<string>("loading");
|
||
const [checklist, setChecklist] = useState<SetupChecklistItem[]>([]);
|
||
const [pct, setPct] = useState<number | null>(null);
|
||
const [step, setStep] = useState(0);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
void (async () => {
|
||
setLoading(true);
|
||
const prods = await loadCommercialProducts();
|
||
setProductsAvail(prods.availability);
|
||
const found = (prods.data || []).find((p) => p.product_code === productCode) || null;
|
||
setProduct(found);
|
||
if (found) pushRecent(found.product_code);
|
||
|
||
if (me?.current_tenant_id) {
|
||
const chk = await loadSetupChecklistFromApi(me.current_tenant_id);
|
||
const items = (chk.data || []).filter(
|
||
(i) =>
|
||
!i.product_code ||
|
||
i.product_code === productCode ||
|
||
i.id.includes(productCode)
|
||
);
|
||
// If API returned general checklist only, synthesize product-aware steps from metadata
|
||
if (!items.length && found) {
|
||
const metaSteps = Array.isArray(found.metadata?.setup_steps)
|
||
? (found.metadata!.setup_steps as Array<Record<string, unknown>>)
|
||
: [];
|
||
if (metaSteps.length) {
|
||
setChecklist(
|
||
metaSteps.map((s, idx) => ({
|
||
id: String(s.id || `meta-${idx}`),
|
||
label: String(s.label || s.title || `گام ${idx + 1}`),
|
||
href: typeof s.href === "string" ? s.href : resolveProductLaunchUrl(found),
|
||
product_code: productCode,
|
||
done: Boolean(s.done),
|
||
required: Boolean(s.required ?? true),
|
||
}))
|
||
);
|
||
} else {
|
||
setChecklist([
|
||
{
|
||
id: "open",
|
||
label: `باز کردن ${found.display_name}`,
|
||
href: resolveProductLaunchUrl(found),
|
||
product_code: productCode,
|
||
required: true,
|
||
},
|
||
{
|
||
id: "branding",
|
||
label: "بررسی برندینگ workspace",
|
||
href: "/dashboard/settings",
|
||
required: false,
|
||
},
|
||
{
|
||
id: "billing",
|
||
label: "تأیید trial / اشتراک",
|
||
href: "/billing",
|
||
required: false,
|
||
},
|
||
]);
|
||
}
|
||
} else {
|
||
setChecklist(items);
|
||
setPct(
|
||
(chk as AdapterResult<SetupChecklistItem[]> & { completion_percent?: number })
|
||
.completion_percent ?? null
|
||
);
|
||
}
|
||
}
|
||
setLoading(false);
|
||
})();
|
||
}, [productCode, me?.current_tenant_id]);
|
||
|
||
const current = checklist[step];
|
||
const launch = product ? resolveProductLaunchUrl(product) : null;
|
||
|
||
const progress = useMemo(() => {
|
||
if (pct != null) return pct;
|
||
if (!checklist.length) return 0;
|
||
return Math.round(((step + 1) / checklist.length) * 100);
|
||
}, [pct, checklist.length, step]);
|
||
|
||
return (
|
||
<PlatformShell
|
||
title={product ? `راهاندازی ${product.display_name}` : "راهاندازی محصول"}
|
||
subtitle="ویزارد اولین اجرا — گامها از checklist / metadata رجیستری"
|
||
>
|
||
{loading ? <CommercialSkeleton rows={4} /> : null}
|
||
|
||
{!loading && !product ? (
|
||
<CommercialEmptyState
|
||
title="محصول در رجیستری نیست"
|
||
description={
|
||
productsAvail === "unavailable"
|
||
? "رجیستری محصول در دسترس نیست."
|
||
: `کد ${productCode} پیدا نشد.`
|
||
}
|
||
actionHref="/apps"
|
||
actionLabel="بازگشت به Apps"
|
||
secondaryHref="/discover"
|
||
secondaryLabel="کشف مجدد"
|
||
/>
|
||
) : null}
|
||
|
||
{!loading && product ? (
|
||
<div className="mx-auto max-w-xl space-y-6">
|
||
<div className="h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
|
||
<div
|
||
className="h-full rounded-full bg-primary transition-all duration-500"
|
||
style={{ width: `${Math.min(100, progress)}%` }}
|
||
/>
|
||
</div>
|
||
<p className="text-xs text-gray-500">پیشرفت {progress}%</p>
|
||
|
||
{current ? (
|
||
<div className="rounded-3xl border border-gray-100 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||
<p className="text-xs text-gray-400">
|
||
گام {step + 1} از {checklist.length}
|
||
</p>
|
||
<h2 className="mt-2 text-xl font-bold text-secondary dark:text-gray-100">
|
||
{current.label}
|
||
</h2>
|
||
<div className="mt-6 flex flex-wrap gap-2">
|
||
{current.href ? (
|
||
<Link href={current.href}>
|
||
<Button>انجام گام</Button>
|
||
</Link>
|
||
) : null}
|
||
{step < checklist.length - 1 ? (
|
||
<Button variant="outline" onClick={() => setStep((s) => s + 1)}>
|
||
بعدی
|
||
</Button>
|
||
) : (
|
||
<Link href={launch || "/apps"}>
|
||
<Button onClick={() => pushRecent(product.product_code)}>ورود به محصول</Button>
|
||
</Link>
|
||
)}
|
||
{step > 0 ? (
|
||
<Button variant="ghost" onClick={() => setStep((s) => s - 1)}>
|
||
قبلی
|
||
</Button>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<CommercialEmptyState
|
||
title="گام راهاندازی تعریف نشده"
|
||
description="Admin میتواند setup_steps را در metadata محصول ثبت کند."
|
||
actionHref={launch || "/apps"}
|
||
actionLabel="باز کردن محصول"
|
||
/>
|
||
)}
|
||
|
||
<SetupChecklist items={checklist} completionPercent={progress} />
|
||
</div>
|
||
) : null}
|
||
</PlatformShell>
|
||
);
|
||
}
|
||
|
||
export function ProductSetupWizardPage() {
|
||
return (
|
||
<AuthGuard>
|
||
<SetupWizardInner />
|
||
</AuthGuard>
|
||
);
|
||
}
|