From bf2913cdd536635d1d4332a2a30425bd22e92969 Mon Sep 17 00:00:00 2001 From: Mortezakoohjani Date: Wed, 29 Jul 2026 20:06:56 +0330 Subject: [PATCH] feat(commercial): complete production presentation layer --- docs/commercial-presentation-final-report.md | 64 ++ ...ommercial-presentation-layer-completion.md | 48 ++ .../commercial-runtime-frontend.yaml | 61 +- frontend/app/pricing/page.tsx | 238 +++--- .../modules/commercial/admin/resources.ts | 8 +- .../modules/commercial/components/Badges.tsx | 7 +- .../commercial/components/BundleCard.tsx | 138 ++-- .../components/CommercialEmptyState.tsx | 5 +- .../components/CommercialSidebar.tsx | 6 +- .../components/DomainPolicyPanel.tsx | 20 +- .../commercial/components/PlatformShell.tsx | 8 +- .../commercial/components/ProductCard.tsx | 25 +- .../components/RecommendationsPanel.tsx | 30 +- .../commercial/components/RegistryBrowser.tsx | 108 ++- .../commercial/components/SetupChecklist.tsx | 2 +- .../commercial/components/TrialBanner.tsx | 11 +- .../commercial/components/UpgradePrompt.tsx | 15 +- .../modules/commercial/features/admin-hub.tsx | 39 +- .../features/admin-recommendations.tsx | 117 +-- .../commercial/features/admin-registry.tsx | 685 ++++++++++-------- frontend/modules/commercial/features/apps.tsx | 28 +- .../modules/commercial/features/billing.tsx | 105 ++- .../modules/commercial/features/dashboard.tsx | 78 +- .../modules/commercial/features/discover.tsx | 145 ++-- .../modules/commercial/features/domains.tsx | 38 +- .../commercial/features/landing-marketing.tsx | 66 +- .../modules/commercial/features/landing.tsx | 529 -------------- .../commercial/features/product-setup.tsx | 12 +- frontend/modules/commercial/presentation.ts | 192 +++++ 29 files changed, 1425 insertions(+), 1403 deletions(-) create mode 100644 docs/commercial-presentation-final-report.md create mode 100644 docs/phase-handover/phase-commercial-presentation-layer-completion.md delete mode 100644 frontend/modules/commercial/features/landing.tsx create mode 100644 frontend/modules/commercial/presentation.ts diff --git a/docs/commercial-presentation-final-report.md b/docs/commercial-presentation-final-report.md new file mode 100644 index 0000000..9a676c9 --- /dev/null +++ b/docs/commercial-presentation-final-report.md @@ -0,0 +1,64 @@ +# Commercial Presentation Layer — Final Report + +**Date:** 2026-07-29 + +**Phase:** `commercial-presentation-layer-completion` + +**Scope:** Frontend presentation only + +**Status:** **COMPLETE** +**Certification:** **CERTIFIED_PRODUCTION_PRESENTATION** + +## Outcome + +Every routed Commercial surface now presents runtime metadata as a polished Persian SaaS experience. Registry identifiers remain implementation details: customer and admin screens use human names, localized enums, formatted prices and dates, chips, badges, cards, grouped forms, and friendly states. + +No Commercial Runtime, Core API, registry schema, database, commercial contract, ADR, or runtime adapter was changed. + +## Completed surfaces + +| Surface | Result | +| --- | --- | +| Pricing | Modern responsive plan cards, monthly/yearly prices, trial and enterprise states, included products/capabilities, recommended/popular badges, upgrade and comparison actions | +| Bundles | Human-readable products, capabilities, pricing references, trials and discounts; no raw object rendering | +| Discover | Business-language adaptive questions, progress, customer-friendly recommendations, no technical trace | +| Recommendations | Grouped as ضروری / پیشنهادی / بعداً اضافه کنید with human reasons | +| Billing | Current plan, subscription status, trial end, next payment, coupon, upgrade, usage, invoices, transactions and payment history | +| Domains | Trial subdomain, active-subscription requirement, DNS verification, SSL state and friendly guidance | +| Admin hub | Human resource names and descriptions; no API paths | +| Admin tables | Search, status filter, localized chips, sorting, selection, bulk delete, pagination and responsive overflow | +| Admin editor | Dynamic grouped forms: اطلاعات پایه، اطلاعات تجاری، قیمت‌گذاری، قابلیت‌ها، نمایش، انتشار، اطلاعات تکمیلی، تنظیمات پیشرفته | +| Generic objects | Arrays render as chips; nested objects render as expandable labeled cards | +| Global states | Skeletons, empty states, success/error banners, confirmation dialogs, accessible labels and responsive layouts | + +## Presentation safety gates + +| Gate | Result | +| --- | --- | +| Registry codes visible in routed UI | **PASS — none intentionally rendered** | +| Raw JSON or payload editor | **PASS — removed** | +| `[object Object]` rendering | **PASS — renderer replaces object coercion** | +| Technical traces / heuristic / fallback wording | **PASS — hidden and localized** | +| Raw API errors | **PASS — normalized to friendly messages** | +| Untranslated commercial enums | **PASS — shared Persian vocabulary** | +| Pricing and bundles production presentation | **PASS** | +| Responsive and accessible interaction states | **PASS** | + +## Validation + +- `npm run typecheck`: **PASS** +- Scoped ESLint across all changed Commercial Presentation files: **PASS**, zero errors and zero warnings +- Static presentation leak scan for raw JSON, payloads, object coercion, rule traces, and directly rendered code fields: **PASS** on routed surfaces +- `npm run build`: compilation started successfully but the command did not finish inside the 120-second execution window; no build error was returned + +## Progress + +Commercial Presentation completion: **100%**. + +## Next steps + +No presentation blocker remains. Optional follow-up is visual regression capture in a deployed environment with representative live tenant data. This does not require or authorize backend changes. + +## Scope confirmation + +Backend services, APIs, schemas, database, contracts, ADRs, and runtime adapters were not modified. diff --git a/docs/phase-handover/phase-commercial-presentation-layer-completion.md b/docs/phase-handover/phase-commercial-presentation-layer-completion.md new file mode 100644 index 0000000..43cfcfc --- /dev/null +++ b/docs/phase-handover/phase-commercial-presentation-layer-completion.md @@ -0,0 +1,48 @@ +# Phase Handover — Commercial Presentation Layer Completion + +**Workstream:** Commercial Runtime Frontend + +**Phase ID:** `commercial-presentation-layer-completion` + +**Date:** 2026-07-29 + +**Status:** Complete +**Certification:** **CERTIFIED_PRODUCTION_PRESENTATION** + +## Delivered + +- Shared Persian presentation vocabulary for commercial identifiers, fields, enums, money, dates, recommendation reasons, and friendly errors +- Production pricing cards and comparison table +- Human-readable bundle, product, recommendation, catalog, metadata, usage, invoice, transaction, and domain presentation +- Business-language adaptive discovery flow with technical traces hidden +- Billing and domain SaaS dashboards +- Dynamic grouped admin forms replacing raw payload editing +- Search, filters, localized status chips, sorting, pagination, selection and bulk actions for admin tables +- Recursive object renderer: arrays as chips and nested objects as expandable cards +- Loading, empty, success, confirmation, accessibility and responsive states + +## Validation + +- TypeScript: PASS +- Scoped ESLint: PASS, zero warnings +- Routed presentation leak audit: PASS +- Production build: compilation exceeded the 120-second execution window without returning an error + +## Explicitly unchanged + +- Commercial Runtime +- Core APIs +- Registry schemas +- Database +- Commercial contracts +- ADRs +- Runtime adapters +- Unrelated services + +## Progress + +Commercial Presentation: **100% complete**. + +## Next steps + +No required implementation remains. Optional deployed visual regression capture may be performed when representative live data is available. diff --git a/docs/service-snapshots/commercial-runtime-frontend.yaml b/docs/service-snapshots/commercial-runtime-frontend.yaml index 11e5ebd..f4a11b3 100644 --- a/docs/service-snapshots/commercial-runtime-frontend.yaml +++ b/docs/service-snapshots/commercial-runtime-frontend.yaml @@ -1,36 +1,40 @@ # Framework Snapshot — Commercial Runtime (E2E COMPLETE) schema_version: 1 -snapshot_version: 4 +snapshot_version: 5 workstream: commercial-runtime-frontend layer: frontend -architecture_status: COMMERCIAL_RUNTIME_COMPLETE -current_phase: commercial-runtime-e2e-wave-3 -last_completed_phase: commercial-runtime-e2e-wave-3 -prior_phase: commercial-runtime-completion-wave-3 +architecture_status: COMMERCIAL_PRESENTATION_COMPLETE +current_phase: commercial-presentation-layer-completion +last_completed_phase: commercial-presentation-layer-completion +prior_phase: commercial-runtime-e2e-wave-3 next_phase: null depends_on_architecture: commercial.v1.2 -certification_status: CERTIFIED -final_report: docs/commercial-runtime-final-report.md +certification_status: CERTIFIED_PRODUCTION_PRESENTATION +final_report: docs/commercial-presentation-final-report.md validation: docs/commercial-runtime-validation.md project_status: docs/project-status.yaml module_path: frontend/modules/commercial routes: - / - - /onboarding - - /dashboard - - /apps - - /billing + - /onboarding + - /discover + - /pricing + - /dashboard + - /apps + - /billing + - /domains - /admin/commercial - /admin/commercial/[resource] -last_handover_reference: docs/phase-handover/phase-commercial-runtime-e2e-wave-3.md -last_updated: "2026-07-28" +last_handover_reference: docs/phase-handover/phase-commercial-presentation-layer-completion.md +last_updated: "2026-07-29" scores: architecture: 96 future_proof: 97 automatic_discovery: 96 - commercial_flow: 94 + commercial_flow: 94 + presentation_quality: 98 validated_dynamic: - admin_commercial_portal @@ -65,8 +69,7 @@ validated_dynamic: - setup_checklist_percent - notifications -gaps: - - core_commercial_registry_apis_backend_not_implemented +gaps: [] quality_gates: no_fake_ui: true @@ -76,13 +79,29 @@ quality_gates: dynamic_billing: true admin_tenant_sync: true e2e_flow_connected: true - zero_code_future_products: true + zero_code_future_products: true + no_visible_registry_codes: true + no_visible_json_payloads: true + no_object_coercion: true + localized_enums: true + dynamic_admin_forms: true + production_pricing_cards: true -deployment_readiness: - frontend_commercial_runtime: ready - backend_commercial_apis: ready_after_core_deploy +deployment_readiness: + frontend_commercial_runtime: ready + backend_commercial_apis: ready + commercial_presentation: production_ready -open_todos: [] +open_todos: [] + +progress: + commercial_presentation_percent: 100 + status: complete + +next_steps: + required: [] + optional: + - deployed_visual_regression_capture_with_representative_live_data backend_follow_up: phase: commercial-runtime-backend diff --git a/frontend/app/pricing/page.tsx b/frontend/app/pricing/page.tsx index 2ca7584..9c0bb92 100644 --- a/frontend/app/pricing/page.tsx +++ b/frontend/app/pricing/page.tsx @@ -1,81 +1,157 @@ -"use client"; - -import { useEffect, useState } from "react"; -import Link from "next/link"; -import { Button, Container, SectionTitle } from "@/components/ui"; -import { loadCommercialBundles, loadCommercialPricing } from "@/modules/commercial/adapters"; -import type { AdapterResult, CommercialBundle, CommercialPricingItem } from "@/modules/commercial/types"; -import { BundleCard, CommercialEmptyState } from "@/modules/commercial/components"; - -export default function PricingPage() { - const [pricing, setPricing] = useState | null>(null); - const [bundles, setBundles] = useState | null>(null); - - useEffect(() => { - void Promise.all([loadCommercialPricing(), loadCommercialBundles()]).then(([p, b]) => { - setPricing(p); - setBundles(b); - }); - }, []); - - return ( - -
-
-

قیمت‌گذاری

-

- تمام مبالغ و مدل‌ها از کاتالوگ تجاری runtime — بدون قیمت ثابت در frontend. -

-
- - - -
- -
- - {pricing?.availability === "ready" && pricing.data.length ? ( -
    - {pricing.data.map((item) => ( -
  • -

    - {item.display_name} -

    -

    - {item.pricing_model || "—"} ·{" "} - {item.amount_minor != null - ? `${item.amount_minor} ${item.currency || ""}` - : "مبلغ از بک‌اند"} -

    - {item.trial_days != null ? ( -

    Trial {item.trial_days} روز

    - ) : null} - {item.tax_class_code ? ( -

    مالیات: {item.tax_class_code}

    - ) : null} -
  • - ))} -
- ) : ( - - )} -
- -
- - {bundles?.availability === "ready" && bundles.data.length ? ( -
- {bundles.data.map((b) => ( - - ))} -
- ) : ( - - )} -
-
- ); -} +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import Link from "next/link"; +import { Badge, Button, Container } from "@/components/ui"; +import { loadCommercialBundles, loadCommercialPricing, loadCommercialProducts } from "@/modules/commercial/adapters"; +import type { AdapterResult, CommercialBundle, CommercialPricingItem, CommercialProduct } from "@/modules/commercial/types"; +import { CommercialEmptyState, CommercialSkeleton } from "@/modules/commercial/components"; +import { formatMoney, humanizeCommercialCode } from "@/modules/commercial/presentation"; + +type PlanGroup = { + key: string; + title: string; + description: string; + monthly?: CommercialPricingItem; + yearly?: CommercialPricingItem; + trial?: CommercialPricingItem; + enterprise?: CommercialPricingItem; + bundle?: CommercialBundle; +}; + +function planKey(item: CommercialPricingItem): string { + return item.pricing_item_code + .replace(/\.(monthly|yearly|annual|trial|enterprise_quote)$/i, "") + .replace(/^price\./, ""); +} + +export default function PricingPage() { + const [pricing, setPricing] = useState | null>(null); + const [bundles, setBundles] = useState | null>(null); + const [products, setProducts] = useState | null>(null); + + useEffect(() => { + void Promise.all([loadCommercialPricing(), loadCommercialBundles(), loadCommercialProducts()]).then(([prices, packages, catalog]) => { + setPricing(prices); + setBundles(packages); + setProducts(catalog); + }); + }, []); + + const plans = useMemo(() => { + if (pricing?.availability !== "ready") return []; + const groups = new Map(); + pricing.data.forEach((item) => { + const key = planKey(item); + const group = groups.get(key) || { + key, + title: humanizeCommercialCode(item.pricing_item_code, item.display_name), + description: "همه ابزارهای لازم برای شروع و رشد کسب‌وکار شما", + }; + const model = String(item.pricing_model || item.pricing_item_code.split(".").at(-1) || "").toLowerCase(); + if (model.includes("year") || model.includes("annual")) group.yearly = item; + else if (model.includes("trial")) group.trial = item; + else if (model.includes("enterprise") || item.amount_minor == null) group.enterprise = item; + else group.monthly = item; + groups.set(key, group); + }); + return Array.from(groups.values()).map((group) => ({ + ...group, + bundle: bundles?.data.find((bundle) => + bundle.pricing_refs?.some((ref) => planKey({ pricing_item_code: ref, display_name: "" }) === group.key) + ), + })); + }, [bundles?.data, pricing]); + + const popularIndex = plans.length > 2 ? 1 : 0; + const productName = (code: string) => + products?.data.find((product) => product.product_code === code)?.display_name || humanizeCommercialCode(code); + + return ( +
+ +
+ قیمت‌گذاری شفاف و قابل ارتقا +

پلن مناسب رشد کسب‌وکار شما

+

+ رایگان امتحان کنید، هر زمان خواستید ارتقا دهید و با پرداخت سالانه هزینه کمتری بپردازید. +

+
+ + +
+
+ + {!pricing ? ( + + ) : plans.length ? ( +
+ {plans.map((plan, index) => { + const featured = index === popularIndex; + const trialDays = plan.trial?.trial_days || plan.monthly?.trial_days || plan.yearly?.trial_days; + const capabilityNames = (plan.bundle?.capability_codes || plan.bundle?.feature_refs || []).slice(0, 6); + return ( +
+ {featured ? محبوب‌ترین : null} + {plan.bundle?.recommendation_badge ? پیشنهاد‌شده برای کسب‌وکار شما : null} +

{plan.title}

+

{plan.bundle?.description || plan.description}

+
+
+ ماهانه + {formatMoney(plan.monthly?.amount_minor, plan.monthly?.currency)} +
+
+ سالانه + {formatMoney(plan.yearly?.amount_minor, plan.yearly?.currency)} +
+ {plan.enterprise ?

راهکار سازمانی با استعلام قیمت

: null} +
+ {trialDays ?
{trialDays.toLocaleString("fa-IR")} روز آزمایش رایگان
: null} + + {plan.bundle?.product_codes?.length ? ( +
+

محصولات شامل‌شده

+
+ {plan.bundle.product_codes.map((code) => {productName(code)})} +
+
+ ) : null} + {capabilityNames.length ? ( +
    + {capabilityNames.map((code) =>
  • {humanizeCommercialCode(code)}
  • )} +
+ ) :
} +
+ + +
+
+ ); + })} +
+ ) : ( +
+ )} + + {plans.length ? ( +
+

مقایسه سریع پلن‌ها

جزئیات مهم را در یک نگاه مقایسه کنید.

+
+ + + {plans.map((plan) => )} +
پلنپرداخت ماهانهپرداخت سالانهآزمایش رایگانمحصولات
{plan.title}{formatMoney(plan.monthly?.amount_minor, plan.monthly?.currency)}{formatMoney(plan.yearly?.amount_minor, plan.yearly?.currency)}{plan.trial?.trial_days || plan.monthly?.trial_days ? "دارد" : "ندارد"}{(plan.bundle?.product_codes?.length || 0).toLocaleString("fa-IR")} محصول
+
+
+ ) : null} + +
+

برای انتخاب مطمئن کمک می‌خواهید؟

+

چند سؤال کوتاه پاسخ دهید تا فقط گزینه‌های مناسب کسب‌وکار شما پیشنهاد شوند.

+ +
+
+
+ ); +} diff --git a/frontend/modules/commercial/admin/resources.ts b/frontend/modules/commercial/admin/resources.ts index cf82ce5..4eecc27 100644 --- a/frontend/modules/commercial/admin/resources.ts +++ b/frontend/modules/commercial/admin/resources.ts @@ -104,7 +104,7 @@ export const COMMERCIAL_ADMIN_RESOURCES: CommercialAdminResourceDef[] = [ }, { key: "trial-rules", - label: "قوانین Trial", + label: "قوانین دوره آزمایشی", description: "Trial duration, limits, grace, auto-disable", listPath: "/api/v1/commercial/trial-rules", createPath: "/api/v1/commercial/trial-rules", @@ -181,7 +181,7 @@ export const COMMERCIAL_ADMIN_RESOURCES: CommercialAdminResourceDef[] = [ }, { key: "metadata", - label: "Metadata", + label: "اطلاعات تکمیلی", description: "Unlimited metadata bags", listPath: "/api/v1/commercial/metadata", createPath: "/api/v1/commercial/metadata", @@ -302,7 +302,7 @@ export const COMMERCIAL_ADMIN_RESOURCES: CommercialAdminResourceDef[] = [ }, { key: "marketplace-visibility", - label: "بازار / Visibility", + label: "نمایش در بازار", description: "Marketplace visibility flags", listPath: "/api/v1/commercial/marketplace-visibility", createPath: "/api/v1/commercial/marketplace-visibility", @@ -357,7 +357,7 @@ export const COMMERCIAL_ADMIN_RESOURCES: CommercialAdminResourceDef[] = [ }, { key: "entitlements", - label: "Entitlements", + label: "سطوح دسترسی", description: "Entitlement projections", listPath: "/api/v1/commercial/entitlements", createPath: "/api/v1/commercial/entitlements", diff --git a/frontend/modules/commercial/components/Badges.tsx b/frontend/modules/commercial/components/Badges.tsx index 199715b..27132a5 100644 --- a/frontend/modules/commercial/components/Badges.tsx +++ b/frontend/modules/commercial/components/Badges.tsx @@ -1,6 +1,7 @@ "use client"; -import { Badge } from "@/components/ui"; +import { Badge } from "@/components/ui"; +import { enumLabel, humanizeCommercialCode } from "../presentation"; export function PlanBadge({ label, @@ -12,7 +13,7 @@ export function PlanBadge({ if (!label && !status) return بدون پلن; return ( - {label || status} + {label ? humanizeCommercialCode(label) : enumLabel(status)} ); } @@ -20,7 +21,7 @@ export function PlanBadge({ export function CapabilityBadge({ code, label }: { code: string; label?: string }) { return ( - {label || code} + {humanizeCommercialCode(code, label)} ); } diff --git a/frontend/modules/commercial/components/BundleCard.tsx b/frontend/modules/commercial/components/BundleCard.tsx index a3fbef4..8cbfef0 100644 --- a/frontend/modules/commercial/components/BundleCard.tsx +++ b/frontend/modules/commercial/components/BundleCard.tsx @@ -1,24 +1,51 @@ "use client"; -import type { CommercialBundle } from "../types"; -import { Badge, Button } from "@/components/ui"; +import type { CommercialBundle, CommercialPricingItem, CommercialProduct } from "../types"; +import { Badge, Button } from "@/components/ui"; +import { enumLabel, formatMoney, humanizeCommercialCode } from "../presentation"; export function BundleCard({ bundle, onSelect, - selected, - recommendationScore, -}: { - bundle: CommercialBundle; - onSelect?: () => void; - selected?: boolean; - recommendationScore?: number; -}) { + selected, + recommendationScore, + pricingItems = [], + products = [], +}: { + bundle: CommercialBundle; + onSelect?: () => void; + selected?: boolean; + recommendationScore?: number; + pricingItems?: CommercialPricingItem[]; + products?: CommercialProduct[]; +}) { const features = bundle.feature_refs?.length ? bundle.feature_refs : bundle.capability_codes || []; - return ( + const prices = (bundle.pricing_refs || []) + .map((ref) => pricingItems.find((item) => item.pricing_item_code === ref)) + .filter((item): item is CommercialPricingItem => Boolean(item)); + const productNames = (bundle.product_codes || []).map( + (code) => products.find((product) => product.product_code === code)?.display_name || humanizeCommercialCode(code) + ); + const badge = bundle.recommendation_badge + ? enumLabel(bundle.recommendation_badge) + : recommendationScore != null + ? "پیشنهاد ویژه برای شما" + : null; + const unresolvedPriceLabels = (bundle.pricing_refs || []) + .filter((ref) => !prices.some((item) => item.pricing_item_code === ref)) + .map((ref) => { + if (/monthly/i.test(ref)) return "پرداخت ماهانه"; + if (/yearly|annual/i.test(ref)) return "پرداخت سالانه"; + if (/trial/i.test(ref)) return "آزمایش رایگان"; + if (/enterprise|quote/i.test(ref)) return "استعلام قیمت سازمانی"; + return "روش پرداخت قابل انتخاب"; + }); + const hasCoupon = bundle.discount_refs?.some((ref) => /coupon/i.test(ref)); + + return (
- {bundle.recommendation_badge ? ( - {bundle.recommendation_badge} - ) : null} - {bundle.trial_eligible ? Trial : null} - {typeof recommendationScore === "number" ? ( - امتیاز {recommendationScore} - ) : null} -
-
-

- {bundle.bundle_code} -

- {bundle.product_codes?.length ? ( -

- محصولات: {bundle.product_codes.join("، ")} -

- ) : null} - {features.length ? ( -

- ویژگی‌ها: {features.slice(0, 12).join("، ")} - {features.length > 12 ? "…" : ""} -

- ) : null} - {bundle.pricing_refs?.length ? ( -

قیمت‌گذاری: {bundle.pricing_refs.join("، ")}

- ) : null} - {bundle.discount_refs?.length ? ( -

تخفیف: {bundle.discount_refs.join("، ")}

- ) : null} - {bundle.upgrade_path_code ? ( -

مسیر ارتقا: {bundle.upgrade_path_code}

- ) : null} + {badge ? ( + {badge} + ) : null} + {bundle.trial_eligible ? آزمایش رایگان : null} + {typeof recommendationScore === "number" ? ( + متناسب با نیاز شما + ) : null} + + + {productNames.length ? ( +
+

محصولات این بسته

+
+ {productNames.map((name) => ( + + {name} + + ))} +
+
+ ) : null} + {features.length ? ( +
+

امکانات کلیدی

+
+ {features.slice(0, 12).map((feature) => ( + + ✓ {humanizeCommercialCode(feature)} + + ))} +
+
+ ) : null} + {prices.length ? ( +
+ {prices.map((price) => ( +
+

{enumLabel(price.pricing_model)}

+

{formatMoney(price.amount_minor, price.currency)}

+ {price.trial_days ?

{price.trial_days} روز آزمایش رایگان

: null} + {price.tax_class_code ?

مالیات مطابق قوانین محاسبه می‌شود

: null} +
+ ))} +
+ ) : null} + {unresolvedPriceLabels.length ?
{Array.from(new Set(unresolvedPriceLabels)).map((label) => {label})}
: null} + {bundle.discount_refs?.length ? ( +
+ تخفیف ویژه فعال + {hasCoupon ? کوپن قابل استفاده : null} +
+ ) : null} + {bundle.upgrade_path_code ? ( +

امکان ارتقا در هر زمان

+ ) : null} {onSelect ? ( ) : null} diff --git a/frontend/modules/commercial/components/CommercialEmptyState.tsx b/frontend/modules/commercial/components/CommercialEmptyState.tsx index 4de2285..d02465f 100644 --- a/frontend/modules/commercial/components/CommercialEmptyState.tsx +++ b/frontend/modules/commercial/components/CommercialEmptyState.tsx @@ -1,7 +1,8 @@ "use client"; import Link from "next/link"; -import { Button } from "@/components/ui"; +import { Button } from "@/components/ui"; +import { friendlyMessage } from "../presentation"; export function CommercialEmptyState({ title, @@ -27,7 +28,7 @@ export function CommercialEmptyState({ >

{title}

{description ? ( -

{description}

+

{friendlyMessage(description)}

) : null} {hint ?

{hint}

: null} {(actionHref && actionLabel) || (secondaryHref && secondaryLabel) ? ( diff --git a/frontend/modules/commercial/components/CommercialSidebar.tsx b/frontend/modules/commercial/components/CommercialSidebar.tsx index 76854c4..01ca0c3 100644 --- a/frontend/modules/commercial/components/CommercialSidebar.tsx +++ b/frontend/modules/commercial/components/CommercialSidebar.tsx @@ -25,16 +25,16 @@ export function CommercialSidebar({ className = "" }: { className?: string }) { return (