TorbatYar/frontend/modules/commercial/features/apps.tsx

309 lines
11 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 { useSearchParams } from "next/navigation";
import { AuthGuard } from "@/components/AuthGuard";
import { Button, SectionTitle } from "@/components/ui";
import { useMe } from "@/hooks/useMe";
import {
loadCommercialAssets,
loadCommercialAutomationPacks,
loadCommercialBundles,
loadCommercialExtensions,
loadCommercialMetadata,
loadCommercialProducts,
} from "../adapters";
import type {
AdapterResult,
CommercialAsset,
CommercialAutomationPack,
CommercialBundle,
CommercialExtension,
CommercialMetadataBag,
CommercialProduct,
} from "../types";
import {
BundleCard,
CommercialEmptyState,
CommercialSkeleton,
MetadataBagView,
PlatformShell,
ProductCard,
RegistryBrowser,
} from "../components";
import {
loadLauncherPrefs,
toggleFavorite,
togglePinned,
type LauncherPrefs,
} from "../launcher-prefs";
const GROUP_ORDER = [
"Operations",
"Sales",
"Marketing",
"Finance",
"Customers",
"Online Presence",
"Automation",
"AI",
"operations",
"sales",
"marketing",
"finance",
"customers",
"online",
"automation",
"ai",
];
function groupLabel(raw: string): string {
const map: Record<string, string> = {
operations: "عملیات",
sales: "فروش",
marketing: "بازاریابی",
finance: "مالی",
customers: "مشتریان",
"online presence": "حضور آنلاین",
online: "حضور آنلاین",
automation: "اتوماسیون",
ai: "هوش مصنوعی",
};
return map[raw.toLowerCase()] || raw;
}
function AppsInner() {
const { me } = useMe();
const searchParams = useSearchParams();
const q = (searchParams.get("q") || "").trim().toLowerCase();
const [products, setProducts] = useState<AdapterResult<CommercialProduct[]> | null>(null);
const [bundles, setBundles] = useState<AdapterResult<CommercialBundle[]> | null>(null);
const [assets, setAssets] = useState<AdapterResult<CommercialAsset[]> | null>(null);
const [extensions, setExtensions] = useState<AdapterResult<CommercialExtension[]> | null>(null);
const [automation, setAutomation] = useState<AdapterResult<CommercialAutomationPack[]> | null>(
null
);
const [meta, setMeta] = useState<AdapterResult<CommercialMetadataBag | null> | null>(null);
const [prefs, setPrefs] = useState<LauncherPrefs>({ favorites: [], pinned: [], recent: [] });
const [booted, setBooted] = useState(false);
useEffect(() => {
setPrefs(loadLauncherPrefs());
void Promise.all([
loadCommercialProducts(),
loadCommercialBundles(),
loadCommercialAssets(),
loadCommercialExtensions(),
loadCommercialAutomationPacks(),
loadCommercialMetadata({ target_kind: "catalog" }),
]).then(([p, b, a, e, auto, m]) => {
setProducts(p);
setBundles(b);
setAssets(a);
setExtensions(e);
setAutomation(auto);
setMeta(m);
setBooted(true);
});
}, []);
const byCode = useMemo(() => {
const m = new Map<string, CommercialProduct>();
for (const p of products?.data ?? []) m.set(p.product_code, p);
return m;
}, [products]);
const pick = (codes: string[]) =>
codes.map((c) => byCode.get(c)).filter((p): p is CommercialProduct => Boolean(p));
const favorites = pick(prefs.favorites);
const pinned = pick(prefs.pinned);
const recent = pick(prefs.recent);
const groups = useMemo(() => {
let list = (products?.data ?? []).filter((p) => p.visibility !== "hidden");
if (q) {
list = list.filter(
(p) =>
p.display_name.toLowerCase().includes(q) ||
p.product_code.toLowerCase().includes(q) ||
(p.category || "").toLowerCase().includes(q)
);
}
const map = new Map<string, CommercialProduct[]>();
for (const p of list) {
const key = p.category?.trim() || "Operations";
const arr = map.get(key) || [];
arr.push(p);
map.set(key, arr);
}
const keys = Array.from(map.keys()).sort((a, b) => {
const ia = GROUP_ORDER.findIndex((g) => g.toLowerCase() === a.toLowerCase());
const ib = GROUP_ORDER.findIndex((g) => g.toLowerCase() === b.toLowerCase());
return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);
});
return keys.map((k) => ({ key: k, label: groupLabel(k), items: map.get(k)! }));
}, [products, q]);
return (
<PlatformShell
title="محصولات من"
subtitle="همه ابزارهای کسب‌وکار شما، مرتب‌شده و آماده استفاده."
>
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
{q ? <p className="text-xs text-gray-500">نتایج برای: {q}</p> : <span />}
<div className="flex gap-2">
<Link href="/search">
<Button variant="ghost" size="sm">
جستجوی سراسری
</Button>
</Link>
<Link href="/discover">
<Button variant="outline" size="sm">
کشف محصول جدید
</Button>
</Link>
</div>
</div>
{!booted ? <CommercialSkeleton rows={4} /> : null}
{booted && pinned.length ? (
<section className="mb-8">
<SectionTitle title="پین شده" />
<div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{pinned.map((p) => (
<ProductCard
key={`pin-${p.product_code}`}
product={p}
tenantId={me?.current_tenant_id || undefined}
favorite={prefs.favorites.includes(p.product_code)}
pinned
onToggleFavorite={() => setPrefs(toggleFavorite(p.product_code))}
onTogglePinned={() => setPrefs(togglePinned(p.product_code))}
/>
))}
</div>
</section>
) : null}
{booted && favorites.length ? (
<section className="mb-8">
<SectionTitle title="علاقه‌مندی‌ها" />
<div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{favorites.map((p) => (
<ProductCard
key={`fav-${p.product_code}`}
product={p}
tenantId={me?.current_tenant_id || undefined}
favorite
pinned={prefs.pinned.includes(p.product_code)}
onToggleFavorite={() => setPrefs(toggleFavorite(p.product_code))}
onTogglePinned={() => setPrefs(togglePinned(p.product_code))}
/>
))}
</div>
</section>
) : null}
{booted && recent.length ? (
<section className="mb-8">
<SectionTitle title="اخیر" />
<div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{recent.map((p) => (
<ProductCard
key={`recent-${p.product_code}`}
product={p}
tenantId={me?.current_tenant_id || undefined}
favorite={prefs.favorites.includes(p.product_code)}
pinned={prefs.pinned.includes(p.product_code)}
onToggleFavorite={() => setPrefs(toggleFavorite(p.product_code))}
onTogglePinned={() => setPrefs(togglePinned(p.product_code))}
/>
))}
</div>
</section>
) : null}
{booted && products?.availability === "ready" && groups.length ? (
<div className="space-y-10">
{groups.map((g) => (
<section key={g.key}>
<SectionTitle title={g.label} />
<div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{g.items.map((p) => (
<ProductCard
key={p.product_code}
product={p}
tenantId={me?.current_tenant_id || undefined}
favorite={prefs.favorites.includes(p.product_code)}
pinned={prefs.pinned.includes(p.product_code)}
onToggleFavorite={() => setPrefs(toggleFavorite(p.product_code))}
onTogglePinned={() => setPrefs(togglePinned(p.product_code))}
/>
))}
</div>
</section>
))}
</div>
) : booted ? (
<CommercialEmptyState
title="هنوز محصولی فعال نکرده‌اید"
description="با پاسخ به چند سؤال کوتاه، محصولات مناسب کسب‌وکار خود را پیدا کنید."
actionHref="/discover"
actionLabel="کشف کسب‌وکار"
secondaryHref="/help"
secondaryLabel="راهنما"
hint="پس از انتخاب و راه‌اندازی، محصولات شما در همین صفحه ظاهر می‌شوند."
/>
) : null}
<section className="mt-12">
<SectionTitle title="بسته‌های شما" />
{bundles?.availability === "ready" && bundles.data.length ? (
<div className="mt-3 grid gap-3">
{bundles.data.map((b) => (
<BundleCard key={b.bundle_code} bundle={b} />
))}
</div>
) : (
<CommercialEmptyState
title="هنوز بسته‌ای انتخاب نکرده‌اید"
description="بسته مناسب، محصولات مکمل را با شرایط بهتر کنار هم قرار می‌دهد."
hint="ارزیابی را انجام دهید تا فقط بسته‌های مناسب شما نمایش داده شود."
actionHref="/discover"
actionLabel="پیدا کردن بسته مناسب"
secondaryHref="/pricing"
secondaryLabel="قیمت‌گذاری"
/>
)}
</section>
<div className="mt-10 space-y-8">
<RegistryBrowser title="دارایی‌های دیجیتال" result={assets} codeKey="asset_code" />
<RegistryBrowser title="افزونه‌ها" result={extensions} codeKey="extension_code" />
<RegistryBrowser
title="بسته‌های خودکارسازی"
result={automation}
codeKey="automation_pack_code"
/>
<MetadataBagView
title="اطلاعات تکمیلی محصولات"
attributes={meta?.data?.attributes}
message={meta?.message}
availability={meta?.availability}
/>
</div>
</PlatformShell>
);
}
export function CommercialAppsPage() {
return (
<AuthGuard>
<AppsInner />
</AuthGuard>
);
}