TorbatYar/frontend/modules/commercial/features/product-setup.tsx

197 lines
7.4 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: "بررسی ظاهر فضای کاری",
href: "/dashboard/settings",
required: false,
},
{
id: "billing",
label: "تأیید دوره آزمایشی یا اشتراک",
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="مراحل ساده برای آماده‌کردن محصول و شروع استفاده"
>
{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="مراحل راه‌اندازی این محصول هنوز آماده نشده است."
actionHref={launch || "/apps"}
actionLabel="باز کردن محصول"
/>
)}
<SetupChecklist items={checklist} completionPercent={progress} />
</div>
) : null}
</PlatformShell>
);
}
export function ProductSetupWizardPage() {
return (
<AuthGuard>
<SetupWizardInner />
</AuthGuard>
);
}