"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Badge, Button, Container } from "@/components/ui";
import { useAuth } from "@/hooks/useAuth";
import { useTheme } from "@/hooks/useTheme";
import {
loadAssessmentSchema,
loadCommercialBundles,
loadCommercialPricing,
loadCommercialProducts,
requestRecommendations,
} from "../adapters";
import type {
AdapterResult,
AssessmentQuestion,
AssessmentSchema,
CommercialBundle,
CommercialPricingItem,
CommercialProduct,
RecommendationResult,
} from "../types";
import {
BundleCard,
CommercialEmptyState,
ProductCard,
} from "../components";
import {
loadAssessmentSession,
saveAssessmentSession,
type AssessmentSession,
} from "../session";
type WizardStep =
| "landing"
| "assessment"
| "recommendations"
| "trial_summary"
| "create_workspace";
export type { AssessmentSession };
export { loadAssessmentSession, saveAssessmentSession };
function QuestionField({
question,
value,
onChange,
}: {
question: AssessmentQuestion;
value: unknown;
onChange: (v: unknown) => void;
}) {
const inputClass =
"mt-1 w-full rounded-xl border border-gray-200 px-4 py-2.5 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary";
if (question.input_type === "boolean") {
return (
onChange(e.target.checked)}
/>
{question.prompt}
);
}
if (question.input_type === "number") {
return (
{question.prompt}
onChange(e.target.value === "" ? null : Number(e.target.value))}
/>
);
}
if (question.input_type === "multi" && question.options?.length) {
const selected = Array.isArray(value) ? (value as string[]) : [];
return (
{question.prompt}
{question.options.map((opt) => {
const on = selected.includes(opt.value);
return (
{
onChange(on ? selected.filter((v) => v !== opt.value) : [...selected, opt.value]);
}}
>
{opt.label}
);
})}
);
}
if (question.input_type === "single" && question.options?.length) {
return (
{question.prompt}
{question.options.map((opt) => (
onChange(opt.value)}
>
{opt.label}
))}
);
}
return (
{question.prompt}
onChange(e.target.value)}
/>
);
}
export function CommercialLandingExperience() {
const { theme } = useTheme();
const { isAuthenticated, login, loading: authLoading } = useAuth();
const router = useRouter();
const siteName = theme?.site_name ?? "TorbatYar";
const [step, setStep] = useState("landing");
const [schemaRes, setSchemaRes] = useState | null>(null);
const [productsRes, setProductsRes] = useState | null>(null);
const [bundlesRes, setBundlesRes] = useState | null>(null);
const [pricingRes, setPricingRes] = useState | null>(null);
const [recoRes, setRecoRes] = useState | null>(null);
const [answers, setAnswers] = useState>({});
const [selectedBundle, setSelectedBundle] = useState(null);
const [qIndex, setQIndex] = useState(0);
const [busy, setBusy] = useState(false);
useEffect(() => {
const session = loadAssessmentSession();
setAnswers(session.answers || {});
if (session.selected_bundle_code) setSelectedBundle(session.selected_bundle_code);
}, []);
const persist = useCallback((next: AssessmentSession) => {
saveAssessmentSession(next);
}, []);
const startAssessment = async () => {
setBusy(true);
const [schema, products, bundles, pricing] = await Promise.all([
loadAssessmentSchema(),
loadCommercialProducts(),
loadCommercialBundles(),
loadCommercialPricing(),
]);
setSchemaRes(schema);
setProductsRes(products);
setBundlesRes(bundles);
setPricingRes(pricing);
setQIndex(0);
setStep("assessment");
setBusy(false);
};
const questions = schemaRes?.data.questions ?? [];
const currentQ = questions[qIndex];
const runRecommendations = async () => {
setBusy(true);
persist({ answers, selected_bundle_code: selectedBundle });
const reco = await requestRecommendations(answers);
setRecoRes(reco);
setStep("recommendations");
setBusy(false);
};
const productByCode = useMemo(() => {
const map = new Map();
(productsRes?.data ?? []).forEach((p) => map.set(p.product_code, p));
return map;
}, [productsRes]);
const bundleByCode = useMemo(() => {
const map = new Map();
(bundlesRes?.data ?? []).forEach((b) => map.set(b.bundle_code, b));
return map;
}, [bundlesRes]);
const recommendedProducts = (recoRes?.data.recommended_products ?? [])
.map((r) => ({ ...r, product: productByCode.get(r.code) }))
.filter((r) => r.product);
const recommendedBundles = (recoRes?.data.recommended_bundles ?? [])
.map((r) => ({ ...r, bundle: bundleByCode.get(r.code) }))
.filter((r) => r.bundle);
const goWorkspace = () => {
persist({
answers,
selected_bundle_code: selectedBundle,
selected_product_codes: recoRes?.data.recommended_products.map((p) => p.code),
});
if (isAuthenticated) {
const intent = typeof answers.business_type_code === "string" ? answers.business_type_code : "";
router.push(intent ? `/onboarding?intent=${encodeURIComponent(intent)}` : "/onboarding");
} else {
setStep("create_workspace");
}
};
return (
{step === "landing" ? (
Commercial SaaS Runtime
{siteName}
نوع کسبوکار و نیازها را مشخص کنید؛ محصولات، باندل و trial از رجیستری پلتفرم پیشنهاد
میشود — نه از لیست ثابت اپلیکیشنها.
void startAssessment()}>
شروع ارزیابی کسبوکار
{isAuthenticated ? (
داشبورد تجاری
) : (
login()} loading={authLoading}>
ورود
)}
{["ارزیابی", "پیشنهاد", "Trial", "Workspace"].map((s, i) => (
{i + 1}. {s}
))}
) : null}
{step === "assessment" ? (
ارزیابی نیازها
سؤالات از metadata بکاند بارگذاری میشوند و برای صنایع آینده بدون تغییر frontend گسترش
مییابند.
{schemaRes?.availability === "unavailable" || schemaRes?.availability === "empty" ? (
میتوانید بدون پیشنهاد موتور، ساخت workspace را ادامه دهید. پس از ثبت schema در Admin،
همین صفحه سؤالات را نشان میدهد.
setStep("landing")}>
بازگشت
{
setStep("recommendations");
setRecoRes({
availability: "unavailable",
data: {
recommended_products: [],
recommended_bundles: [],
recommended_capabilities: [],
recommended_pricing: [],
},
message: schemaRes.message,
source: "none",
});
}}
>
ادامه بدون پیشنهاد
) : currentQ ? (
سؤال {qIndex + 1} از {questions.length}
{
const next = { ...answers, [currentQ.question_id]: v };
setAnswers(next);
persist({ answers: next, selected_bundle_code: selectedBundle });
}}
/>
setQIndex((i) => i - 1)}>
قبلی
{qIndex < questions.length - 1 ? (
setQIndex((i) => i + 1)}
disabled={currentQ.required && answers[currentQ.question_id] == null}
>
بعدی
) : (
void runRecommendations()}>
دریافت پیشنهاد
)}
) : null}
) : null}
{step === "recommendations" ? (
پیشنهاد محصولات و باندل
نتایج از رجیستری/موتور پیشنهاد؛ در صورت نبود سرویس، داده جعلی نمایش داده نمیشود.
{recoRes?.message ? (
{recoRes.message}
) : null}
محصولات پیشنهادی
{recommendedProducts.length ? (
{recommendedProducts.map((r) => (
))}
) : productsRes?.availability === "ready" && productsRes.data.length ? (
{productsRes.data.slice(0, 8).map((p) => (
))}
) : (
)}
باندل پیشنهادی
{recommendedBundles.length ? (
{recommendedBundles.map((r) => (
{
setSelectedBundle(r.code);
persist({ answers, selected_bundle_code: r.code });
}}
/>
))}
) : bundlesRes?.availability === "ready" && bundlesRes.data.length ? (
{bundlesRes.data.map((b) => (
{
setSelectedBundle(b.bundle_code);
persist({ answers, selected_bundle_code: b.bundle_code });
}}
/>
))}
) : (
)}
setStep("assessment")}>
بازگشت به ارزیابی
setStep("trial_summary")}>خلاصه Trial
) : null}
{step === "trial_summary" ? (
خلاصه دوره آزمایشی
جزئیات trial (روز، سهمیه، قوانین) از سیاستها و قیمتگذاری بکاند خوانده میشود. تا زمان
در دسترس بودن رجیستری، فقط وضعیت آمادگی نمایش داده میشود — بدون اعداد ساختگی.
{pricingRes?.availability === "ready" && pricingRes.data.length ? (
{pricingRes.data
.filter((p) => p.pricing_model === "trial" || p.trial_days)
.map((p) => (
{p.display_name}
{p.trial_days ? ` — ${p.trial_days} روز` : ""}
{p.pricing_item_code}
))}
) : (
)}
{selectedBundle ? (
باندل انتخابی: {selectedBundle}
) : (
باندلی انتخاب نشده است.
)}
ساخت Workspace
) : null}
{step === "create_workspace" ? (
ورود برای ساخت Workspace
برای ایجاد tenant و داشبورد تجاری وارد شوید. انتخابهای ارزیابی در session نگه داشته
میشود.
{
persist({ answers, selected_bundle_code: selectedBundle });
login();
}}
>
ورود / ثبتنام
اگر وارد شدهاید، ادامه onboarding
) : null}
);
}