TorbatYar/frontend/modules/commercial/features/product-setup.tsx
2026-07-28 20:39:10 +03:30

197 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}